github.com/annwntech/go-micro/v2@v2.9.5/util/kubernetes/client/client.go (about)

     1  // Package client provides an implementation of a restricted subset of kubernetes API client
     2  package client
     3  
     4  import (
     5  	"bytes"
     6  	"crypto/tls"
     7  	"errors"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"os"
    12  	"path"
    13  	"regexp"
    14  	"strings"
    15  
    16  	"github.com/annwntech/go-micro/v2/logger"
    17  	"github.com/annwntech/go-micro/v2/util/kubernetes/api"
    18  )
    19  
    20  var (
    21  	// path to kubernetes service account token
    22  	serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
    23  	// ErrReadNamespace is returned when the names could not be read from service account
    24  	ErrReadNamespace = errors.New("Could not read namespace from service account secret")
    25  	// DefaultImage is default micro image
    26  	DefaultImage = "micro/go-micro"
    27  	// DefaultNamespace is the default k8s namespace
    28  	DefaultNamespace = "default"
    29  )
    30  
    31  // Client ...
    32  type client struct {
    33  	opts *api.Options
    34  }
    35  
    36  // Kubernetes client
    37  type Client interface {
    38  	// Create creates new API resource
    39  	Create(*Resource, ...CreateOption) error
    40  	// Get queries API resrouces
    41  	Get(*Resource, ...GetOption) error
    42  	// Update patches existing API object
    43  	Update(*Resource, ...UpdateOption) error
    44  	// Delete deletes API resource
    45  	Delete(*Resource, ...DeleteOption) error
    46  	// List lists API resources
    47  	List(*Resource, ...ListOption) error
    48  	// Log gets log for a pod
    49  	Log(*Resource, ...LogOption) (io.ReadCloser, error)
    50  	// Watch for events
    51  	Watch(*Resource, ...WatchOption) (Watcher, error)
    52  }
    53  
    54  // Create creates new API object
    55  func (c *client) Create(r *Resource, opts ...CreateOption) error {
    56  	options := CreateOptions{
    57  		Namespace: c.opts.Namespace,
    58  	}
    59  	for _, o := range opts {
    60  		o(&options)
    61  	}
    62  
    63  	b := new(bytes.Buffer)
    64  	if err := renderTemplate(r.Kind, b, r.Value); err != nil {
    65  		return err
    66  	}
    67  
    68  	return api.NewRequest(c.opts).
    69  		Post().
    70  		SetHeader("Content-Type", "application/yaml").
    71  		Namespace(options.Namespace).
    72  		Resource(r.Kind).
    73  		Body(b).
    74  		Do().
    75  		Error()
    76  }
    77  
    78  var (
    79  	nameRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
    80  )
    81  
    82  // SerializeResourceName removes all spacial chars from a string so it
    83  // can be used as a k8s resource name
    84  func SerializeResourceName(ns string) string {
    85  	return nameRegex.ReplaceAllString(ns, "-")
    86  }
    87  
    88  // Get queries API objects and stores the result in r
    89  func (c *client) Get(r *Resource, opts ...GetOption) error {
    90  	options := GetOptions{
    91  		Namespace: c.opts.Namespace,
    92  	}
    93  	for _, o := range opts {
    94  		o(&options)
    95  	}
    96  
    97  	return api.NewRequest(c.opts).
    98  		Get().
    99  		Resource(r.Kind).
   100  		Namespace(options.Namespace).
   101  		Params(&api.Params{LabelSelector: options.Labels}).
   102  		Do().
   103  		Into(r.Value)
   104  }
   105  
   106  // Log returns logs for a pod
   107  func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) {
   108  	options := LogOptions{
   109  		Namespace: c.opts.Namespace,
   110  	}
   111  	for _, o := range opts {
   112  		o(&options)
   113  	}
   114  
   115  	req := api.NewRequest(c.opts).
   116  		Get().
   117  		Resource(r.Kind).
   118  		SubResource("log").
   119  		Name(r.Name).
   120  		Namespace(options.Namespace)
   121  
   122  	if options.Params != nil {
   123  		req.Params(&api.Params{Additional: options.Params})
   124  	}
   125  
   126  	resp, err := req.Raw()
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   131  		resp.Body.Close()
   132  		return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status)
   133  	}
   134  	return resp.Body, nil
   135  }
   136  
   137  // Update updates API object
   138  func (c *client) Update(r *Resource, opts ...UpdateOption) error {
   139  	options := UpdateOptions{
   140  		Namespace: c.opts.Namespace,
   141  	}
   142  	for _, o := range opts {
   143  		o(&options)
   144  	}
   145  
   146  	req := api.NewRequest(c.opts).
   147  		Patch().
   148  		SetHeader("Content-Type", "application/strategic-merge-patch+json").
   149  		Resource(r.Kind).
   150  		Name(r.Name).
   151  		Namespace(options.Namespace)
   152  
   153  	switch r.Kind {
   154  	case "service":
   155  		req.Body(r.Value.(*Service))
   156  	case "deployment":
   157  		req.Body(r.Value.(*Deployment))
   158  	case "pod":
   159  		req.Body(r.Value.(*Pod))
   160  	default:
   161  		return errors.New("unsupported resource")
   162  	}
   163  
   164  	return req.Do().Error()
   165  }
   166  
   167  // Delete removes API object
   168  func (c *client) Delete(r *Resource, opts ...DeleteOption) error {
   169  	options := DeleteOptions{
   170  		Namespace: c.opts.Namespace,
   171  	}
   172  	for _, o := range opts {
   173  		o(&options)
   174  	}
   175  
   176  	return api.NewRequest(c.opts).
   177  		Delete().
   178  		Resource(r.Kind).
   179  		Name(r.Name).
   180  		Namespace(options.Namespace).
   181  		Do().
   182  		Error()
   183  }
   184  
   185  // List lists API objects and stores the result in r
   186  func (c *client) List(r *Resource, opts ...ListOption) error {
   187  	options := ListOptions{
   188  		Namespace: c.opts.Namespace,
   189  	}
   190  	for _, o := range opts {
   191  		o(&options)
   192  	}
   193  
   194  	return c.Get(r, GetNamespace(options.Namespace))
   195  }
   196  
   197  // Watch returns an event stream
   198  func (c *client) Watch(r *Resource, opts ...WatchOption) (Watcher, error) {
   199  	options := WatchOptions{
   200  		Namespace: c.opts.Namespace,
   201  	}
   202  	for _, o := range opts {
   203  		o(&options)
   204  	}
   205  
   206  	// set the watch param
   207  	params := &api.Params{Additional: map[string]string{
   208  		"watch": "true",
   209  	}}
   210  
   211  	// get options params
   212  	if options.Params != nil {
   213  		for k, v := range options.Params {
   214  			params.Additional[k] = v
   215  		}
   216  	}
   217  
   218  	req := api.NewRequest(c.opts).
   219  		Get().
   220  		Resource(r.Kind).
   221  		Name(r.Name).
   222  		Namespace(options.Namespace).
   223  		Params(params)
   224  
   225  	return newWatcher(req)
   226  }
   227  
   228  // NewService returns default micro kubernetes service definition
   229  func NewService(name, version, typ, namespace string) *Service {
   230  	if logger.V(logger.TraceLevel, logger.DefaultLogger) {
   231  		logger.Tracef("kubernetes default service: name: %s, version: %s", name, version)
   232  	}
   233  
   234  	Labels := map[string]string{
   235  		"name":    name,
   236  		"version": version,
   237  		"micro":   typ,
   238  	}
   239  
   240  	svcName := name
   241  	if len(version) > 0 {
   242  		// API service object name joins name and version over "-"
   243  		svcName = strings.Join([]string{name, version}, "-")
   244  	}
   245  
   246  	if len(namespace) == 0 {
   247  		namespace = DefaultNamespace
   248  	}
   249  
   250  	Metadata := &Metadata{
   251  		Name:      svcName,
   252  		Namespace: SerializeResourceName(namespace),
   253  		Version:   version,
   254  		Labels:    Labels,
   255  	}
   256  
   257  	Spec := &ServiceSpec{
   258  		Type:     "ClusterIP",
   259  		Selector: Labels,
   260  		Ports: []ServicePort{{
   261  			"service-port", 8080, "",
   262  		}},
   263  	}
   264  
   265  	return &Service{
   266  		Metadata: Metadata,
   267  		Spec:     Spec,
   268  	}
   269  }
   270  
   271  // NewService returns default micro kubernetes deployment definition
   272  func NewDeployment(name, version, typ, namespace string) *Deployment {
   273  	if logger.V(logger.TraceLevel, logger.DefaultLogger) {
   274  		logger.Tracef("kubernetes default deployment: name: %s, version: %s", name, version)
   275  	}
   276  
   277  	Labels := map[string]string{
   278  		"name":    name,
   279  		"version": version,
   280  		"micro":   typ,
   281  	}
   282  
   283  	depName := name
   284  	if len(version) > 0 {
   285  		// API deployment object name joins name and version over "-"
   286  		depName = strings.Join([]string{name, version}, "-")
   287  	}
   288  
   289  	if len(namespace) == 0 {
   290  		namespace = DefaultNamespace
   291  	}
   292  
   293  	Metadata := &Metadata{
   294  		Name:        depName,
   295  		Namespace:   SerializeResourceName(namespace),
   296  		Version:     version,
   297  		Labels:      Labels,
   298  		Annotations: map[string]string{},
   299  	}
   300  
   301  	// enable go modules by default
   302  	env := EnvVar{
   303  		Name:  "GO111MODULE",
   304  		Value: "on",
   305  	}
   306  
   307  	Spec := &DeploymentSpec{
   308  		Replicas: 1,
   309  		Selector: &LabelSelector{
   310  			MatchLabels: Labels,
   311  		},
   312  		Template: &Template{
   313  			Metadata: Metadata,
   314  			PodSpec: &PodSpec{
   315  				ServiceAccountName: namespace,
   316  				Containers: []Container{{
   317  					Name:    name,
   318  					Image:   DefaultImage,
   319  					Env:     []EnvVar{env},
   320  					Command: []string{"go", "run", "."},
   321  					Ports: []ContainerPort{{
   322  						Name:          "service-port",
   323  						ContainerPort: 8080,
   324  					}},
   325  				}},
   326  			},
   327  		},
   328  	}
   329  
   330  	return &Deployment{
   331  		Metadata: Metadata,
   332  		Spec:     Spec,
   333  	}
   334  }
   335  
   336  // NewLocalClient returns a client that can be used with `kubectl proxy`
   337  func NewLocalClient(hosts ...string) *client {
   338  	if len(hosts) == 0 {
   339  		hosts[0] = "http://localhost:8001"
   340  	}
   341  	return &client{
   342  		opts: &api.Options{
   343  			Client:    http.DefaultClient,
   344  			Host:      hosts[0],
   345  			Namespace: "default",
   346  		},
   347  	}
   348  }
   349  
   350  // NewClusterClient creates a Kubernetes client for use from within a k8s pod.
   351  func NewClusterClient() *client {
   352  	host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT")
   353  
   354  	s, err := os.Stat(serviceAccountPath)
   355  	if err != nil {
   356  		logger.Fatal(err)
   357  	}
   358  	if s == nil || !s.IsDir() {
   359  		logger.Fatal(errors.New("service account not found"))
   360  	}
   361  
   362  	token, err := ioutil.ReadFile(path.Join(serviceAccountPath, "token"))
   363  	if err != nil {
   364  		logger.Fatal(err)
   365  	}
   366  	t := string(token)
   367  
   368  	crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt"))
   369  	if err != nil {
   370  		logger.Fatal(err)
   371  	}
   372  
   373  	c := &http.Client{
   374  		Transport: &http.Transport{
   375  			TLSClientConfig: &tls.Config{
   376  				RootCAs: crt,
   377  			},
   378  			DisableCompression: true,
   379  		},
   380  	}
   381  
   382  	return &client{
   383  		opts: &api.Options{
   384  			Client:      c,
   385  			Host:        host,
   386  			BearerToken: &t,
   387  			Namespace:   DefaultNamespace,
   388  		},
   389  	}
   390  }