many lint fixes and optimizations #17
@ -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
|
||||
}
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user