1
0
mirror of https://github.com/go-micro/go-micro.git synced 2025-06-30 22:33:49 +02:00

Move runtime/kubernetes/client to util/kubernetes/client

This commit is contained in:
Jake Sanders
2019-12-17 11:32:38 +00:00
parent 0489ae91e9
commit e95f44d3f8
11 changed files with 3 additions and 3 deletions

View File

@ -0,0 +1,169 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
type testcase struct {
Token string
ReqFn func(opts *Options) *Request
Method string
URI string
Body interface{}
Header map[string]string
Assert func(req *http.Request) bool
}
type assertFn func(req *http.Request) bool
var tests = []testcase{
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Get().Resource("service")
},
Method: "GET",
URI: "/api/v1/namespaces/default/services/",
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Get().Resource("service").Name("foo")
},
Method: "GET",
URI: "/api/v1/namespaces/default/services/foo",
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Get().Resource("service").Namespace("test").Name("bar")
},
Method: "GET",
URI: "/api/v1/namespaces/test/services/bar",
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Get().Resource("deployment").Name("foo")
},
Method: "GET",
URI: "/apis/apps/v1/namespaces/default/deployments/foo",
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Get().Resource("deployment").Namespace("test").Name("foo")
},
Method: "GET",
URI: "/apis/apps/v1/namespaces/test/deployments/foo",
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Get().Resource("pod").Params(&Params{LabelSelector: map[string]string{"foo": "bar"}})
},
Method: "GET",
URI: "/api/v1/namespaces/default/pods/?labelSelector=foo%3Dbar",
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Post().Resource("service").Name("foo").Body(map[string]string{"foo": "bar"})
},
Method: "POST",
URI: "/api/v1/namespaces/default/services/foo",
Body: map[string]string{"foo": "bar"},
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Post().Resource("deployment").Namespace("test").Name("foo").Body(map[string]string{"foo": "bar"})
},
Method: "POST",
URI: "/apis/apps/v1/namespaces/test/deployments/foo",
Body: map[string]string{"foo": "bar"},
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Put().Resource("endpoint").Name("baz").Body(map[string]string{"bam": "bar"})
},
Method: "PUT",
URI: "/api/v1/namespaces/default/endpoints/baz",
Body: map[string]string{"bam": "bar"},
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Patch().Resource("endpoint").Name("baz").Body(map[string]string{"bam": "bar"})
},
Method: "PATCH",
URI: "/api/v1/namespaces/default/endpoints/baz",
Body: map[string]string{"bam": "bar"},
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Patch().Resource("endpoint").Name("baz").SetHeader("foo", "bar")
},
Method: "PATCH",
URI: "/api/v1/namespaces/default/endpoints/baz",
Header: map[string]string{"foo": "bar"},
},
testcase{
ReqFn: func(opts *Options) *Request {
return NewRequest(opts).Patch().Resource("deployment").Name("baz").SetHeader("foo", "bar")
},
Method: "PATCH",
URI: "/apis/apps/v1/namespaces/default/deployments/baz",
Header: map[string]string{"foo": "bar"},
},
}
var wrappedHandler = func(test *testcase, t *testing.T) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if len(test.Token) > 0 && (len(auth) == 0 || auth != "Bearer "+test.Token) {
t.Errorf("test case token (%s) did not match expected token (%s)", "Bearer "+test.Token, auth)
}
if len(test.Method) > 0 && test.Method != r.Method {
t.Errorf("test case Method (%s) did not match expected Method (%s)", test.Method, r.Method)
}
if len(test.URI) > 0 && test.URI != r.URL.RequestURI() {
t.Errorf("test case URI (%s) did not match expected URI (%s)", test.URI, r.URL.RequestURI())
}
if test.Body != nil {
var res map[string]string
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&res); err != nil {
t.Errorf("decoding body failed: %v", err)
}
if !reflect.DeepEqual(res, test.Body) {
t.Error("body did not match")
}
}
if test.Header != nil {
for k, v := range test.Header {
if r.Header.Get(k) != v {
t.Error("header did not exist")
}
}
}
w.WriteHeader(http.StatusOK)
})
}
func TestRequest(t *testing.T) {
for _, test := range tests {
ts := httptest.NewServer(wrappedHandler(&test, t))
req := test.ReqFn(&Options{
Host: ts.URL,
Client: &http.Client{},
BearerToken: &test.Token,
Namespace: "default",
})
res := req.Do()
if res.Error() != nil {
t.Errorf("request failed with %v", res.Error())
}
ts.Close()
}
}

View File

@ -0,0 +1,228 @@
package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"github.com/micro/go-micro/util/log"
)
// Request is used to construct a http request for the k8s API.
type Request struct {
client *http.Client
header http.Header
params url.Values
method string
host string
namespace string
resource string
resourceName *string
body io.Reader
err error
}
// Params is the object to pass in to set paramaters
// on a request.
type Params struct {
LabelSelector map[string]string
Annotations map[string]string
}
// verb sets method
func (r *Request) verb(method string) *Request {
r.method = method
return r
}
// 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 {
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
}
// 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
}
log.Debugf("Request body: %v", b)
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
}
log.Debugf("Request body: %v", b)
r.body = b
return r
}
// Params isused to set paramters 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)
}
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 "pod", "service", "endpoint":
// /api/v1/namespaces/{namespace}/pods
url = fmt.Sprintf("%s/api/v1/namespaces/%s/%ss/", r.host, r.namespace, r.resource)
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)
}
// append resourceName if it is present
if r.resourceName != nil {
url += *r.resourceName
}
// append any query params
if len(r.params) > 0 {
url += "?" + r.params.Encode()
}
// build request
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,
}
}
log.Debugf("kubernetes api request: %v", req)
res, err := r.client.Do(req)
if err != nil {
return &Response{
err: err,
}
}
log.Debugf("kubernetes api response: %v", res)
// return res, err
return newResponse(res, err)
}
// 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
}

View File

@ -0,0 +1,95 @@
package api
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/micro/go-micro/util/log"
)
// Errors ...
var (
ErrNotFound = errors.New("kubernetes: resource not found")
ErrDecode = errors.New("kubernetes: error decoding")
ErrUnknown = errors.New("kubernetes: unknown error")
)
// Status is an object that is returned when a request
// failed or delete succeeded.
// type Status struct {
// Kind string `json:"kind"`
// Status string `json:"status"`
// Message string `json:"message"`
// Reason string `json:"reason"`
// Code int `json:"code"`
// }
// Response ...
type Response struct {
res *http.Response
err error
body []byte
}
// Error returns an error
func (r *Response) Error() error {
return r.err
}
// StatusCode returns status code for response
func (r *Response) StatusCode() int {
return r.res.StatusCode
}
// Into decode body into `data`
func (r *Response) Into(data interface{}) error {
if r.err != nil {
return r.err
}
defer r.res.Body.Close()
decoder := json.NewDecoder(r.res.Body)
err := decoder.Decode(&data)
if err != nil {
return ErrDecode
}
return r.err
}
func newResponse(res *http.Response, err error) *Response {
r := &Response{
res: res,
err: err,
}
if err != nil {
return r
}
if r.res.StatusCode == http.StatusOK ||
r.res.StatusCode == http.StatusCreated ||
r.res.StatusCode == http.StatusNoContent {
// Non error status code
return r
}
if r.res.StatusCode == http.StatusNotFound {
r.err = ErrNotFound
return r
}
log.Logf("kubernetes: request failed with code %v", r.res.StatusCode)
b, err := ioutil.ReadAll(r.res.Body)
if err == nil {
log.Log("kubernetes: request failed with body:")
log.Log(string(b))
}
r.err = ErrUnknown
return r
}