package api import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "github.com/unistack-org/micro/v3/logger" ) // Request is used to construct a http request for the k8s API. type Request struct { // the request context context context.Context client *http.Client header http.Header params url.Values method string host string namespace string resource string resourceName *string subResource *string body io.Reader err error } // Params is the object to pass in to set parameters // on a request. type Params struct { LabelSelector map[string]string Annotations map[string]string Additional map[string]string } // verb sets method func (r *Request) verb(method string) *Request { r.method = method return r } func (r *Request) Context(ctx context.Context) { r.context = ctx } // Get request func (r *Request) Get() *Request { return r.verb("GET") } // Post request func (r *Request) Post() *Request { return r.verb("POST") } // Put request func (r *Request) Put() *Request { return r.verb("PUT") } // Patch request func (r *Request) Patch() *Request { return r.verb("PATCH") } // Delete request func (r *Request) Delete() *Request { return r.verb("DELETE") } // Namespace is to set the namespace to operate on func (r *Request) Namespace(s string) *Request { if len(s) > 0 { r.namespace = s } return r } // Resource is the type of resource the operation is // for, such as "services", "endpoints" or "pods" func (r *Request) Resource(s string) *Request { r.resource = s return r } // SubResource sets a sub resource on a resource, // e.g. pods/log for pod logs func (r *Request) SubResource(s string) *Request { r.subResource = &s return r } // Name is for targeting a specific resource by id func (r *Request) Name(s string) *Request { r.resourceName = &s return r } // Body pass in a body to set, this is for POST, PUT and PATCH requests func (r *Request) Body(in interface{}) *Request { b := new(bytes.Buffer) // if we're not sending YAML request, we encode to JSON if r.header.Get("Content-Type") != "application/yaml" { if err := json.NewEncoder(b).Encode(&in); err != nil { r.err = err return r } r.body = b return r } // if application/yaml is set, we assume we get a raw bytes so we just copy over body, ok := in.(io.Reader) if !ok { r.err = errors.New("invalid data") return r } // copy over data to the bytes buffer if _, err := io.Copy(b, body); err != nil { r.err = err return r } r.body = b return r } // Params is used to set parameters on a request func (r *Request) Params(p *Params) *Request { for k, v := range p.LabelSelector { // create new key=value pair value := fmt.Sprintf("%s=%s", k, v) // check if there's an existing value if label := r.params.Get("labelSelector"); len(label) > 0 { value = fmt.Sprintf("%s,%s", label, value) } // set and overwrite the value r.params.Set("labelSelector", value) } for k, v := range p.Additional { r.params.Set(k, v) } return r } // SetHeader sets a header on a request with // a `key` and `value` func (r *Request) SetHeader(key, value string) *Request { r.header.Add(key, value) return r } // request builds the http.Request from the options func (r *Request) request() (*http.Request, error) { var url string switch r.resource { case "namespace": // /api/v1/namespaces/ url = fmt.Sprintf("%s/api/v1/namespaces/", r.host) case "deployment": // /apis/apps/v1/namespaces/{namespace}/deployments/{name} url = fmt.Sprintf("%s/apis/apps/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource) default: // /api/v1/namespaces/{namespace}/{resource} url = fmt.Sprintf("%s/api/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource) } // append resourceName if it is present if r.resourceName != nil { url += *r.resourceName if r.subResource != nil { url += "/" + *r.subResource } } // append any query params if len(r.params) > 0 { url += "?" + r.params.Encode() } var req *http.Request var err error // build request if r.context != nil { req, err = http.NewRequestWithContext(r.context, r.method, url, r.body) } else { req, err = http.NewRequest(r.method, url, r.body) } if err != nil { return nil, err } // set headers on request req.Header = r.header return req, nil } // Do builds and triggers the request func (r *Request) Do() *Response { if r.err != nil { return &Response{ err: r.err, } } req, err := r.request() if err != nil { return &Response{ err: err, } } logger.Debugf("[Kubernetes] %v %v", req.Method, req.URL.String()) res, err := r.client.Do(req) if err != nil { return &Response{ err: err, } } // return res, err return newResponse(res, err) } // Raw performs a Raw HTTP request to the Kubernetes API func (r *Request) Raw() (*http.Response, error) { req, err := r.request() if err != nil { return nil, err } res, err := r.client.Do(req) if err != nil { return nil, err } return res, nil } // Options ... type Options struct { Host string Namespace string BearerToken *string Client *http.Client } // NewRequest creates a k8s api request func NewRequest(opts *Options) *Request { req := &Request{ header: make(http.Header), params: make(url.Values), client: opts.Client, namespace: opts.Namespace, host: opts.Host, } if opts.BearerToken != nil { req.SetHeader("Authorization", "Bearer "+*opts.BearerToken) } return req }