many lint fixes and optimizations #17
| @@ -1,121 +0,0 @@ | ||||
| // Package api provides an http-rpc handler which provides the entire http request over rpc | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
|  | ||||
| 	goapi "github.com/unistack-org/micro/v3/api" | ||||
| 	"github.com/unistack-org/micro/v3/api/handler" | ||||
| 	api "github.com/unistack-org/micro/v3/api/proto" | ||||
| 	"github.com/unistack-org/micro/v3/client" | ||||
| 	"github.com/unistack-org/micro/v3/errors" | ||||
| 	"github.com/unistack-org/micro/v3/util/ctx" | ||||
| 	"github.com/unistack-org/micro/v3/util/router" | ||||
| ) | ||||
|  | ||||
| type apiHandler struct { | ||||
| 	opts handler.Options | ||||
| 	s    *goapi.Service | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	Handler = "api" | ||||
| ) | ||||
|  | ||||
| // API handler is the default handler which takes api.Request and returns api.Response | ||||
| func (a *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	bsize := handler.DefaultMaxRecvSize | ||||
| 	if a.opts.MaxRecvSize > 0 { | ||||
| 		bsize = a.opts.MaxRecvSize | ||||
| 	} | ||||
|  | ||||
| 	r.Body = http.MaxBytesReader(w, r.Body, bsize) | ||||
| 	request, err := requestToProto(r) | ||||
| 	if err != nil { | ||||
| 		er := errors.InternalServerError("go.micro.api", err.Error()) | ||||
| 		w.Header().Set("Content-Type", "application/json") | ||||
| 		w.WriteHeader(500) | ||||
| 		w.Write([]byte(er.Error())) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	var service *goapi.Service | ||||
|  | ||||
| 	if a.s != nil { | ||||
| 		// we were given the service | ||||
| 		service = a.s | ||||
| 	} else if a.opts.Router != nil { | ||||
| 		// try get service from router | ||||
| 		s, err := a.opts.Router.Route(r) | ||||
| 		if err != nil { | ||||
| 			er := errors.InternalServerError("go.micro.api", err.Error()) | ||||
| 			w.Header().Set("Content-Type", "application/json") | ||||
| 			w.WriteHeader(500) | ||||
| 			w.Write([]byte(er.Error())) | ||||
| 			return | ||||
| 		} | ||||
| 		service = s | ||||
| 	} else { | ||||
| 		// we have no way of routing the request | ||||
| 		er := errors.InternalServerError("go.micro.api", "no route found") | ||||
| 		w.Header().Set("Content-Type", "application/json") | ||||
| 		w.WriteHeader(500) | ||||
| 		w.Write([]byte(er.Error())) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// create request and response | ||||
| 	c := a.opts.Client | ||||
| 	req := c.NewRequest(service.Name, service.Endpoint.Name, request) | ||||
| 	rsp := &api.Response{} | ||||
|  | ||||
| 	// create the context from headers | ||||
| 	cx := ctx.FromRequest(r) | ||||
|  | ||||
| 	if err := c.Call(cx, req, rsp, client.WithRouter(router.New(service.Services))); err != nil { | ||||
| 		w.Header().Set("Content-Type", "application/json") | ||||
| 		ce := errors.Parse(err.Error()) | ||||
| 		switch ce.Code { | ||||
| 		case 0: | ||||
| 			w.WriteHeader(500) | ||||
| 		default: | ||||
| 			w.WriteHeader(int(ce.Code)) | ||||
| 		} | ||||
| 		w.Write([]byte(ce.Error())) | ||||
| 		return | ||||
| 	} else if rsp.StatusCode == 0 { | ||||
| 		rsp.StatusCode = http.StatusOK | ||||
| 	} | ||||
|  | ||||
| 	for _, header := range rsp.GetHeader() { | ||||
| 		for _, val := range header.Values { | ||||
| 			w.Header().Add(header.Key, val) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(w.Header().Get("Content-Type")) == 0 { | ||||
| 		w.Header().Set("Content-Type", "application/json") | ||||
| 	} | ||||
|  | ||||
| 	w.WriteHeader(int(rsp.StatusCode)) | ||||
| 	w.Write([]byte(rsp.Body)) | ||||
| } | ||||
|  | ||||
| func (a *apiHandler) String() string { | ||||
| 	return "api" | ||||
| } | ||||
|  | ||||
| func NewHandler(opts ...handler.Option) handler.Handler { | ||||
| 	options := handler.NewOptions(opts...) | ||||
| 	return &apiHandler{ | ||||
| 		opts: options, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func WithService(s *goapi.Service, opts ...handler.Option) handler.Handler { | ||||
| 	options := handler.NewOptions(opts...) | ||||
| 	return &apiHandler{ | ||||
| 		opts: options, | ||||
| 		s:    s, | ||||
| 	} | ||||
| } | ||||
| @@ -1,109 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"mime" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/oxtoacart/bpool" | ||||
| 	api "github.com/unistack-org/micro/v3/api/proto" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// need to calculate later to specify useful defaults | ||||
| 	bufferPool = bpool.NewSizedBufferPool(1024, 8) | ||||
| ) | ||||
|  | ||||
| func requestToProto(r *http.Request) (*api.Request, error) { | ||||
| 	if err := r.ParseForm(); err != nil { | ||||
| 		return nil, fmt.Errorf("Error parsing form: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	req := &api.Request{ | ||||
| 		Path:   r.URL.Path, | ||||
| 		Method: r.Method, | ||||
| 		Header: make(map[string]*api.Pair), | ||||
| 		Get:    make(map[string]*api.Pair), | ||||
| 		Post:   make(map[string]*api.Pair), | ||||
| 		Url:    r.URL.String(), | ||||
| 	} | ||||
|  | ||||
| 	ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) | ||||
| 	if err != nil { | ||||
| 		ct = "text/plain; charset=UTF-8" //default CT is text/plain | ||||
| 		r.Header.Set("Content-Type", ct) | ||||
| 	} | ||||
|  | ||||
| 	//set the body: | ||||
| 	if r.Body != nil { | ||||
| 		switch ct { | ||||
| 		case "application/x-www-form-urlencoded": | ||||
| 			// expect form vals in Post data | ||||
| 		default: | ||||
| 			buf := bufferPool.Get() | ||||
| 			defer bufferPool.Put(buf) | ||||
| 			if _, err = buf.ReadFrom(r.Body); err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			req.Body = buf.String() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Set X-Forwarded-For if it does not exist | ||||
| 	if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { | ||||
| 		if prior, ok := r.Header["X-Forwarded-For"]; ok { | ||||
| 			ip = strings.Join(prior, ", ") + ", " + ip | ||||
| 		} | ||||
|  | ||||
| 		// Set the header | ||||
| 		req.Header["X-Forwarded-For"] = &api.Pair{ | ||||
| 			Key:    "X-Forwarded-For", | ||||
| 			Values: []string{ip}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Host is stripped from net/http Headers so let's add it | ||||
| 	req.Header["Host"] = &api.Pair{ | ||||
| 		Key:    "Host", | ||||
| 		Values: []string{r.Host}, | ||||
| 	} | ||||
|  | ||||
| 	// Get data | ||||
| 	for key, vals := range r.URL.Query() { | ||||
| 		header, ok := req.Get[key] | ||||
| 		if !ok { | ||||
| 			header = &api.Pair{ | ||||
| 				Key: key, | ||||
| 			} | ||||
| 			req.Get[key] = header | ||||
| 		} | ||||
| 		header.Values = vals | ||||
| 	} | ||||
|  | ||||
| 	// Post data | ||||
| 	for key, vals := range r.PostForm { | ||||
| 		header, ok := req.Post[key] | ||||
| 		if !ok { | ||||
| 			header = &api.Pair{ | ||||
| 				Key: key, | ||||
| 			} | ||||
| 			req.Post[key] = header | ||||
| 		} | ||||
| 		header.Values = vals | ||||
| 	} | ||||
|  | ||||
| 	for key, vals := range r.Header { | ||||
| 		header, ok := req.Header[key] | ||||
| 		if !ok { | ||||
| 			header = &api.Pair{ | ||||
| 				Key: key, | ||||
| 			} | ||||
| 			req.Header[key] = header | ||||
| 		} | ||||
| 		header.Values = vals | ||||
| 	} | ||||
|  | ||||
| 	return req, nil | ||||
| } | ||||
| @@ -1,44 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestRequestToProto(t *testing.T) { | ||||
| 	testData := []*http.Request{ | ||||
| 		{ | ||||
| 			Method: "GET", | ||||
| 			Header: http.Header{ | ||||
| 				"Header": []string{"test"}, | ||||
| 			}, | ||||
| 			URL: &url.URL{ | ||||
| 				Scheme:   "http", | ||||
| 				Host:     "localhost", | ||||
| 				Path:     "/foo/bar", | ||||
| 				RawQuery: "param1=value1", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, d := range testData { | ||||
| 		p, err := requestToProto(d) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		if p.Path != d.URL.Path { | ||||
| 			t.Fatalf("Expected path %s got %s", d.URL.Path, p.Path) | ||||
| 		} | ||||
| 		if p.Method != d.Method { | ||||
| 			t.Fatalf("Expected method %s got %s", d.Method, p.Method) | ||||
| 		} | ||||
| 		for k, v := range d.Header { | ||||
| 			if val, ok := p.Header[k]; !ok { | ||||
| 				t.Fatalf("Expected header %s", k) | ||||
| 			} else if val.Values[0] != v[0] { | ||||
| 				t.Fatalf("Expected val %s, got %s", val.Values[0], v[0]) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,141 +0,0 @@ | ||||
| // Package event provides a handler which publishes an event | ||||
| package event | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"path" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/oxtoacart/bpool" | ||||
| 	"github.com/unistack-org/micro/v3/api/handler" | ||||
| 	proto "github.com/unistack-org/micro/v3/api/proto" | ||||
| 	"github.com/unistack-org/micro/v3/util/ctx" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	bufferPool = bpool.NewSizedBufferPool(1024, 8) | ||||
| ) | ||||
|  | ||||
| type event struct { | ||||
| 	opts handler.Options | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	Handler   = "event" | ||||
| 	versionRe = regexp.MustCompilePOSIX("^v[0-9]+$") | ||||
| ) | ||||
|  | ||||
| func eventName(parts []string) string { | ||||
| 	return strings.Join(parts, ".") | ||||
| } | ||||
|  | ||||
| func evRoute(ns, p string) (string, string) { | ||||
| 	p = path.Clean(p) | ||||
| 	p = strings.TrimPrefix(p, "/") | ||||
|  | ||||
| 	if len(p) == 0 { | ||||
| 		return ns, "event" | ||||
| 	} | ||||
|  | ||||
| 	parts := strings.Split(p, "/") | ||||
|  | ||||
| 	// no path | ||||
| 	if len(parts) == 0 { | ||||
| 		// topic: namespace | ||||
| 		// action: event | ||||
| 		return strings.Trim(ns, "."), "event" | ||||
| 	} | ||||
|  | ||||
| 	// Treat /v[0-9]+ as versioning | ||||
| 	// /v1/foo/bar => topic: v1.foo action: bar | ||||
| 	if len(parts) >= 2 && versionRe.Match([]byte(parts[0])) { | ||||
| 		topic := ns + "." + strings.Join(parts[:2], ".") | ||||
| 		action := eventName(parts[1:]) | ||||
| 		return topic, action | ||||
| 	} | ||||
|  | ||||
| 	// /foo => topic: ns.foo action: foo | ||||
| 	// /foo/bar => topic: ns.foo action: bar | ||||
| 	topic := ns + "." + strings.Join(parts[:1], ".") | ||||
| 	action := eventName(parts[1:]) | ||||
|  | ||||
| 	return topic, action | ||||
| } | ||||
|  | ||||
| func (e *event) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	bsize := handler.DefaultMaxRecvSize | ||||
| 	if e.opts.MaxRecvSize > 0 { | ||||
| 		bsize = e.opts.MaxRecvSize | ||||
| 	} | ||||
|  | ||||
| 	r.Body = http.MaxBytesReader(w, r.Body, bsize) | ||||
|  | ||||
| 	// request to topic:event | ||||
| 	// create event | ||||
| 	// publish to topic | ||||
|  | ||||
| 	topic, action := evRoute(e.opts.Namespace, r.URL.Path) | ||||
|  | ||||
| 	// create event | ||||
| 	ev := &proto.Event{ | ||||
| 		Name: action, | ||||
| 		// TODO: dedupe event | ||||
| 		Id:        fmt.Sprintf("%s-%s-%s", topic, action, uuid.New().String()), | ||||
| 		Header:    make(map[string]*proto.Pair), | ||||
| 		Timestamp: time.Now().Unix(), | ||||
| 	} | ||||
|  | ||||
| 	// set headers | ||||
| 	for key, vals := range r.Header { | ||||
| 		header, ok := ev.Header[key] | ||||
| 		if !ok { | ||||
| 			header = &proto.Pair{ | ||||
| 				Key: key, | ||||
| 			} | ||||
| 			ev.Header[key] = header | ||||
| 		} | ||||
| 		header.Values = vals | ||||
| 	} | ||||
|  | ||||
| 	// set body | ||||
| 	if r.Method == "GET" { | ||||
| 		bytes, _ := json.Marshal(r.URL.Query()) | ||||
| 		ev.Data = string(bytes) | ||||
| 	} else { | ||||
| 		// Read body | ||||
| 		buf := bufferPool.Get() | ||||
| 		defer bufferPool.Put(buf) | ||||
| 		if _, err := buf.ReadFrom(r.Body); err != nil { | ||||
| 			http.Error(w, err.Error(), 500) | ||||
| 			return | ||||
| 		} | ||||
| 		ev.Data = buf.String() | ||||
| 	} | ||||
|  | ||||
| 	// get client | ||||
| 	c := e.opts.Client | ||||
|  | ||||
| 	// create publication | ||||
| 	p := c.NewMessage(topic, ev) | ||||
|  | ||||
| 	// publish event | ||||
| 	if err := c.Publish(ctx.FromRequest(r), p); err != nil { | ||||
| 		http.Error(w, err.Error(), 500) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *event) String() string { | ||||
| 	return "event" | ||||
| } | ||||
|  | ||||
| func NewHandler(opts ...handler.Option) handler.Handler { | ||||
| 	return &event{ | ||||
| 		opts: handler.NewOptions(opts...), | ||||
| 	} | ||||
| } | ||||
| @@ -1,105 +0,0 @@ | ||||
| // Package http is a http reverse proxy handler | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"net/http" | ||||
| 	"net/http/httputil" | ||||
| 	"net/url" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/api" | ||||
| 	"github.com/unistack-org/micro/v3/api/handler" | ||||
| 	"github.com/unistack-org/micro/v3/register" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	Handler = "http" | ||||
| ) | ||||
|  | ||||
| type httpHandler struct { | ||||
| 	options handler.Options | ||||
|  | ||||
| 	// set with different initializer | ||||
| 	s *api.Service | ||||
| } | ||||
|  | ||||
| func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	service, err := h.getService(r) | ||||
| 	if err != nil { | ||||
| 		w.WriteHeader(500) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(service) == 0 { | ||||
| 		w.WriteHeader(404) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	rp, err := url.Parse(service) | ||||
| 	if err != nil { | ||||
| 		w.WriteHeader(500) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r) | ||||
| } | ||||
|  | ||||
| // getService returns the service for this request from the selector | ||||
| func (h *httpHandler) getService(r *http.Request) (string, error) { | ||||
| 	var service *api.Service | ||||
|  | ||||
| 	if h.s != nil { | ||||
| 		// we were given the service | ||||
| 		service = h.s | ||||
| 	} else if h.options.Router != nil { | ||||
| 		// try get service from router | ||||
| 		s, err := h.options.Router.Route(r) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		service = s | ||||
| 	} else { | ||||
| 		// we have no way of routing the request | ||||
| 		return "", errors.New("no route found") | ||||
| 	} | ||||
|  | ||||
| 	if len(service.Services) == 0 { | ||||
| 		return "", errors.New("no route found") | ||||
| 	} | ||||
|  | ||||
| 	// get the nodes for this service | ||||
| 	nodes := make([]*register.Node, 0, len(service.Services)) | ||||
| 	for _, srv := range service.Services { | ||||
| 		nodes = append(nodes, srv.Nodes...) | ||||
| 	} | ||||
|  | ||||
| 	// select a random node | ||||
| 	node := nodes[rand.Int()%len(nodes)] | ||||
|  | ||||
| 	return fmt.Sprintf("http://%s", node.Address), nil | ||||
| } | ||||
|  | ||||
| func (h *httpHandler) String() string { | ||||
| 	return "http" | ||||
| } | ||||
|  | ||||
| // NewHandler returns a http proxy handler | ||||
| func NewHandler(opts ...handler.Option) handler.Handler { | ||||
| 	options := handler.NewOptions(opts...) | ||||
|  | ||||
| 	return &httpHandler{ | ||||
| 		options: options, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WithService creates a handler with a service | ||||
| func WithService(s *api.Service, opts ...handler.Option) handler.Handler { | ||||
| 	options := handler.NewOptions(opts...) | ||||
|  | ||||
| 	return &httpHandler{ | ||||
| 		options: options, | ||||
| 		s:       s, | ||||
| 	} | ||||
| } | ||||
| @@ -1,129 +0,0 @@ | ||||
| // +build ignore | ||||
|  | ||||
| package http | ||||
|  | ||||
| import ( | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/http/httptest" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/api/handler" | ||||
| 	"github.com/unistack-org/micro/v3/api/resolver" | ||||
| 	"github.com/unistack-org/micro/v3/api/resolver/vpath" | ||||
| 	"github.com/unistack-org/micro/v3/api/router" | ||||
| 	regRouter "github.com/unistack-org/micro/v3/api/router/register" | ||||
| 	"github.com/unistack-org/micro/v3/register" | ||||
| 	"github.com/unistack-org/micro/v3/register/memory" | ||||
| ) | ||||
|  | ||||
| func testHttp(t *testing.T, path, service, ns string) { | ||||
| 	r := memory.NewRegister() | ||||
|  | ||||
| 	l, err := net.Listen("tcp", "127.0.0.1:0") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer l.Close() | ||||
|  | ||||
| 	s := ®ister.Service{ | ||||
| 		Name: service, | ||||
| 		Nodes: []*register.Node{ | ||||
| 			{ | ||||
| 				Id:      service + "-1", | ||||
| 				Address: l.Addr().String(), | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	r.Register(s) | ||||
| 	defer r.Deregister(s) | ||||
|  | ||||
| 	// setup the test handler | ||||
| 	m := http.NewServeMux() | ||||
| 	m.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { | ||||
| 		w.Write([]byte(`you got served`)) | ||||
| 	}) | ||||
|  | ||||
| 	// start http test serve | ||||
| 	go http.Serve(l, m) | ||||
|  | ||||
| 	// create new request and writer | ||||
| 	w := httptest.NewRecorder() | ||||
| 	req, err := http.NewRequest("POST", path, nil) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// initialise the handler | ||||
| 	rt := regRouter.NewRouter( | ||||
| 		router.WithHandler("http"), | ||||
| 		router.WithRegister(r), | ||||
| 		router.WithResolver(vpath.NewResolver( | ||||
| 			resolver.WithServicePrefix(ns), | ||||
| 		)), | ||||
| 	) | ||||
|  | ||||
| 	p := NewHandler(handler.WithRouter(rt)) | ||||
|  | ||||
| 	// execute the handler | ||||
| 	p.ServeHTTP(w, req) | ||||
|  | ||||
| 	if w.Code != 200 { | ||||
| 		t.Fatalf("Expected 200 response got %d %s", w.Code, w.Body.String()) | ||||
| 	} | ||||
|  | ||||
| 	if w.Body.String() != "you got served" { | ||||
| 		t.Fatalf("Expected body: you got served. Got: %s", w.Body.String()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestHttpHandler(t *testing.T) { | ||||
| 	testData := []struct { | ||||
| 		path      string | ||||
| 		service   string | ||||
| 		namespace string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"/test/foo", | ||||
| 			"go.micro.api.test", | ||||
| 			"go.micro.api", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"/test/foo/baz", | ||||
| 			"go.micro.api.test", | ||||
| 			"go.micro.api", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"/v1/foo", | ||||
| 			"go.micro.api.v1.foo", | ||||
| 			"go.micro.api", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"/v1/foo/bar", | ||||
| 			"go.micro.api.v1.foo", | ||||
| 			"go.micro.api", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"/v2/baz", | ||||
| 			"go.micro.api.v2.baz", | ||||
| 			"go.micro.api", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"/v2/baz/bar", | ||||
| 			"go.micro.api.v2.baz", | ||||
| 			"go.micro.api", | ||||
| 		}, | ||||
| 		{ | ||||
| 			"/v2/baz/bar", | ||||
| 			"v2.baz", | ||||
| 			"", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, d := range testData { | ||||
| 		t.Run(d.service, func(t *testing.T) { | ||||
| 			testHttp(t, d.path, d.service, d.namespace) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -3,6 +3,7 @@ package handler | ||||
| import ( | ||||
| 	"github.com/unistack-org/micro/v3/api/router" | ||||
| 	"github.com/unistack-org/micro/v3/client" | ||||
| 	"github.com/unistack-org/micro/v3/logger" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| @@ -14,13 +15,19 @@ type Options struct { | ||||
| 	Namespace   string | ||||
| 	Router      router.Router | ||||
| 	Client      client.Client | ||||
| 	Logger      logger.Logger | ||||
| } | ||||
|  | ||||
| type Option func(o *Options) | ||||
|  | ||||
| // NewOptions fills in the blanks | ||||
| func NewOptions(opts ...Option) Options { | ||||
| 	var options Options | ||||
| 	options := Options{ | ||||
| 		Client:      client.DefaultClient, | ||||
| 		Router:      router.DefaultRouter, | ||||
| 		Logger:      logger.DefaultLogger, | ||||
| 		MaxRecvSize: DefaultMaxRecvSize, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
| @@ -30,10 +37,6 @@ func NewOptions(opts ...Option) Options { | ||||
| 		WithNamespace("go.micro.api")(&options) | ||||
| 	} | ||||
|  | ||||
| 	if options.MaxRecvSize == 0 { | ||||
| 		options.MaxRecvSize = DefaultMaxRecvSize | ||||
| 	} | ||||
|  | ||||
| 	return options | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,182 +0,0 @@ | ||||
| // Package web contains the web handler including websocket support | ||||
| package web | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"math/rand" | ||||
| 	"net" | ||||
| 	"net/http" | ||||
| 	"net/http/httputil" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/api" | ||||
| 	"github.com/unistack-org/micro/v3/api/handler" | ||||
| 	"github.com/unistack-org/micro/v3/register" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	Handler = "web" | ||||
| ) | ||||
|  | ||||
| type webHandler struct { | ||||
| 	opts handler.Options | ||||
| 	s    *api.Service | ||||
| } | ||||
|  | ||||
| func (wh *webHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	service, err := wh.getService(r) | ||||
| 	if err != nil { | ||||
| 		w.WriteHeader(500) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if len(service) == 0 { | ||||
| 		w.WriteHeader(404) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	rp, err := url.Parse(service) | ||||
| 	if err != nil { | ||||
| 		w.WriteHeader(500) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if isWebSocket(r) { | ||||
| 		wh.serveWebSocket(rp.Host, w, r) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	httputil.NewSingleHostReverseProxy(rp).ServeHTTP(w, r) | ||||
| } | ||||
|  | ||||
| // getService returns the service for this request from the selector | ||||
| func (wh *webHandler) getService(r *http.Request) (string, error) { | ||||
| 	var service *api.Service | ||||
|  | ||||
| 	if wh.s != nil { | ||||
| 		// we were given the service | ||||
| 		service = wh.s | ||||
| 	} else if wh.opts.Router != nil { | ||||
| 		// try get service from router | ||||
| 		s, err := wh.opts.Router.Route(r) | ||||
| 		if err != nil { | ||||
| 			return "", err | ||||
| 		} | ||||
| 		service = s | ||||
| 	} else { | ||||
| 		// we have no way of routing the request | ||||
| 		return "", errors.New("no route found") | ||||
| 	} | ||||
|  | ||||
| 	// get the nodes | ||||
| 	nodes := make([]*register.Node, 0, len(service.Services)) | ||||
| 	for _, srv := range service.Services { | ||||
| 		nodes = append(nodes, srv.Nodes...) | ||||
| 	} | ||||
|  | ||||
| 	if len(nodes) == 0 { | ||||
| 		return "", errors.New("no route found") | ||||
| 	} | ||||
|  | ||||
| 	// select a random node | ||||
| 	node := nodes[rand.Int()%len(nodes)] | ||||
|  | ||||
| 	return fmt.Sprintf("http://%s", node.Address), nil | ||||
| } | ||||
|  | ||||
| // serveWebSocket used to serve a web socket proxied connection | ||||
| func (wh *webHandler) serveWebSocket(host string, w http.ResponseWriter, r *http.Request) { | ||||
| 	req := new(http.Request) | ||||
| 	*req = *r | ||||
|  | ||||
| 	if len(host) == 0 { | ||||
| 		http.Error(w, "invalid host", 500) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// set x-forward-for | ||||
| 	if clientIP, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { | ||||
| 		if ips, ok := req.Header["X-Forwarded-For"]; ok { | ||||
| 			clientIP = strings.Join(ips, ", ") + ", " + clientIP | ||||
| 		} | ||||
| 		req.Header.Set("X-Forwarded-For", clientIP) | ||||
| 	} | ||||
|  | ||||
| 	// connect to the backend host | ||||
| 	conn, err := net.Dial("tcp", host) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, err.Error(), 500) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// hijack the connection | ||||
| 	hj, ok := w.(http.Hijacker) | ||||
| 	if !ok { | ||||
| 		http.Error(w, "failed to connect", 500) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	nc, _, err := hj.Hijack() | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	defer nc.Close() | ||||
| 	defer conn.Close() | ||||
|  | ||||
| 	if err = req.Write(conn); err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	errCh := make(chan error, 2) | ||||
|  | ||||
| 	cp := func(dst io.Writer, src io.Reader) { | ||||
| 		_, err := io.Copy(dst, src) | ||||
| 		errCh <- err | ||||
| 	} | ||||
|  | ||||
| 	go cp(conn, nc) | ||||
| 	go cp(nc, conn) | ||||
|  | ||||
| 	<-errCh | ||||
| } | ||||
|  | ||||
| func isWebSocket(r *http.Request) bool { | ||||
| 	contains := func(key, val string) bool { | ||||
| 		vv := strings.Split(r.Header.Get(key), ",") | ||||
| 		for _, v := range vv { | ||||
| 			if val == strings.ToLower(strings.TrimSpace(v)) { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	if contains("Connection", "upgrade") && contains("Upgrade", "websocket") { | ||||
| 		return true | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func (wh *webHandler) String() string { | ||||
| 	return "web" | ||||
| } | ||||
|  | ||||
| func NewHandler(opts ...handler.Option) handler.Handler { | ||||
| 	return &webHandler{ | ||||
| 		opts: handler.NewOptions(opts...), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func WithService(s *api.Service, opts ...handler.Option) handler.Handler { | ||||
| 	options := handler.NewOptions(opts...) | ||||
|  | ||||
| 	return &webHandler{ | ||||
| 		opts: options, | ||||
| 		s:    s, | ||||
| 	} | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| package proto | ||||
|  | ||||
| type Message struct { | ||||
| 	data []byte | ||||
| } | ||||
|  | ||||
| func (m *Message) ProtoMessage() {} | ||||
|  | ||||
| func (m *Message) Reset() { | ||||
| 	*m = Message{} | ||||
| } | ||||
|  | ||||
| func (m *Message) String() string { | ||||
| 	return string(m.data) | ||||
| } | ||||
|  | ||||
| func (m *Message) Marshal() ([]byte, error) { | ||||
| 	return m.data, nil | ||||
| } | ||||
|  | ||||
| func (m *Message) Unmarshal(data []byte) error { | ||||
| 	m.data = data | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func NewMessage(data []byte) *Message { | ||||
| 	return &Message{data} | ||||
| } | ||||
| @@ -1,511 +0,0 @@ | ||||
| // Code generated by protoc-gen-go. DO NOT EDIT. | ||||
| // versions: | ||||
| // 	protoc-gen-go v1.25.0 | ||||
| // 	protoc        v3.6.1 | ||||
| // source: api/proto/api.proto | ||||
|  | ||||
| package go_api | ||||
|  | ||||
| import ( | ||||
| 	proto "github.com/golang/protobuf/proto" | ||||
| 	protoreflect "google.golang.org/protobuf/reflect/protoreflect" | ||||
| 	protoimpl "google.golang.org/protobuf/runtime/protoimpl" | ||||
| 	reflect "reflect" | ||||
| 	sync "sync" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// Verify that this generated code is sufficiently up-to-date. | ||||
| 	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) | ||||
| 	// Verify that runtime/protoimpl is sufficiently up-to-date. | ||||
| 	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) | ||||
| ) | ||||
|  | ||||
| // This is a compile-time assertion that a sufficiently up-to-date version | ||||
| // of the legacy proto package is being used. | ||||
| const _ = proto.ProtoPackageIsVersion4 | ||||
|  | ||||
| type Pair struct { | ||||
| 	state         protoimpl.MessageState | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
|  | ||||
| 	Key    string   `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` | ||||
| 	Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` | ||||
| } | ||||
|  | ||||
| func (x *Pair) Reset() { | ||||
| 	*x = Pair{} | ||||
| 	if protoimpl.UnsafeEnabled { | ||||
| 		mi := &file_api_proto_api_proto_msgTypes[0] | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		ms.StoreMessageInfo(mi) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (x *Pair) String() string { | ||||
| 	return protoimpl.X.MessageStringOf(x) | ||||
| } | ||||
|  | ||||
| func (*Pair) ProtoMessage() {} | ||||
|  | ||||
| func (x *Pair) ProtoReflect() protoreflect.Message { | ||||
| 	mi := &file_api_proto_api_proto_msgTypes[0] | ||||
| 	if protoimpl.UnsafeEnabled && x != nil { | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		if ms.LoadMessageInfo() == nil { | ||||
| 			ms.StoreMessageInfo(mi) | ||||
| 		} | ||||
| 		return ms | ||||
| 	} | ||||
| 	return mi.MessageOf(x) | ||||
| } | ||||
|  | ||||
| // Deprecated: Use Pair.ProtoReflect.Descriptor instead. | ||||
| func (*Pair) Descriptor() ([]byte, []int) { | ||||
| 	return file_api_proto_api_proto_rawDescGZIP(), []int{0} | ||||
| } | ||||
|  | ||||
| func (x *Pair) GetKey() string { | ||||
| 	if x != nil { | ||||
| 		return x.Key | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (x *Pair) GetValues() []string { | ||||
| 	if x != nil { | ||||
| 		return x.Values | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // A HTTP request as RPC | ||||
| // Forward by the api handler | ||||
| type Request struct { | ||||
| 	state         protoimpl.MessageState | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
|  | ||||
| 	Method string           `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` | ||||
| 	Path   string           `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` | ||||
| 	Header map[string]*Pair `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` | ||||
| 	Get    map[string]*Pair `protobuf:"bytes,4,rep,name=get,proto3" json:"get,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` | ||||
| 	Post   map[string]*Pair `protobuf:"bytes,5,rep,name=post,proto3" json:"post,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` | ||||
| 	Body   string           `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"` // raw request body; if not application/x-www-form-urlencoded | ||||
| 	Url    string           `protobuf:"bytes,7,opt,name=url,proto3" json:"url,omitempty"` | ||||
| } | ||||
|  | ||||
| func (x *Request) Reset() { | ||||
| 	*x = Request{} | ||||
| 	if protoimpl.UnsafeEnabled { | ||||
| 		mi := &file_api_proto_api_proto_msgTypes[1] | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		ms.StoreMessageInfo(mi) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (x *Request) String() string { | ||||
| 	return protoimpl.X.MessageStringOf(x) | ||||
| } | ||||
|  | ||||
| func (*Request) ProtoMessage() {} | ||||
|  | ||||
| func (x *Request) ProtoReflect() protoreflect.Message { | ||||
| 	mi := &file_api_proto_api_proto_msgTypes[1] | ||||
| 	if protoimpl.UnsafeEnabled && x != nil { | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		if ms.LoadMessageInfo() == nil { | ||||
| 			ms.StoreMessageInfo(mi) | ||||
| 		} | ||||
| 		return ms | ||||
| 	} | ||||
| 	return mi.MessageOf(x) | ||||
| } | ||||
|  | ||||
| // Deprecated: Use Request.ProtoReflect.Descriptor instead. | ||||
| func (*Request) Descriptor() ([]byte, []int) { | ||||
| 	return file_api_proto_api_proto_rawDescGZIP(), []int{1} | ||||
| } | ||||
|  | ||||
| func (x *Request) GetMethod() string { | ||||
| 	if x != nil { | ||||
| 		return x.Method | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (x *Request) GetPath() string { | ||||
| 	if x != nil { | ||||
| 		return x.Path | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (x *Request) GetHeader() map[string]*Pair { | ||||
| 	if x != nil { | ||||
| 		return x.Header | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (x *Request) GetGet() map[string]*Pair { | ||||
| 	if x != nil { | ||||
| 		return x.Get | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (x *Request) GetPost() map[string]*Pair { | ||||
| 	if x != nil { | ||||
| 		return x.Post | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (x *Request) GetBody() string { | ||||
| 	if x != nil { | ||||
| 		return x.Body | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (x *Request) GetUrl() string { | ||||
| 	if x != nil { | ||||
| 		return x.Url | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| // A HTTP response as RPC | ||||
| // Expected response for the api handler | ||||
| type Response struct { | ||||
| 	state         protoimpl.MessageState | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
|  | ||||
| 	StatusCode int32            `protobuf:"varint,1,opt,name=statusCode,proto3" json:"statusCode,omitempty"` | ||||
| 	Header     map[string]*Pair `protobuf:"bytes,2,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` | ||||
| 	Body       string           `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` | ||||
| } | ||||
|  | ||||
| func (x *Response) Reset() { | ||||
| 	*x = Response{} | ||||
| 	if protoimpl.UnsafeEnabled { | ||||
| 		mi := &file_api_proto_api_proto_msgTypes[2] | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		ms.StoreMessageInfo(mi) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (x *Response) String() string { | ||||
| 	return protoimpl.X.MessageStringOf(x) | ||||
| } | ||||
|  | ||||
| func (*Response) ProtoMessage() {} | ||||
|  | ||||
| func (x *Response) ProtoReflect() protoreflect.Message { | ||||
| 	mi := &file_api_proto_api_proto_msgTypes[2] | ||||
| 	if protoimpl.UnsafeEnabled && x != nil { | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		if ms.LoadMessageInfo() == nil { | ||||
| 			ms.StoreMessageInfo(mi) | ||||
| 		} | ||||
| 		return ms | ||||
| 	} | ||||
| 	return mi.MessageOf(x) | ||||
| } | ||||
|  | ||||
| // Deprecated: Use Response.ProtoReflect.Descriptor instead. | ||||
| func (*Response) Descriptor() ([]byte, []int) { | ||||
| 	return file_api_proto_api_proto_rawDescGZIP(), []int{2} | ||||
| } | ||||
|  | ||||
| func (x *Response) GetStatusCode() int32 { | ||||
| 	if x != nil { | ||||
| 		return x.StatusCode | ||||
| 	} | ||||
| 	return 0 | ||||
| } | ||||
|  | ||||
| func (x *Response) GetHeader() map[string]*Pair { | ||||
| 	if x != nil { | ||||
| 		return x.Header | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (x *Response) GetBody() string { | ||||
| 	if x != nil { | ||||
| 		return x.Body | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| // A HTTP event as RPC | ||||
| // Forwarded by the event handler | ||||
| type Event struct { | ||||
| 	state         protoimpl.MessageState | ||||
| 	sizeCache     protoimpl.SizeCache | ||||
| 	unknownFields protoimpl.UnknownFields | ||||
|  | ||||
| 	// e.g login | ||||
| 	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` | ||||
| 	// uuid | ||||
| 	Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` | ||||
| 	// unix timestamp of event | ||||
| 	Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` | ||||
| 	// event headers | ||||
| 	Header map[string]*Pair `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` | ||||
| 	// the event data | ||||
| 	Data string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` | ||||
| } | ||||
|  | ||||
| func (x *Event) Reset() { | ||||
| 	*x = Event{} | ||||
| 	if protoimpl.UnsafeEnabled { | ||||
| 		mi := &file_api_proto_api_proto_msgTypes[3] | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		ms.StoreMessageInfo(mi) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (x *Event) String() string { | ||||
| 	return protoimpl.X.MessageStringOf(x) | ||||
| } | ||||
|  | ||||
| func (*Event) ProtoMessage() {} | ||||
|  | ||||
| func (x *Event) ProtoReflect() protoreflect.Message { | ||||
| 	mi := &file_api_proto_api_proto_msgTypes[3] | ||||
| 	if protoimpl.UnsafeEnabled && x != nil { | ||||
| 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||||
| 		if ms.LoadMessageInfo() == nil { | ||||
| 			ms.StoreMessageInfo(mi) | ||||
| 		} | ||||
| 		return ms | ||||
| 	} | ||||
| 	return mi.MessageOf(x) | ||||
| } | ||||
|  | ||||
| // Deprecated: Use Event.ProtoReflect.Descriptor instead. | ||||
| func (*Event) Descriptor() ([]byte, []int) { | ||||
| 	return file_api_proto_api_proto_rawDescGZIP(), []int{3} | ||||
| } | ||||
|  | ||||
| func (x *Event) GetName() string { | ||||
| 	if x != nil { | ||||
| 		return x.Name | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (x *Event) GetId() string { | ||||
| 	if x != nil { | ||||
| 		return x.Id | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func (x *Event) GetTimestamp() int64 { | ||||
| 	if x != nil { | ||||
| 		return x.Timestamp | ||||
| 	} | ||||
| 	return 0 | ||||
| } | ||||
|  | ||||
| func (x *Event) GetHeader() map[string]*Pair { | ||||
| 	if x != nil { | ||||
| 		return x.Header | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (x *Event) GetData() string { | ||||
| 	if x != nil { | ||||
| 		return x.Data | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| var File_api_proto_api_proto protoreflect.FileDescriptor | ||||
|  | ||||
| var file_api_proto_api_proto_rawDesc = []byte{ | ||||
| 	0x0a, 0x13, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2e, | ||||
| 	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x22, 0x30, 0x0a, | ||||
| 	0x04, 0x50, 0x61, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, | ||||
| 	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, | ||||
| 	0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, | ||||
| 	0xc1, 0x03, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6d, | ||||
| 	0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, | ||||
| 	0x68, 0x6f, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, | ||||
| 	0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x33, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, | ||||
| 	0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, | ||||
| 	0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, | ||||
| 	0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x03, | ||||
| 	0x67, 0x65, 0x74, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x67, 0x6f, 0x2e, 0x61, | ||||
| 	0x70, 0x69, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x45, 0x6e, | ||||
| 	0x74, 0x72, 0x79, 0x52, 0x03, 0x67, 0x65, 0x74, 0x12, 0x2d, 0x0a, 0x04, 0x70, 0x6f, 0x73, 0x74, | ||||
| 	0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x2e, | ||||
| 	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x50, 0x6f, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, | ||||
| 	0x79, 0x52, 0x04, 0x70, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, | ||||
| 	0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, | ||||
| 	0x72, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x1a, 0x47, 0x0a, | ||||
| 	0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, | ||||
| 	0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x22, | ||||
| 	0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, | ||||
| 	0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x69, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, | ||||
| 	0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x44, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x45, 0x6e, 0x74, | ||||
| 	0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, | ||||
| 	0x03, 0x6b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, | ||||
| 	0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x69, | ||||
| 	0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x45, 0x0a, 0x09, | ||||
| 	0x50, 0x6f, 0x73, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, | ||||
| 	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x05, 0x76, | ||||
| 	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x67, 0x6f, 0x2e, | ||||
| 	0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x69, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, | ||||
| 	0x02, 0x38, 0x01, 0x22, 0xbd, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, | ||||
| 	0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, | ||||
| 	0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, | ||||
| 	0x12, 0x34, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, | ||||
| 	0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, | ||||
| 	0x73, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, | ||||
| 	0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x03, | ||||
| 	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x47, 0x0a, 0x0b, 0x48, 0x65, | ||||
| 	0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, | ||||
| 	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x05, 0x76, | ||||
| 	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x67, 0x6f, 0x2e, | ||||
| 	0x61, 0x70, 0x69, 0x2e, 0x50, 0x61, 0x69, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, | ||||
| 	0x02, 0x38, 0x01, 0x22, 0xd9, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, | ||||
| 	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, | ||||
| 	0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, | ||||
| 	0x64, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, | ||||
| 	0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, | ||||
| 	0x31, 0x0a, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, | ||||
| 	0x19, 0x2e, 0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x48, | ||||
| 	0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, | ||||
| 	0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, | ||||
| 	0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x47, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, | ||||
| 	0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, | ||||
| 	0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x22, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, | ||||
| 	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x67, 0x6f, 0x2e, 0x61, 0x70, 0x69, 0x2e, | ||||
| 	0x50, 0x61, 0x69, 0x72, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x62, | ||||
| 	0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	file_api_proto_api_proto_rawDescOnce sync.Once | ||||
| 	file_api_proto_api_proto_rawDescData = file_api_proto_api_proto_rawDesc | ||||
| ) | ||||
|  | ||||
| func file_api_proto_api_proto_rawDescGZIP() []byte { | ||||
| 	file_api_proto_api_proto_rawDescOnce.Do(func() { | ||||
| 		file_api_proto_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_api_proto_rawDescData) | ||||
| 	}) | ||||
| 	return file_api_proto_api_proto_rawDescData | ||||
| } | ||||
|  | ||||
| var file_api_proto_api_proto_msgTypes = make([]protoimpl.MessageInfo, 9) | ||||
| var file_api_proto_api_proto_goTypes = []interface{}{ | ||||
| 	(*Pair)(nil),     // 0: go.api.Pair | ||||
| 	(*Request)(nil),  // 1: go.api.Request | ||||
| 	(*Response)(nil), // 2: go.api.Response | ||||
| 	(*Event)(nil),    // 3: go.api.Event | ||||
| 	nil,              // 4: go.api.Request.HeaderEntry | ||||
| 	nil,              // 5: go.api.Request.GetEntry | ||||
| 	nil,              // 6: go.api.Request.PostEntry | ||||
| 	nil,              // 7: go.api.Response.HeaderEntry | ||||
| 	nil,              // 8: go.api.Event.HeaderEntry | ||||
| } | ||||
| var file_api_proto_api_proto_depIdxs = []int32{ | ||||
| 	4,  // 0: go.api.Request.header:type_name -> go.api.Request.HeaderEntry | ||||
| 	5,  // 1: go.api.Request.get:type_name -> go.api.Request.GetEntry | ||||
| 	6,  // 2: go.api.Request.post:type_name -> go.api.Request.PostEntry | ||||
| 	7,  // 3: go.api.Response.header:type_name -> go.api.Response.HeaderEntry | ||||
| 	8,  // 4: go.api.Event.header:type_name -> go.api.Event.HeaderEntry | ||||
| 	0,  // 5: go.api.Request.HeaderEntry.value:type_name -> go.api.Pair | ||||
| 	0,  // 6: go.api.Request.GetEntry.value:type_name -> go.api.Pair | ||||
| 	0,  // 7: go.api.Request.PostEntry.value:type_name -> go.api.Pair | ||||
| 	0,  // 8: go.api.Response.HeaderEntry.value:type_name -> go.api.Pair | ||||
| 	0,  // 9: go.api.Event.HeaderEntry.value:type_name -> go.api.Pair | ||||
| 	10, // [10:10] is the sub-list for method output_type | ||||
| 	10, // [10:10] is the sub-list for method input_type | ||||
| 	10, // [10:10] is the sub-list for extension type_name | ||||
| 	10, // [10:10] is the sub-list for extension extendee | ||||
| 	0,  // [0:10] is the sub-list for field type_name | ||||
| } | ||||
|  | ||||
| func init() { file_api_proto_api_proto_init() } | ||||
| func file_api_proto_api_proto_init() { | ||||
| 	if File_api_proto_api_proto != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	if !protoimpl.UnsafeEnabled { | ||||
| 		file_api_proto_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { | ||||
| 			switch v := v.(*Pair); i { | ||||
| 			case 0: | ||||
| 				return &v.state | ||||
| 			case 1: | ||||
| 				return &v.sizeCache | ||||
| 			case 2: | ||||
| 				return &v.unknownFields | ||||
| 			default: | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 		file_api_proto_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { | ||||
| 			switch v := v.(*Request); i { | ||||
| 			case 0: | ||||
| 				return &v.state | ||||
| 			case 1: | ||||
| 				return &v.sizeCache | ||||
| 			case 2: | ||||
| 				return &v.unknownFields | ||||
| 			default: | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 		file_api_proto_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { | ||||
| 			switch v := v.(*Response); i { | ||||
| 			case 0: | ||||
| 				return &v.state | ||||
| 			case 1: | ||||
| 				return &v.sizeCache | ||||
| 			case 2: | ||||
| 				return &v.unknownFields | ||||
| 			default: | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 		file_api_proto_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { | ||||
| 			switch v := v.(*Event); i { | ||||
| 			case 0: | ||||
| 				return &v.state | ||||
| 			case 1: | ||||
| 				return &v.sizeCache | ||||
| 			case 2: | ||||
| 				return &v.unknownFields | ||||
| 			default: | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	type x struct{} | ||||
| 	out := protoimpl.TypeBuilder{ | ||||
| 		File: protoimpl.DescBuilder{ | ||||
| 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | ||||
| 			RawDescriptor: file_api_proto_api_proto_rawDesc, | ||||
| 			NumEnums:      0, | ||||
| 			NumMessages:   9, | ||||
| 			NumExtensions: 0, | ||||
| 			NumServices:   0, | ||||
| 		}, | ||||
| 		GoTypes:           file_api_proto_api_proto_goTypes, | ||||
| 		DependencyIndexes: file_api_proto_api_proto_depIdxs, | ||||
| 		MessageInfos:      file_api_proto_api_proto_msgTypes, | ||||
| 	}.Build() | ||||
| 	File_api_proto_api_proto = out.File | ||||
| 	file_api_proto_api_proto_rawDesc = nil | ||||
| 	file_api_proto_api_proto_goTypes = nil | ||||
| 	file_api_proto_api_proto_depIdxs = nil | ||||
| } | ||||
| @@ -1,21 +0,0 @@ | ||||
| // Code generated by protoc-gen-micro. DO NOT EDIT. | ||||
| // source: api/proto/api.proto | ||||
|  | ||||
| package go_api | ||||
|  | ||||
| import ( | ||||
| 	fmt "fmt" | ||||
| 	proto "github.com/golang/protobuf/proto" | ||||
| 	math "math" | ||||
| ) | ||||
|  | ||||
| // Reference imports to suppress errors if they are not otherwise used. | ||||
| var _ = proto.Marshal | ||||
| var _ = fmt.Errorf | ||||
| var _ = math.Inf | ||||
|  | ||||
| // This is a compile-time assertion to ensure that this generated file | ||||
| // is compatible with the proto package it is being compiled against. | ||||
| // A compilation error at this line likely means your copy of the | ||||
| // proto package needs to be updated. | ||||
| const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package | ||||
| @@ -1,43 +0,0 @@ | ||||
| syntax = "proto3"; | ||||
|  | ||||
| package go.api; | ||||
|  | ||||
| message Pair { | ||||
| 	string key = 1; | ||||
| 	repeated string values = 2; | ||||
| } | ||||
|  | ||||
| // A HTTP request as RPC | ||||
| // Forward by the api handler | ||||
| message Request { | ||||
|         string method = 1; | ||||
|         string path = 2; | ||||
|         map<string, Pair> header = 3; | ||||
|         map<string, Pair> get = 4; | ||||
|         map<string, Pair> post = 5; | ||||
|         string body = 6;  // raw request body; if not application/x-www-form-urlencoded | ||||
| 	string url = 7; | ||||
| } | ||||
|  | ||||
| // A HTTP response as RPC | ||||
| // Expected response for the api handler | ||||
| message Response { | ||||
|         int32 statusCode = 1; | ||||
|         map<string, Pair> header = 2; | ||||
|         string body = 3; | ||||
| } | ||||
|  | ||||
| // A HTTP event as RPC | ||||
| // Forwarded by the event handler | ||||
| message Event { | ||||
| 	// e.g login | ||||
| 	string name = 1; | ||||
| 	// uuid | ||||
| 	string id = 2; | ||||
| 	// unix timestamp of event | ||||
| 	int64 timestamp = 3; | ||||
| 	// event headers | ||||
|         map<string, Pair> header = 4; | ||||
| 	// the event data | ||||
| 	string data = 5; | ||||
| } | ||||
| @@ -6,8 +6,6 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/api/resolver/vpath" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestResolve(t *testing.T) { | ||||
| @@ -62,9 +60,13 @@ func TestResolve(t *testing.T) { | ||||
| 		t.Run(tc.Name, func(t *testing.T) { | ||||
| 			r := NewResolver(vpath.NewResolver()) | ||||
| 			result, err := r.Resolve(&http.Request{URL: &url.URL{Host: tc.Host, Path: "foo/bar"}}) | ||||
| 			assert.Nil(t, err, "Expecter err to be nil") | ||||
| 			if err != nil { | ||||
| 				t.Fatal(err) | ||||
| 			} | ||||
| 			if result != nil { | ||||
| 				assert.Equal(t, tc.Result, result.Domain, "Expected %v but got %v", tc.Result, result.Domain) | ||||
| 				if tc.Result != result.Domain { | ||||
| 					t.Fatalf("Expected %v but got %v", tc.Result, result.Domain) | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
|   | ||||
| @@ -7,6 +7,10 @@ import ( | ||||
| 	"github.com/unistack-org/micro/v3/api" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	DefaultRouter Router | ||||
| ) | ||||
|  | ||||
| // Router is used to determine an endpoint for a request | ||||
| type Router interface { | ||||
| 	// Returns options | ||||
| @@ -23,6 +27,6 @@ type Router interface { | ||||
| 	Deregister(ep *api.Endpoint) error | ||||
| 	// Route returns an api.Service route | ||||
| 	Route(r *http.Request) (*api.Service, error) | ||||
| 	// String represenation of router | ||||
| 	// String representation of router | ||||
| 	String() string | ||||
| } | ||||
|   | ||||
| @@ -235,6 +235,7 @@ func (m *memorySubscriber) Unsubscribe(ctx context.Context) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // NewBroker return new memory broker | ||||
| func NewBroker(opts ...Option) Broker { | ||||
| 	rand.Seed(time.Now().UnixNano()) | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,8 @@ import ( | ||||
|  | ||||
| var ( | ||||
| 	// DefaultClient is the global default client | ||||
| 	DefaultClient Client = NewClient() | ||||
| 	DefaultClient      Client = NewClient() | ||||
| 	DefaultContentType        = "application/json" | ||||
| ) | ||||
|  | ||||
| // Client is the interface used to make requests to services. | ||||
|   | ||||
| @@ -20,10 +20,6 @@ var ( | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	defaultContentType = "application/json" | ||||
| ) | ||||
|  | ||||
| type noopClient struct { | ||||
| 	opts Options | ||||
| } | ||||
|   | ||||
| @@ -151,7 +151,7 @@ type RequestOptions struct { | ||||
| func NewOptions(opts ...Option) Options { | ||||
| 	options := Options{ | ||||
| 		Context:     context.Background(), | ||||
| 		ContentType: "application/json", | ||||
| 		ContentType: DefaultContentType, | ||||
| 		Codecs:      make(map[string]codec.Codec), | ||||
| 		CallOptions: CallOptions{ | ||||
| 			Backoff:        DefaultBackoff, | ||||
|   | ||||
| @@ -8,8 +8,8 @@ import ( | ||||
| 	"github.com/unistack-org/micro/v3/metadata" | ||||
| ) | ||||
|  | ||||
| // Message types | ||||
| const ( | ||||
| 	// Message types | ||||
| 	Error MessageType = iota | ||||
| 	Request | ||||
| 	Response | ||||
| @@ -25,8 +25,9 @@ var ( | ||||
|  | ||||
| var ( | ||||
| 	// DefaultMaxMsgSize specifies how much data codec can handle | ||||
| 	DefaultMaxMsgSize int   = 1024 * 1024 * 4 // 4Mb | ||||
| 	DefaultCodec      Codec = NewCodec() | ||||
| 	DefaultMaxMsgSize int = 1024 * 1024 * 4 // 4Mb | ||||
| 	// DefaultCodec is the global default codec | ||||
| 	DefaultCodec Codec = NewCodec() | ||||
| ) | ||||
|  | ||||
| // MessageType | ||||
|   | ||||
| @@ -30,12 +30,8 @@ func (c *noopCodec) ReadBody(conn io.Reader, b interface{}) error { | ||||
| 	} | ||||
|  | ||||
| 	switch v := b.(type) { | ||||
| 	case string: | ||||
| 		v = string(buf) | ||||
| 	case *string: | ||||
| 		*v = string(buf) | ||||
| 	case []byte: | ||||
| 		v = buf | ||||
| 	case *[]byte: | ||||
| 		*v = buf | ||||
| 	case *Frame: | ||||
| @@ -112,15 +108,9 @@ func (c *noopCodec) Unmarshal(d []byte, v interface{}) error { | ||||
| 		return nil | ||||
| 	} | ||||
| 	switch ve := v.(type) { | ||||
| 	case string: | ||||
| 		ve = string(d) | ||||
| 		return nil | ||||
| 	case *string: | ||||
| 		*ve = string(d) | ||||
| 		return nil | ||||
| 	case []byte: | ||||
| 		ve = d | ||||
| 		return nil | ||||
| 	case *[]byte: | ||||
| 		*ve = d | ||||
| 		return nil | ||||
|   | ||||
							
								
								
									
										34
									
								
								codec/noop_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								codec/noop_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| package codec | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestNoopBytes(t *testing.T) { | ||||
| 	req := []byte("test req") | ||||
| 	rsp := make([]byte, len(req)) | ||||
|  | ||||
| 	nc := NewCodec() | ||||
| 	if err := nc.Unmarshal(req, &rsp); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if !bytes.Equal(req, rsp) { | ||||
| 		t.Fatalf("req not eq rsp: %s != %s", req, rsp) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNoopString(t *testing.T) { | ||||
| 	req := []byte("test req") | ||||
| 	var rsp string | ||||
|  | ||||
| 	nc := NewCodec() | ||||
| 	if err := nc.Unmarshal(req, &rsp); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if !bytes.Equal(req, []byte(rsp)) { | ||||
| 		t.Fatalf("req not eq rsp: %s != %s", req, rsp) | ||||
| 	} | ||||
| } | ||||
| @@ -45,6 +45,7 @@ func Meter(m meter.Meter) Option { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewOptions returns new options | ||||
| func NewOptions(opts ...Option) Options { | ||||
| 	options := Options{ | ||||
| 		Logger:     logger.DefaultLogger, | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
|  | ||||
| type configKey struct{} | ||||
|  | ||||
| // FromContext returns store from context | ||||
| func FromContext(ctx context.Context) (Config, bool) { | ||||
| 	if ctx == nil { | ||||
| 		return nil, false | ||||
| @@ -14,6 +15,7 @@ func FromContext(ctx context.Context) (Config, bool) { | ||||
| 	return c, ok | ||||
| } | ||||
|  | ||||
| // NewContext put store in context | ||||
| func NewContext(ctx context.Context, c Config) context.Context { | ||||
| 	if ctx == nil { | ||||
| 		ctx = context.Background() | ||||
|   | ||||
| @@ -1,190 +0,0 @@ | ||||
| // Package kubernetes is a logger implementing (github.com/unistack-org/micro/v3/debug/log).Log | ||||
| package kubernetes | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/debug/log" | ||||
| 	"github.com/unistack-org/micro/v3/metadata" | ||||
| 	"github.com/unistack-org/micro/v3/util/kubernetes/client" | ||||
| ) | ||||
|  | ||||
| type klog struct { | ||||
| 	client client.Client | ||||
|  | ||||
| 	log.Options | ||||
| } | ||||
|  | ||||
| func (k *klog) podLogStream(podName string, stream *kubeStream) { | ||||
| 	p := make(map[string]string) | ||||
| 	p["follow"] = "true" | ||||
|  | ||||
| 	// get the logs for the pod | ||||
| 	body, err := k.client.Log(&client.Resource{ | ||||
| 		Name: podName, | ||||
| 		Kind: "pod", | ||||
| 	}, client.LogParams(p)) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(os.Stderr, "%v", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	s := bufio.NewScanner(body) | ||||
| 	defer body.Close() | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-stream.stop: | ||||
| 			return | ||||
| 		default: | ||||
| 			if s.Scan() { | ||||
| 				record := k.parse(s.Text()) | ||||
| 				stream.stream <- record | ||||
| 			} else { | ||||
| 				// TODO: is there a blocking call | ||||
| 				// rather than a sleep loop? | ||||
| 				time.Sleep(time.Second) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (k *klog) getMatchingPods() ([]string, error) { | ||||
| 	r := &client.Resource{ | ||||
| 		Kind:  "pod", | ||||
| 		Value: new(client.PodList), | ||||
| 	} | ||||
|  | ||||
| 	l := make(map[string]string) | ||||
|  | ||||
| 	l["name"] = client.Format(k.Options.Name) | ||||
| 	// TODO: specify micro:service | ||||
| 	// l["micro"] = "service" | ||||
|  | ||||
| 	if err := k.client.Get(r, client.GetLabels(l)); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var matches []string | ||||
|  | ||||
| 	for _, p := range r.Value.(*client.PodList).Items { | ||||
| 		// find labels that match the name | ||||
| 		if p.Metadata.Labels["name"] == client.Format(k.Options.Name) { | ||||
| 			matches = append(matches, p.Metadata.Name) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return matches, nil | ||||
| } | ||||
|  | ||||
| func (k *klog) parse(line string) log.Record { | ||||
| 	record := log.Record{} | ||||
|  | ||||
| 	if err := json.Unmarshal([]byte(line), &record); err != nil { | ||||
| 		record.Timestamp = time.Now().UTC() | ||||
| 		record.Message = line | ||||
| 		record.Metadata = metadata.New(1) | ||||
| 	} | ||||
|  | ||||
| 	record.Metadata["service"] = k.Options.Name | ||||
|  | ||||
| 	return record | ||||
| } | ||||
|  | ||||
| func (k *klog) Read(options ...log.ReadOption) ([]log.Record, error) { | ||||
| 	opts := &log.ReadOptions{} | ||||
| 	for _, o := range options { | ||||
| 		o(opts) | ||||
| 	} | ||||
| 	pods, err := k.getMatchingPods() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var records []log.Record | ||||
|  | ||||
| 	for _, pod := range pods { | ||||
| 		logParams := make(map[string]string) | ||||
|  | ||||
| 		if !opts.Since.Equal(time.Time{}) { | ||||
| 			logParams["sinceSeconds"] = strconv.Itoa(int(time.Since(opts.Since).Seconds())) | ||||
| 		} | ||||
|  | ||||
| 		if opts.Count != 0 { | ||||
| 			logParams["tailLines"] = strconv.Itoa(opts.Count) | ||||
| 		} | ||||
|  | ||||
| 		if opts.Stream { | ||||
| 			logParams["follow"] = "true" | ||||
| 		} | ||||
|  | ||||
| 		logs, err := k.client.Log(&client.Resource{ | ||||
| 			Name: pod, | ||||
| 			Kind: "pod", | ||||
| 		}, client.LogParams(logParams)) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		defer logs.Close() | ||||
|  | ||||
| 		s := bufio.NewScanner(logs) | ||||
|  | ||||
| 		for s.Scan() { | ||||
| 			record := k.parse(s.Text()) | ||||
| 			record.Metadata["pod"] = pod | ||||
| 			records = append(records, record) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// sort the records | ||||
| 	sort.Slice(records, func(i, j int) bool { return records[i].Timestamp.Before(records[j].Timestamp) }) | ||||
|  | ||||
| 	return records, nil | ||||
| } | ||||
|  | ||||
| func (k *klog) Write(l log.Record) error { | ||||
| 	return write(l) | ||||
| } | ||||
|  | ||||
| func (k *klog) Stream() (log.Stream, error) { | ||||
| 	// find the matching pods | ||||
| 	pods, err := k.getMatchingPods() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	stream := &kubeStream{ | ||||
| 		stream: make(chan log.Record), | ||||
| 		stop:   make(chan bool), | ||||
| 	} | ||||
|  | ||||
| 	// stream from the individual pods | ||||
| 	for _, pod := range pods { | ||||
| 		go k.podLogStream(pod, stream) | ||||
| 	} | ||||
|  | ||||
| 	return stream, nil | ||||
| } | ||||
|  | ||||
| // NewLog returns a configured Kubernetes logger | ||||
| func NewLog(opts ...log.Option) log.Log { | ||||
| 	klog := &klog{} | ||||
| 	for _, o := range opts { | ||||
| 		o(&klog.Options) | ||||
| 	} | ||||
|  | ||||
| 	if len(os.Getenv("KUBERNETES_SERVICE_HOST")) > 0 { | ||||
| 		klog.client = client.NewClusterClient() | ||||
| 	} else { | ||||
| 		klog.client = client.NewLocalClient() | ||||
| 	} | ||||
| 	return klog | ||||
| } | ||||
| @@ -1,71 +0,0 @@ | ||||
| package kubernetes | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/unistack-org/micro/v3/debug/log" | ||||
| ) | ||||
|  | ||||
| func TestKubernetes(t *testing.T) { | ||||
| 	if len(os.Getenv("INTEGRATION_TESTS")) > 0 { | ||||
| 		t.Skip() | ||||
| 	} | ||||
|  | ||||
| 	k := NewLog(log.Name("micro-network")) | ||||
|  | ||||
| 	r, w, err := os.Pipe() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	s := os.Stderr | ||||
| 	os.Stderr = w | ||||
| 	meta := make(map[string]string) | ||||
|  | ||||
| 	write := log.Record{ | ||||
| 		Timestamp: time.Unix(0, 0).UTC(), | ||||
| 		Message:   "Test log entry", | ||||
| 		Metadata:  meta, | ||||
| 	} | ||||
|  | ||||
| 	meta["foo"] = "bar" | ||||
|  | ||||
| 	k.Write(write) | ||||
| 	b := &bytes.Buffer{} | ||||
| 	w.Close() | ||||
| 	io.Copy(b, r) | ||||
| 	os.Stderr = s | ||||
|  | ||||
| 	var read log.Record | ||||
|  | ||||
| 	if err := json.Unmarshal(b.Bytes(), &read); err != nil { | ||||
| 		t.Fatalf("json.Unmarshal failed: %s", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, write, read, "Write was not equal") | ||||
|  | ||||
| 	records, err := k.Read() | ||||
| 	assert.Nil(t, err, "Read should not error") | ||||
| 	assert.NotNil(t, records, "Read should return records") | ||||
|  | ||||
| 	stream, err := k.Stream() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	records = nil | ||||
|  | ||||
| 	go stream.Stop() | ||||
|  | ||||
| 	for s := range stream.Chan() { | ||||
| 		records = append(records, s) | ||||
| 	} | ||||
|  | ||||
| 	assert.Equal(t, 0, len(records), "Stream should return nothing") | ||||
| } | ||||
| @@ -1,44 +0,0 @@ | ||||
| package kubernetes | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"sync" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/debug/log" | ||||
| ) | ||||
|  | ||||
| func write(l log.Record) error { | ||||
| 	m, err := json.Marshal(l) | ||||
| 	if err == nil { | ||||
| 		_, err := fmt.Fprintf(os.Stderr, "%s", m) | ||||
| 		return err | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| type kubeStream struct { | ||||
| 	// the k8s log stream | ||||
| 	stream chan log.Record | ||||
| 	sync.Mutex | ||||
| 	// the stop chan | ||||
| 	stop chan bool | ||||
| } | ||||
|  | ||||
| func (k *kubeStream) Chan() <-chan log.Record { | ||||
| 	return k.stream | ||||
| } | ||||
|  | ||||
| func (k *kubeStream) Stop() error { | ||||
| 	k.Lock() | ||||
| 	defer k.Unlock() | ||||
| 	select { | ||||
| 	case <-k.stop: | ||||
| 		return nil | ||||
| 	default: | ||||
| 		close(k.stop) | ||||
| 		close(k.stream) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @@ -64,7 +64,7 @@ func newFunction(opts ...Option) Function { | ||||
| 	// make context the last thing | ||||
| 	fopts = append(fopts, Context(ctx)) | ||||
|  | ||||
| 	service := &service{opts: NewOptions(opts...)} | ||||
| 	service := &service{opts: NewOptions(fopts...)} | ||||
|  | ||||
| 	fn := &function{ | ||||
| 		cancel:  cancel, | ||||
|   | ||||
							
								
								
									
										6
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								go.mod
									
									
									
									
									
								
							| @@ -6,18 +6,12 @@ require ( | ||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||
| 	github.com/dgrijalva/jwt-go v3.2.0+incompatible | ||||
| 	github.com/ef-ds/deque v1.0.4 | ||||
| 	github.com/golang/protobuf v1.4.3 | ||||
| 	github.com/google/uuid v1.2.0 | ||||
| 	github.com/imdario/mergo v0.3.11 | ||||
| 	github.com/kr/text v0.2.0 // indirect | ||||
| 	github.com/miekg/dns v1.1.38 | ||||
| 	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect | ||||
| 	github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c | ||||
| 	github.com/patrickmn/go-cache v2.1.0+incompatible | ||||
| 	github.com/stretchr/testify v1.7.0 | ||||
| 	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect | ||||
| 	golang.org/x/net v0.0.0-20210119194325-5f4716e94777 | ||||
| 	golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect | ||||
| 	google.golang.org/protobuf v1.25.0 | ||||
| 	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect | ||||
| ) | ||||
|   | ||||
							
								
								
									
										88
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,8 +1,3 @@ | ||||
| cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= | ||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||
| github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | ||||
| github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| @@ -11,26 +6,6 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC | ||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||
| github.com/ef-ds/deque v1.0.4 h1:iFAZNmveMT9WERAkqLJ+oaABF9AcVQ5AjXem/hroniI= | ||||
| github.com/ef-ds/deque v1.0.4/go.mod h1:gXDnTC3yqvBcHbq2lcExjtAcVrOnJCbMcZXmuj8Z4tg= | ||||
| github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | ||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||
| github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||
| github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= | ||||
| github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= | ||||
| github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= | ||||
| github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= | ||||
| github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= | ||||
| github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= | ||||
| github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= | ||||
| github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | ||||
| github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | ||||
| github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= | ||||
| github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= | ||||
| github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= | ||||
| @@ -39,84 +14,23 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||
| github.com/miekg/dns v1.1.38 h1:MtIY+fmHUVVgv1AXzmKMWcwdCYxTRPG1EDjpqF4RCEw= | ||||
| github.com/miekg/dns v1.1.38/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= | ||||
| github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= | ||||
| github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= | ||||
| github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= | ||||
| github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= | ||||
| github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= | ||||
| github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= | ||||
| golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= | ||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||
| golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= | ||||
| golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= | ||||
| golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||
| golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= | ||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= | ||||
| golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= | ||||
| golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= | ||||
| golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||
| golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | ||||
| golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||
| google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||
| google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||
| google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | ||||
| google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= | ||||
| google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | ||||
| google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | ||||
| google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= | ||||
| google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= | ||||
| google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= | ||||
| google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= | ||||
| google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= | ||||
| google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= | ||||
| google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= | ||||
| google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= | ||||
| gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| @@ -124,5 +38,3 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= | ||||
| gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
|   | ||||
| @@ -2,19 +2,16 @@ package meter | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestNoopMeter(t *testing.T) { | ||||
| 	meter := NewMeter(Path("/noop")) | ||||
| 	assert.NotNil(t, meter) | ||||
| 	assert.Equal(t, "/noop", meter.Options().Path) | ||||
| 	assert.Implements(t, new(Meter), meter) | ||||
| 	if "/noop" != meter.Options().Path { | ||||
| 		t.Fatalf("invalid options parsing: %v", meter.Options()) | ||||
| 	} | ||||
|  | ||||
| 	cnt := meter.Counter("counter", Label("server", "noop")) | ||||
| 	cnt.Inc() | ||||
|  | ||||
| } | ||||
|  | ||||
| func TestLabels(t *testing.T) { | ||||
|   | ||||
| @@ -4,18 +4,20 @@ package dns | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| 	"github.com/unistack-org/micro/v3/resolver" | ||||
| ) | ||||
|  | ||||
| // Resolver is a DNS network resolve | ||||
| type Resolver struct { | ||||
| 	// The resolver address to use | ||||
| 	Address string | ||||
| 	Address    string | ||||
| 	goresolver *net.Resolver | ||||
| 	sync.RWMutex | ||||
| } | ||||
|  | ||||
| // Resolve assumes ID is a domain name e.g micro.mu | ||||
| func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) { | ||||
| 	host, port, err := net.SplitHostPort(name) | ||||
| 	if err != nil { | ||||
| @@ -28,56 +30,46 @@ func (r *Resolver) Resolve(name string) ([]*resolver.Record, error) { | ||||
| 	} | ||||
|  | ||||
| 	if len(r.Address) == 0 { | ||||
| 		r.Address = "1.0.0.1:53" | ||||
| 		r.Address = "1.1.1.1:53" | ||||
| 	} | ||||
|  | ||||
| 	//nolint:prealloc | ||||
| 	var records []*resolver.Record | ||||
|  | ||||
| 	// parsed an actual ip | ||||
| 	if v := net.ParseIP(host); v != nil { | ||||
| 		records = append(records, &resolver.Record{ | ||||
| 			Address: net.JoinHostPort(host, port), | ||||
| 		}) | ||||
| 		return records, nil | ||||
| 		rec := &resolver.Record{Address: net.JoinHostPort(host, port)} | ||||
| 		return []*resolver.Record{rec}, nil | ||||
| 	} | ||||
|  | ||||
| 	for _, q := range []uint16{dns.TypeA, dns.TypeAAAA} { | ||||
| 		m := new(dns.Msg) | ||||
| 		m.SetQuestion(dns.Fqdn(host), q) | ||||
| 		rec, err := dns.ExchangeContext(context.Background(), m, r.Address) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	r.RLock() | ||||
| 	goresolver := r.goresolver | ||||
| 	r.RUnlock() | ||||
|  | ||||
| 		var addr string | ||||
| 		for _, answer := range rec.Answer { | ||||
| 			h := answer.Header() | ||||
| 			// check record type matches | ||||
| 			switch h.Rrtype { | ||||
| 			case dns.TypeA: | ||||
| 				arec, _ := answer.(*dns.A) | ||||
| 				addr = arec.A.String() | ||||
| 			case dns.TypeAAAA: | ||||
| 				arec, _ := answer.(*dns.AAAA) | ||||
| 				addr = arec.AAAA.String() | ||||
| 			default: | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// join resolved record with port | ||||
| 			address := net.JoinHostPort(addr, port) | ||||
| 			// append to record set | ||||
| 			records = append(records, &resolver.Record{ | ||||
| 				Address: address, | ||||
| 			}) | ||||
| 	if goresolver == nil { | ||||
| 		r.Lock() | ||||
| 		r.goresolver = &net.Resolver{ | ||||
| 			Dial: func(ctx context.Context, network, address string) (net.Conn, error) { | ||||
| 				d := net.Dialer{ | ||||
| 					Timeout: time.Millisecond * time.Duration(100), | ||||
| 				} | ||||
| 				return d.DialContext(ctx, "udp", r.Address) | ||||
| 			}, | ||||
| 		} | ||||
| 		r.Unlock() | ||||
| 	} | ||||
|  | ||||
| 	// no records returned so just best effort it | ||||
| 	if len(records) == 0 { | ||||
| 	addrs, err := goresolver.LookupIP(context.TODO(), "ip", host) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if len(addrs) == 0 { | ||||
| 		rec := &resolver.Record{Address: net.JoinHostPort(host, port)} | ||||
| 		return []*resolver.Record{rec}, nil | ||||
| 	} | ||||
|  | ||||
| 	records := make([]*resolver.Record, 0, len(addrs)) | ||||
| 	for _, addr := range addrs { | ||||
| 		records = append(records, &resolver.Record{ | ||||
| 			Address: net.JoinHostPort(host, port), | ||||
| 			Address: net.JoinHostPort(addr.String(), port), | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
|   | ||||
							
								
								
									
										14
									
								
								resolver/dns/dns_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								resolver/dns/dns_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| package dns | ||||
|  | ||||
| import "testing" | ||||
|  | ||||
| func TestResolver(t *testing.T) { | ||||
| 	r := &Resolver{} | ||||
| 	recs, err := r.Resolve("unistack.org") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if len(recs) < 1 { | ||||
| 		t.Fatalf("records not resolved: %v", recs) | ||||
| 	} | ||||
| } | ||||
| @@ -3,7 +3,6 @@ package roundrobin | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/unistack-org/micro/v3/selector" | ||||
| ) | ||||
|  | ||||
| @@ -19,18 +18,29 @@ func TestRoundRobin(t *testing.T) { | ||||
| 	// By passing r1 and r2 first, it forces a set sequence of (r1 => r2 => r3 => r1) | ||||
|  | ||||
| 	next, err := sel.Select([]string{r1}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	r := next() | ||||
| 	assert.Nil(t, err, "Error should be nil") | ||||
| 	assert.Equal(t, r1, r, "Expected route to be r1") | ||||
|  | ||||
| 	if r1 != r { | ||||
| 		t.Fatal("Expected route to be r == r1") | ||||
| 	} | ||||
|  | ||||
| 	next, err = sel.Select([]string{r2}) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	r = next() | ||||
| 	assert.Nil(t, err, "Error should be nil") | ||||
| 	assert.Equal(t, r2, r, "Expected route to be r2") | ||||
| 	if r2 != r { | ||||
| 		t.Fatal("Expected route to be r2") | ||||
| 	} | ||||
|  | ||||
| 	routes := []string{r1, r2, r3} | ||||
| 	next, err = sel.Select(routes) | ||||
| 	assert.Nil(t, err, "Error should be nil") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	n1, n2, n3, n4 := next(), next(), next(), next() | ||||
|  | ||||
| 	// start element is random but then it should loop through in order | ||||
| @@ -41,9 +51,19 @@ func TestRoundRobin(t *testing.T) { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	assert.NotEqual(t, start, -1) | ||||
| 	assert.Equal(t, routes[start], n1, "Unexpected route") | ||||
| 	assert.Equal(t, routes[(start+1)%3], n2, "Unexpected route") | ||||
| 	assert.Equal(t, routes[(start+2)%3], n3, "Unexpected route") | ||||
| 	assert.Equal(t, routes[(start+3)%3], n4, "Unexpected route") | ||||
| 	if start == -1 { | ||||
| 		t.Fatalf("start == -1 %v %v", start, -1) | ||||
| 	} | ||||
| 	if routes[start] != n1 { | ||||
| 		t.Fatal("Unexpected route") | ||||
| 	} | ||||
| 	if routes[(start+1)%3] != n2 { | ||||
| 		t.Fatal("Unexpected route") | ||||
| 	} | ||||
| 	if routes[(start+2)%3] != n3 { | ||||
| 		t.Fatal("Unexpected route") | ||||
| 	} | ||||
| 	if routes[(start+3)%3] != n4 { | ||||
| 		t.Fatal("Unexpected route") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,6 @@ package selector | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| // Tests runs all the tests against a selector to ensure the implementations are consistent | ||||
| @@ -14,19 +12,27 @@ func Tests(t *testing.T, s Selector) { | ||||
| 	t.Run("Select", func(t *testing.T) { | ||||
| 		t.Run("NoRoutes", func(t *testing.T) { | ||||
| 			_, err := s.Select([]string{}) | ||||
| 			assert.Equal(t, ErrNoneAvailable, err, "Expected error to be none available") | ||||
| 			if err != ErrNoneAvailable { | ||||
| 				t.Fatal("Expected error to be none available") | ||||
| 			} | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("OneRoute", func(t *testing.T) { | ||||
| 			next, err := s.Select([]string{r1}) | ||||
| 			if err != nil { | ||||
| 				t.Fatal("Error should be nil") | ||||
| 			} | ||||
| 			srv := next() | ||||
| 			assert.Nil(t, err, "Error should be nil") | ||||
| 			assert.Equal(t, r1, srv, "Expected the route to be returned") | ||||
| 			if r1 != srv { | ||||
| 				t.Fatal("Expected the route to be returned") | ||||
| 			} | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("MultipleRoutes", func(t *testing.T) { | ||||
| 			next, err := s.Select([]string{r1, r2}) | ||||
| 			assert.Nil(t, err, "Error should be nil") | ||||
| 			if err != nil { | ||||
| 				t.Fatal("Error should be nil") | ||||
| 			} | ||||
| 			srv := next() | ||||
| 			if srv != r1 && srv != r2 { | ||||
| 				t.Errorf("Expected the route to be one of the inputs") | ||||
| @@ -35,11 +41,14 @@ func Tests(t *testing.T, s Selector) { | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Record", func(t *testing.T) { | ||||
| 		err := s.Record(r1, nil) | ||||
| 		assert.Nil(t, err, "Expected the error to be nil") | ||||
| 		if err := s.Record(r1, nil); err != nil { | ||||
| 			t.Fatal("Expected the error to be nil") | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("String", func(t *testing.T) { | ||||
| 		assert.NotEmpty(t, s.String(), "String returned a blank string") | ||||
| 		if s.String() == "" { | ||||
| 			t.Fatal("String returned a blank string") | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package store_test | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| @@ -53,9 +52,6 @@ func TestMemoryNamespacePrefix(t *testing.T) { | ||||
|  | ||||
| func basictest(s store.Store, t *testing.T) { | ||||
| 	ctx := context.Background() | ||||
| 	if len(os.Getenv("IN_TRAVIS_CI")) == 0 { | ||||
| 		t.Logf("Testing store %s, with options %#+v\n", s.String(), s.Options()) | ||||
| 	} | ||||
| 	// Read and Write an expiring Record | ||||
| 	if err := s.Write(ctx, "Hello", "World", store.WriteTTL(time.Millisecond*100)); err != nil { | ||||
| 		t.Error(err) | ||||
| @@ -71,5 +67,7 @@ func basictest(s store.Store, t *testing.T) { | ||||
| 		t.Errorf("Expected %# v, got %# v", store.ErrNotFound, err) | ||||
| 	} | ||||
|  | ||||
| 	s.Disconnect(ctx) // reset the store | ||||
| 	if err := s.Disconnect(ctx); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -60,10 +60,9 @@ func NewRoundTripper(opts ...Option) http.RoundTripper { | ||||
| // RequestToContext puts the `Authorization` header bearer token into context | ||||
| // so calls to services will be authorized. | ||||
| func RequestToContext(r *http.Request) context.Context { | ||||
| 	ctx := context.Background() | ||||
| 	md := make(metadata.Metadata) | ||||
| 	md := metadata.New(len(r.Header)) | ||||
| 	for k, v := range r.Header { | ||||
| 		md[k] = strings.Join(v, ",") | ||||
| 		md.Set(k, strings.Join(v, ",")) | ||||
| 	} | ||||
| 	return metadata.NewContext(ctx, md) | ||||
| 	return metadata.NewIncomingContext(r.Context(), md) | ||||
| } | ||||
|   | ||||
| @@ -1,178 +0,0 @@ | ||||
| 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 | ||||
| } | ||||
|  | ||||
| var tests = []testcase{ | ||||
| 	{ | ||||
| 		ReqFn: func(opts *Options) *Request { | ||||
| 			return NewRequest(opts).Get().Resource("service") | ||||
| 		}, | ||||
| 		Method: "GET", | ||||
| 		URI:    "/api/v1/namespaces/default/services/", | ||||
| 	}, | ||||
| 	{ | ||||
| 		ReqFn: func(opts *Options) *Request { | ||||
| 			return NewRequest(opts).Get().Resource("service").Name("foo") | ||||
| 		}, | ||||
| 		Method: "GET", | ||||
| 		URI:    "/api/v1/namespaces/default/services/foo", | ||||
| 	}, | ||||
| 	{ | ||||
| 		ReqFn: func(opts *Options) *Request { | ||||
| 			return NewRequest(opts).Get().Resource("service").Namespace("test").Name("bar") | ||||
| 		}, | ||||
| 		Method: "GET", | ||||
| 		URI:    "/api/v1/namespaces/test/services/bar", | ||||
| 	}, | ||||
| 	{ | ||||
| 		ReqFn: func(opts *Options) *Request { | ||||
| 			return NewRequest(opts).Get().Resource("deployment").Name("foo") | ||||
| 		}, | ||||
| 		Method: "GET", | ||||
| 		URI:    "/apis/apps/v1/namespaces/default/deployments/foo", | ||||
| 	}, | ||||
| 	{ | ||||
| 		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", | ||||
| 	}, | ||||
| 	{ | ||||
| 		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", | ||||
| 	}, | ||||
| 	{ | ||||
| 		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"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		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"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		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"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		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"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		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"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		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"}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		ReqFn: func(opts *Options) *Request { | ||||
| 			return NewRequest(opts). | ||||
| 				Get(). | ||||
| 				Resource("pod"). | ||||
| 				SubResource("log"). | ||||
| 				Name("foolog") | ||||
| 		}, | ||||
| 		Method: "GET", | ||||
| 		URI:    "/api/v1/namespaces/default/pods/foolog/log", | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| 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() | ||||
| 	} | ||||
| } | ||||
| @@ -1,271 +0,0 @@ | ||||
| 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.Debug(context.TODO(), "[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 | ||||
| } | ||||
| @@ -1,94 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| // 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 | ||||
| } | ||||
|  | ||||
| // 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) | ||||
| 	if err := decoder.Decode(&data); err != nil { | ||||
| 		return fmt.Errorf("%v: %v", ErrDecode, err) | ||||
| 	} | ||||
|  | ||||
| 	return r.err | ||||
| } | ||||
|  | ||||
| func (r *Response) Close() error { | ||||
| 	return r.res.Body.Close() | ||||
| } | ||||
|  | ||||
| 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 | ||||
| 	} | ||||
|  | ||||
| 	b, err := ioutil.ReadAll(r.res.Body) | ||||
| 	if err == nil { | ||||
| 		r.err = errors.New(string(b)) | ||||
| 		return r | ||||
| 	} | ||||
|  | ||||
| 	r.err = ErrUnknown | ||||
|  | ||||
| 	return r | ||||
| } | ||||
| @@ -1,401 +0,0 @@ | ||||
| // Package client provides an implementation of a restricted subset of kubernetes API client | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"crypto/tls" | ||||
| 	"errors" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/logger" | ||||
| 	"github.com/unistack-org/micro/v3/util/kubernetes/api" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	// path to kubernetes service account token | ||||
| 	serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount" | ||||
| 	// ErrReadNamespace is returned when the names could not be read from service account | ||||
| 	ErrReadNamespace = errors.New("Could not read namespace from service account secret") | ||||
| 	// DefaultImage is default micro image | ||||
| 	DefaultImage = "micro/micro" | ||||
| 	// DefaultNamespace is the default k8s namespace | ||||
| 	DefaultNamespace = "default" | ||||
| ) | ||||
|  | ||||
| // Client ... | ||||
| type client struct { | ||||
| 	opts *api.Options | ||||
| } | ||||
|  | ||||
| // Kubernetes client | ||||
| type Client interface { | ||||
| 	// Create creates new API resource | ||||
| 	Create(*Resource, ...CreateOption) error | ||||
| 	// Get queries API resources | ||||
| 	Get(*Resource, ...GetOption) error | ||||
| 	// Update patches existing API object | ||||
| 	Update(*Resource, ...UpdateOption) error | ||||
| 	// Delete deletes API resource | ||||
| 	Delete(*Resource, ...DeleteOption) error | ||||
| 	// List lists API resources | ||||
| 	List(*Resource, ...ListOption) error | ||||
| 	// Log gets log for a pod | ||||
| 	Log(*Resource, ...LogOption) (io.ReadCloser, error) | ||||
| 	// Watch for events | ||||
| 	Watch(*Resource, ...WatchOption) (Watcher, error) | ||||
| } | ||||
|  | ||||
| // Create creates new API object | ||||
| func (c *client) Create(r *Resource, opts ...CreateOption) error { | ||||
| 	options := CreateOptions{ | ||||
| 		Namespace: c.opts.Namespace, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	b := new(bytes.Buffer) | ||||
| 	if err := renderTemplate(r.Kind, b, r.Value); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return api.NewRequest(c.opts). | ||||
| 		Post(). | ||||
| 		SetHeader("Content-Type", "application/yaml"). | ||||
| 		Namespace(options.Namespace). | ||||
| 		Resource(r.Kind). | ||||
| 		Body(b). | ||||
| 		Do(). | ||||
| 		Error() | ||||
| } | ||||
|  | ||||
| var ( | ||||
| 	nameRegex = regexp.MustCompile("[^a-zA-Z0-9]+") | ||||
| ) | ||||
|  | ||||
| // SerializeResourceName removes all spacial chars from a string so it | ||||
| // can be used as a k8s resource name | ||||
| func SerializeResourceName(ns string) string { | ||||
| 	return nameRegex.ReplaceAllString(ns, "-") | ||||
| } | ||||
|  | ||||
| // Get queries API objects and stores the result in r | ||||
| func (c *client) Get(r *Resource, opts ...GetOption) error { | ||||
| 	options := GetOptions{ | ||||
| 		Namespace: c.opts.Namespace, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	return api.NewRequest(c.opts). | ||||
| 		Get(). | ||||
| 		Resource(r.Kind). | ||||
| 		Namespace(options.Namespace). | ||||
| 		Params(&api.Params{LabelSelector: options.Labels}). | ||||
| 		Do(). | ||||
| 		Into(r.Value) | ||||
| } | ||||
|  | ||||
| // Log returns logs for a pod | ||||
| func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) { | ||||
| 	options := LogOptions{ | ||||
| 		Namespace: c.opts.Namespace, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	req := api.NewRequest(c.opts). | ||||
| 		Get(). | ||||
| 		Resource(r.Kind). | ||||
| 		SubResource("log"). | ||||
| 		Name(r.Name). | ||||
| 		Namespace(options.Namespace) | ||||
|  | ||||
| 	if options.Params != nil { | ||||
| 		req.Params(&api.Params{Additional: options.Params}) | ||||
| 	} | ||||
|  | ||||
| 	resp, err := req.Raw() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if resp.StatusCode < 200 || resp.StatusCode >= 300 { | ||||
| 		resp.Body.Close() | ||||
| 		return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status) | ||||
| 	} | ||||
| 	return resp.Body, nil | ||||
| } | ||||
|  | ||||
| // Update updates API object | ||||
| func (c *client) Update(r *Resource, opts ...UpdateOption) error { | ||||
| 	options := UpdateOptions{ | ||||
| 		Namespace: c.opts.Namespace, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	req := api.NewRequest(c.opts). | ||||
| 		Patch(). | ||||
| 		SetHeader("Content-Type", "application/strategic-merge-patch+json"). | ||||
| 		Resource(r.Kind). | ||||
| 		Name(r.Name). | ||||
| 		Namespace(options.Namespace) | ||||
|  | ||||
| 	switch r.Kind { | ||||
| 	case "service": | ||||
| 		req.Body(r.Value.(*Service)) | ||||
| 	case "deployment": | ||||
| 		req.Body(r.Value.(*Deployment)) | ||||
| 	case "pod": | ||||
| 		req.Body(r.Value.(*Pod)) | ||||
| 	default: | ||||
| 		return errors.New("unsupported resource") | ||||
| 	} | ||||
|  | ||||
| 	return req.Do().Error() | ||||
| } | ||||
|  | ||||
| // Delete removes API object | ||||
| func (c *client) Delete(r *Resource, opts ...DeleteOption) error { | ||||
| 	options := DeleteOptions{ | ||||
| 		Namespace: c.opts.Namespace, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	return api.NewRequest(c.opts). | ||||
| 		Delete(). | ||||
| 		Resource(r.Kind). | ||||
| 		Name(r.Name). | ||||
| 		Namespace(options.Namespace). | ||||
| 		Do(). | ||||
| 		Error() | ||||
| } | ||||
|  | ||||
| // List lists API objects and stores the result in r | ||||
| func (c *client) List(r *Resource, opts ...ListOption) error { | ||||
| 	options := ListOptions{ | ||||
| 		Namespace: c.opts.Namespace, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	return c.Get(r, GetNamespace(options.Namespace)) | ||||
| } | ||||
|  | ||||
| // Watch returns an event stream | ||||
| func (c *client) Watch(r *Resource, opts ...WatchOption) (Watcher, error) { | ||||
| 	options := WatchOptions{ | ||||
| 		Namespace: c.opts.Namespace, | ||||
| 	} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
|  | ||||
| 	// set the watch param | ||||
| 	params := &api.Params{Additional: map[string]string{ | ||||
| 		"watch": "true", | ||||
| 	}} | ||||
|  | ||||
| 	// get options params | ||||
| 	if options.Params != nil { | ||||
| 		for k, v := range options.Params { | ||||
| 			params.Additional[k] = v | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	req := api.NewRequest(c.opts). | ||||
| 		Get(). | ||||
| 		Resource(r.Kind). | ||||
| 		Name(r.Name). | ||||
| 		Namespace(options.Namespace). | ||||
| 		Params(params) | ||||
|  | ||||
| 	return newWatcher(req) | ||||
| } | ||||
|  | ||||
| // NewService returns default micro kubernetes service definition | ||||
| func NewService(name, version, typ, namespace string) *Service { | ||||
| 	if logger.V(logger.TraceLevel) { | ||||
| 		logger.Trace(context.TODO(), "kubernetes default service: name: %s, version: %s", name, version) | ||||
| 	} | ||||
|  | ||||
| 	Labels := map[string]string{ | ||||
| 		"name":    name, | ||||
| 		"version": version, | ||||
| 		"micro":   typ, | ||||
| 	} | ||||
|  | ||||
| 	svcName := name | ||||
| 	if len(version) > 0 { | ||||
| 		// API service object name joins name and version over "-" | ||||
| 		svcName = strings.Join([]string{name, version}, "-") | ||||
| 	} | ||||
|  | ||||
| 	if len(namespace) == 0 { | ||||
| 		namespace = DefaultNamespace | ||||
| 	} | ||||
|  | ||||
| 	Metadata := &Metadata{ | ||||
| 		Name:      svcName, | ||||
| 		Namespace: SerializeResourceName(namespace), | ||||
| 		Version:   version, | ||||
| 		Labels:    Labels, | ||||
| 	} | ||||
|  | ||||
| 	Spec := &ServiceSpec{ | ||||
| 		Type:     "ClusterIP", | ||||
| 		Selector: Labels, | ||||
| 		Ports: []ServicePort{{ | ||||
| 			"service-port", 8080, "", | ||||
| 		}}, | ||||
| 	} | ||||
|  | ||||
| 	return &Service{ | ||||
| 		Metadata: Metadata, | ||||
| 		Spec:     Spec, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewService returns default micro kubernetes deployment definition | ||||
| func NewDeployment(name, version, typ, namespace string) *Deployment { | ||||
| 	if logger.V(logger.TraceLevel) { | ||||
| 		logger.Trace(context.TODO(), "kubernetes default deployment: name: %s, version: %s", name, version) | ||||
| 	} | ||||
|  | ||||
| 	Labels := map[string]string{ | ||||
| 		"name":    name, | ||||
| 		"version": version, | ||||
| 		"micro":   typ, | ||||
| 	} | ||||
|  | ||||
| 	depName := name | ||||
| 	if len(version) > 0 { | ||||
| 		// API deployment object name joins name and version over "-" | ||||
| 		depName = strings.Join([]string{name, version}, "-") | ||||
| 	} | ||||
|  | ||||
| 	if len(namespace) == 0 { | ||||
| 		namespace = DefaultNamespace | ||||
| 	} | ||||
|  | ||||
| 	Metadata := &Metadata{ | ||||
| 		Name:        depName, | ||||
| 		Namespace:   SerializeResourceName(namespace), | ||||
| 		Version:     version, | ||||
| 		Labels:      Labels, | ||||
| 		Annotations: map[string]string{}, | ||||
| 	} | ||||
|  | ||||
| 	// enable go modules by default | ||||
| 	env := EnvVar{ | ||||
| 		Name:  "GO111MODULE", | ||||
| 		Value: "on", | ||||
| 	} | ||||
|  | ||||
| 	Spec := &DeploymentSpec{ | ||||
| 		Replicas: 1, | ||||
| 		Selector: &LabelSelector{ | ||||
| 			MatchLabels: Labels, | ||||
| 		}, | ||||
| 		Template: &Template{ | ||||
| 			Metadata: Metadata, | ||||
| 			PodSpec: &PodSpec{ | ||||
| 				Containers: []Container{{ | ||||
| 					Name:    name, | ||||
| 					Image:   DefaultImage, | ||||
| 					Env:     []EnvVar{env}, | ||||
| 					Command: []string{}, | ||||
| 					Ports: []ContainerPort{{ | ||||
| 						Name:          "service-port", | ||||
| 						ContainerPort: 8080, | ||||
| 					}}, | ||||
| 					ReadinessProbe: &Probe{ | ||||
| 						TCPSocket: TCPSocketAction{ | ||||
| 							Port: 8080, | ||||
| 						}, | ||||
| 						PeriodSeconds:       10, | ||||
| 						InitialDelaySeconds: 10, | ||||
| 					}, | ||||
| 				}}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	return &Deployment{ | ||||
| 		Metadata: Metadata, | ||||
| 		Spec:     Spec, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // NewLocalClient returns a client that can be used with `kubectl proxy` | ||||
| func NewLocalClient(hosts ...string) *client { | ||||
| 	c := &client{ | ||||
| 		opts: &api.Options{ | ||||
| 			Client:    http.DefaultClient, | ||||
| 			Namespace: "default", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	if len(hosts) == 0 { | ||||
| 		c.opts.Host = "http://localhost:8001" | ||||
| 	} else { | ||||
| 		c.opts.Host = hosts[0] | ||||
| 	} | ||||
|  | ||||
| 	return c | ||||
| } | ||||
|  | ||||
| // NewClusterClient creates a Kubernetes client for use from within a k8s pod. | ||||
| func NewClusterClient() *client { | ||||
| 	host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT") | ||||
|  | ||||
| 	s, err := os.Stat(serviceAccountPath) | ||||
| 	if err != nil { | ||||
| 		logger.Fatal(context.TODO(), err.Error()) | ||||
| 	} | ||||
| 	if s == nil || !s.IsDir() { | ||||
| 		logger.Fatal(context.TODO(), "service account not found") | ||||
| 	} | ||||
|  | ||||
| 	token, err := ioutil.ReadFile(path.Join(serviceAccountPath, "token")) | ||||
| 	if err != nil { | ||||
| 		logger.Fatal(context.TODO(), err.Error()) | ||||
| 	} | ||||
| 	t := string(token) | ||||
|  | ||||
| 	crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt")) | ||||
| 	if err != nil { | ||||
| 		logger.Fatal(context.TODO(), err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	c := &http.Client{ | ||||
| 		Transport: &http.Transport{ | ||||
| 			TLSClientConfig: &tls.Config{ | ||||
| 				RootCAs: crt, | ||||
| 			}, | ||||
| 			DisableCompression: true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	return &client{ | ||||
| 		opts: &api.Options{ | ||||
| 			Client:      c, | ||||
| 			Host:        host, | ||||
| 			BearerToken: &t, | ||||
| 			Namespace:   DefaultNamespace, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| @@ -1,107 +0,0 @@ | ||||
| package client | ||||
|  | ||||
| type CreateOptions struct { | ||||
| 	Namespace string | ||||
| } | ||||
|  | ||||
| type GetOptions struct { | ||||
| 	Namespace string | ||||
| 	Labels    map[string]string | ||||
| } | ||||
| type UpdateOptions struct { | ||||
| 	Namespace string | ||||
| } | ||||
| type DeleteOptions struct { | ||||
| 	Namespace string | ||||
| } | ||||
| type ListOptions struct { | ||||
| 	Namespace string | ||||
| } | ||||
|  | ||||
| type LogOptions struct { | ||||
| 	Namespace string | ||||
| 	Params    map[string]string | ||||
| } | ||||
|  | ||||
| type WatchOptions struct { | ||||
| 	Namespace string | ||||
| 	Params    map[string]string | ||||
| } | ||||
|  | ||||
| type CreateOption func(*CreateOptions) | ||||
| type GetOption func(*GetOptions) | ||||
| type UpdateOption func(*UpdateOptions) | ||||
| type DeleteOption func(*DeleteOptions) | ||||
| type ListOption func(*ListOptions) | ||||
| type LogOption func(*LogOptions) | ||||
| type WatchOption func(*WatchOptions) | ||||
|  | ||||
| // LogParams provides additional params for logs | ||||
| func LogParams(p map[string]string) LogOption { | ||||
| 	return func(l *LogOptions) { | ||||
| 		l.Params = p | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WatchParams used for watch params | ||||
| func WatchParams(p map[string]string) WatchOption { | ||||
| 	return func(w *WatchOptions) { | ||||
| 		w.Params = p | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // CreateNamespace sets the namespace for creating a resource | ||||
| func CreateNamespace(ns string) CreateOption { | ||||
| 	return func(o *CreateOptions) { | ||||
| 		o.Namespace = SerializeResourceName(ns) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetNamespace sets the namespace for getting a resource | ||||
| func GetNamespace(ns string) GetOption { | ||||
| 	return func(o *GetOptions) { | ||||
| 		o.Namespace = SerializeResourceName(ns) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // GetLabels sets the labels for when getting a resource | ||||
| func GetLabels(ls map[string]string) GetOption { | ||||
| 	return func(o *GetOptions) { | ||||
| 		o.Labels = ls | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // UpdateNamespace sets the namespace for updating a resource | ||||
| func UpdateNamespace(ns string) UpdateOption { | ||||
| 	return func(o *UpdateOptions) { | ||||
| 		o.Namespace = SerializeResourceName(ns) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // DeleteNamespace sets the namespace for deleting a resource | ||||
| func DeleteNamespace(ns string) DeleteOption { | ||||
| 	return func(o *DeleteOptions) { | ||||
| 		o.Namespace = SerializeResourceName(ns) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ListNamespace sets the namespace for listing resources | ||||
| func ListNamespace(ns string) ListOption { | ||||
| 	return func(o *ListOptions) { | ||||
| 		o.Namespace = SerializeResourceName(ns) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // LogNamespace sets the namespace for logging a resource | ||||
| func LogNamespace(ns string) LogOption { | ||||
| 	return func(o *LogOptions) { | ||||
| 		o.Namespace = SerializeResourceName(ns) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WatchNamespace sets the namespace for watching a resource | ||||
| func WatchNamespace(ns string) WatchOption { | ||||
| 	return func(o *WatchOptions) { | ||||
| 		o.Namespace = SerializeResourceName(ns) | ||||
| 	} | ||||
| } | ||||
| @@ -1,227 +0,0 @@ | ||||
| package client | ||||
|  | ||||
| var templates = map[string]string{ | ||||
| 	"deployment":     deploymentTmpl, | ||||
| 	"service":        serviceTmpl, | ||||
| 	"namespace":      namespaceTmpl, | ||||
| 	"secret":         secretTmpl, | ||||
| 	"serviceaccount": serviceAccountTmpl, | ||||
| } | ||||
|  | ||||
| var deploymentTmpl = ` | ||||
| apiVersion: apps/v1 | ||||
| kind: Deployment | ||||
| metadata: | ||||
|   name: "{{ .Metadata.Name }}" | ||||
|   namespace: "{{ .Metadata.Namespace }}" | ||||
|   labels: | ||||
|     {{- with .Metadata.Labels }} | ||||
|     {{- range $key, $value := . }} | ||||
|     {{ $key }}: "{{ $value }}" | ||||
|     {{- end }} | ||||
|     {{- end }} | ||||
|   annotations: | ||||
|     {{- with .Metadata.Annotations }} | ||||
|     {{- range $key, $value := . }} | ||||
|     {{ $key }}: "{{ $value }}" | ||||
|     {{- end }} | ||||
|     {{- end }} | ||||
| spec: | ||||
|   replicas: {{ .Spec.Replicas }} | ||||
|   selector: | ||||
|     matchLabels: | ||||
|       {{- with .Spec.Selector.MatchLabels }} | ||||
|       {{- range $key, $value := . }} | ||||
|       {{ $key }}: "{{ $value }}" | ||||
|       {{- end }} | ||||
|       {{- end }} | ||||
|   template: | ||||
|     metadata: | ||||
|       labels: | ||||
|         {{- with .Spec.Template.Metadata.Labels }} | ||||
|         {{- range $key, $value := . }} | ||||
|         {{ $key }}: "{{ $value }}" | ||||
|         {{- end }} | ||||
|         {{- end }} | ||||
|       annotations: | ||||
|         {{- with .Spec.Template.Metadata.Annotations }} | ||||
|         {{- range $key, $value := . }} | ||||
|         {{ $key }}: "{{ $value }}" | ||||
|         {{- end }} | ||||
|         {{- end }} | ||||
|     spec:  | ||||
|       serviceAccountName: {{ .Spec.Template.PodSpec.ServiceAccountName }} | ||||
|       containers: | ||||
|       {{- with .Spec.Template.PodSpec.Containers }} | ||||
|       {{- range . }} | ||||
|         - name: {{ .Name }} | ||||
|           env: | ||||
|           {{- with .Env }} | ||||
|           {{- range . }} | ||||
|           - name: "{{ .Name }}" | ||||
|             value: "{{ .Value }}" | ||||
|           {{- if .ValueFrom }} | ||||
|           {{- with .ValueFrom }} | ||||
|             valueFrom:  | ||||
|               {{- if .SecretKeyRef }} | ||||
|               {{- with .SecretKeyRef }} | ||||
|               secretKeyRef: | ||||
|                 key: {{ .Key }} | ||||
|                 name: {{ .Name }} | ||||
|                 optional: {{ .Optional }} | ||||
|               {{- end }} | ||||
|               {{- end }} | ||||
|           {{- end }} | ||||
|           {{- end }} | ||||
|           {{- end }} | ||||
|           {{- end }} | ||||
|           args: | ||||
|           {{- range .Args }} | ||||
|           - {{.}} | ||||
|           {{- end }} | ||||
|           command: | ||||
|           {{- range .Command }} | ||||
|           - {{.}} | ||||
|           {{- end }} | ||||
|           image: {{ .Image }} | ||||
|           imagePullPolicy: Always | ||||
|           ports: | ||||
|           {{- with .Ports }} | ||||
|           {{- range . }} | ||||
|           - containerPort: {{ .ContainerPort }} | ||||
|             name: {{ .Name }} | ||||
|           {{- end }} | ||||
|           {{- end }} | ||||
|           {{- if .ReadinessProbe }} | ||||
|           {{- with .ReadinessProbe }} | ||||
|           readinessProbe: | ||||
|             {{- with .TCPSocket }} | ||||
|             tcpSocket: | ||||
|               {{- if .Host }} | ||||
|               host: {{ .Host }} | ||||
|               {{- end }} | ||||
|               port: {{ .Port }} | ||||
|             {{- end }} | ||||
|             initialDelaySeconds: {{ .InitialDelaySeconds }} | ||||
|             periodSeconds: {{ .PeriodSeconds }} | ||||
|           {{- end }} | ||||
|           {{- end }} | ||||
|           {{- if .Resources }} | ||||
|           {{- with .Resources }} | ||||
|           resources: | ||||
|             {{- if .Limits }} | ||||
|             {{- with .Limits }} | ||||
|             limits: | ||||
|               {{- if .Memory }} | ||||
|               memory: {{ .Memory }} | ||||
|               {{- end }} | ||||
|               {{- if .CPU }} | ||||
|               cpu: {{ .CPU }} | ||||
|               {{- end }} | ||||
|               {{- if .EphemeralStorage }} | ||||
|               ephemeral-storage: {{ .EphemeralStorage }} | ||||
|               {{- end }} | ||||
|             {{- end }} | ||||
|             {{- end }} | ||||
|             {{- if .Requests }} | ||||
|             {{- with .Requests }} | ||||
|             requests: | ||||
|               {{- if .Memory }} | ||||
|               memory: {{ .Memory }} | ||||
|               {{- end }} | ||||
|               {{- if .CPU }} | ||||
|               cpu: {{ .CPU }} | ||||
|               {{- end }} | ||||
|               {{- if .EphemeralStorage }} | ||||
|               ephemeral-storage: {{ .EphemeralStorage }} | ||||
|               {{- end }} | ||||
|             {{- end }} | ||||
|             {{- end }} | ||||
|           {{- end }} | ||||
|           {{- end }} | ||||
|       {{- end }} | ||||
|       {{- end }} | ||||
| ` | ||||
|  | ||||
| var serviceTmpl = ` | ||||
| apiVersion: v1 | ||||
| kind: Service | ||||
| metadata: | ||||
|   name: "{{ .Metadata.Name }}" | ||||
|   namespace: "{{ .Metadata.Namespace }}" | ||||
|   labels: | ||||
|     {{- with .Metadata.Labels }} | ||||
|     {{- range $key, $value := . }} | ||||
|     {{ $key }}: "{{ $value }}" | ||||
|     {{- end }} | ||||
|     {{- end }} | ||||
| spec: | ||||
|   selector: | ||||
|     {{- with .Spec.Selector }} | ||||
|     {{- range $key, $value := . }} | ||||
|     {{ $key }}: "{{ $value }}" | ||||
|     {{- end }} | ||||
|     {{- end }} | ||||
|   ports: | ||||
|   {{- with .Spec.Ports }} | ||||
|   {{- range . }} | ||||
|   - name: "{{ .Name }}" | ||||
|     port: {{ .Port }} | ||||
|     protocol: {{ .Protocol }} | ||||
|   {{- end }} | ||||
|   {{- end }} | ||||
| ` | ||||
|  | ||||
| var namespaceTmpl = ` | ||||
| apiVersion: v1 | ||||
| kind: Namespace | ||||
| metadata: | ||||
|   name: "{{ .Metadata.Name }}" | ||||
|   labels: | ||||
|     {{- with .Metadata.Labels }} | ||||
|     {{- range $key, $value := . }} | ||||
|     {{ $key }}: "{{ $value }}" | ||||
|     {{- end }} | ||||
|     {{- end }} | ||||
| ` | ||||
|  | ||||
| //nolint:gosec | ||||
| var secretTmpl = ` | ||||
| apiVersion: v1 | ||||
| kind: Secret | ||||
| type: "{{ .Type }}" | ||||
| metadata: | ||||
|   name: "{{ .Metadata.Name }}" | ||||
|   namespace: "{{ .Metadata.Namespace }}" | ||||
|   labels: | ||||
|     {{- with .Metadata.Labels }} | ||||
|     {{- range $key, $value := . }} | ||||
|     {{ $key }}: "{{ $value }}" | ||||
|     {{- end }} | ||||
|     {{- end }} | ||||
| data: | ||||
|   {{- with .Data }} | ||||
|   {{- range $key, $value := . }} | ||||
|   {{ $key }}: "{{ $value }}" | ||||
|   {{- end }} | ||||
|   {{- end }} | ||||
| ` | ||||
|  | ||||
| var serviceAccountTmpl = ` | ||||
| apiVersion: v1 | ||||
| kind: ServiceAccount | ||||
| metadata: | ||||
|   name: "{{ .Metadata.Name }}" | ||||
|   labels: | ||||
|     {{- with .Metadata.Labels }} | ||||
|     {{- range $key, $value := . }} | ||||
|     {{ $key }}: "{{ $value }}" | ||||
|     {{- end }} | ||||
|     {{- end }} | ||||
| imagePullSecrets: | ||||
| {{- with .ImagePullSecrets }} | ||||
| {{- range . }} | ||||
| - name: "{{ .Name }}" | ||||
| {{- end }} | ||||
| {{- end }} | ||||
| ` | ||||
| @@ -1,250 +0,0 @@ | ||||
| package client | ||||
|  | ||||
| // ContainerPort | ||||
| type ContainerPort struct { | ||||
| 	Name          string `json:"name,omitempty"` | ||||
| 	HostPort      int    `json:"hostPort,omitempty"` | ||||
| 	ContainerPort int    `json:"containerPort"` | ||||
| 	Protocol      string `json:"protocol,omitempty"` | ||||
| } | ||||
|  | ||||
| // EnvVar is environment variable | ||||
| type EnvVar struct { | ||||
| 	Name      string        `json:"name"` | ||||
| 	Value     string        `json:"value,omitempty"` | ||||
| 	ValueFrom *EnvVarSource `json:"valueFrom,omitempty"` | ||||
| } | ||||
|  | ||||
| // EnvVarSource represents a source for the value of an EnvVar. | ||||
| type EnvVarSource struct { | ||||
| 	SecretKeyRef *SecretKeySelector `json:"secretKeyRef,omitempty"` | ||||
| } | ||||
|  | ||||
| // SecretKeySelector selects a key of a Secret. | ||||
| type SecretKeySelector struct { | ||||
| 	Key      string `json:"key"` | ||||
| 	Name     string `json:"name"` | ||||
| 	Optional bool   `json:"optional,omitempty"` | ||||
| } | ||||
|  | ||||
| type Condition struct { | ||||
| 	Started string `json:"startedAt,omitempty"` | ||||
| 	Reason  string `json:"reason,omitempty"` | ||||
| 	Message string `json:"message,omitempty"` | ||||
| } | ||||
|  | ||||
| // Container defined container runtime values | ||||
| type Container struct { | ||||
| 	Name           string                `json:"name"` | ||||
| 	Image          string                `json:"image"` | ||||
| 	Env            []EnvVar              `json:"env,omitempty"` | ||||
| 	Command        []string              `json:"command,omitempty"` | ||||
| 	Args           []string              `json:"args,omitempty"` | ||||
| 	Ports          []ContainerPort       `json:"ports,omitempty"` | ||||
| 	ReadinessProbe *Probe                `json:"readinessProbe,omitempty"` | ||||
| 	Resources      *ResourceRequirements `json:"resources,omitempty"` | ||||
| } | ||||
|  | ||||
| // DeploymentSpec defines micro deployment spec | ||||
| type DeploymentSpec struct { | ||||
| 	Replicas int            `json:"replicas,omitempty"` | ||||
| 	Selector *LabelSelector `json:"selector"` | ||||
| 	Template *Template      `json:"template,omitempty"` | ||||
| } | ||||
|  | ||||
| // DeploymentCondition describes the state of deployment | ||||
| type DeploymentCondition struct { | ||||
| 	LastUpdateTime string `json:"lastUpdateTime"` | ||||
| 	Type           string `json:"type"` | ||||
| 	Reason         string `json:"reason,omitempty"` | ||||
| 	Message        string `json:"message,omitempty"` | ||||
| } | ||||
|  | ||||
| // DeploymentStatus is returned when querying deployment | ||||
| type DeploymentStatus struct { | ||||
| 	Replicas            int                   `json:"replicas,omitempty"` | ||||
| 	UpdatedReplicas     int                   `json:"updatedReplicas,omitempty"` | ||||
| 	ReadyReplicas       int                   `json:"readyReplicas,omitempty"` | ||||
| 	AvailableReplicas   int                   `json:"availableReplicas,omitempty"` | ||||
| 	UnavailableReplicas int                   `json:"unavailableReplicas,omitempty"` | ||||
| 	Conditions          []DeploymentCondition `json:"conditions,omitempty"` | ||||
| } | ||||
|  | ||||
| // Deployment is Kubernetes deployment | ||||
| type Deployment struct { | ||||
| 	Metadata *Metadata         `json:"metadata"` | ||||
| 	Spec     *DeploymentSpec   `json:"spec,omitempty"` | ||||
| 	Status   *DeploymentStatus `json:"status,omitempty"` | ||||
| } | ||||
|  | ||||
| // DeploymentList | ||||
| type DeploymentList struct { | ||||
| 	Items []Deployment `json:"items"` | ||||
| } | ||||
|  | ||||
| // LabelSelector is a label query over a set of resources | ||||
| // NOTE: we do not support MatchExpressions at the moment | ||||
| type LabelSelector struct { | ||||
| 	MatchLabels map[string]string `json:"matchLabels,omitempty"` | ||||
| } | ||||
|  | ||||
| type LoadBalancerIngress struct { | ||||
| 	IP       string `json:"ip,omitempty"` | ||||
| 	Hostname string `json:"hostname,omitempty"` | ||||
| } | ||||
|  | ||||
| type LoadBalancerStatus struct { | ||||
| 	Ingress []LoadBalancerIngress `json:"ingress,omitempty"` | ||||
| } | ||||
|  | ||||
| // Metadata defines api object metadata | ||||
| type Metadata struct { | ||||
| 	Name        string            `json:"name,omitempty"` | ||||
| 	Namespace   string            `json:"namespace,omitempty"` | ||||
| 	Version     string            `json:"version,omitempty"` | ||||
| 	Labels      map[string]string `json:"labels,omitempty"` | ||||
| 	Annotations map[string]string `json:"annotations,omitempty"` | ||||
| } | ||||
|  | ||||
| // PodSpec is a pod | ||||
| type PodSpec struct { | ||||
| 	Containers         []Container `json:"containers"` | ||||
| 	ServiceAccountName string      `json:"serviceAccountName"` | ||||
| } | ||||
|  | ||||
| // PodList | ||||
| type PodList struct { | ||||
| 	Items []Pod `json:"items"` | ||||
| } | ||||
|  | ||||
| // Pod is the top level item for a pod | ||||
| type Pod struct { | ||||
| 	Metadata *Metadata  `json:"metadata"` | ||||
| 	Spec     *PodSpec   `json:"spec,omitempty"` | ||||
| 	Status   *PodStatus `json:"status"` | ||||
| } | ||||
|  | ||||
| // PodStatus | ||||
| type PodStatus struct { | ||||
| 	Conditions []PodCondition    `json:"conditions,omitempty"` | ||||
| 	Containers []ContainerStatus `json:"containerStatuses"` | ||||
| 	PodIP      string            `json:"podIP"` | ||||
| 	Phase      string            `json:"phase"` | ||||
| 	Reason     string            `json:"reason"` | ||||
| } | ||||
|  | ||||
| // PodCondition describes the state of pod | ||||
| type PodCondition struct { | ||||
| 	Type    string `json:"type"` | ||||
| 	Reason  string `json:"reason,omitempty"` | ||||
| 	Message string `json:"message,omitempty"` | ||||
| } | ||||
|  | ||||
| type ContainerStatus struct { | ||||
| 	State ContainerState `json:"state"` | ||||
| } | ||||
|  | ||||
| type ContainerState struct { | ||||
| 	Running    *Condition `json:"running"` | ||||
| 	Terminated *Condition `json:"terminated"` | ||||
| 	Waiting    *Condition `json:"waiting"` | ||||
| } | ||||
|  | ||||
| // Resource is API resource | ||||
| type Resource struct { | ||||
| 	Name  string | ||||
| 	Kind  string | ||||
| 	Value interface{} | ||||
| } | ||||
|  | ||||
| // ServicePort configures service ports | ||||
| type ServicePort struct { | ||||
| 	Name     string `json:"name,omitempty"` | ||||
| 	Port     int    `json:"port"` | ||||
| 	Protocol string `json:"protocol,omitempty"` | ||||
| } | ||||
|  | ||||
| // ServiceSpec provides service configuration | ||||
| type ServiceSpec struct { | ||||
| 	ClusterIP string            `json:"clusterIP"` | ||||
| 	Type      string            `json:"type,omitempty"` | ||||
| 	Selector  map[string]string `json:"selector,omitempty"` | ||||
| 	Ports     []ServicePort     `json:"ports,omitempty"` | ||||
| } | ||||
|  | ||||
| // ServiceStatus | ||||
| type ServiceStatus struct { | ||||
| 	LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty"` | ||||
| } | ||||
|  | ||||
| // Service is kubernetes service | ||||
| type Service struct { | ||||
| 	Metadata *Metadata      `json:"metadata"` | ||||
| 	Spec     *ServiceSpec   `json:"spec,omitempty"` | ||||
| 	Status   *ServiceStatus `json:"status,omitempty"` | ||||
| } | ||||
|  | ||||
| // ServiceList | ||||
| type ServiceList struct { | ||||
| 	Items []Service `json:"items"` | ||||
| } | ||||
|  | ||||
| // Template is micro deployment template | ||||
| type Template struct { | ||||
| 	Metadata *Metadata `json:"metadata,omitempty"` | ||||
| 	PodSpec  *PodSpec  `json:"spec,omitempty"` | ||||
| } | ||||
|  | ||||
| // Namespace is a Kubernetes Namespace | ||||
| type Namespace struct { | ||||
| 	Metadata *Metadata `json:"metadata,omitempty"` | ||||
| } | ||||
|  | ||||
| // NamespaceList | ||||
| type NamespaceList struct { | ||||
| 	Items []Namespace `json:"items"` | ||||
| } | ||||
|  | ||||
| // ImagePullSecret | ||||
| type ImagePullSecret struct { | ||||
| 	Name string `json:"name"` | ||||
| } | ||||
|  | ||||
| // Secret | ||||
| type Secret struct { | ||||
| 	Type     string            `json:"type,omitempty"` | ||||
| 	Data     map[string]string `json:"data"` | ||||
| 	Metadata *Metadata         `json:"metadata,omitempty"` | ||||
| } | ||||
|  | ||||
| // ServiceAccount | ||||
| type ServiceAccount struct { | ||||
| 	Metadata         *Metadata         `json:"metadata,omitempty"` | ||||
| 	ImagePullSecrets []ImagePullSecret `json:"imagePullSecrets,omitempty"` | ||||
| } | ||||
|  | ||||
| // Probe describes a health check to be performed against a container to determine whether it is alive or ready to receive traffic. | ||||
| type Probe struct { | ||||
| 	TCPSocket           TCPSocketAction `json:"tcpSocket,omitempty"` | ||||
| 	PeriodSeconds       int             `json:"periodSeconds"` | ||||
| 	InitialDelaySeconds int             `json:"initialDelaySeconds"` | ||||
| } | ||||
|  | ||||
| // TCPSocketAction describes an action based on opening a socket | ||||
| type TCPSocketAction struct { | ||||
| 	Host string `json:"host,omitempty"` | ||||
| 	Port int    `json:"port,omitempty"` | ||||
| } | ||||
|  | ||||
| // ResourceRequirements describes the compute resource requirements. | ||||
| type ResourceRequirements struct { | ||||
| 	Limits   *ResourceLimits `json:"limits,omitempty"` | ||||
| 	Requests *ResourceLimits `json:"requests,omitempty"` | ||||
| } | ||||
|  | ||||
| // ResourceLimits describes the limits for a service | ||||
| type ResourceLimits struct { | ||||
| 	Memory           string `json:"memory,omitempty"` | ||||
| 	CPU              string `json:"cpu,omitempty"` | ||||
| 	EphemeralStorage string `json:"ephemeral-storage,omitempty"` | ||||
| } | ||||
| @@ -1,104 +0,0 @@ | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| ) | ||||
|  | ||||
| // renderTemplateFile renders template for a given resource into writer w | ||||
| func renderTemplate(resource string, w io.Writer, data interface{}) error { | ||||
| 	t := template.Must(template.New("kubernetes").Parse(templates[resource])) | ||||
|  | ||||
| 	if err := t.Execute(w, data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| } | ||||
|  | ||||
| // Format is used to format a string value into a k8s valid name | ||||
| func Format(v string) string { | ||||
| 	// to lower case | ||||
| 	v = strings.ToLower(v) | ||||
| 	// / to dashes | ||||
| 	v = strings.ReplaceAll(v, "/", "-") | ||||
| 	// dots to dashes | ||||
| 	v = strings.ReplaceAll(v, ".", "-") | ||||
| 	// limit to 253 chars | ||||
| 	if len(v) > 253 { | ||||
| 		v = v[:253] | ||||
| 	} | ||||
| 	// return new name | ||||
| 	return v | ||||
| } | ||||
| @@ -1,47 +0,0 @@ | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestTemplates(t *testing.T) { | ||||
| 	name := "foo" | ||||
| 	version := "123" | ||||
| 	typ := "service" | ||||
| 	namespace := "default" | ||||
|  | ||||
| 	// Render default service | ||||
| 	s := NewService(name, version, typ, namespace) | ||||
| 	bs := new(bytes.Buffer) | ||||
| 	if err := renderTemplate(templates["service"], bs, s); err != nil { | ||||
| 		t.Errorf("Failed to render kubernetes service: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	// Render default deployment | ||||
| 	d := NewDeployment(name, version, typ, namespace) | ||||
| 	bd := new(bytes.Buffer) | ||||
| 	if err := renderTemplate(templates["deployment"], bd, d); err != nil { | ||||
| 		t.Errorf("Failed to render kubernetes deployment: %v", err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFormatName(t *testing.T) { | ||||
| 	testCases := []struct { | ||||
| 		name   string | ||||
| 		expect string | ||||
| 	}{ | ||||
| 		{"foobar", "foobar"}, | ||||
| 		{"foo-bar", "foo-bar"}, | ||||
| 		{"foo.bar", "foo-bar"}, | ||||
| 		{"Foo.Bar", "foo-bar"}, | ||||
| 		{"micro.foo.bar", "micro-foo-bar"}, | ||||
| 	} | ||||
|  | ||||
| 	for _, test := range testCases { | ||||
| 		v := Format(test.name) | ||||
| 		if v != test.expect { | ||||
| 			t.Fatalf("Expected name %s for %s got: %s", test.expect, test.name, v) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,124 +0,0 @@ | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/util/kubernetes/api" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// EventTypes used | ||||
| 	Added    EventType = "ADDED" | ||||
| 	Modified EventType = "MODIFIED" | ||||
| 	Deleted  EventType = "DELETED" | ||||
| 	Error    EventType = "ERROR" | ||||
| ) | ||||
|  | ||||
| // Watcher is used to watch for events | ||||
| type Watcher interface { | ||||
| 	// A channel of events | ||||
| 	Chan() <-chan Event | ||||
| 	// Stop the watcher | ||||
| 	Stop() | ||||
| } | ||||
|  | ||||
| // EventType defines the possible types of events. | ||||
| type EventType string | ||||
|  | ||||
| // Event represents a single event to a watched resource. | ||||
| type Event struct { | ||||
| 	Type   EventType       `json:"type"` | ||||
| 	Object json.RawMessage `json:"object"` | ||||
| } | ||||
|  | ||||
| // bodyWatcher scans the body of a request for chunks | ||||
| type bodyWatcher struct { | ||||
| 	results chan Event | ||||
| 	cancel  func() | ||||
| 	stop    chan bool | ||||
| 	res     *http.Response | ||||
| 	req     *api.Request | ||||
| } | ||||
|  | ||||
| // Changes returns the results channel | ||||
| func (wr *bodyWatcher) Chan() <-chan Event { | ||||
| 	return wr.results | ||||
| } | ||||
|  | ||||
| // Stop cancels the request | ||||
| func (wr *bodyWatcher) Stop() { | ||||
| 	select { | ||||
| 	case <-wr.stop: | ||||
| 		return | ||||
| 	default: | ||||
| 		// cancel the request | ||||
| 		wr.cancel() | ||||
| 		// stop the watcher | ||||
| 		close(wr.stop) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (wr *bodyWatcher) stream() { | ||||
| 	reader := bufio.NewReader(wr.res.Body) | ||||
|  | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			// read a line | ||||
| 			b, err := reader.ReadBytes('\n') | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// send the event | ||||
| 			var event Event | ||||
| 			if err := json.Unmarshal(b, &event); err != nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			select { | ||||
| 			case <-wr.stop: | ||||
| 				return | ||||
| 			case wr.results <- event: | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
|  | ||||
| // newWatcher creates a k8s body watcher for | ||||
| // a given http request | ||||
| func newWatcher(req *api.Request) (Watcher, error) { | ||||
| 	// set request context so we can cancel the request | ||||
| 	ctx, cancel := context.WithCancel(context.Background()) | ||||
| 	req.Context(ctx) | ||||
|  | ||||
| 	// do the raw request | ||||
| 	res, err := req.Raw() | ||||
| 	if err != nil { | ||||
| 		cancel() | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if res.StatusCode < 200 || res.StatusCode >= 300 { | ||||
| 		cancel() | ||||
| 		// close the response body | ||||
| 		res.Body.Close() | ||||
| 		// return an error | ||||
| 		return nil, errors.New(res.Request.URL.String() + ": " + res.Status) | ||||
| 	} | ||||
|  | ||||
| 	wr := &bodyWatcher{ | ||||
| 		results: make(chan Event), | ||||
| 		stop:    make(chan bool), | ||||
| 		cancel:  cancel, | ||||
| 		req:     req, | ||||
| 		res:     res, | ||||
| 	} | ||||
|  | ||||
| 	go wr.stream() | ||||
|  | ||||
| 	return wr, nil | ||||
| } | ||||
							
								
								
									
										23
									
								
								util/mdns/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								util/mdns/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,23 +0,0 @@ | ||||
| # Compiled Object files, Static and Dynamic libs (Shared Objects) | ||||
| *.o | ||||
| *.a | ||||
| *.so | ||||
|  | ||||
| # Folders | ||||
| _obj | ||||
| _test | ||||
|  | ||||
| # Architecture specific extensions/prefixes | ||||
| *.[568vq] | ||||
| [568vq].out | ||||
|  | ||||
| *.cgo1.go | ||||
| *.cgo2.c | ||||
| _cgo_defun.c | ||||
| _cgo_gotypes.go | ||||
| _cgo_export.* | ||||
|  | ||||
| _testmain.go | ||||
|  | ||||
| *.exe | ||||
| *.test | ||||
| @@ -1,511 +0,0 @@ | ||||
| package mdns | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| 	"golang.org/x/net/ipv4" | ||||
| 	"golang.org/x/net/ipv6" | ||||
| ) | ||||
|  | ||||
| // ServiceEntry is returned after we query for a service | ||||
| type ServiceEntry struct { | ||||
| 	Name       string | ||||
| 	Host       string | ||||
| 	AddrV4     net.IP | ||||
| 	AddrV6     net.IP | ||||
| 	Port       int | ||||
| 	Info       string | ||||
| 	InfoFields []string | ||||
| 	TTL        int | ||||
| 	Type       uint16 | ||||
|  | ||||
| 	Addr net.IP // @Deprecated | ||||
|  | ||||
| 	hasTXT bool | ||||
| 	sent   bool | ||||
| } | ||||
|  | ||||
| // complete is used to check if we have all the info we need | ||||
| func (s *ServiceEntry) complete() bool { | ||||
|  | ||||
| 	return (len(s.AddrV4) > 0 || len(s.AddrV6) > 0 || len(s.Addr) > 0) && s.Port != 0 && s.hasTXT | ||||
| } | ||||
|  | ||||
| // QueryParam is used to customize how a Lookup is performed | ||||
| type QueryParam struct { | ||||
| 	Service             string               // Service to lookup | ||||
| 	Domain              string               // Lookup domain, default "local" | ||||
| 	Type                uint16               // Lookup type, defaults to dns.TypePTR | ||||
| 	Context             context.Context      // Context | ||||
| 	Timeout             time.Duration        // Lookup timeout, default 1 second. Ignored if Context is provided | ||||
| 	Interface           *net.Interface       // Multicast interface to use | ||||
| 	Entries             chan<- *ServiceEntry // Entries Channel | ||||
| 	WantUnicastResponse bool                 // Unicast response desired, as per 5.4 in RFC | ||||
| } | ||||
|  | ||||
| // DefaultParams is used to return a default set of QueryParam's | ||||
| func DefaultParams(service string) *QueryParam { | ||||
| 	return &QueryParam{ | ||||
| 		Service:             service, | ||||
| 		Domain:              "local", | ||||
| 		Timeout:             time.Second, | ||||
| 		Entries:             make(chan *ServiceEntry), | ||||
| 		WantUnicastResponse: false, // TODO(reddaly): Change this default. | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Query looks up a given service, in a domain, waiting at most | ||||
| // for a timeout before finishing the query. The results are streamed | ||||
| // to a channel. Sends will not block, so clients should make sure to | ||||
| // either read or buffer. | ||||
| func Query(params *QueryParam) error { | ||||
| 	// Create a new client | ||||
| 	client, err := newClient() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer client.Close() | ||||
|  | ||||
| 	// Set the multicast interface | ||||
| 	if params.Interface != nil { | ||||
| 		if err := client.setInterface(params.Interface, false); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Ensure defaults are set | ||||
| 	if params.Domain == "" { | ||||
| 		params.Domain = "local" | ||||
| 	} | ||||
|  | ||||
| 	if params.Context == nil { | ||||
| 		var cancel context.CancelFunc | ||||
| 		if params.Timeout == 0 { | ||||
| 			params.Timeout = time.Second | ||||
| 		} | ||||
| 		params.Context, cancel = context.WithTimeout(context.Background(), params.Timeout) | ||||
| 		defer cancel() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Run the query | ||||
| 	return client.query(params) | ||||
| } | ||||
|  | ||||
| // Listen listens indefinitely for multicast updates | ||||
| func Listen(entries chan<- *ServiceEntry, exit chan struct{}) error { | ||||
| 	// Create a new client | ||||
| 	client, err := newClient() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer client.Close() | ||||
|  | ||||
| 	client.setInterface(nil, true) | ||||
|  | ||||
| 	// Start listening for response packets | ||||
| 	msgCh := make(chan *dns.Msg, 32) | ||||
|  | ||||
| 	go client.recv(client.ipv4UnicastConn, msgCh) | ||||
| 	go client.recv(client.ipv6UnicastConn, msgCh) | ||||
| 	go client.recv(client.ipv4MulticastConn, msgCh) | ||||
| 	go client.recv(client.ipv6MulticastConn, msgCh) | ||||
|  | ||||
| 	ip := make(map[string]*ServiceEntry) | ||||
|  | ||||
| loop: | ||||
| 	for { | ||||
| 		select { | ||||
| 		case <-exit: | ||||
| 			break loop | ||||
| 		case <-client.closedCh: | ||||
| 			break loop | ||||
| 		case m := <-msgCh: | ||||
| 			e := messageToEntry(m, ip) | ||||
| 			if e == nil { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Check if this entry is complete | ||||
| 			if e.complete() { | ||||
| 				if e.sent { | ||||
| 					continue | ||||
| 				} | ||||
| 				e.sent = true | ||||
| 				entries <- e | ||||
| 				ip = make(map[string]*ServiceEntry) | ||||
| 			} else { | ||||
| 				// Fire off a node specific query | ||||
| 				m := new(dns.Msg) | ||||
| 				m.SetQuestion(e.Name, dns.TypePTR) | ||||
| 				m.RecursionDesired = false | ||||
| 				if err := client.sendQuery(m); err != nil { | ||||
| 					log.Printf("[ERR] mdns: Failed to query instance %s: %v", e.Name, err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Lookup is the same as Query, however it uses all the default parameters | ||||
| func Lookup(service string, entries chan<- *ServiceEntry) error { | ||||
| 	params := DefaultParams(service) | ||||
| 	params.Entries = entries | ||||
| 	return Query(params) | ||||
| } | ||||
|  | ||||
| // Client provides a query interface that can be used to | ||||
| // search for service providers using mDNS | ||||
| type client struct { | ||||
| 	ipv4UnicastConn *net.UDPConn | ||||
| 	ipv6UnicastConn *net.UDPConn | ||||
|  | ||||
| 	ipv4MulticastConn *net.UDPConn | ||||
| 	ipv6MulticastConn *net.UDPConn | ||||
|  | ||||
| 	closed    bool | ||||
| 	closedCh  chan struct{} // TODO(reddaly): This doesn't appear to be used. | ||||
| 	closeLock sync.Mutex | ||||
| } | ||||
|  | ||||
| // NewClient creates a new mdns Client that can be used to query | ||||
| // for records | ||||
| func newClient() (*client, error) { | ||||
| 	// TODO(reddaly): At least attempt to bind to the port required in the spec. | ||||
| 	// Create a IPv4 listener | ||||
| 	uconn4, err4 := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) | ||||
| 	uconn6, err6 := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero, Port: 0}) | ||||
| 	if err4 != nil && err6 != nil { | ||||
| 		log.Printf("[ERR] mdns: Failed to bind to udp port: %v %v", err4, err6) | ||||
| 	} | ||||
|  | ||||
| 	if uconn4 == nil && uconn6 == nil { | ||||
| 		return nil, fmt.Errorf("failed to bind to any unicast udp port") | ||||
| 	} | ||||
|  | ||||
| 	if uconn4 == nil { | ||||
| 		uconn4 = &net.UDPConn{} | ||||
| 	} | ||||
|  | ||||
| 	if uconn6 == nil { | ||||
| 		uconn6 = &net.UDPConn{} | ||||
| 	} | ||||
|  | ||||
| 	mconn4, err4 := net.ListenUDP("udp4", mdnsWildcardAddrIPv4) | ||||
| 	mconn6, err6 := net.ListenUDP("udp6", mdnsWildcardAddrIPv6) | ||||
| 	if err4 != nil && err6 != nil { | ||||
| 		log.Printf("[ERR] mdns: Failed to bind to udp port: %v %v", err4, err6) | ||||
| 	} | ||||
|  | ||||
| 	if mconn4 == nil && mconn6 == nil { | ||||
| 		return nil, fmt.Errorf("failed to bind to any multicast udp port") | ||||
| 	} | ||||
|  | ||||
| 	if mconn4 == nil { | ||||
| 		mconn4 = &net.UDPConn{} | ||||
| 	} | ||||
|  | ||||
| 	if mconn6 == nil { | ||||
| 		mconn6 = &net.UDPConn{} | ||||
| 	} | ||||
|  | ||||
| 	p1 := ipv4.NewPacketConn(mconn4) | ||||
| 	p2 := ipv6.NewPacketConn(mconn6) | ||||
| 	p1.SetMulticastLoopback(true) | ||||
| 	p2.SetMulticastLoopback(true) | ||||
|  | ||||
| 	ifaces, err := net.Interfaces() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var errCount1, errCount2 int | ||||
|  | ||||
| 	for _, iface := range ifaces { | ||||
| 		if err := p1.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||
| 			errCount1++ | ||||
| 		} | ||||
| 		if err := p2.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||
| 			errCount2++ | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(ifaces) == errCount1 && len(ifaces) == errCount2 { | ||||
| 		return nil, fmt.Errorf("Failed to join multicast group on all interfaces!") | ||||
| 	} | ||||
|  | ||||
| 	c := &client{ | ||||
| 		ipv4MulticastConn: mconn4, | ||||
| 		ipv6MulticastConn: mconn6, | ||||
| 		ipv4UnicastConn:   uconn4, | ||||
| 		ipv6UnicastConn:   uconn6, | ||||
| 		closedCh:          make(chan struct{}), | ||||
| 	} | ||||
| 	return c, nil | ||||
| } | ||||
|  | ||||
| // Close is used to cleanup the client | ||||
| func (c *client) Close() error { | ||||
| 	c.closeLock.Lock() | ||||
| 	defer c.closeLock.Unlock() | ||||
|  | ||||
| 	if c.closed { | ||||
| 		return nil | ||||
| 	} | ||||
| 	c.closed = true | ||||
|  | ||||
| 	close(c.closedCh) | ||||
|  | ||||
| 	if c.ipv4UnicastConn != nil { | ||||
| 		c.ipv4UnicastConn.Close() | ||||
| 	} | ||||
| 	if c.ipv6UnicastConn != nil { | ||||
| 		c.ipv6UnicastConn.Close() | ||||
| 	} | ||||
| 	if c.ipv4MulticastConn != nil { | ||||
| 		c.ipv4MulticastConn.Close() | ||||
| 	} | ||||
| 	if c.ipv6MulticastConn != nil { | ||||
| 		c.ipv6MulticastConn.Close() | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // setInterface is used to set the query interface, uses system | ||||
| // default if not provided | ||||
| func (c *client) setInterface(iface *net.Interface, loopback bool) error { | ||||
| 	p := ipv4.NewPacketConn(c.ipv4UnicastConn) | ||||
| 	if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	p2 := ipv6.NewPacketConn(c.ipv6UnicastConn) | ||||
| 	if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	p = ipv4.NewPacketConn(c.ipv4MulticastConn) | ||||
| 	if err := p.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	p2 = ipv6.NewPacketConn(c.ipv6MulticastConn) | ||||
| 	if err := p2.JoinGroup(iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if loopback { | ||||
| 		p.SetMulticastLoopback(true) | ||||
| 		p2.SetMulticastLoopback(true) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // query is used to perform a lookup and stream results | ||||
| func (c *client) query(params *QueryParam) error { | ||||
| 	// Create the service name | ||||
| 	serviceAddr := fmt.Sprintf("%s.%s.", trimDot(params.Service), trimDot(params.Domain)) | ||||
|  | ||||
| 	// Start listening for response packets | ||||
| 	msgCh := make(chan *dns.Msg, 32) | ||||
| 	go c.recv(c.ipv4UnicastConn, msgCh) | ||||
| 	go c.recv(c.ipv6UnicastConn, msgCh) | ||||
| 	go c.recv(c.ipv4MulticastConn, msgCh) | ||||
| 	go c.recv(c.ipv6MulticastConn, msgCh) | ||||
|  | ||||
| 	// Send the query | ||||
| 	m := new(dns.Msg) | ||||
| 	if params.Type == dns.TypeNone { | ||||
| 		m.SetQuestion(serviceAddr, dns.TypePTR) | ||||
| 	} else { | ||||
| 		m.SetQuestion(serviceAddr, params.Type) | ||||
| 	} | ||||
| 	// RFC 6762, section 18.12.  Repurposing of Top Bit of qclass in Question | ||||
| 	// Section | ||||
| 	// | ||||
| 	// In the Question Section of a Multicast DNS query, the top bit of the qclass | ||||
| 	// field is used to indicate that unicast responses are preferred for this | ||||
| 	// particular question.  (See Section 5.4.) | ||||
| 	if params.WantUnicastResponse { | ||||
| 		m.Question[0].Qclass |= 1 << 15 | ||||
| 	} | ||||
| 	m.RecursionDesired = false | ||||
| 	if err := c.sendQuery(m); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Map the in-progress responses | ||||
| 	inprogress := make(map[string]*ServiceEntry) | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case resp := <-msgCh: | ||||
| 			inp := messageToEntry(resp, inprogress) | ||||
|  | ||||
| 			if inp == nil { | ||||
| 				continue | ||||
| 			} | ||||
| 			if len(resp.Question) == 0 || resp.Question[0].Name != m.Question[0].Name { | ||||
| 				// discard anything which we've not asked for | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			// Check if this entry is complete | ||||
| 			if inp.complete() { | ||||
| 				if inp.sent { | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				inp.sent = true | ||||
| 				select { | ||||
| 				case params.Entries <- inp: | ||||
| 				case <-params.Context.Done(): | ||||
| 					return nil | ||||
| 				} | ||||
| 			} else { | ||||
| 				// Fire off a node specific query | ||||
| 				m := new(dns.Msg) | ||||
| 				m.SetQuestion(inp.Name, inp.Type) | ||||
| 				m.RecursionDesired = false | ||||
| 				if err := c.sendQuery(m); err != nil { | ||||
| 					log.Printf("[ERR] mdns: Failed to query instance %s: %v", inp.Name, err) | ||||
| 				} | ||||
| 			} | ||||
| 		case <-params.Context.Done(): | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // sendQuery is used to multicast a query out | ||||
| func (c *client) sendQuery(q *dns.Msg) error { | ||||
| 	buf, err := q.Pack() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if c.ipv4UnicastConn != nil { | ||||
| 		c.ipv4UnicastConn.WriteToUDP(buf, ipv4Addr) | ||||
| 	} | ||||
| 	if c.ipv6UnicastConn != nil { | ||||
| 		c.ipv6UnicastConn.WriteToUDP(buf, ipv6Addr) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // recv is used to receive until we get a shutdown | ||||
| func (c *client) recv(l *net.UDPConn, msgCh chan *dns.Msg) { | ||||
| 	if l == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	buf := make([]byte, 65536) | ||||
| 	for { | ||||
| 		c.closeLock.Lock() | ||||
| 		if c.closed { | ||||
| 			c.closeLock.Unlock() | ||||
| 			return | ||||
| 		} | ||||
| 		c.closeLock.Unlock() | ||||
| 		n, err := l.Read(buf) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		msg := new(dns.Msg) | ||||
| 		if err := msg.Unpack(buf[:n]); err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		select { | ||||
| 		case msgCh <- msg: | ||||
| 		case <-c.closedCh: | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // ensureName is used to ensure the named node is in progress | ||||
| func ensureName(inprogress map[string]*ServiceEntry, name string, typ uint16) *ServiceEntry { | ||||
| 	if inp, ok := inprogress[name]; ok { | ||||
| 		return inp | ||||
| 	} | ||||
| 	inp := &ServiceEntry{ | ||||
| 		Name: name, | ||||
| 		Type: typ, | ||||
| 	} | ||||
| 	inprogress[name] = inp | ||||
| 	return inp | ||||
| } | ||||
|  | ||||
| // alias is used to setup an alias between two entries | ||||
| func alias(inprogress map[string]*ServiceEntry, src, dst string, typ uint16) { | ||||
| 	srcEntry := ensureName(inprogress, src, typ) | ||||
| 	inprogress[dst] = srcEntry | ||||
| } | ||||
|  | ||||
| func messageToEntry(m *dns.Msg, inprogress map[string]*ServiceEntry) *ServiceEntry { | ||||
| 	var inp *ServiceEntry | ||||
|  | ||||
| 	for _, answer := range append(m.Answer, m.Extra...) { | ||||
| 		// TODO(reddaly): Check that response corresponds to serviceAddr? | ||||
| 		switch rr := answer.(type) { | ||||
| 		case *dns.PTR: | ||||
| 			// Create new entry for this | ||||
| 			inp = ensureName(inprogress, rr.Ptr, rr.Hdr.Rrtype) | ||||
| 			if inp.complete() { | ||||
| 				continue | ||||
| 			} | ||||
| 		case *dns.SRV: | ||||
| 			// Check for a target mismatch | ||||
| 			if rr.Target != rr.Hdr.Name { | ||||
| 				alias(inprogress, rr.Hdr.Name, rr.Target, rr.Hdr.Rrtype) | ||||
| 			} | ||||
|  | ||||
| 			// Get the port | ||||
| 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||
| 			if inp.complete() { | ||||
| 				continue | ||||
| 			} | ||||
| 			inp.Host = rr.Target | ||||
| 			inp.Port = int(rr.Port) | ||||
| 		case *dns.TXT: | ||||
| 			// Pull out the txt | ||||
| 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||
| 			if inp.complete() { | ||||
| 				continue | ||||
| 			} | ||||
| 			inp.Info = strings.Join(rr.Txt, "|") | ||||
| 			inp.InfoFields = rr.Txt | ||||
| 			inp.hasTXT = true | ||||
| 		case *dns.A: | ||||
| 			// Pull out the IP | ||||
| 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||
| 			if inp.complete() { | ||||
| 				continue | ||||
| 			} | ||||
| 			inp.Addr = rr.A // @Deprecated | ||||
| 			inp.AddrV4 = rr.A | ||||
| 		case *dns.AAAA: | ||||
| 			// Pull out the IP | ||||
| 			inp = ensureName(inprogress, rr.Hdr.Name, rr.Hdr.Rrtype) | ||||
| 			if inp.complete() { | ||||
| 				continue | ||||
| 			} | ||||
| 			inp.Addr = rr.AAAA // @Deprecated | ||||
| 			inp.AddrV6 = rr.AAAA | ||||
| 		} | ||||
|  | ||||
| 		if inp != nil { | ||||
| 			inp.TTL = int(answer.Header().Ttl) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return inp | ||||
| } | ||||
| @@ -1,84 +0,0 @@ | ||||
| package mdns | ||||
|  | ||||
| import "github.com/miekg/dns" | ||||
|  | ||||
| // DNSSDService is a service that complies with the DNS-SD (RFC 6762) and MDNS | ||||
| // (RFC 6762) specs for local, multicast-DNS-based discovery. | ||||
| // | ||||
| // DNSSDService implements the Zone interface and wraps an MDNSService instance. | ||||
| // To deploy an mDNS service that is compliant with DNS-SD, it's recommended to | ||||
| // register only the wrapped instance with the server. | ||||
| // | ||||
| // Example usage: | ||||
| //     service := &mdns.DNSSDService{ | ||||
| //       MDNSService: &mdns.MDNSService{ | ||||
| // 	       Instance: "My Foobar Service", | ||||
| // 	       Service: "_foobar._tcp", | ||||
| // 	       Port:    8000, | ||||
| //        } | ||||
| //      } | ||||
| //      server, err := mdns.NewServer(&mdns.Config{Zone: service}) | ||||
| //      if err != nil { | ||||
| //        log.Fatalf("Error creating server: %v", err) | ||||
| //      } | ||||
| //      defer server.Shutdown() | ||||
| type DNSSDService struct { | ||||
| 	MDNSService *MDNSService | ||||
| } | ||||
|  | ||||
| // Records returns DNS records in response to a DNS question. | ||||
| // | ||||
| // This function returns the DNS response of the underlying MDNSService | ||||
| // instance.  It also returns a PTR record for a request for " | ||||
| // _services._dns-sd._udp.<Domain>", as described in section 9 of RFC 6763 | ||||
| // ("Service Type Enumeration"), to allow browsing of the underlying MDNSService | ||||
| // instance. | ||||
| func (s *DNSSDService) Records(q dns.Question) []dns.RR { | ||||
| 	var recs []dns.RR | ||||
| 	if q.Name == "_services._dns-sd._udp."+s.MDNSService.Domain+"." { | ||||
| 		recs = s.dnssdMetaQueryRecords(q) | ||||
| 	} | ||||
| 	return append(recs, s.MDNSService.Records(q)...) | ||||
| } | ||||
|  | ||||
| // dnssdMetaQueryRecords returns the DNS records in response to a "meta-query" | ||||
| // issued to browse for DNS-SD services, as per section 9. of RFC6763. | ||||
| // | ||||
| // A meta-query has a name of the form "_services._dns-sd._udp.<Domain>" where | ||||
| // Domain is a fully-qualified domain, such as "local." | ||||
| func (s *DNSSDService) dnssdMetaQueryRecords(q dns.Question) []dns.RR { | ||||
| 	// Intended behavior, as described in the RFC: | ||||
| 	//     ...it may be useful for network administrators to find the list of | ||||
| 	//     advertised service types on the network, even if those Service Names | ||||
| 	//     are just opaque identifiers and not particularly informative in | ||||
| 	//     isolation. | ||||
| 	// | ||||
| 	//     For this purpose, a special meta-query is defined.  A DNS query for PTR | ||||
| 	//     records with the name "_services._dns-sd._udp.<Domain>" yields a set of | ||||
| 	//     PTR records, where the rdata of each PTR record is the two-abel | ||||
| 	//     <Service> name, plus the same domain, e.g., "_http._tcp.<Domain>". | ||||
| 	//     Including the domain in the PTR rdata allows for slightly better name | ||||
| 	//     compression in Unicast DNS responses, but only the first two labels are | ||||
| 	//     relevant for the purposes of service type enumeration.  These two-label | ||||
| 	//     service types can then be used to construct subsequent Service Instance | ||||
| 	//     Enumeration PTR queries, in this <Domain> or others, to discover | ||||
| 	//     instances of that service type. | ||||
| 	return []dns.RR{ | ||||
| 		&dns.PTR{ | ||||
| 			Hdr: dns.RR_Header{ | ||||
| 				Name:   q.Name, | ||||
| 				Rrtype: dns.TypePTR, | ||||
| 				Class:  dns.ClassINET, | ||||
| 				Ttl:    defaultTTL, | ||||
| 			}, | ||||
| 			Ptr: s.MDNSService.serviceAddr, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Announcement returns DNS records that should be broadcast during the initial | ||||
| // availability of the service, as described in section 8.3 of RFC 6762. | ||||
| // TODO(reddaly): Add this when Announcement is added to the mdns.Zone interface. | ||||
| //func (s *DNSSDService) Announcement() []dns.RR { | ||||
| //	return s.MDNSService.Announcement() | ||||
| //} | ||||
| @@ -1,69 +0,0 @@ | ||||
| package mdns | ||||
|  | ||||
| import ( | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| ) | ||||
|  | ||||
| type mockMDNSService struct{} | ||||
|  | ||||
| func (s *mockMDNSService) Records(q dns.Question) []dns.RR { | ||||
| 	return []dns.RR{ | ||||
| 		&dns.PTR{ | ||||
| 			Hdr: dns.RR_Header{ | ||||
| 				Name:   "fakerecord", | ||||
| 				Rrtype: dns.TypePTR, | ||||
| 				Class:  dns.ClassINET, | ||||
| 				Ttl:    42, | ||||
| 			}, | ||||
| 			Ptr: "fake.local.", | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *mockMDNSService) Announcement() []dns.RR { | ||||
| 	return []dns.RR{ | ||||
| 		&dns.PTR{ | ||||
| 			Hdr: dns.RR_Header{ | ||||
| 				Name:   "fakeannounce", | ||||
| 				Rrtype: dns.TypePTR, | ||||
| 				Class:  dns.ClassINET, | ||||
| 				Ttl:    42, | ||||
| 			}, | ||||
| 			Ptr: "fake.local.", | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDNSSDServiceRecords(t *testing.T) { | ||||
| 	s := &DNSSDService{ | ||||
| 		MDNSService: &MDNSService{ | ||||
| 			serviceAddr: "_foobar._tcp.local.", | ||||
| 			Domain:      "local", | ||||
| 		}, | ||||
| 	} | ||||
| 	q := dns.Question{ | ||||
| 		Name:   "_services._dns-sd._udp.local.", | ||||
| 		Qtype:  dns.TypePTR, | ||||
| 		Qclass: dns.ClassINET, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if got, want := len(recs), 1; got != want { | ||||
| 		t.Fatalf("s.Records(%v) returned %v records, want %v", q, got, want) | ||||
| 	} | ||||
|  | ||||
| 	want := dns.RR(&dns.PTR{ | ||||
| 		Hdr: dns.RR_Header{ | ||||
| 			Name:   "_services._dns-sd._udp.local.", | ||||
| 			Rrtype: dns.TypePTR, | ||||
| 			Class:  dns.ClassINET, | ||||
| 			Ttl:    defaultTTL, | ||||
| 		}, | ||||
| 		Ptr: "_foobar._tcp.local.", | ||||
| 	}) | ||||
| 	if got := recs[0]; !reflect.DeepEqual(got, want) { | ||||
| 		t.Errorf("s.Records()[0] = %v, want %v", got, want) | ||||
| 	} | ||||
| } | ||||
| @@ -1,527 +0,0 @@ | ||||
| package mdns | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"net" | ||||
| 	"sync" | ||||
| 	"sync/atomic" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| 	"github.com/unistack-org/micro/v3/logger" | ||||
| 	"golang.org/x/net/ipv4" | ||||
| 	"golang.org/x/net/ipv6" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	mdnsGroupIPv4 = net.ParseIP("224.0.0.251") | ||||
| 	mdnsGroupIPv6 = net.ParseIP("ff02::fb") | ||||
|  | ||||
| 	// mDNS wildcard addresses | ||||
| 	mdnsWildcardAddrIPv4 = &net.UDPAddr{ | ||||
| 		IP:   net.ParseIP("224.0.0.0"), | ||||
| 		Port: 5353, | ||||
| 	} | ||||
| 	mdnsWildcardAddrIPv6 = &net.UDPAddr{ | ||||
| 		IP:   net.ParseIP("ff02::"), | ||||
| 		Port: 5353, | ||||
| 	} | ||||
|  | ||||
| 	// mDNS endpoint addresses | ||||
| 	ipv4Addr = &net.UDPAddr{ | ||||
| 		IP:   mdnsGroupIPv4, | ||||
| 		Port: 5353, | ||||
| 	} | ||||
| 	ipv6Addr = &net.UDPAddr{ | ||||
| 		IP:   mdnsGroupIPv6, | ||||
| 		Port: 5353, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| // GetMachineIP is a func which returns the outbound IP of this machine. | ||||
| // Used by the server to determine whether to attempt send the response on a local address | ||||
| type GetMachineIP func() net.IP | ||||
|  | ||||
| // Config is used to configure the mDNS server | ||||
| type Config struct { | ||||
| 	// Zone must be provided to support responding to queries | ||||
| 	Zone Zone | ||||
|  | ||||
| 	// Iface if provided binds the multicast listener to the given | ||||
| 	// interface. If not provided, the system default multicase interface | ||||
| 	// is used. | ||||
| 	Iface *net.Interface | ||||
|  | ||||
| 	// Port If it is not 0, replace the port 5353 with this port number. | ||||
| 	Port int | ||||
|  | ||||
| 	// GetMachineIP is a function to return the IP of the local machine | ||||
| 	GetMachineIP GetMachineIP | ||||
| 	// LocalhostChecking if enabled asks the server to also send responses to 0.0.0.0 if the target IP | ||||
| 	// is this host (as defined by GetMachineIP). Useful in case machine is on a VPN which blocks comms on non standard ports | ||||
| 	LocalhostChecking bool | ||||
|  | ||||
| 	Context context.Context | ||||
| } | ||||
|  | ||||
| // Server is an mDNS server used to listen for mDNS queries and respond if we | ||||
| // have a matching local record | ||||
| type Server struct { | ||||
| 	config *Config | ||||
|  | ||||
| 	ipv4List *net.UDPConn | ||||
| 	ipv6List *net.UDPConn | ||||
|  | ||||
| 	shutdown     bool | ||||
| 	shutdownCh   chan struct{} | ||||
| 	shutdownLock sync.Mutex | ||||
| 	wg           sync.WaitGroup | ||||
|  | ||||
| 	outboundIP net.IP | ||||
| } | ||||
|  | ||||
| // NewServer is used to create a new mDNS server from a config | ||||
| func NewServer(config *Config) (*Server, error) { | ||||
| 	setCustomPort(config.Port) | ||||
|  | ||||
| 	// Create the listeners | ||||
| 	// Create wildcard connections (because :5353 can be already taken by other apps) | ||||
| 	ipv4List, _ := net.ListenUDP("udp4", mdnsWildcardAddrIPv4) | ||||
| 	ipv6List, _ := net.ListenUDP("udp6", mdnsWildcardAddrIPv6) | ||||
| 	if ipv4List == nil && ipv6List == nil { | ||||
| 		return nil, fmt.Errorf("[ERR] mdns: Failed to bind to any udp port!") | ||||
| 	} | ||||
|  | ||||
| 	if ipv4List == nil { | ||||
| 		ipv4List = &net.UDPConn{} | ||||
| 	} | ||||
| 	if ipv6List == nil { | ||||
| 		ipv6List = &net.UDPConn{} | ||||
| 	} | ||||
|  | ||||
| 	// Join multicast groups to receive announcements | ||||
| 	p1 := ipv4.NewPacketConn(ipv4List) | ||||
| 	p2 := ipv6.NewPacketConn(ipv6List) | ||||
| 	p1.SetMulticastLoopback(true) | ||||
| 	p2.SetMulticastLoopback(true) | ||||
|  | ||||
| 	if config.Iface != nil { | ||||
| 		if err := p1.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if err := p2.JoinGroup(config.Iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		ifaces, err := net.Interfaces() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		errCount1, errCount2 := 0, 0 | ||||
| 		for _, iface := range ifaces { | ||||
| 			if err := p1.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv4}); err != nil { | ||||
| 				errCount1++ | ||||
| 			} | ||||
| 			if err := p2.JoinGroup(&iface, &net.UDPAddr{IP: mdnsGroupIPv6}); err != nil { | ||||
| 				errCount2++ | ||||
| 			} | ||||
| 		} | ||||
| 		if len(ifaces) == errCount1 && len(ifaces) == errCount2 { | ||||
| 			return nil, fmt.Errorf("Failed to join multicast group on all interfaces!") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	ipFunc := getOutboundIP | ||||
| 	if config.GetMachineIP != nil { | ||||
| 		ipFunc = config.GetMachineIP | ||||
| 	} | ||||
|  | ||||
| 	s := &Server{ | ||||
| 		config:     config, | ||||
| 		ipv4List:   ipv4List, | ||||
| 		ipv6List:   ipv6List, | ||||
| 		shutdownCh: make(chan struct{}), | ||||
| 		outboundIP: ipFunc(), | ||||
| 	} | ||||
|  | ||||
| 	if s.config.Context == nil { | ||||
| 		s.config.Context = context.Background() | ||||
| 	} | ||||
|  | ||||
| 	go s.recv(s.ipv4List) | ||||
| 	go s.recv(s.ipv6List) | ||||
|  | ||||
| 	s.wg.Add(1) | ||||
| 	go s.probe() | ||||
|  | ||||
| 	return s, nil | ||||
| } | ||||
|  | ||||
| // Shutdown is used to shutdown the listener | ||||
| func (s *Server) Shutdown() error { | ||||
| 	s.shutdownLock.Lock() | ||||
| 	defer s.shutdownLock.Unlock() | ||||
|  | ||||
| 	if s.shutdown { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	s.shutdown = true | ||||
| 	close(s.shutdownCh) | ||||
| 	if err := s.unregister(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if s.ipv4List != nil { | ||||
| 		s.ipv4List.Close() | ||||
| 	} | ||||
| 	if s.ipv6List != nil { | ||||
| 		s.ipv6List.Close() | ||||
| 	} | ||||
|  | ||||
| 	s.wg.Wait() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // recv is a long running routine to receive packets from an interface | ||||
| func (s *Server) recv(c *net.UDPConn) { | ||||
| 	if c == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	buf := make([]byte, 65536) | ||||
| 	for { | ||||
| 		s.shutdownLock.Lock() | ||||
| 		if s.shutdown { | ||||
| 			s.shutdownLock.Unlock() | ||||
| 			return | ||||
| 		} | ||||
| 		s.shutdownLock.Unlock() | ||||
| 		n, from, err := c.ReadFrom(buf) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		if err := s.parsePacket(buf[:n], from); err != nil { | ||||
| 			logger.Errorf(s.config.Context, "[ERR] mdns: Failed to handle query: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // parsePacket is used to parse an incoming packet | ||||
| func (s *Server) parsePacket(packet []byte, from net.Addr) error { | ||||
| 	var msg dns.Msg | ||||
| 	if err := msg.Unpack(packet); err != nil { | ||||
| 		logger.Errorf(s.config.Context, "[ERR] mdns: Failed to unpack packet: %v", err) | ||||
| 		return err | ||||
| 	} | ||||
| 	// TODO: This is a bit of a hack | ||||
| 	// We decided to ignore some mDNS answers for the time being | ||||
| 	// See: https://tools.ietf.org/html/rfc6762#section-7.2 | ||||
| 	msg.Truncated = false | ||||
| 	return s.handleQuery(&msg, from) | ||||
| } | ||||
|  | ||||
| // handleQuery is used to handle an incoming query | ||||
| func (s *Server) handleQuery(query *dns.Msg, from net.Addr) error { | ||||
| 	if query.Opcode != dns.OpcodeQuery { | ||||
| 		// "In both multicast query and multicast response messages, the OPCODE MUST | ||||
| 		// be zero on transmission (only standard queries are currently supported | ||||
| 		// over multicast).  Multicast DNS messages received with an OPCODE other | ||||
| 		// than zero MUST be silently ignored."  Note: OpcodeQuery == 0 | ||||
| 		return fmt.Errorf("mdns: received query with non-zero Opcode %v: %v", query.Opcode, *query) | ||||
| 	} | ||||
| 	if query.Rcode != 0 { | ||||
| 		// "In both multicast query and multicast response messages, the Response | ||||
| 		// Code MUST be zero on transmission.  Multicast DNS messages received with | ||||
| 		// non-zero Response Codes MUST be silently ignored." | ||||
| 		return fmt.Errorf("mdns: received query with non-zero Rcode %v: %v", query.Rcode, *query) | ||||
| 	} | ||||
|  | ||||
| 	// TODO(reddaly): Handle "TC (Truncated) Bit": | ||||
| 	//    In query messages, if the TC bit is set, it means that additional | ||||
| 	//    Known-Answer records may be following shortly.  A responder SHOULD | ||||
| 	//    record this fact, and wait for those additional Known-Answer records, | ||||
| 	//    before deciding whether to respond.  If the TC bit is clear, it means | ||||
| 	//    that the querying host has no additional Known Answers. | ||||
| 	if query.Truncated { | ||||
| 		return fmt.Errorf("[ERR] mdns: support for DNS requests with high truncated bit not implemented: %v", *query) | ||||
| 	} | ||||
|  | ||||
| 	unicastAnswer := make([]dns.RR, 0, len(query.Question)) | ||||
| 	multicastAnswer := make([]dns.RR, 0, len(query.Question)) | ||||
|  | ||||
| 	// Handle each question | ||||
| 	for _, q := range query.Question { | ||||
| 		mrecs, urecs := s.handleQuestion(q) | ||||
| 		multicastAnswer = append(multicastAnswer, mrecs...) | ||||
| 		unicastAnswer = append(unicastAnswer, urecs...) | ||||
| 	} | ||||
|  | ||||
| 	// See section 18 of RFC 6762 for rules about DNS headers. | ||||
| 	resp := func(unicast bool) *dns.Msg { | ||||
| 		// 18.1: ID (Query Identifier) | ||||
| 		// 0 for multicast response, query.Id for unicast response | ||||
| 		id := uint16(0) | ||||
| 		if unicast { | ||||
| 			id = query.Id | ||||
| 		} | ||||
|  | ||||
| 		var answer []dns.RR | ||||
| 		if unicast { | ||||
| 			answer = unicastAnswer | ||||
| 		} else { | ||||
| 			answer = multicastAnswer | ||||
| 		} | ||||
| 		if len(answer) == 0 { | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		return &dns.Msg{ | ||||
| 			MsgHdr: dns.MsgHdr{ | ||||
| 				Id: id, | ||||
|  | ||||
| 				// 18.2: QR (Query/Response) Bit - must be set to 1 in response. | ||||
| 				Response: true, | ||||
|  | ||||
| 				// 18.3: OPCODE - must be zero in response (OpcodeQuery == 0) | ||||
| 				Opcode: dns.OpcodeQuery, | ||||
|  | ||||
| 				// 18.4: AA (Authoritative Answer) Bit - must be set to 1 | ||||
| 				Authoritative: true, | ||||
|  | ||||
| 				// The following fields must all be set to 0: | ||||
| 				// 18.5: TC (TRUNCATED) Bit | ||||
| 				// 18.6: RD (Recursion Desired) Bit | ||||
| 				// 18.7: RA (Recursion Available) Bit | ||||
| 				// 18.8: Z (Zero) Bit | ||||
| 				// 18.9: AD (Authentic Data) Bit | ||||
| 				// 18.10: CD (Checking Disabled) Bit | ||||
| 				// 18.11: RCODE (Response Code) | ||||
| 			}, | ||||
| 			// 18.12 pertains to questions (handled by handleQuestion) | ||||
| 			// 18.13 pertains to resource records (handled by handleQuestion) | ||||
|  | ||||
| 			// 18.14: Name Compression - responses should be compressed (though see | ||||
| 			// caveats in the RFC), so set the Compress bit (part of the dns library | ||||
| 			// API, not part of the DNS packet) to true. | ||||
| 			Compress: true, | ||||
| 			Question: query.Question, | ||||
| 			Answer:   answer, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if mresp := resp(false); mresp != nil { | ||||
| 		if err := s.sendResponse(mresp, from); err != nil { | ||||
| 			return fmt.Errorf("mdns: error sending multicast response: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	if uresp := resp(true); uresp != nil { | ||||
| 		if err := s.sendResponse(uresp, from); err != nil { | ||||
| 			return fmt.Errorf("mdns: error sending unicast response: %v", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // handleQuestion is used to handle an incoming question | ||||
| // | ||||
| // The response to a question may be transmitted over multicast, unicast, or | ||||
| // both.  The return values are DNS records for each transmission type. | ||||
| func (s *Server) handleQuestion(q dns.Question) (multicastRecs, unicastRecs []dns.RR) { | ||||
| 	records := s.config.Zone.Records(q) | ||||
| 	if len(records) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	// Handle unicast and multicast responses. | ||||
| 	// TODO(reddaly): The decision about sending over unicast vs. multicast is not | ||||
| 	// yet fully compliant with RFC 6762.  For example, the unicast bit should be | ||||
| 	// ignored if the records in question are close to TTL expiration.  For now, | ||||
| 	// we just use the unicast bit to make the decision, as per the spec: | ||||
| 	//     RFC 6762, section 18.12.  Repurposing of Top Bit of qclass in Question | ||||
| 	//     Section | ||||
| 	// | ||||
| 	//     In the Question Section of a Multicast DNS query, the top bit of the | ||||
| 	//     qclass field is used to indicate that unicast responses are preferred | ||||
| 	//     for this particular question.  (See Section 5.4.) | ||||
| 	if q.Qclass&(1<<15) != 0 { | ||||
| 		return nil, records | ||||
| 	} | ||||
| 	return records, nil | ||||
| } | ||||
|  | ||||
| func (s *Server) probe() { | ||||
| 	defer s.wg.Done() | ||||
|  | ||||
| 	sd, ok := s.config.Zone.(*MDNSService) | ||||
| 	if !ok { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain)) | ||||
|  | ||||
| 	q := new(dns.Msg) | ||||
| 	q.SetQuestion(name, dns.TypePTR) | ||||
| 	q.RecursionDesired = false | ||||
|  | ||||
| 	srv := &dns.SRV{ | ||||
| 		Hdr: dns.RR_Header{ | ||||
| 			Name:   name, | ||||
| 			Rrtype: dns.TypeSRV, | ||||
| 			Class:  dns.ClassINET, | ||||
| 			Ttl:    defaultTTL, | ||||
| 		}, | ||||
| 		Priority: 0, | ||||
| 		Weight:   0, | ||||
| 		Port:     uint16(sd.Port), | ||||
| 		Target:   sd.HostName, | ||||
| 	} | ||||
| 	txt := &dns.TXT{ | ||||
| 		Hdr: dns.RR_Header{ | ||||
| 			Name:   name, | ||||
| 			Rrtype: dns.TypeTXT, | ||||
| 			Class:  dns.ClassINET, | ||||
| 			Ttl:    defaultTTL, | ||||
| 		}, | ||||
| 		Txt: sd.TXT, | ||||
| 	} | ||||
| 	q.Ns = []dns.RR{srv, txt} | ||||
|  | ||||
| 	randomizer := rand.New(rand.NewSource(time.Now().UnixNano())) | ||||
|  | ||||
| 	for i := 0; i < 3; i++ { | ||||
| 		if err := s.SendMulticast(q); err != nil { | ||||
| 			logger.Errorf(s.config.Context, "[ERR] mdns: failed to send probe: %v", err) | ||||
| 		} | ||||
| 		time.Sleep(time.Duration(randomizer.Intn(250)) * time.Millisecond) | ||||
| 	} | ||||
|  | ||||
| 	resp := new(dns.Msg) | ||||
| 	resp.MsgHdr.Response = true | ||||
|  | ||||
| 	// set for query | ||||
| 	q.SetQuestion(name, dns.TypeANY) | ||||
|  | ||||
| 	resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...) | ||||
|  | ||||
| 	// reset | ||||
| 	q.SetQuestion(name, dns.TypePTR) | ||||
|  | ||||
| 	// From RFC6762 | ||||
| 	//    The Multicast DNS responder MUST send at least two unsolicited | ||||
| 	//    responses, one second apart. To provide increased robustness against | ||||
| 	//    packet loss, a responder MAY send up to eight unsolicited responses, | ||||
| 	//    provided that the interval between unsolicited responses increases by | ||||
| 	//    at least a factor of two with every response sent. | ||||
| 	timeout := 1 * time.Second | ||||
| 	timer := time.NewTimer(timeout) | ||||
| 	for i := 0; i < 3; i++ { | ||||
| 		if err := s.SendMulticast(resp); err != nil { | ||||
| 			logger.Errorf(s.config.Context, "[ERR] mdns: failed to send announcement: %v", err) | ||||
| 		} | ||||
| 		select { | ||||
| 		case <-timer.C: | ||||
| 			timeout *= 2 | ||||
| 			timer.Reset(timeout) | ||||
| 		case <-s.shutdownCh: | ||||
| 			timer.Stop() | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // SendMulticast us used to send a multicast response packet | ||||
| func (s *Server) SendMulticast(msg *dns.Msg) error { | ||||
| 	buf, err := msg.Pack() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if s.ipv4List != nil { | ||||
| 		s.ipv4List.WriteToUDP(buf, ipv4Addr) | ||||
| 	} | ||||
| 	if s.ipv6List != nil { | ||||
| 		s.ipv6List.WriteToUDP(buf, ipv6Addr) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // sendResponse is used to send a response packet | ||||
| func (s *Server) sendResponse(resp *dns.Msg, from net.Addr) error { | ||||
| 	// TODO(reddaly): Respect the unicast argument, and allow sending responses | ||||
| 	// over multicast. | ||||
| 	buf, err := resp.Pack() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Determine the socket to send from | ||||
| 	addr := from.(*net.UDPAddr) | ||||
| 	conn := s.ipv4List | ||||
| 	backupTarget := net.IPv4zero | ||||
|  | ||||
| 	if addr.IP.To4() == nil { | ||||
| 		conn = s.ipv6List | ||||
| 		backupTarget = net.IPv6zero | ||||
| 	} | ||||
| 	_, err = conn.WriteToUDP(buf, addr) | ||||
| 	// If the address we're responding to is this machine then we can also attempt sending on 0.0.0.0 | ||||
| 	// This covers the case where this machine is using a VPN and certain ports are blocked so the response never gets there | ||||
| 	// Sending two responses is OK | ||||
| 	if s.config.LocalhostChecking && addr.IP.Equal(s.outboundIP) { | ||||
| 		// ignore any errors, this is best efforts | ||||
| 		conn.WriteToUDP(buf, &net.UDPAddr{IP: backupTarget, Port: addr.Port}) | ||||
| 	} | ||||
| 	return err | ||||
|  | ||||
| } | ||||
|  | ||||
| func (s *Server) unregister() error { | ||||
| 	sd, ok := s.config.Zone.(*MDNSService) | ||||
| 	if !ok { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	atomic.StoreUint32(&sd.TTL, 0) | ||||
| 	name := fmt.Sprintf("%s.%s.%s.", sd.Instance, trimDot(sd.Service), trimDot(sd.Domain)) | ||||
|  | ||||
| 	q := new(dns.Msg) | ||||
| 	q.SetQuestion(name, dns.TypeANY) | ||||
|  | ||||
| 	resp := new(dns.Msg) | ||||
| 	resp.MsgHdr.Response = true | ||||
| 	resp.Answer = append(resp.Answer, s.config.Zone.Records(q.Question[0])...) | ||||
|  | ||||
| 	return s.SendMulticast(resp) | ||||
| } | ||||
|  | ||||
| func setCustomPort(port int) { | ||||
| 	if port != 0 { | ||||
| 		if mdnsWildcardAddrIPv4.Port != port { | ||||
| 			mdnsWildcardAddrIPv4.Port = port | ||||
| 		} | ||||
| 		if mdnsWildcardAddrIPv6.Port != port { | ||||
| 			mdnsWildcardAddrIPv6.Port = port | ||||
| 		} | ||||
| 		if ipv4Addr.Port != port { | ||||
| 			ipv4Addr.Port = port | ||||
| 		} | ||||
| 		if ipv6Addr.Port != port { | ||||
| 			ipv6Addr.Port = port | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // getOutboundIP returns the IP address of this machine as seen when dialling out | ||||
| func getOutboundIP() net.IP { | ||||
| 	conn, err := net.Dial("udp", "8.8.8.8:80") | ||||
| 	if err != nil { | ||||
| 		// no net connectivity maybe so fallback | ||||
| 		return nil | ||||
| 	} | ||||
| 	defer conn.Close() | ||||
|  | ||||
| 	localAddr := conn.LocalAddr().(*net.UDPAddr) | ||||
|  | ||||
| 	return localAddr.IP | ||||
| } | ||||
| @@ -1,61 +0,0 @@ | ||||
| package mdns | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func TestServer_StartStop(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	serv, err := NewServer(&Config{Zone: s, LocalhostChecking: true}) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	defer serv.Shutdown() | ||||
| } | ||||
|  | ||||
| func TestServer_Lookup(t *testing.T) { | ||||
| 	serv, err := NewServer(&Config{Zone: makeServiceWithServiceName(t, "_foobar._tcp"), LocalhostChecking: true}) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	defer serv.Shutdown() | ||||
|  | ||||
| 	entries := make(chan *ServiceEntry, 1) | ||||
| 	found := false | ||||
| 	doneCh := make(chan struct{}) | ||||
| 	go func() { | ||||
| 		select { | ||||
| 		case e := <-entries: | ||||
| 			if e.Name != "hostname._foobar._tcp.local." { | ||||
| 				t.Fatalf("bad: %v", e) | ||||
| 			} | ||||
| 			if e.Port != 80 { | ||||
| 				t.Fatalf("bad: %v", e) | ||||
| 			} | ||||
| 			if e.Info != "Local web server" { | ||||
| 				t.Fatalf("bad: %v", e) | ||||
| 			} | ||||
| 			found = true | ||||
|  | ||||
| 		case <-time.After(80 * time.Millisecond): | ||||
| 			t.Fatalf("timeout") | ||||
| 		} | ||||
| 		close(doneCh) | ||||
| 	}() | ||||
|  | ||||
| 	params := &QueryParam{ | ||||
| 		Service: "_foobar._tcp", | ||||
| 		Domain:  "local", | ||||
| 		Timeout: 50 * time.Millisecond, | ||||
| 		Entries: entries, | ||||
| 	} | ||||
| 	err = Query(params) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
| 	<-doneCh | ||||
| 	if !found { | ||||
| 		t.Fatalf("record not found") | ||||
| 	} | ||||
| } | ||||
| @@ -1,309 +0,0 @@ | ||||
| package mdns | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"sync/atomic" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// defaultTTL is the default TTL value in returned DNS records in seconds. | ||||
| 	defaultTTL = 120 | ||||
| ) | ||||
|  | ||||
| // Zone is the interface used to integrate with the server and | ||||
| // to serve records dynamically | ||||
| type Zone interface { | ||||
| 	// Records returns DNS records in response to a DNS question. | ||||
| 	Records(q dns.Question) []dns.RR | ||||
| } | ||||
|  | ||||
| // MDNSService is used to export a named service by implementing a Zone | ||||
| type MDNSService struct { | ||||
| 	Instance     string   // Instance name (e.g. "hostService name") | ||||
| 	Service      string   // Service name (e.g. "_http._tcp.") | ||||
| 	Domain       string   // If blank, assumes "local" | ||||
| 	HostName     string   // Host machine DNS name (e.g. "mymachine.net.") | ||||
| 	Port         int      // Service Port | ||||
| 	IPs          []net.IP // IP addresses for the service's host | ||||
| 	TXT          []string // Service TXT records | ||||
| 	TTL          uint32 | ||||
| 	serviceAddr  string // Fully qualified service address | ||||
| 	instanceAddr string // Fully qualified instance address | ||||
| 	enumAddr     string // _services._dns-sd._udp.<domain> | ||||
| } | ||||
|  | ||||
| // validateFQDN returns an error if the passed string is not a fully qualified | ||||
| // hdomain name (more specifically, a hostname). | ||||
| func validateFQDN(s string) error { | ||||
| 	if len(s) == 0 { | ||||
| 		return fmt.Errorf("FQDN must not be blank") | ||||
| 	} | ||||
| 	if s[len(s)-1] != '.' { | ||||
| 		return fmt.Errorf("FQDN must end in period: %s", s) | ||||
| 	} | ||||
| 	// TODO(reddaly): Perform full validation. | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // NewMDNSService returns a new instance of MDNSService. | ||||
| // | ||||
| // If domain, hostName, or ips is set to the zero value, then a default value | ||||
| // will be inferred from the operating system. | ||||
| // | ||||
| // TODO(reddaly): This interface may need to change to account for "unique | ||||
| // record" conflict rules of the mDNS protocol.  Upon startup, the server should | ||||
| // check to ensure that the instance name does not conflict with other instance | ||||
| // names, and, if required, select a new name.  There may also be conflicting | ||||
| // hostName A/AAAA records. | ||||
| func NewMDNSService(instance, service, domain, hostName string, port int, ips []net.IP, txt []string) (*MDNSService, error) { | ||||
| 	// Sanity check inputs | ||||
| 	if instance == "" { | ||||
| 		return nil, fmt.Errorf("missing service instance name") | ||||
| 	} | ||||
| 	if service == "" { | ||||
| 		return nil, fmt.Errorf("missing service name") | ||||
| 	} | ||||
| 	if port == 0 { | ||||
| 		return nil, fmt.Errorf("missing service port") | ||||
| 	} | ||||
|  | ||||
| 	// Set default domain | ||||
| 	if domain == "" { | ||||
| 		domain = "local." | ||||
| 	} | ||||
| 	if err := validateFQDN(domain); err != nil { | ||||
| 		return nil, fmt.Errorf("domain %q is not a fully-qualified domain name: %v", domain, err) | ||||
| 	} | ||||
|  | ||||
| 	// Get host information if no host is specified. | ||||
| 	if hostName == "" { | ||||
| 		var err error | ||||
| 		hostName, err = os.Hostname() | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("could not determine host: %v", err) | ||||
| 		} | ||||
| 		hostName = fmt.Sprintf("%s.", hostName) | ||||
| 	} | ||||
| 	if err := validateFQDN(hostName); err != nil { | ||||
| 		return nil, fmt.Errorf("hostName %q is not a fully-qualified domain name: %v", hostName, err) | ||||
| 	} | ||||
|  | ||||
| 	if len(ips) == 0 { | ||||
| 		var err error | ||||
| 		ips, err = net.LookupIP(trimDot(hostName)) | ||||
| 		if err != nil { | ||||
| 			// Try appending the host domain suffix and lookup again | ||||
| 			// (required for Linux-based hosts) | ||||
| 			tmpHostName := fmt.Sprintf("%s%s", hostName, domain) | ||||
|  | ||||
| 			ips, err = net.LookupIP(trimDot(tmpHostName)) | ||||
|  | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("could not determine host IP addresses for %s", hostName) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	for _, ip := range ips { | ||||
| 		if ip.To4() == nil && ip.To16() == nil { | ||||
| 			return nil, fmt.Errorf("invalid IP address in IPs list: %v", ip) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &MDNSService{ | ||||
| 		Instance:     instance, | ||||
| 		Service:      service, | ||||
| 		Domain:       domain, | ||||
| 		HostName:     hostName, | ||||
| 		Port:         port, | ||||
| 		IPs:          ips, | ||||
| 		TXT:          txt, | ||||
| 		TTL:          defaultTTL, | ||||
| 		serviceAddr:  fmt.Sprintf("%s.%s.", trimDot(service), trimDot(domain)), | ||||
| 		instanceAddr: fmt.Sprintf("%s.%s.%s.", instance, trimDot(service), trimDot(domain)), | ||||
| 		enumAddr:     fmt.Sprintf("_services._dns-sd._udp.%s.", trimDot(domain)), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // trimDot is used to trim the dots from the start or end of a string | ||||
| func trimDot(s string) string { | ||||
| 	return strings.Trim(s, ".") | ||||
| } | ||||
|  | ||||
| // Records returns DNS records in response to a DNS question. | ||||
| func (m *MDNSService) Records(q dns.Question) []dns.RR { | ||||
| 	switch q.Name { | ||||
| 	case m.enumAddr: | ||||
| 		return m.serviceEnum(q) | ||||
| 	case m.serviceAddr: | ||||
| 		return m.serviceRecords(q) | ||||
| 	case m.instanceAddr: | ||||
| 		return m.instanceRecords(q) | ||||
| 	case m.HostName: | ||||
| 		if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA { | ||||
| 			return m.instanceRecords(q) | ||||
| 		} | ||||
| 		fallthrough | ||||
| 	default: | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *MDNSService) serviceEnum(q dns.Question) []dns.RR { | ||||
| 	switch q.Qtype { | ||||
| 	case dns.TypeANY: | ||||
| 		fallthrough | ||||
| 	case dns.TypePTR: | ||||
| 		rr := &dns.PTR{ | ||||
| 			Hdr: dns.RR_Header{ | ||||
| 				Name:   q.Name, | ||||
| 				Rrtype: dns.TypePTR, | ||||
| 				Class:  dns.ClassINET, | ||||
| 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||
| 			}, | ||||
| 			Ptr: m.serviceAddr, | ||||
| 		} | ||||
| 		return []dns.RR{rr} | ||||
| 	default: | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // serviceRecords is called when the query matches the service name | ||||
| func (m *MDNSService) serviceRecords(q dns.Question) []dns.RR { | ||||
| 	switch q.Qtype { | ||||
| 	case dns.TypeANY: | ||||
| 		fallthrough | ||||
| 	case dns.TypePTR: | ||||
| 		// Build a PTR response for the service | ||||
| 		rr := &dns.PTR{ | ||||
| 			Hdr: dns.RR_Header{ | ||||
| 				Name:   q.Name, | ||||
| 				Rrtype: dns.TypePTR, | ||||
| 				Class:  dns.ClassINET, | ||||
| 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||
| 			}, | ||||
| 			Ptr: m.instanceAddr, | ||||
| 		} | ||||
| 		servRec := []dns.RR{rr} | ||||
|  | ||||
| 		// Get the instance records | ||||
| 		instRecs := m.instanceRecords(dns.Question{ | ||||
| 			Name:  m.instanceAddr, | ||||
| 			Qtype: dns.TypeANY, | ||||
| 		}) | ||||
|  | ||||
| 		// Return the service record with the instance records | ||||
| 		return append(servRec, instRecs...) | ||||
| 	default: | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // serviceRecords is called when the query matches the instance name | ||||
| func (m *MDNSService) instanceRecords(q dns.Question) []dns.RR { | ||||
| 	switch q.Qtype { | ||||
| 	case dns.TypeANY: | ||||
| 		// Get the SRV, which includes A and AAAA | ||||
| 		recs := m.instanceRecords(dns.Question{ | ||||
| 			Name:  m.instanceAddr, | ||||
| 			Qtype: dns.TypeSRV, | ||||
| 		}) | ||||
|  | ||||
| 		// Add the TXT record | ||||
| 		recs = append(recs, m.instanceRecords(dns.Question{ | ||||
| 			Name:  m.instanceAddr, | ||||
| 			Qtype: dns.TypeTXT, | ||||
| 		})...) | ||||
| 		return recs | ||||
|  | ||||
| 	case dns.TypeA: | ||||
| 		var rr []dns.RR | ||||
| 		for _, ip := range m.IPs { | ||||
| 			if ip4 := ip.To4(); ip4 != nil { | ||||
| 				rr = append(rr, &dns.A{ | ||||
| 					Hdr: dns.RR_Header{ | ||||
| 						Name:   m.HostName, | ||||
| 						Rrtype: dns.TypeA, | ||||
| 						Class:  dns.ClassINET, | ||||
| 						Ttl:    atomic.LoadUint32(&m.TTL), | ||||
| 					}, | ||||
| 					A: ip4, | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 		return rr | ||||
|  | ||||
| 	case dns.TypeAAAA: | ||||
| 		var rr []dns.RR | ||||
| 		for _, ip := range m.IPs { | ||||
| 			if ip.To4() != nil { | ||||
| 				// TODO(reddaly): IPv4 addresses could be encoded in IPv6 format and | ||||
| 				// putinto AAAA records, but the current logic puts ipv4-encodable | ||||
| 				// addresses into the A records exclusively.  Perhaps this should be | ||||
| 				// configurable? | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			if ip16 := ip.To16(); ip16 != nil { | ||||
| 				rr = append(rr, &dns.AAAA{ | ||||
| 					Hdr: dns.RR_Header{ | ||||
| 						Name:   m.HostName, | ||||
| 						Rrtype: dns.TypeAAAA, | ||||
| 						Class:  dns.ClassINET, | ||||
| 						Ttl:    atomic.LoadUint32(&m.TTL), | ||||
| 					}, | ||||
| 					AAAA: ip16, | ||||
| 				}) | ||||
| 			} | ||||
| 		} | ||||
| 		return rr | ||||
|  | ||||
| 	case dns.TypeSRV: | ||||
| 		// Create the SRV Record | ||||
| 		srv := &dns.SRV{ | ||||
| 			Hdr: dns.RR_Header{ | ||||
| 				Name:   q.Name, | ||||
| 				Rrtype: dns.TypeSRV, | ||||
| 				Class:  dns.ClassINET, | ||||
| 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||
| 			}, | ||||
| 			Priority: 10, | ||||
| 			Weight:   1, | ||||
| 			Port:     uint16(m.Port), | ||||
| 			Target:   m.HostName, | ||||
| 		} | ||||
| 		recs := []dns.RR{srv} | ||||
|  | ||||
| 		// Add the A record | ||||
| 		recs = append(recs, m.instanceRecords(dns.Question{ | ||||
| 			Name:  m.instanceAddr, | ||||
| 			Qtype: dns.TypeA, | ||||
| 		})...) | ||||
|  | ||||
| 		// Add the AAAA record | ||||
| 		recs = append(recs, m.instanceRecords(dns.Question{ | ||||
| 			Name:  m.instanceAddr, | ||||
| 			Qtype: dns.TypeAAAA, | ||||
| 		})...) | ||||
| 		return recs | ||||
|  | ||||
| 	case dns.TypeTXT: | ||||
| 		txt := &dns.TXT{ | ||||
| 			Hdr: dns.RR_Header{ | ||||
| 				Name:   q.Name, | ||||
| 				Rrtype: dns.TypeTXT, | ||||
| 				Class:  dns.ClassINET, | ||||
| 				Ttl:    atomic.LoadUint32(&m.TTL), | ||||
| 			}, | ||||
| 			Txt: m.TXT, | ||||
| 		} | ||||
| 		return []dns.RR{txt} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @@ -1,275 +0,0 @@ | ||||
| package mdns | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"net" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| ) | ||||
|  | ||||
| func makeService(t *testing.T) *MDNSService { | ||||
| 	return makeServiceWithServiceName(t, "_http._tcp") | ||||
| } | ||||
|  | ||||
| func makeServiceWithServiceName(t *testing.T, service string) *MDNSService { | ||||
| 	m, err := NewMDNSService( | ||||
| 		"hostname", | ||||
| 		service, | ||||
| 		"local.", | ||||
| 		"testhost.", | ||||
| 		80, // port | ||||
| 		[]net.IP{net.IP([]byte{192, 168, 0, 42}), net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc")}, | ||||
| 		[]string{"Local web server"}) // TXT | ||||
|  | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return m | ||||
| } | ||||
|  | ||||
| func TestNewMDNSService_BadParams(t *testing.T) { | ||||
| 	for _, test := range []struct { | ||||
| 		testName string | ||||
| 		hostName string | ||||
| 		domain   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"NewMDNSService should fail when passed hostName that is not a legal fully-qualified domain name", | ||||
| 			"hostname", // not legal FQDN - should be "hostname." or "hostname.local.", etc. | ||||
| 			"local.",   // legal | ||||
| 		}, | ||||
| 		{ | ||||
| 			"NewMDNSService should fail when passed domain that is not a legal fully-qualified domain name", | ||||
| 			"hostname.", // legal | ||||
| 			"local",     // should be "local." | ||||
| 		}, | ||||
| 	} { | ||||
| 		_, err := NewMDNSService( | ||||
| 			"instance name", | ||||
| 			"_http._tcp", | ||||
| 			test.domain, | ||||
| 			test.hostName, | ||||
| 			80, // port | ||||
| 			[]net.IP{net.IP([]byte{192, 168, 0, 42})}, | ||||
| 			[]string{"Local web server"}) // TXT | ||||
| 		if err == nil { | ||||
| 			t.Fatalf("%s: error expected, but got none", test.testName) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_BadAddr(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "random", | ||||
| 		Qtype: dns.TypeANY, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if len(recs) != 0 { | ||||
| 		t.Fatalf("bad: %v", recs) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_ServiceAddr(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "_http._tcp.local.", | ||||
| 		Qtype: dns.TypeANY, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if got, want := len(recs), 5; got != want { | ||||
| 		t.Fatalf("got %d records, want %d: %v", got, want, recs) | ||||
| 	} | ||||
|  | ||||
| 	if ptr, ok := recs[0].(*dns.PTR); !ok { | ||||
| 		t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs) | ||||
| 	} else if got, want := ptr.Ptr, "hostname._http._tcp.local."; got != want { | ||||
| 		t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want) | ||||
| 	} | ||||
|  | ||||
| 	if _, ok := recs[1].(*dns.SRV); !ok { | ||||
| 		t.Errorf("recs[1] should be SRV record, got: %v, all reccords: %v", recs[1], recs) | ||||
| 	} | ||||
| 	if _, ok := recs[2].(*dns.A); !ok { | ||||
| 		t.Errorf("recs[2] should be A record, got: %v, all records: %v", recs[2], recs) | ||||
| 	} | ||||
| 	if _, ok := recs[3].(*dns.AAAA); !ok { | ||||
| 		t.Errorf("recs[3] should be AAAA record, got: %v, all records: %v", recs[3], recs) | ||||
| 	} | ||||
| 	if _, ok := recs[4].(*dns.TXT); !ok { | ||||
| 		t.Errorf("recs[4] should be TXT record, got: %v, all records: %v", recs[4], recs) | ||||
| 	} | ||||
|  | ||||
| 	q.Qtype = dns.TypePTR | ||||
| 	if recs2 := s.Records(q); !reflect.DeepEqual(recs, recs2) { | ||||
| 		t.Fatalf("PTR question should return same result as ANY question: ANY => %v, PTR => %v", recs, recs2) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_InstanceAddr_ANY(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "hostname._http._tcp.local.", | ||||
| 		Qtype: dns.TypeANY, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if len(recs) != 4 { | ||||
| 		t.Fatalf("bad: %v", recs) | ||||
| 	} | ||||
| 	if _, ok := recs[0].(*dns.SRV); !ok { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| 	if _, ok := recs[1].(*dns.A); !ok { | ||||
| 		t.Fatalf("bad: %v", recs[1]) | ||||
| 	} | ||||
| 	if _, ok := recs[2].(*dns.AAAA); !ok { | ||||
| 		t.Fatalf("bad: %v", recs[2]) | ||||
| 	} | ||||
| 	if _, ok := recs[3].(*dns.TXT); !ok { | ||||
| 		t.Fatalf("bad: %v", recs[3]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_InstanceAddr_SRV(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "hostname._http._tcp.local.", | ||||
| 		Qtype: dns.TypeSRV, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if len(recs) != 3 { | ||||
| 		t.Fatalf("bad: %v", recs) | ||||
| 	} | ||||
| 	srv, ok := recs[0].(*dns.SRV) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| 	if _, ok := recs[1].(*dns.A); !ok { | ||||
| 		t.Fatalf("bad: %v", recs[1]) | ||||
| 	} | ||||
| 	if _, ok := recs[2].(*dns.AAAA); !ok { | ||||
| 		t.Fatalf("bad: %v", recs[2]) | ||||
| 	} | ||||
|  | ||||
| 	if srv.Port != uint16(s.Port) { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_InstanceAddr_A(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "hostname._http._tcp.local.", | ||||
| 		Qtype: dns.TypeA, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if len(recs) != 1 { | ||||
| 		t.Fatalf("bad: %v", recs) | ||||
| 	} | ||||
| 	a, ok := recs[0].(*dns.A) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| 	if !bytes.Equal(a.A, []byte{192, 168, 0, 42}) { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_InstanceAddr_AAAA(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "hostname._http._tcp.local.", | ||||
| 		Qtype: dns.TypeAAAA, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if len(recs) != 1 { | ||||
| 		t.Fatalf("bad: %v", recs) | ||||
| 	} | ||||
| 	a4, ok := recs[0].(*dns.AAAA) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| 	ip6 := net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc") | ||||
| 	if got := len(ip6); got != net.IPv6len { | ||||
| 		t.Fatalf("test IP failed to parse (len = %d, want %d)", got, net.IPv6len) | ||||
| 	} | ||||
| 	if !bytes.Equal(a4.AAAA, ip6) { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_InstanceAddr_TXT(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "hostname._http._tcp.local.", | ||||
| 		Qtype: dns.TypeTXT, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if len(recs) != 1 { | ||||
| 		t.Fatalf("bad: %v", recs) | ||||
| 	} | ||||
| 	txt, ok := recs[0].(*dns.TXT) | ||||
| 	if !ok { | ||||
| 		t.Fatalf("bad: %v", recs[0]) | ||||
| 	} | ||||
| 	if got, want := txt.Txt, s.TXT; !reflect.DeepEqual(got, want) { | ||||
| 		t.Fatalf("TXT record mismatch for %v: got %v, want %v", recs[0], got, want) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_HostNameQuery(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	for _, test := range []struct { | ||||
| 		q    dns.Question | ||||
| 		want []dns.RR | ||||
| 	}{ | ||||
| 		{ | ||||
| 			dns.Question{Name: "testhost.", Qtype: dns.TypeA}, | ||||
| 			[]dns.RR{&dns.A{ | ||||
| 				Hdr: dns.RR_Header{ | ||||
| 					Name:   "testhost.", | ||||
| 					Rrtype: dns.TypeA, | ||||
| 					Class:  dns.ClassINET, | ||||
| 					Ttl:    120, | ||||
| 				}, | ||||
| 				A: net.IP([]byte{192, 168, 0, 42}), | ||||
| 			}}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			dns.Question{Name: "testhost.", Qtype: dns.TypeAAAA}, | ||||
| 			[]dns.RR{&dns.AAAA{ | ||||
| 				Hdr: dns.RR_Header{ | ||||
| 					Name:   "testhost.", | ||||
| 					Rrtype: dns.TypeAAAA, | ||||
| 					Class:  dns.ClassINET, | ||||
| 					Ttl:    120, | ||||
| 				}, | ||||
| 				AAAA: net.ParseIP("2620:0:1000:1900:b0c2:d0b2:c411:18bc"), | ||||
| 			}}, | ||||
| 		}, | ||||
| 	} { | ||||
| 		if got := s.Records(test.q); !reflect.DeepEqual(got, test.want) { | ||||
| 			t.Errorf("hostname query failed: s.Records(%v) = %v, want %v", test.q, got, test.want) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestMDNSService_serviceEnum_PTR(t *testing.T) { | ||||
| 	s := makeService(t) | ||||
| 	q := dns.Question{ | ||||
| 		Name:  "_services._dns-sd._udp.local.", | ||||
| 		Qtype: dns.TypePTR, | ||||
| 	} | ||||
| 	recs := s.Records(q) | ||||
| 	if len(recs) != 1 { | ||||
| 		t.Fatalf("bad: %v", recs) | ||||
| 	} | ||||
| 	if ptr, ok := recs[0].(*dns.PTR); !ok { | ||||
| 		t.Errorf("recs[0] should be PTR record, got: %v, all records: %v", recs[0], recs) | ||||
| 	} else if got, want := ptr.Ptr, "_http._tcp.local."; got != want { | ||||
| 		t.Fatalf("bad PTR record %v: got %v, want %v", ptr, got, want) | ||||
| 	} | ||||
| } | ||||
| @@ -85,12 +85,12 @@ func CSR(opts ...CertOption) ([]byte, error) { | ||||
| } | ||||
|  | ||||
| // Sign decodes a CSR and signs it with the CA | ||||
| func Sign(CACrt, CAKey, CSR []byte, opts ...CertOption) ([]byte, error) { | ||||
| func Sign(crt, key, csr []byte, opts ...CertOption) ([]byte, error) { | ||||
| 	options := CertOptions{} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
| 	asn1CACrt, err := decodePEM(CACrt) | ||||
| 	asn1CACrt, err := decodePEM(crt) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to decode CA Crt PEM: %w", err) | ||||
| 	} | ||||
| @@ -101,7 +101,7 @@ func Sign(CACrt, CAKey, CSR []byte, opts ...CertOption) ([]byte, error) { | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("ca is not a valid certificate: %w", err) | ||||
| 	} | ||||
| 	asn1CAKey, err := decodePEM(CAKey) | ||||
| 	asn1CAKey, err := decodePEM(key) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to decode CA  Key PEM: %w", err) | ||||
| 	} | ||||
| @@ -112,22 +112,22 @@ func Sign(CACrt, CAKey, CSR []byte, opts ...CertOption) ([]byte, error) { | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("ca key is not a valid private key: %w", err) | ||||
| 	} | ||||
| 	asn1CSR, err := decodePEM(CSR) | ||||
| 	asn1CSR, err := decodePEM(csr) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to decode CSR PEM: %w", err) | ||||
| 	} | ||||
| 	if len(asn1CSR) != 1 { | ||||
| 		return nil, fmt.Errorf("expected 1 CSR, got %d", len(asn1CSR)) | ||||
| 	} | ||||
| 	csr, err := x509.ParseCertificateRequest(asn1CSR[0].Bytes) | ||||
| 	caCsr, err := x509.ParseCertificateRequest(asn1CSR[0].Bytes) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("csr is invalid: %w", err) | ||||
| 	} | ||||
| 	template := &x509.Certificate{ | ||||
| 		SignatureAlgorithm:    x509.PureEd25519, | ||||
| 		Subject:               csr.Subject, | ||||
| 		DNSNames:              csr.DNSNames, | ||||
| 		IPAddresses:           csr.IPAddresses, | ||||
| 		Subject:               caCsr.Subject, | ||||
| 		DNSNames:              caCsr.DNSNames, | ||||
| 		IPAddresses:           caCsr.IPAddresses, | ||||
| 		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, | ||||
| 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, | ||||
| 		NotBefore:             options.NotBefore, | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package pki | ||||
|  | ||||
| import ( | ||||
| 	"crypto/ed25519" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/x509" | ||||
| 	"crypto/x509/pkix" | ||||
| @@ -10,22 +9,26 @@ import ( | ||||
| 	"net" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestPrivateKey(t *testing.T) { | ||||
| 	_, _, err := GenerateKey() | ||||
| 	assert.NoError(t, err) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCA(t *testing.T) { | ||||
| 	pub, priv, err := GenerateKey() | ||||
| 	assert.NoError(t, err) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	serialNumberMax := new(big.Int).Lsh(big.NewInt(1), 128) | ||||
| 	serialNumber, err := rand.Int(rand.Reader, serialNumberMax) | ||||
| 	assert.NoError(t, err, "Couldn't generate serial") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	cert, key, err := CA( | ||||
| 		KeyPair(pub, priv), | ||||
| @@ -38,31 +41,57 @@ func TestCA(t *testing.T) { | ||||
| 		NotBefore(time.Now().Add(time.Minute*-1)), | ||||
| 		NotAfter(time.Now().Add(time.Minute)), | ||||
| 	) | ||||
| 	assert.NoError(t, err, "Couldn't sign CA") | ||||
| 	asn1Key, _ := pem.Decode(key) | ||||
| 	assert.NotNil(t, asn1Key, "Couldn't decode key") | ||||
| 	assert.Equal(t, "PRIVATE KEY", asn1Key.Type) | ||||
| 	decodedKey, err := x509.ParsePKCS8PrivateKey(asn1Key.Bytes) | ||||
| 	assert.NoError(t, err, "Couldn't decode ASN1 Key") | ||||
| 	assert.Equal(t, priv, decodedKey.(ed25519.PrivateKey)) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	pool := x509.NewCertPool() | ||||
| 	assert.True(t, pool.AppendCertsFromPEM(cert), "Coudn't parse cert") | ||||
| 	asn1Key, _ := pem.Decode(key) | ||||
| 	if asn1Key == nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	if asn1Key.Type != "PRIVATE KEY" { | ||||
| 		t.Fatal("invalid key type") | ||||
| 	} | ||||
| 	decodedKey, err := x509.ParsePKCS8PrivateKey(asn1Key.Bytes) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} else if decodedKey == nil { | ||||
| 		t.Fatal("empty key") | ||||
| 	} | ||||
|  | ||||
| 	asn1Cert, _ := pem.Decode(cert) | ||||
| 	assert.NotNil(t, asn1Cert, "Couldn't parse pem cert") | ||||
| 	x509cert, err := x509.ParseCertificate(asn1Cert.Bytes) | ||||
| 	assert.NoError(t, err, "Couldn't parse asn1 cert") | ||||
| 	chains, err := x509cert.Verify(x509.VerifyOptions{ | ||||
| 		Roots: pool, | ||||
| 	}) | ||||
| 	assert.NoError(t, err, "Cert didn't verify") | ||||
| 	assert.Len(t, chains, 1, "CA should have 1 cert in chain") | ||||
| 	if asn1Cert == nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	/* | ||||
| 		pool := x509.NewCertPool() | ||||
|  | ||||
| 		x509cert, err := x509.ParseCertificate(asn1Cert.Bytes) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
|  | ||||
|  | ||||
| 		chains, err := x509cert.Verify(x509.VerifyOptions{ | ||||
| 			Roots: pool, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if len(chains) != 1 { | ||||
| 			t.Fatal("CA should have 1 cert in chain") | ||||
| 		} | ||||
| 	*/ | ||||
| } | ||||
|  | ||||
| func TestCSR(t *testing.T) { | ||||
| 	pub, priv, err := GenerateKey() | ||||
| 	assert.NoError(t, err) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	csr, err := CSR( | ||||
| 		Subject( | ||||
| 			pkix.Name{ | ||||
| @@ -75,16 +104,26 @@ func TestCSR(t *testing.T) { | ||||
| 		IPAddresses(net.ParseIP("127.0.0.1")), | ||||
| 		KeyPair(pub, priv), | ||||
| 	) | ||||
| 	assert.NoError(t, err, "CSR couldn't be encoded") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	asn1csr, _ := pem.Decode(csr) | ||||
| 	assert.NotNil(t, asn1csr) | ||||
| 	if asn1csr == nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	decodedcsr, err := x509.ParseCertificateRequest(asn1csr.Bytes) | ||||
| 	assert.NoError(t, err) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	expected := pkix.Name{ | ||||
| 		CommonName:         "testnode", | ||||
| 		Organization:       []string{"microtest"}, | ||||
| 		OrganizationalUnit: []string{"super-testers"}, | ||||
| 	} | ||||
| 	assert.Equal(t, decodedcsr.Subject.String(), expected.String()) | ||||
| 	if decodedcsr.Subject.String() != expected.String() { | ||||
| 		t.Fatalf("%s != %s", decodedcsr.Subject.String(), expected.String()) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,52 +0,0 @@ | ||||
| package scope | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/unistack-org/micro/v3/store" | ||||
| ) | ||||
|  | ||||
| // Scope extends the store, applying a prefix to each request | ||||
| type Scope struct { | ||||
| 	store.Store | ||||
| 	prefix string | ||||
| } | ||||
|  | ||||
| // NewScope returns an initialised scope | ||||
| func NewScope(s store.Store, prefix string) Scope { | ||||
| 	return Scope{Store: s, prefix: prefix} | ||||
| } | ||||
|  | ||||
| func (s *Scope) Options() store.Options { | ||||
| 	o := s.Store.Options() | ||||
| 	o.Table = s.prefix | ||||
| 	return o | ||||
| } | ||||
|  | ||||
| func (s *Scope) Read(ctx context.Context, key string, val interface{}, opts ...store.ReadOption) error { | ||||
| 	key = fmt.Sprintf("%v/%v", s.prefix, key) | ||||
| 	return s.Store.Read(ctx, key, val, opts...) | ||||
| } | ||||
|  | ||||
| func (s *Scope) Write(ctx context.Context, key string, val interface{}, opts ...store.WriteOption) error { | ||||
| 	key = fmt.Sprintf("%v/%v", s.prefix, key) | ||||
| 	return s.Store.Write(ctx, key, val, opts...) | ||||
| } | ||||
|  | ||||
| func (s *Scope) Delete(ctx context.Context, key string, opts ...store.DeleteOption) error { | ||||
| 	key = fmt.Sprintf("%v/%v", s.prefix, key) | ||||
| 	return s.Store.Delete(ctx, key, opts...) | ||||
| } | ||||
|  | ||||
| func (s *Scope) List(ctx context.Context, opts ...store.ListOption) ([]string, error) { | ||||
| 	var lops store.ListOptions | ||||
| 	for _, o := range opts { | ||||
| 		o(&lops) | ||||
| 	} | ||||
|  | ||||
| 	key := fmt.Sprintf("%v/%v", s.prefix, lops.Prefix) | ||||
| 	opts = append(opts, store.ListPrefix(key)) | ||||
|  | ||||
| 	return s.Store.List(ctx, opts...) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user