many lint fixes and optimizations (#17)
* util/kubernetes: drop stale files * debug/log/kubernetes: drop stale files * util/scope: remove stale files * util/mdns: drop stale files * lint fixes Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
This commit is contained in:
parent
abb9937787
commit
82248eb3b0
@ -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...)
|
||||
}
|
Loading…
Reference in New Issue
Block a user