2019-12-24 20:45:17 +03:00
|
|
|
// Package client provides an implementation of a restricted subset of kubernetes API client
|
2019-11-02 16:25:10 +03:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
2019-11-15 16:41:40 +03:00
|
|
|
"bytes"
|
2019-11-02 16:25:10 +03:00
|
|
|
"crypto/tls"
|
|
|
|
"errors"
|
2019-12-17 19:09:51 +03:00
|
|
|
"io"
|
2019-11-02 16:25:10 +03:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path"
|
2019-12-24 20:45:17 +03:00
|
|
|
"strings"
|
2019-11-02 16:25:10 +03:00
|
|
|
|
2020-01-30 14:39:00 +03:00
|
|
|
"github.com/micro/go-micro/v2/util/kubernetes/api"
|
|
|
|
"github.com/micro/go-micro/v2/util/log"
|
2019-11-02 16:25:10 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2019-11-15 16:41:40 +03:00
|
|
|
// path to kubernetes service account token
|
2019-11-02 16:25:10 +03:00
|
|
|
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")
|
2019-12-24 20:45:17 +03:00
|
|
|
// DefaultImage is default micro image
|
|
|
|
DefaultImage = "micro/go-micro"
|
2019-11-02 16:25:10 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// Client ...
|
|
|
|
type client struct {
|
|
|
|
opts *api.Options
|
|
|
|
}
|
|
|
|
|
2019-12-24 20:45:17 +03:00
|
|
|
// Kubernetes client
|
|
|
|
type Client interface {
|
|
|
|
// Create creates new API resource
|
|
|
|
Create(*Resource) error
|
|
|
|
// Get queries API resrouces
|
|
|
|
Get(*Resource, map[string]string) error
|
|
|
|
// Update patches existing API object
|
|
|
|
Update(*Resource) error
|
|
|
|
// Delete deletes API resource
|
|
|
|
Delete(*Resource) error
|
|
|
|
// List lists API resources
|
|
|
|
List(*Resource) error
|
|
|
|
// Log gets log for a pod
|
|
|
|
Log(*Resource, ...LogOption) (io.ReadCloser, error)
|
2019-12-27 23:08:46 +03:00
|
|
|
// Watch for events
|
|
|
|
Watch(*Resource, ...WatchOption) (Watcher, error)
|
2019-11-02 16:25:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func detectNamespace() (string, error) {
|
|
|
|
nsPath := path.Join(serviceAccountPath, "namespace")
|
|
|
|
|
|
|
|
// Make sure it's a file and we can read it
|
|
|
|
if s, e := os.Stat(nsPath); e != nil {
|
|
|
|
return "", e
|
|
|
|
} else if s.IsDir() {
|
|
|
|
return "", ErrReadNamespace
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read the file, and cast to a string
|
|
|
|
if ns, e := ioutil.ReadFile(nsPath); e != nil {
|
|
|
|
return string(ns), e
|
|
|
|
} else {
|
|
|
|
return string(ns), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-15 16:41:40 +03:00
|
|
|
// Create creates new API object
|
|
|
|
func (c *client) Create(r *Resource) error {
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
if err := renderTemplate(r.Kind, b, r.Value); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-11-02 16:25:10 +03:00
|
|
|
return api.NewRequest(c.opts).
|
2019-11-15 16:41:40 +03:00
|
|
|
Post().
|
|
|
|
SetHeader("Content-Type", "application/yaml").
|
|
|
|
Resource(r.Kind).
|
|
|
|
Body(b).
|
2019-11-02 16:25:10 +03:00
|
|
|
Do().
|
|
|
|
Error()
|
|
|
|
}
|
2019-11-07 10:44:57 +03:00
|
|
|
|
2019-11-15 16:41:40 +03:00
|
|
|
// Get queries API objects and stores the result in r
|
|
|
|
func (c *client) Get(r *Resource, labels map[string]string) error {
|
|
|
|
return api.NewRequest(c.opts).
|
2019-11-07 10:44:57 +03:00
|
|
|
Get().
|
2019-11-15 16:41:40 +03:00
|
|
|
Resource(r.Kind).
|
2019-11-07 10:44:57 +03:00
|
|
|
Params(&api.Params{LabelSelector: labels}).
|
|
|
|
Do().
|
2019-11-15 16:41:40 +03:00
|
|
|
Into(r.Value)
|
|
|
|
}
|
2019-11-07 10:44:57 +03:00
|
|
|
|
2019-12-24 20:33:05 +03:00
|
|
|
// Log returns logs for a pod
|
|
|
|
func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) {
|
|
|
|
var options LogOptions
|
|
|
|
for _, o := range opts {
|
|
|
|
o(&options)
|
2019-12-21 02:16:05 +03:00
|
|
|
}
|
2019-12-24 20:33:05 +03:00
|
|
|
|
2019-12-17 19:09:51 +03:00
|
|
|
req := api.NewRequest(c.opts).
|
|
|
|
Get().
|
2019-12-24 20:33:05 +03:00
|
|
|
Resource(r.Kind).
|
2019-12-17 19:09:51 +03:00
|
|
|
SubResource("log").
|
2019-12-24 20:33:05 +03:00
|
|
|
Name(r.Name)
|
2019-12-17 19:09:51 +03:00
|
|
|
|
2019-12-24 20:33:05 +03:00
|
|
|
if options.Params != nil {
|
|
|
|
req.Params(&api.Params{Additional: options.Params})
|
2019-12-21 02:16:05 +03:00
|
|
|
}
|
|
|
|
|
2019-12-17 19:09:51 +03:00
|
|
|
resp, err := req.Raw()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-12-21 02:16:05 +03:00
|
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
|
resp.Body.Close()
|
|
|
|
return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status)
|
|
|
|
}
|
2019-12-17 19:09:51 +03:00
|
|
|
return resp.Body, nil
|
|
|
|
}
|
|
|
|
|
2019-11-15 16:41:40 +03:00
|
|
|
// Update updates API object
|
|
|
|
func (c *client) Update(r *Resource) error {
|
|
|
|
req := api.NewRequest(c.opts).
|
|
|
|
Patch().
|
|
|
|
SetHeader("Content-Type", "application/strategic-merge-patch+json").
|
|
|
|
Resource(r.Kind).
|
|
|
|
Name(r.Name)
|
|
|
|
|
|
|
|
switch r.Kind {
|
|
|
|
case "service":
|
2019-11-27 01:28:08 +03:00
|
|
|
req.Body(r.Value.(*Service))
|
2019-11-15 16:41:40 +03:00
|
|
|
case "deployment":
|
2019-11-27 01:28:08 +03:00
|
|
|
req.Body(r.Value.(*Deployment))
|
2019-12-27 23:08:46 +03:00
|
|
|
case "pod":
|
|
|
|
req.Body(r.Value.(*Pod))
|
2019-11-15 16:41:40 +03:00
|
|
|
default:
|
|
|
|
return errors.New("unsupported resource")
|
|
|
|
}
|
|
|
|
|
|
|
|
return req.Do().Error()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete removes API object
|
|
|
|
func (c *client) Delete(r *Resource) error {
|
|
|
|
return api.NewRequest(c.opts).
|
|
|
|
Delete().
|
|
|
|
Resource(r.Kind).
|
|
|
|
Name(r.Name).
|
|
|
|
Do().
|
|
|
|
Error()
|
|
|
|
}
|
|
|
|
|
|
|
|
// List lists API objects and stores the result in r
|
|
|
|
func (c *client) List(r *Resource) error {
|
|
|
|
labels := map[string]string{
|
|
|
|
"micro": "service",
|
|
|
|
}
|
2019-11-22 20:10:00 +03:00
|
|
|
return c.Get(r, labels)
|
2019-11-07 10:44:57 +03:00
|
|
|
}
|
2019-12-24 20:45:17 +03:00
|
|
|
|
2019-12-27 23:08:46 +03:00
|
|
|
// Watch returns an event stream
|
|
|
|
func (c *client) Watch(r *Resource, opts ...WatchOption) (Watcher, error) {
|
|
|
|
var options WatchOptions
|
|
|
|
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).
|
|
|
|
Params(params)
|
|
|
|
|
|
|
|
return newWatcher(req)
|
|
|
|
}
|
|
|
|
|
2019-12-24 20:45:17 +03:00
|
|
|
// NewService returns default micro kubernetes service definition
|
|
|
|
func NewService(name, version, typ string) *Service {
|
|
|
|
log.Tracef("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}, "-")
|
|
|
|
}
|
|
|
|
|
|
|
|
Metadata := &Metadata{
|
|
|
|
Name: svcName,
|
|
|
|
Namespace: "default",
|
|
|
|
Version: version,
|
|
|
|
Labels: Labels,
|
|
|
|
}
|
|
|
|
|
|
|
|
Spec := &ServiceSpec{
|
|
|
|
Type: "ClusterIP",
|
|
|
|
Selector: Labels,
|
|
|
|
Ports: []ServicePort{{
|
|
|
|
"service-port", 9090, "",
|
|
|
|
}},
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Service{
|
|
|
|
Metadata: Metadata,
|
|
|
|
Spec: Spec,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewService returns default micro kubernetes deployment definition
|
2020-02-06 12:17:10 +03:00
|
|
|
func NewDeployment(name, version, typ string) *Deployment {
|
2019-12-24 20:45:17 +03:00
|
|
|
log.Tracef("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}, "-")
|
|
|
|
}
|
|
|
|
|
|
|
|
Metadata := &Metadata{
|
|
|
|
Name: depName,
|
|
|
|
Namespace: "default",
|
|
|
|
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,
|
2020-02-06 12:17:10 +03:00
|
|
|
Image: DefaultImage,
|
2019-12-24 20:45:17 +03:00
|
|
|
Env: []EnvVar{env},
|
|
|
|
Command: []string{"go", "run", "main.go"},
|
|
|
|
Ports: []ContainerPort{{
|
|
|
|
Name: "service-port",
|
|
|
|
ContainerPort: 8080,
|
|
|
|
}},
|
|
|
|
}},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return &Deployment{
|
|
|
|
Metadata: Metadata,
|
|
|
|
Spec: Spec,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-27 23:08:46 +03:00
|
|
|
// NewLocalClient returns a client that can be used with `kubectl proxy`
|
|
|
|
func NewLocalClient(hosts ...string) *client {
|
|
|
|
if len(hosts) == 0 {
|
|
|
|
hosts[0] = "http://localhost:8001"
|
2019-12-24 20:45:17 +03:00
|
|
|
}
|
|
|
|
return &client{
|
|
|
|
opts: &api.Options{
|
|
|
|
Client: http.DefaultClient,
|
2019-12-27 23:08:46 +03:00
|
|
|
Host: hosts[0],
|
2019-12-24 20:45:17 +03:00
|
|
|
Namespace: "default",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-27 23:08:46 +03:00
|
|
|
// NewClusterClient creates a Kubernetes client for use from within a k8s pod.
|
|
|
|
func NewClusterClient() *client {
|
2019-12-24 20:45:17 +03:00
|
|
|
host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT")
|
|
|
|
|
|
|
|
s, err := os.Stat(serviceAccountPath)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
if s == nil || !s.IsDir() {
|
|
|
|
log.Fatal(errors.New("service account not found"))
|
|
|
|
}
|
|
|
|
|
|
|
|
token, err := ioutil.ReadFile(path.Join(serviceAccountPath, "token"))
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
t := string(token)
|
|
|
|
|
|
|
|
ns, err := detectNamespace()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt"))
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
c := &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
RootCAs: crt,
|
|
|
|
},
|
|
|
|
DisableCompression: true,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return &client{
|
|
|
|
opts: &api.Options{
|
|
|
|
Client: c,
|
|
|
|
Host: host,
|
|
|
|
Namespace: ns,
|
|
|
|
BearerToken: &t,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|