gitee.com/sasukebo/go-micro/v4@v4.7.1/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  	"net/http"
    10  	"os"
    11  	"path"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"gitee.com/sasukebo/go-micro/v4/logger"
    16  	"gitee.com/sasukebo/go-micro/v4/util/kubernetes/api"
    17  )
    18  
    19  var (
    20  	// path to kubernetes service account token
    21  	serviceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
    22  	// ErrReadNamespace is returned when the names could not be read from service account
    23  	ErrReadNamespace = errors.New("Could not read namespace from service account secret")
    24  	// DefaultImage is default micro image
    25  	DefaultImage = "micro/go-micro"
    26  	// DefaultNamespace is the default k8s namespace
    27  	DefaultNamespace = "default"
    28  )
    29  
    30  // Client ...
    31  type client struct {
    32  	opts *api.Options
    33  }
    34  
    35  // Kubernetes client
    36  type Client interface {
    37  	// Create creates new API resource
    38  	Create(*Resource, ...CreateOption) error
    39  	// Get queries API resrouces
    40  	Get(*Resource, ...GetOption) error
    41  	// Update patches existing API object
    42  	Update(*Resource, ...UpdateOption) error
    43  	// Delete deletes API resource
    44  	Delete(*Resource, ...DeleteOption) error
    45  	// List lists API resources
    46  	List(*Resource, ...ListOption) error
    47  	// Log gets log for a pod
    48  	Log(*Resource, ...LogOption) (io.ReadCloser, error)
    49  	// Watch for events
    50  	Watch(*Resource, ...WatchOption) (Watcher, error)
    51  }
    52  
    53  // Create creates new API object
    54  func (c *client) Create(r *Resource, opts ...CreateOption) error {
    55  	options := CreateOptions{
    56  		Namespace: c.opts.Namespace,
    57  	}
    58  	for _, o := range opts {
    59  		o(&options)
    60  	}
    61  
    62  	b := new(bytes.Buffer)
    63  	if err := renderTemplate(r.Kind, b, r.Value); err != nil {
    64  		return err
    65  	}
    66  	resp := api.NewRequest(c.opts).
    67  		Post().
    68  		SetHeader("Content-Type", "application/yaml").
    69  		Namespace(options.Namespace).
    70  		Resource(r.Kind).
    71  		Body(b).
    72  		Do()
    73  	resp.Close()
    74  	return resp.Error()
    75  }
    76  
    77  var (
    78  	nameRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
    79  )
    80  
    81  // SerializeResourceName removes all spacial chars from a string so it
    82  // can be used as a k8s resource name
    83  func SerializeResourceName(ns string) string {
    84  	return nameRegex.ReplaceAllString(ns, "-")
    85  }
    86  
    87  // Get queries API objects and stores the result in r
    88  func (c *client) Get(r *Resource, opts ...GetOption) error {
    89  	options := GetOptions{
    90  		Namespace: c.opts.Namespace,
    91  	}
    92  	for _, o := range opts {
    93  		o(&options)
    94  	}
    95  
    96  	return api.NewRequest(c.opts).
    97  		Get().
    98  		Resource(r.Kind).
    99  		Namespace(options.Namespace).
   100  		Params(&api.Params{LabelSelector: options.Labels}).
   101  		Do().
   102  		Into(r.Value)
   103  }
   104  
   105  // Log returns logs for a pod
   106  func (c *client) Log(r *Resource, opts ...LogOption) (io.ReadCloser, error) {
   107  	options := LogOptions{
   108  		Namespace: c.opts.Namespace,
   109  	}
   110  	for _, o := range opts {
   111  		o(&options)
   112  	}
   113  
   114  	req := api.NewRequest(c.opts).
   115  		Get().
   116  		Resource(r.Kind).
   117  		SubResource("log").
   118  		Name(r.Name).
   119  		Namespace(options.Namespace)
   120  
   121  	if options.Params != nil {
   122  		req.Params(&api.Params{Additional: options.Params})
   123  	}
   124  
   125  	resp, err := req.Raw()
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
   130  		resp.Body.Close()
   131  		return nil, errors.New(resp.Request.URL.String() + ": " + resp.Status)
   132  	}
   133  	return resp.Body, nil
   134  }
   135  
   136  // Update updates API object
   137  func (c *client) Update(r *Resource, opts ...UpdateOption) error {
   138  	options := UpdateOptions{
   139  		Namespace: c.opts.Namespace,
   140  	}
   141  	for _, o := range opts {
   142  		o(&options)
   143  	}
   144  
   145  	req := api.NewRequest(c.opts).
   146  		Patch().
   147  		SetHeader("Content-Type", "application/strategic-merge-patch+json").
   148  		Resource(r.Kind).
   149  		Name(r.Name).
   150  		Namespace(options.Namespace)
   151  
   152  	switch r.Kind {
   153  	case "service":
   154  		req.Body(r.Value.(*Service))
   155  	case "deployment":
   156  		req.Body(r.Value.(*Deployment))
   157  	case "pod":
   158  		req.Body(r.Value.(*Pod))
   159  	default:
   160  		return errors.New("unsupported resource")
   161  	}
   162  	resp := req.Do()
   163  	resp.Close()
   164  	return resp.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  	resp := api.NewRequest(c.opts).
   176  		Delete().
   177  		Resource(r.Kind).
   178  		Name(r.Name).
   179  		Namespace(options.Namespace).
   180  		Do()
   181  	resp.Close()
   182  	return resp.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 := os.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  }