github.com/tickoalcantara12/micro/v3@v3.0.0-20221007104245-9d75b9bcbab9/service/runtime/kubernetes/client/client.go (about)

     1  // Licensed under the Apache License, Version 2.0 (the "License");
     2  // you may not use this file except in compliance with the License.
     3  // You may obtain a copy of the License at
     4  //
     5  //     https://www.apache.org/licenses/LICENSE-2.0
     6  //
     7  // Unless required by applicable law or agreed to in writing, software
     8  // distributed under the License is distributed on an "AS IS" BASIS,
     9  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    10  // See the License for the specific language governing permissions and
    11  // limitations under the License.
    12  //
    13  // Original source: github.com/micro/go-micro/v3/util/kubernetes/client/client.go
    14  
    15  // Package client provides an implementation of a restricted subset of kubernetes API client
    16  package client
    17  
    18  import (
    19  	"bytes"
    20  	"crypto/tls"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"os"
    27  	"path"
    28  	"regexp"
    29  	"strconv"
    30  	"strings"
    31  
    32  	"github.com/tickoalcantara12/micro/v3/service/logger"
    33  	"github.com/tickoalcantara12/micro/v3/service/runtime"
    34  	"github.com/tickoalcantara12/micro/v3/service/runtime/kubernetes/api"
    35  )
    36  
    37  var (
    38  	// path to kubernetes service account token
    39  	serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
    40  	// ErrReadNamespace is returned when the names could not be read from service account
    41  	ErrReadNamespace = errors.New("Could not read namespace from service account secret")
    42  	// DefaultImage is default micro image
    43  	DefaultImage = "ghcr.io/micro/cells:v3"
    44  	// DefaultNamespace is the default k8s namespace
    45  	DefaultNamespace = "default"
    46  	// DefaultPort to expose on a service
    47  	DefaultPort = 8080
    48  )
    49  
    50  // Client ...
    51  type client struct {
    52  	opts *api.Options
    53  }
    54  
    55  // Kubernetes client
    56  type Client interface {
    57  	// Create creates new API resource
    58  	Create(*Resource, ...CreateOption) error
    59  	// Get queries API resources
    60  	Get(*Resource, ...GetOption) error
    61  	// Update patches existing API object
    62  	Update(*Resource, ...UpdateOption) error
    63  	// Delete deletes API resource
    64  	Delete(*Resource, ...DeleteOption) error
    65  	// List lists API resources
    66  	List(*Resource, ...ListOption) error
    67  	// Log gets log for a pod
    68  	Log(*Resource, ...LogOption) (io.ReadCloser, error)
    69  	// Watch for events
    70  	Watch(*Resource, ...WatchOption) (Watcher, error)
    71  }
    72  
    73  // Create creates new API object
    74  func (c *client) Create(r *Resource, opts ...CreateOption) error {
    75  	options := CreateOptions{
    76  		Namespace: c.opts.Namespace,
    77  	}
    78  	for _, o := range opts {
    79  		o(&options)
    80  	}
    81  
    82  	b := new(bytes.Buffer)
    83  	if err := renderTemplate(r.Kind, b, r.Value); err != nil {
    84  		return err
    85  	}
    86  
    87  	return api.NewRequest(c.opts).
    88  		Post().
    89  		SetHeader("Content-Type", "application/yaml").
    90  		Namespace(options.Namespace).
    91  		Resource(r.Kind).
    92  		Body(b).
    93  		Do().
    94  		Error()
    95  }
    96  
    97  var (
    98  	nameRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
    99  )
   100  
   101  // Get queries API objects and stores the result in r
   102  func (c *client) Get(r *Resource, opts ...GetOption) error {
   103  	options := GetOptions{
   104  		Namespace: c.opts.Namespace,
   105  	}
   106  	for _, o := range opts {
   107  		o(&options)
   108  	}
   109  
   110  	return api.NewRequest(c.opts).
   111  		Get().
   112  		Resource(r.Kind).
   113  		Namespace(options.Namespace).
   114  		Params(&api.Params{LabelSelector: options.Labels}).
   115  		Do().
   116  		Into(r.Value)
   117  }
   118  
   119  // Log returns logs for a pod
   120  func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) {
   121  	options := LogOptions{
   122  		Namespace: c.opts.Namespace,
   123  	}
   124  	for _, o := range opts {
   125  		o(&options)
   126  	}
   127  
   128  	req := api.NewRequest(c.opts).
   129  		Get().
   130  		Resource(r.Kind).
   131  		SubResource("log").
   132  		Name(r.Name).
   133  		Namespace(options.Namespace)
   134  
   135  	if options.Params != nil {
   136  		req.Params(&api.Params{Additional: options.Params})
   137  	}
   138  
   139  	resp, err := req.Raw()
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   144  		resp.Body.Close()
   145  		return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status)
   146  	}
   147  	return resp.Body, nil
   148  }
   149  
   150  // Update updates API object
   151  func (c *client) Update(r *Resource, opts ...UpdateOption) error {
   152  	options := UpdateOptions{
   153  		Namespace: c.opts.Namespace,
   154  	}
   155  	for _, o := range opts {
   156  		o(&options)
   157  	}
   158  
   159  	req := api.NewRequest(c.opts).
   160  		Patch().
   161  		SetHeader("Content-Type", "application/strategic-merge-patch+json").
   162  		Resource(r.Kind).
   163  		Name(r.Name).
   164  		Namespace(options.Namespace)
   165  
   166  	switch r.Kind {
   167  	case "service":
   168  		req.Body(r.Value.(*Service))
   169  	case "deployment":
   170  		req.Body(r.Value.(*Deployment))
   171  	case "pod":
   172  		req.Body(r.Value.(*Pod))
   173  	case "networkpolicy", "networkpolicies":
   174  		req.Body(r.Value.(*NetworkPolicy))
   175  	case "resourcequota":
   176  		req.Body(r.Value.(*ResourceQuota))
   177  	default:
   178  		return errors.New("unsupported resource")
   179  	}
   180  	return req.Do().Error()
   181  }
   182  
   183  // Delete removes API object
   184  func (c *client) Delete(r *Resource, opts ...DeleteOption) error {
   185  	options := DeleteOptions{
   186  		Namespace: c.opts.Namespace,
   187  	}
   188  	for _, o := range opts {
   189  		o(&options)
   190  	}
   191  
   192  	return api.NewRequest(c.opts).
   193  		Delete().
   194  		Resource(r.Kind).
   195  		Name(r.Name).
   196  		Namespace(options.Namespace).
   197  		Do().
   198  		Error()
   199  }
   200  
   201  // List lists API objects and stores the result in r
   202  func (c *client) List(r *Resource, opts ...ListOption) error {
   203  	options := ListOptions{
   204  		Namespace: c.opts.Namespace,
   205  	}
   206  	for _, o := range opts {
   207  		o(&options)
   208  	}
   209  
   210  	return c.Get(r, GetNamespace(options.Namespace))
   211  }
   212  
   213  // Watch returns an event stream
   214  func (c *client) Watch(r *Resource, opts ...WatchOption) (Watcher, error) {
   215  	options := WatchOptions{
   216  		Namespace: c.opts.Namespace,
   217  	}
   218  	for _, o := range opts {
   219  		o(&options)
   220  	}
   221  
   222  	// set the watch param
   223  	params := &api.Params{Additional: map[string]string{
   224  		"watch": "true",
   225  	}}
   226  
   227  	// get options params
   228  	if options.Params != nil {
   229  		for k, v := range options.Params {
   230  			params.Additional[k] = v
   231  		}
   232  	}
   233  
   234  	req := api.NewRequest(c.opts).
   235  		Get().
   236  		Resource(r.Kind).
   237  		Name(r.Name).
   238  		Namespace(options.Namespace).
   239  		Params(params)
   240  
   241  	return newWatcher(req)
   242  }
   243  
   244  // NewService returns default micro kubernetes service definition
   245  func NewService(s *runtime.Service, opts *runtime.CreateOptions) *Resource {
   246  	labels := map[string]string{
   247  		"name":    Format(s.Name),
   248  		"version": Format(s.Version),
   249  		"micro":   Format(opts.Type),
   250  	}
   251  
   252  	metadata := &Metadata{
   253  		Name:      Format(s.Name),
   254  		Namespace: Format(opts.Namespace),
   255  		Version:   Format(s.Version),
   256  		Labels:    labels,
   257  	}
   258  
   259  	port := DefaultPort
   260  	if len(opts.Port) > 0 {
   261  		port, _ = strconv.Atoi(opts.Port)
   262  	}
   263  
   264  	return &Resource{
   265  		Kind: "service",
   266  		Name: metadata.Name,
   267  		Value: &Service{
   268  			Metadata: metadata,
   269  			Spec: &ServiceSpec{
   270  				Type:     "ClusterIP",
   271  				Selector: labels,
   272  				Ports: []ServicePort{{
   273  					"service-port", port, "",
   274  				}},
   275  			},
   276  		},
   277  	}
   278  }
   279  
   280  // NewDeployment returns default micro kubernetes deployment definition
   281  func NewDeployment(s *runtime.Service, opts *runtime.CreateOptions) *Resource {
   282  	labels := map[string]string{
   283  		"name":    Format(s.Name),
   284  		"version": Format(s.Version),
   285  		"micro":   Format(opts.Type),
   286  	}
   287  
   288  	// attach our values to the deployment; name, version, source
   289  	annotations := map[string]string{
   290  		"name":    s.Name,
   291  		"version": s.Version,
   292  		"source":  s.Source,
   293  	}
   294  	for k, v := range s.Metadata {
   295  		annotations[k] = v
   296  	}
   297  
   298  	// construct the metadata for the deployment
   299  	metadata := &Metadata{
   300  		Name:        fmt.Sprintf("%v-%v", Format(s.Name), Format(s.Version)),
   301  		Namespace:   Format(opts.Namespace),
   302  		Version:     Format(s.Version),
   303  		Labels:      labels,
   304  		Annotations: annotations,
   305  	}
   306  
   307  	// set the image
   308  	image := opts.Image
   309  	if len(image) == 0 {
   310  		image = DefaultImage
   311  	}
   312  
   313  	// pass the env vars
   314  	env := make([]EnvVar, 0, len(opts.Env))
   315  	for _, evar := range opts.Env {
   316  		if comps := strings.Split(evar, "="); len(comps) == 2 {
   317  			env = append(env, EnvVar{Name: comps[0], Value: comps[1]})
   318  		}
   319  	}
   320  
   321  	// pass the secrets
   322  	for key := range opts.Secrets {
   323  		env = append(env, EnvVar{
   324  			Name: key,
   325  			ValueFrom: &EnvVarSource{
   326  				SecretKeyRef: &SecretKeySelector{
   327  					Name: metadata.Name,
   328  					Key:  key,
   329  				},
   330  			},
   331  		})
   332  	}
   333  
   334  	// parse resource limits
   335  	var resReqs *ResourceRequirements
   336  	if opts.Resources != nil {
   337  		resReqs = &ResourceRequirements{Limits: &ResourceLimits{}}
   338  
   339  		if opts.Resources.CPU > 0 {
   340  			resReqs.Limits.CPU = fmt.Sprintf("%vm", opts.Resources.CPU)
   341  		}
   342  		if opts.Resources.Mem > 0 {
   343  			resReqs.Limits.Memory = fmt.Sprintf("%vMi", opts.Resources.Mem)
   344  		}
   345  		if opts.Resources.Disk > 0 {
   346  			resReqs.Limits.EphemeralStorage = fmt.Sprintf("%vMi", opts.Resources.Disk)
   347  		}
   348  	}
   349  
   350  	// parse the port option
   351  	port := DefaultPort
   352  	if len(opts.Port) > 0 {
   353  		port, _ = strconv.Atoi(opts.Port)
   354  	}
   355  
   356  	// set the number of replicas to run
   357  	replicas := 1
   358  	if opts.Instances > 1 {
   359  		replicas = int(opts.Instances)
   360  	}
   361  
   362  	return &Resource{
   363  		Kind: "deployment",
   364  		Name: metadata.Name,
   365  		Value: &Deployment{
   366  			Metadata: metadata,
   367  			Spec: &DeploymentSpec{
   368  				Replicas: replicas,
   369  				Selector: &LabelSelector{
   370  					MatchLabels: labels,
   371  				},
   372  				Template: &Template{
   373  					Metadata: metadata,
   374  					PodSpec: &PodSpec{
   375  						ServiceAccountName: opts.ServiceAccount,
   376  						Containers: []Container{{
   377  							Name:    Format(s.Name),
   378  							Image:   image,
   379  							Env:     env,
   380  							Command: opts.Command,
   381  							Args:    opts.Args,
   382  							Ports: []ContainerPort{{
   383  								Name:          "service-port",
   384  								ContainerPort: port,
   385  							}},
   386  							ReadinessProbe: &Probe{
   387  								TCPSocket: &TCPSocketAction{
   388  									Port: port,
   389  								},
   390  								PeriodSeconds:       10,
   391  								InitialDelaySeconds: 10,
   392  							},
   393  							Resources: resReqs,
   394  						}},
   395  					},
   396  				},
   397  			},
   398  		},
   399  	}
   400  }
   401  
   402  // NewLocalClient returns a client that can be used with `kubectl proxy`
   403  func NewLocalClient(hosts ...string) *client {
   404  	if len(hosts) == 0 {
   405  		hosts[0] = "http://localhost:8001"
   406  	}
   407  	return &client{
   408  		opts: &api.Options{
   409  			Client:    http.DefaultClient,
   410  			Host:      hosts[0],
   411  			Namespace: "default",
   412  		},
   413  	}
   414  }
   415  
   416  // NewClusterClient creates a Kubernetes client for use from within a k8s pod.
   417  func NewClusterClient() *client {
   418  	host := "https://" + os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT")
   419  
   420  	s, err := os.Stat(serviceAccountPath)
   421  	if err != nil {
   422  		logger.Fatal(err)
   423  	}
   424  	if s == nil || !s.IsDir() {
   425  		logger.Fatal(errors.New("service account not found"))
   426  	}
   427  
   428  	token, err := ioutil.ReadFile(path.Join(serviceAccountPath, "token"))
   429  	if err != nil {
   430  		logger.Fatal(err)
   431  	}
   432  	t := string(token)
   433  
   434  	crt, err := CertPoolFromFile(path.Join(serviceAccountPath, "ca.crt"))
   435  	if err != nil {
   436  		logger.Fatal(err)
   437  	}
   438  
   439  	c := &http.Client{
   440  		Transport: &http.Transport{
   441  			TLSClientConfig: &tls.Config{
   442  				RootCAs: crt,
   443  			},
   444  			DisableCompression: true,
   445  		},
   446  	}
   447  
   448  	return &client{
   449  		opts: &api.Options{
   450  			Client:      c,
   451  			Host:        host,
   452  			BearerToken: &t,
   453  			Namespace:   DefaultNamespace,
   454  		},
   455  	}
   456  }
   457  
   458  // NewNetworkPolicy returns a network policy allowing ingress from the given labels
   459  func NewNetworkPolicy(name, namespace string, allowedLabels map[string]string) *NetworkPolicy {
   460  	np := &NetworkPolicy{
   461  		Metadata: &Metadata{
   462  			Name:      name,
   463  			Namespace: namespace,
   464  		},
   465  		Spec: &NetworkPolicySpec{
   466  			Ingress: []NetworkPolicyRule{
   467  				{
   468  					From: []IngressRuleSelector{
   469  						{ // allow pods in this namespace to talk to each other
   470  							PodSelector: &Selector{},
   471  						},
   472  					},
   473  				},
   474  				{
   475  					From: []IngressRuleSelector{
   476  						{
   477  							NamespaceSelector: &Selector{
   478  								MatchLabels: allowedLabels,
   479  							},
   480  						},
   481  					},
   482  				},
   483  			},
   484  			PodSelector: &Selector{},
   485  			PolicyTypes: []string{"Ingress"},
   486  		},
   487  	}
   488  	return np
   489  
   490  }
   491  
   492  func NewResourceQuota(resourceQuota *runtime.ResourceQuota) *ResourceQuota {
   493  	rq := &ResourceQuota{
   494  		Metadata: &Metadata{
   495  			Name:      resourceQuota.Name,
   496  			Namespace: resourceQuota.Namespace,
   497  		},
   498  		Spec: &ResourceQuotaSpec{
   499  			Hard: &ResourceQuotaSpecs{},
   500  		},
   501  	}
   502  	if resourceQuota.Limits != nil {
   503  		if resourceQuota.Limits.CPU > 0 {
   504  			rq.Spec.Hard.LimitsCPU = fmt.Sprintf("%dm", resourceQuota.Limits.CPU)
   505  		}
   506  		if resourceQuota.Limits.Disk > 0 {
   507  			rq.Spec.Hard.LimitsEphemeralStorage = fmt.Sprintf("%dMi", resourceQuota.Limits.Disk)
   508  		}
   509  		if resourceQuota.Limits.Mem > 0 {
   510  			rq.Spec.Hard.LimitsMemory = fmt.Sprintf("%dMi", resourceQuota.Limits.Mem)
   511  		}
   512  	}
   513  	if resourceQuota.Requests != nil {
   514  		if resourceQuota.Requests.CPU > 0 {
   515  			rq.Spec.Hard.RequestsCPU = fmt.Sprintf("%dm", resourceQuota.Requests.CPU)
   516  		}
   517  		if resourceQuota.Requests.Disk > 0 {
   518  			rq.Spec.Hard.RequestsEphemeralStorage = fmt.Sprintf("%dMi", resourceQuota.Requests.Disk)
   519  		}
   520  		if resourceQuota.Requests.Mem > 0 {
   521  			rq.Spec.Hard.RequestsMemory = fmt.Sprintf("%dMi", resourceQuota.Requests.Mem)
   522  		}
   523  	}
   524  
   525  	return rq
   526  }