mirror of
https://github.com/go-micro/go-micro.git
synced 2025-07-12 22:41:07 +02:00
add all the plugins
This commit is contained in:
141
plugins/registry/kubernetes/client/api/api_test.go
Normal file
141
plugins/registry/kubernetes/client/api/api_test.go
Normal file
@ -0,0 +1,141 @@
|
||||
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("services")
|
||||
},
|
||||
Method: "GET",
|
||||
URI: "/api/v1/namespaces/default/services/",
|
||||
},
|
||||
testcase{
|
||||
ReqFn: func(opts *Options) *Request {
|
||||
return NewRequest(opts).Get().Resource("services").Name("foo")
|
||||
},
|
||||
Method: "GET",
|
||||
URI: "/api/v1/namespaces/default/services/foo",
|
||||
},
|
||||
testcase{
|
||||
ReqFn: func(opts *Options) *Request {
|
||||
return NewRequest(opts).Get().Resource("services").Namespace("test").Name("bar")
|
||||
},
|
||||
Method: "GET",
|
||||
URI: "/api/v1/namespaces/test/services/bar",
|
||||
},
|
||||
testcase{
|
||||
ReqFn: func(opts *Options) *Request {
|
||||
return NewRequest(opts).Get().Resource("pods").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("services").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).Put().Resource("endpoints").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("endpoints").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("endpoints").Name("baz").SetHeader("foo", "bar")
|
||||
},
|
||||
Method: "PATCH",
|
||||
URI: "/api/v1/namespaces/default/endpoints/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("Did not expect to fail with %v", res.Error())
|
||||
}
|
||||
|
||||
ts.Close()
|
||||
}
|
||||
}
|
215
plugins/registry/kubernetes/client/api/request.go
Normal file
215
plugins/registry/kubernetes/client/api/request.go
Normal file
@ -0,0 +1,215 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client/watch"
|
||||
)
|
||||
|
||||
// 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 parameters
|
||||
// on a request.
|
||||
type Params struct {
|
||||
LabelSelector map[string]string
|
||||
Watch bool
|
||||
}
|
||||
|
||||
// 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
|
||||
// https://github.com/kubernetes/kubernetes/blob/master/docs/devel/api-conventions.md#patch-operations
|
||||
func (r *Request) Patch() *Request {
|
||||
return r.verb("PATCH").SetHeader("Content-Type", "application/strategic-merge-patch+json")
|
||||
}
|
||||
|
||||
// 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 err := json.NewEncoder(b).Encode(&in); err != nil {
|
||||
r.err = err
|
||||
return r
|
||||
}
|
||||
r.body = b
|
||||
return r
|
||||
}
|
||||
|
||||
// Params isused 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
url := fmt.Sprintf("%s/api/v1/namespaces/%s/%s/", 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,
|
||||
}
|
||||
}
|
||||
|
||||
res, err := r.client.Do(req)
|
||||
if err != nil {
|
||||
return &Response{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// return res, err
|
||||
return newResponse(res, err)
|
||||
}
|
||||
|
||||
// Watch builds and triggers the request, but
|
||||
// will watch instead of return an object
|
||||
func (r *Request) Watch() (watch.Watch, error) {
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
r.params.Set("watch", "true")
|
||||
|
||||
req, err := r.request()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w, err := watch.NewBodyWatcher(req, r.client)
|
||||
return w, 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
|
||||
}
|
94
plugins/registry/kubernetes/client/api/response.go
Normal file
94
plugins/registry/kubernetes/client/api/response.go
Normal file
@ -0,0 +1,94 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
log "github.com/micro/go-micro/v2/logger"
|
||||
)
|
||||
|
||||
// Errors ...
|
||||
var (
|
||||
ErrNotFound = errors.New("K8s: not found")
|
||||
ErrDecode = errors.New("K8s: error decoding")
|
||||
ErrOther = errors.New("K8s: 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.Errorf("K8s: request failed with code %v", r.res.StatusCode)
|
||||
|
||||
b, err := ioutil.ReadAll(r.res.Body)
|
||||
if err == nil {
|
||||
log.Error("K8s: request failed with body:")
|
||||
log.Error(string(b))
|
||||
}
|
||||
r.err = ErrOther
|
||||
return r
|
||||
}
|
133
plugins/registry/kubernetes/client/client.go
Normal file
133
plugins/registry/kubernetes/client/client.go
Normal file
@ -0,0 +1,133 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
log "github.com/micro/go-micro/v2/logger"
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client/api"
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client/watch"
|
||||
)
|
||||
|
||||
var (
|
||||
serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
|
||||
|
||||
ErrReadNamespace = errors.New("Could not read namespace from service account secret")
|
||||
)
|
||||
|
||||
// Client ...
|
||||
type client struct {
|
||||
opts *api.Options
|
||||
}
|
||||
|
||||
// ListPods ...
|
||||
func (c *client) ListPods(labels map[string]string) (*PodList, error) {
|
||||
var pods PodList
|
||||
err := api.NewRequest(c.opts).Get().Resource("pods").Params(&api.Params{LabelSelector: labels}).Do().Into(&pods)
|
||||
return &pods, err
|
||||
}
|
||||
|
||||
// UpdatePod ...
|
||||
func (c *client) UpdatePod(name string, p *Pod) (*Pod, error) {
|
||||
var pod Pod
|
||||
err := api.NewRequest(c.opts).Patch().Resource("pods").Name(name).Body(p).Do().Into(&pod)
|
||||
return &pod, err
|
||||
}
|
||||
|
||||
// WatchPods ...
|
||||
func (c *client) WatchPods(labels map[string]string) (watch.Watch, error) {
|
||||
return api.NewRequest(c.opts).Get().Resource("pods").Params(&api.Params{LabelSelector: labels}).Watch()
|
||||
}
|
||||
|
||||
func detectNamespace() (string, error) {
|
||||
nsPath := path.Join(serviceAccountPath, "namespace")
|
||||
|
||||
// Make sure it's a file and we can read it
|
||||
if s, e := os.Stat(nsPath); e != nil {
|
||||
return "", e
|
||||
} else if s.IsDir() {
|
||||
return "", ErrReadNamespace
|
||||
}
|
||||
|
||||
// Read the file, and cast to a string
|
||||
if ns, e := ioutil.ReadFile(nsPath); e != nil {
|
||||
return string(ns), e
|
||||
} else {
|
||||
return string(ns), nil
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientByHost sets up a client by host
|
||||
func NewClientByHost(host string) Kubernetes {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
DisableCompression: true,
|
||||
}
|
||||
|
||||
c := &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
|
||||
return &client{
|
||||
opts: &api.Options{
|
||||
Client: c,
|
||||
Host: host,
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewClientInCluster should work similarily to the official api
|
||||
// NewInClient by setting up a client configuration for use within
|
||||
// a k8s pod.
|
||||
func NewClientInCluster() Kubernetes {
|
||||
host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||
|
||||
s, err := os.Stat(serviceAccountPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if s == nil || !s.IsDir() {
|
||||
log.Fatal(errors.New("no k8s service account found"))
|
||||
}
|
||||
|
||||
token, err := ioutil.ReadFile(path.Join(serviceAccountPath, "token"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
t := string(token)
|
||||
|
||||
ns, err := detectNamespace()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
c := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: crt,
|
||||
},
|
||||
DisableCompression: true,
|
||||
},
|
||||
}
|
||||
|
||||
return &client{
|
||||
opts: &api.Options{
|
||||
Client: c,
|
||||
Host: host,
|
||||
Namespace: ns,
|
||||
BearerToken: &t,
|
||||
},
|
||||
}
|
||||
}
|
34
plugins/registry/kubernetes/client/kubernetes.go
Normal file
34
plugins/registry/kubernetes/client/kubernetes.go
Normal file
@ -0,0 +1,34 @@
|
||||
package client
|
||||
|
||||
import "github.com/micro/go-micro/plugins/registry/kubernetes/v2/client/watch"
|
||||
|
||||
// Kubernetes ...
|
||||
type Kubernetes interface {
|
||||
ListPods(labels map[string]string) (*PodList, error)
|
||||
UpdatePod(podName string, pod *Pod) (*Pod, error)
|
||||
WatchPods(labels map[string]string) (watch.Watch, error)
|
||||
}
|
||||
|
||||
// PodList ...
|
||||
type PodList struct {
|
||||
Items []Pod `json:"items"`
|
||||
}
|
||||
|
||||
// Pod is the top level item for a pod
|
||||
type Pod struct {
|
||||
Metadata *Meta `json:"metadata"`
|
||||
Status *Status `json:"status"`
|
||||
}
|
||||
|
||||
// Meta ...
|
||||
type Meta struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Labels map[string]*string `json:"labels,omitempty"`
|
||||
Annotations map[string]*string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// Status ...
|
||||
type Status struct {
|
||||
PodIP string `json:"podIP"`
|
||||
Phase string `json:"phase"`
|
||||
}
|
108
plugins/registry/kubernetes/client/mock/kubernetes.go
Normal file
108
plugins/registry/kubernetes/client/mock/kubernetes.go
Normal file
@ -0,0 +1,108 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client"
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client/api"
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client/watch"
|
||||
)
|
||||
|
||||
// Client ...
|
||||
type Client struct {
|
||||
sync.Mutex
|
||||
Pods map[string]*client.Pod
|
||||
events chan watch.Event
|
||||
watchers []*mockWatcher
|
||||
}
|
||||
|
||||
// UpdatePod ...
|
||||
func (m *Client) UpdatePod(podName string, pod *client.Pod) (*client.Pod, error) {
|
||||
p, ok := m.Pods[podName]
|
||||
if !ok {
|
||||
return nil, api.ErrNotFound
|
||||
}
|
||||
|
||||
updateMetadata(p.Metadata, pod.Metadata)
|
||||
|
||||
pstr, _ := json.Marshal(p)
|
||||
|
||||
m.events <- watch.Event{
|
||||
Type: watch.Modified,
|
||||
Object: json.RawMessage(pstr),
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ListPods ...
|
||||
func (m *Client) ListPods(labels map[string]string) (*client.PodList, error) {
|
||||
var pods []client.Pod
|
||||
|
||||
for _, v := range m.Pods {
|
||||
if labelFilterMatch(v.Metadata.Labels, labels) {
|
||||
pods = append(pods, *v)
|
||||
}
|
||||
}
|
||||
return &client.PodList{
|
||||
Items: pods,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WatchPods ...
|
||||
func (m *Client) WatchPods(labels map[string]string) (watch.Watch, error) {
|
||||
w := &mockWatcher{
|
||||
results: make(chan watch.Event),
|
||||
stop: make(chan bool),
|
||||
}
|
||||
|
||||
i := len(m.watchers) // length of watchers is current index
|
||||
m.watchers = append(m.watchers, w)
|
||||
|
||||
go func() {
|
||||
<-w.stop
|
||||
m.watchers = append(m.watchers[:i], m.watchers[i+1:]...)
|
||||
}()
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// newClient ...
|
||||
func newClient() client.Kubernetes {
|
||||
return &Client{}
|
||||
}
|
||||
|
||||
// NewClient ...
|
||||
func NewClient() *Client {
|
||||
c := &Client{
|
||||
Pods: make(map[string]*client.Pod),
|
||||
events: make(chan watch.Event),
|
||||
}
|
||||
|
||||
// broadcast events to watchers
|
||||
go func() {
|
||||
for e := range c.events {
|
||||
for _, w := range c.watchers {
|
||||
w.results <- e
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Teardown ...
|
||||
func Teardown(c *Client) {
|
||||
|
||||
for _, p := range c.Pods {
|
||||
pstr, _ := json.Marshal(p)
|
||||
|
||||
c.events <- watch.Event{
|
||||
Type: watch.Deleted,
|
||||
Object: json.RawMessage(pstr),
|
||||
}
|
||||
}
|
||||
|
||||
c.Pods = make(map[string]*client.Pod)
|
||||
}
|
67
plugins/registry/kubernetes/client/mock/utils.go
Normal file
67
plugins/registry/kubernetes/client/mock/utils.go
Normal file
@ -0,0 +1,67 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client"
|
||||
"github.com/micro/go-micro/plugins/registry/kubernetes/v2/client/watch"
|
||||
)
|
||||
|
||||
type mockWatcher struct {
|
||||
results chan watch.Event
|
||||
stop chan bool
|
||||
}
|
||||
|
||||
// Changes returns the results channel
|
||||
func (w *mockWatcher) ResultChan() <-chan watch.Event {
|
||||
return w.results
|
||||
}
|
||||
|
||||
// Stop closes any channels
|
||||
func (w *mockWatcher) Stop() {
|
||||
select {
|
||||
case <-w.stop:
|
||||
return
|
||||
default:
|
||||
close(w.stop)
|
||||
close(w.results)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMetadata(a, b *client.Meta) {
|
||||
if a == nil || b == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if b.Labels != nil {
|
||||
for lk, lv := range b.Labels {
|
||||
labels := a.Labels
|
||||
if lv == nil {
|
||||
delete(labels, lk)
|
||||
continue
|
||||
}
|
||||
labels[lk] = lv
|
||||
}
|
||||
}
|
||||
|
||||
if b.Annotations != nil {
|
||||
for ak, av := range b.Annotations {
|
||||
ann := a.Annotations
|
||||
if av == nil {
|
||||
delete(ann, ak)
|
||||
continue
|
||||
}
|
||||
ann[ak] = av
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func labelFilterMatch(a map[string]*string, b map[string]string) bool {
|
||||
match := true
|
||||
for lk, lv := range b {
|
||||
ml, ok := a[lk]
|
||||
if !ok || *ml != lv {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return match
|
||||
}
|
74
plugins/registry/kubernetes/client/utils.go
Normal file
74
plugins/registry/kubernetes/client/utils.go
Normal file
@ -0,0 +1,74 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// COPIED FROM
|
||||
// https://github.com/kubernetes/kubernetes/blob/7a725418af4661067b56506faabc2d44c6d7703a/pkg/util/crypto/crypto.go
|
||||
|
||||
// CertPoolFromFile returns an x509.CertPool containing the certificates in the given PEM-encoded file.
|
||||
// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates
|
||||
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
|
||||
certs, err := certificatesFromFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
for _, cert := range certs {
|
||||
pool.AddCert(cert)
|
||||
}
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// certificatesFromFile returns the x509.Certificates contained in the given PEM-encoded file.
|
||||
// Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates
|
||||
func certificatesFromFile(file string) ([]*x509.Certificate, error) {
|
||||
if len(file) == 0 {
|
||||
return nil, errors.New("error reading certificates from an empty filename")
|
||||
}
|
||||
pemBlock, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs, err := CertsFromPEM(pemBlock)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", file, err)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// CertsFromPEM returns the x509.Certificates contained in the given PEM-encoded byte array
|
||||
// Returns an error if a certificate could not be parsed, or if the data does not contain any certificates
|
||||
func CertsFromPEM(pemCerts []byte) ([]*x509.Certificate, error) {
|
||||
ok := false
|
||||
certs := []*x509.Certificate{}
|
||||
for len(pemCerts) > 0 {
|
||||
var block *pem.Block
|
||||
block, pemCerts = pem.Decode(pemCerts)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
// Only use PEM "CERTIFICATE" blocks without extra headers
|
||||
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return certs, err
|
||||
}
|
||||
|
||||
certs = append(certs, cert)
|
||||
ok = true
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return certs, errors.New("could not read any certificates")
|
||||
}
|
||||
return certs, nil
|
||||
}
|
92
plugins/registry/kubernetes/client/watch/body.go
Normal file
92
plugins/registry/kubernetes/client/watch/body.go
Normal file
@ -0,0 +1,92 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// bodyWatcher scans the body of a request for chunks
|
||||
type bodyWatcher struct {
|
||||
results chan Event
|
||||
stop chan struct{}
|
||||
res *http.Response
|
||||
req *http.Request
|
||||
}
|
||||
|
||||
// Changes returns the results channel
|
||||
func (wr *bodyWatcher) ResultChan() <-chan Event {
|
||||
return wr.results
|
||||
}
|
||||
|
||||
// Stop cancels the request
|
||||
func (wr *bodyWatcher) Stop() {
|
||||
select {
|
||||
case <-wr.stop:
|
||||
return
|
||||
default:
|
||||
close(wr.stop)
|
||||
close(wr.results)
|
||||
}
|
||||
}
|
||||
|
||||
func (wr *bodyWatcher) stream() {
|
||||
reader := bufio.NewReader(wr.res.Body)
|
||||
|
||||
// ignore first few messages from stream,
|
||||
// as they are usually old.
|
||||
ignore := true
|
||||
|
||||
go func() {
|
||||
<-time.After(time.Second)
|
||||
ignore = false
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// read a line
|
||||
b, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// ignore for the first second
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
// send the event
|
||||
var event Event
|
||||
if err := json.Unmarshal(b, &event); err != nil {
|
||||
continue
|
||||
}
|
||||
wr.results <- event
|
||||
}
|
||||
|
||||
// stop the watcher
|
||||
wr.Stop()
|
||||
}()
|
||||
}
|
||||
|
||||
// NewBodyWatcher creates a k8s body watcher for
|
||||
// a given http request
|
||||
func NewBodyWatcher(req *http.Request, client *http.Client) (Watch, error) {
|
||||
stop := make(chan struct{})
|
||||
req.Cancel = stop
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wr := &bodyWatcher{
|
||||
results: make(chan Event),
|
||||
stop: stop,
|
||||
req: req,
|
||||
res: res,
|
||||
}
|
||||
|
||||
go wr.stream()
|
||||
return wr, nil
|
||||
}
|
26
plugins/registry/kubernetes/client/watch/watch.go
Normal file
26
plugins/registry/kubernetes/client/watch/watch.go
Normal file
@ -0,0 +1,26 @@
|
||||
package watch
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Watch ...
|
||||
type Watch interface {
|
||||
Stop()
|
||||
ResultChan() <-chan Event
|
||||
}
|
||||
|
||||
// EventType defines the possible types of events.
|
||||
type EventType string
|
||||
|
||||
// EventTypes used
|
||||
const (
|
||||
Added EventType = "ADDED"
|
||||
Modified EventType = "MODIFIED"
|
||||
Deleted EventType = "DELETED"
|
||||
Error EventType = "ERROR"
|
||||
)
|
||||
|
||||
// Event represents a single event to a watched resource.
|
||||
type Event struct {
|
||||
Type EventType `json:"type"`
|
||||
Object json.RawMessage `json:"object"`
|
||||
}
|
71
plugins/registry/kubernetes/client/watch/watch_test.go
Normal file
71
plugins/registry/kubernetes/client/watch/watch_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package watch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var actions = []string{
|
||||
`{"type": "create", "object":{"foo": "bar"}}`,
|
||||
`{"type": "delete", INVALID}`,
|
||||
`{"type": "update", "object":{"foo": {"foo": "bar"}}}`,
|
||||
`{"type": "delete", "object":null}`,
|
||||
}
|
||||
|
||||
func TestBodyWatcher(t *testing.T) {
|
||||
// set up server with handler to flush strings from ch.
|
||||
ch := make(chan string)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
t.Fatal("expected ResponseWriter to be a flusher")
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\n")
|
||||
flusher.Flush()
|
||||
|
||||
for v := range ch {
|
||||
fmt.Fprintf(w, "%s\n", v)
|
||||
flusher.Flush()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect NewRequest to return err: %v", err)
|
||||
}
|
||||
|
||||
// setup body watcher
|
||||
w, err := NewBodyWatcher(req, http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatalf("did not expect NewBodyWatcher to return %v", err)
|
||||
}
|
||||
|
||||
<-time.After(time.Second)
|
||||
|
||||
// send action strings in, and expect result back
|
||||
ch <- actions[0]
|
||||
if r := <-w.ResultChan(); r.Type != "create" {
|
||||
t.Fatalf("expected result to be create")
|
||||
}
|
||||
|
||||
ch <- actions[1] // should be ignored as its invalid json
|
||||
ch <- actions[2]
|
||||
if r := <-w.ResultChan(); r.Type != "update" {
|
||||
t.Fatalf("expected result to be update")
|
||||
}
|
||||
|
||||
ch <- actions[3]
|
||||
if r := <-w.ResultChan(); r.Type != "delete" {
|
||||
t.Fatalf("expected result to be delete")
|
||||
}
|
||||
|
||||
// stop should clean up all channels.
|
||||
w.Stop()
|
||||
close(ch)
|
||||
}
|
Reference in New Issue
Block a user