github.com/azure-devops-engineer/helm@v3.0.0-alpha.2+incompatible/pkg/kube/client.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package kube // import "helm.sh/helm/pkg/kube"
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"log"
    25  	"strings"
    26  	"time"
    27  
    28  	jsonpatch "github.com/evanphx/json-patch"
    29  	"github.com/pkg/errors"
    30  	batch "k8s.io/api/batch/v1"
    31  	v1 "k8s.io/api/core/v1"
    32  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    33  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/types"
    37  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    38  	"k8s.io/apimachinery/pkg/watch"
    39  	"k8s.io/cli-runtime/pkg/genericclioptions"
    40  	"k8s.io/cli-runtime/pkg/resource"
    41  	"k8s.io/client-go/kubernetes"
    42  	"k8s.io/client-go/kubernetes/scheme"
    43  	watchtools "k8s.io/client-go/tools/watch"
    44  	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
    45  )
    46  
    47  // ErrNoObjectsVisited indicates that during a visit operation, no matching objects were found.
    48  var ErrNoObjectsVisited = errors.New("no objects visited")
    49  
    50  // Client represents a client capable of communicating with the Kubernetes API.
    51  type Client struct {
    52  	Factory Factory
    53  	Log     func(string, ...interface{})
    54  }
    55  
    56  // New creates a new Client.
    57  func New(getter genericclioptions.RESTClientGetter) *Client {
    58  	if getter == nil {
    59  		getter = genericclioptions.NewConfigFlags(true)
    60  	}
    61  	return &Client{
    62  		Factory: cmdutil.NewFactory(getter),
    63  		Log:     nopLogger,
    64  	}
    65  }
    66  
    67  // KubernetesClientSet returns a client set from the client factory.
    68  func (c *Client) KubernetesClientSet() (*kubernetes.Clientset, error) {
    69  	return c.Factory.KubernetesClientSet()
    70  }
    71  
    72  var nopLogger = func(_ string, _ ...interface{}) {}
    73  
    74  // Create creates Kubernetes resources from an io.reader.
    75  //
    76  // Namespace will set the namespace.
    77  func (c *Client) Create(reader io.Reader) error {
    78  	c.Log("building resources from manifest")
    79  	infos, err := c.BuildUnstructured(reader)
    80  	if err != nil {
    81  		return err
    82  	}
    83  	c.Log("creating %d resource(s)", len(infos))
    84  	return perform(infos, createResource)
    85  }
    86  
    87  func (c *Client) Wait(reader io.Reader, timeout time.Duration) error {
    88  	infos, err := c.BuildUnstructured(reader)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	cs, err := c.KubernetesClientSet()
    93  	if err != nil {
    94  		return err
    95  	}
    96  	w := waiter{
    97  		c:       cs,
    98  		log:     c.Log,
    99  		timeout: timeout,
   100  	}
   101  	return w.waitForResources(infos)
   102  }
   103  
   104  func (c *Client) namespace() string {
   105  	if ns, _, err := c.Factory.ToRawKubeConfigLoader().Namespace(); err == nil {
   106  		return ns
   107  	}
   108  	return v1.NamespaceDefault
   109  }
   110  
   111  // newBuilder returns a new resource builder for structured api objects.
   112  func (c *Client) newBuilder() *resource.Builder {
   113  	return c.Factory.NewBuilder().
   114  		ContinueOnError().
   115  		NamespaceParam(c.namespace()).
   116  		DefaultNamespace().
   117  		Flatten()
   118  }
   119  
   120  func (c *Client) validator() resource.ContentValidator {
   121  	schema, err := c.Factory.Validator(true)
   122  	if err != nil {
   123  		c.Log("warning: failed to load schema: %s", err)
   124  	}
   125  	return schema
   126  }
   127  
   128  // BuildUnstructured validates for Kubernetes objects and returns unstructured infos.
   129  func (c *Client) BuildUnstructured(reader io.Reader) (Result, error) {
   130  	result, err := c.newBuilder().
   131  		Unstructured().
   132  		Stream(reader, "").
   133  		Do().Infos()
   134  	return result, scrubValidationError(err)
   135  }
   136  
   137  // Build validates for Kubernetes objects and returns resource Infos from a io.Reader.
   138  func (c *Client) Build(reader io.Reader) (Result, error) {
   139  	result, err := c.newBuilder().
   140  		WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
   141  		Schema(c.validator()).
   142  		Stream(reader, "").
   143  		Do().
   144  		Infos()
   145  	return result, scrubValidationError(err)
   146  }
   147  
   148  // Update reads in the current configuration and a target configuration from io.reader
   149  // and creates resources that don't already exists, updates resources that have been modified
   150  // in the target configuration and deletes resources from the current configuration that are
   151  // not present in the target configuration.
   152  //
   153  // Namespace will set the namespaces.
   154  func (c *Client) Update(originalReader, targetReader io.Reader, force, recreate bool) error {
   155  	original, err := c.BuildUnstructured(originalReader)
   156  	if err != nil {
   157  		return errors.Wrap(err, "failed decoding reader into objects")
   158  	}
   159  
   160  	c.Log("building resources from updated manifest")
   161  	target, err := c.BuildUnstructured(targetReader)
   162  	if err != nil {
   163  		return errors.Wrap(err, "failed decoding reader into objects")
   164  	}
   165  
   166  	updateErrors := []string{}
   167  
   168  	c.Log("checking %d resources for changes", len(target))
   169  	err = target.Visit(func(info *resource.Info, err error) error {
   170  		if err != nil {
   171  			return err
   172  		}
   173  
   174  		helper := resource.NewHelper(info.Client, info.Mapping)
   175  		if _, err := helper.Get(info.Namespace, info.Name, info.Export); err != nil {
   176  			if !apierrors.IsNotFound(err) {
   177  				return errors.Wrap(err, "could not get information about the resource")
   178  			}
   179  
   180  			// Since the resource does not exist, create it.
   181  			if err := createResource(info); err != nil {
   182  				return errors.Wrap(err, "failed to create resource")
   183  			}
   184  
   185  			kind := info.Mapping.GroupVersionKind.Kind
   186  			c.Log("Created a new %s called %q\n", kind, info.Name)
   187  			return nil
   188  		}
   189  
   190  		originalInfo := original.Get(info)
   191  		if originalInfo == nil {
   192  			kind := info.Mapping.GroupVersionKind.Kind
   193  			return errors.Errorf("no %s with the name %q found", kind, info.Name)
   194  		}
   195  
   196  		if err := updateResource(c, info, originalInfo.Object, force, recreate); err != nil {
   197  			c.Log("error updating the resource %q:\n\t %v", info.Name, err)
   198  			updateErrors = append(updateErrors, err.Error())
   199  		}
   200  
   201  		return nil
   202  	})
   203  
   204  	switch {
   205  	case err != nil:
   206  		return err
   207  	case len(updateErrors) != 0:
   208  		return errors.Errorf(strings.Join(updateErrors, " && "))
   209  	}
   210  
   211  	for _, info := range original.Difference(target) {
   212  		c.Log("Deleting %q in %s...", info.Name, info.Namespace)
   213  		if err := deleteResource(info); err != nil {
   214  			c.Log("Failed to delete %q, err: %s", info.Name, err)
   215  		}
   216  	}
   217  	return nil
   218  }
   219  
   220  // Delete deletes Kubernetes resources from an io.reader.
   221  //
   222  // Namespace will set the namespace.
   223  func (c *Client) Delete(reader io.Reader) error {
   224  	infos, err := c.BuildUnstructured(reader)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	return perform(infos, func(info *resource.Info) error {
   229  		c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
   230  		err := deleteResource(info)
   231  		return c.skipIfNotFound(err)
   232  	})
   233  }
   234  
   235  func (c *Client) skipIfNotFound(err error) error {
   236  	if apierrors.IsNotFound(err) {
   237  		c.Log("%v", err)
   238  		return nil
   239  	}
   240  	return err
   241  }
   242  
   243  func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
   244  	return func(info *resource.Info) error {
   245  		return c.watchUntilReady(t, info)
   246  	}
   247  }
   248  
   249  // WatchUntilReady watches the resource given in the reader, and waits until it is ready.
   250  //
   251  // This function is mainly for hook implementations. It watches for a resource to
   252  // hit a particular milestone. The milestone depends on the Kind.
   253  //
   254  // For most kinds, it checks to see if the resource is marked as Added or Modified
   255  // by the Kubernetes event stream. For some kinds, it does more:
   256  //
   257  // - Jobs: A job is marked "Ready" when it has successfully completed. This is
   258  //   ascertained by watching the Status fields in a job's output.
   259  //
   260  // Handling for other kinds will be added as necessary.
   261  func (c *Client) WatchUntilReady(reader io.Reader, timeout time.Duration) error {
   262  	infos, err := c.Build(reader)
   263  	if err != nil {
   264  		return err
   265  	}
   266  	// For jobs, there's also the option to do poll c.Jobs(namespace).Get():
   267  	// https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300
   268  	return perform(infos, c.watchTimeout(timeout))
   269  }
   270  
   271  func perform(infos Result, fn func(*resource.Info) error) error {
   272  	if len(infos) == 0 {
   273  		return ErrNoObjectsVisited
   274  	}
   275  
   276  	for _, info := range infos {
   277  		if err := fn(info); err != nil {
   278  			return err
   279  		}
   280  	}
   281  	return nil
   282  }
   283  
   284  func createResource(info *resource.Info) error {
   285  	obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil)
   286  	if err != nil {
   287  		return err
   288  	}
   289  	return info.Refresh(obj, true)
   290  }
   291  
   292  func deleteResource(info *resource.Info) error {
   293  	policy := metav1.DeletePropagationBackground
   294  	opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
   295  	_, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, opts)
   296  	return err
   297  }
   298  
   299  func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) {
   300  	oldData, err := json.Marshal(current)
   301  	if err != nil {
   302  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration")
   303  	}
   304  	newData, err := json.Marshal(target.Object)
   305  	if err != nil {
   306  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration")
   307  	}
   308  
   309  	// While different objects need different merge types, the parent function
   310  	// that calls this does not try to create a patch when the data (first
   311  	// returned object) is nil. We can skip calculating the merge type as
   312  	// the returned merge type is ignored.
   313  	if apiequality.Semantic.DeepEqual(oldData, newData) {
   314  		return nil, types.StrategicMergePatchType, nil
   315  	}
   316  
   317  	// Get a versioned object
   318  	versionedObject := asVersioned(target)
   319  
   320  	// Unstructured objects, such as CRDs, may not have an not registered error
   321  	// returned from ConvertToVersion. Anything that's unstructured should
   322  	// use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
   323  	// on objects like CRDs.
   324  	if _, ok := versionedObject.(runtime.Unstructured); ok {
   325  		// fall back to generic JSON merge patch
   326  		patch, err := jsonpatch.CreateMergePatch(oldData, newData)
   327  		return patch, types.MergePatchType, err
   328  	}
   329  	patch, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, versionedObject)
   330  	return patch, types.StrategicMergePatchType, err
   331  }
   332  
   333  func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force, recreate bool) error {
   334  	patch, patchType, err := createPatch(target, currentObj)
   335  	if err != nil {
   336  		return errors.Wrap(err, "failed to create patch")
   337  	}
   338  	if patch == nil {
   339  		c.Log("Looks like there are no changes for %s %q", target.Mapping.GroupVersionKind.Kind, target.Name)
   340  		// This needs to happen to make sure that tiller has the latest info from the API
   341  		// Otherwise there will be no labels and other functions that use labels will panic
   342  		if err := target.Get(); err != nil {
   343  			return errors.Wrap(err, "error trying to refresh resource information")
   344  		}
   345  	} else {
   346  		// send patch to server
   347  		helper := resource.NewHelper(target.Client, target.Mapping)
   348  
   349  		obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
   350  		if err != nil {
   351  			kind := target.Mapping.GroupVersionKind.Kind
   352  			log.Printf("Cannot patch %s: %q (%v)", kind, target.Name, err)
   353  
   354  			if force {
   355  				// Attempt to delete...
   356  				if err := deleteResource(target); err != nil {
   357  					return err
   358  				}
   359  				log.Printf("Deleted %s: %q", kind, target.Name)
   360  
   361  				// ... and recreate
   362  				if err := createResource(target); err != nil {
   363  					return errors.Wrap(err, "failed to recreate resource")
   364  				}
   365  				log.Printf("Created a new %s called %q\n", kind, target.Name)
   366  
   367  				// No need to refresh the target, as we recreated the resource based
   368  				// on it. In addition, it might not exist yet and a call to `Refresh`
   369  				// may fail.
   370  			} else {
   371  				log.Print("Use --force to force recreation of the resource")
   372  				return err
   373  			}
   374  		} else {
   375  			// When patch succeeds without needing to recreate, refresh target.
   376  			target.Refresh(obj, true)
   377  		}
   378  	}
   379  
   380  	if !recreate {
   381  		return nil
   382  	}
   383  
   384  	versioned := asVersioned(target)
   385  	selector, err := selectorsForObject(versioned)
   386  	if err != nil {
   387  		return nil
   388  	}
   389  
   390  	client, err := c.KubernetesClientSet()
   391  	if err != nil {
   392  		return err
   393  	}
   394  
   395  	pods, err := client.CoreV1().Pods(target.Namespace).List(metav1.ListOptions{
   396  		LabelSelector: selector.String(),
   397  	})
   398  	if err != nil {
   399  		return err
   400  	}
   401  
   402  	// Restart pods
   403  	for _, pod := range pods.Items {
   404  		c.Log("Restarting pod: %v/%v", pod.Namespace, pod.Name)
   405  
   406  		// Delete each pod for get them restarted with changed spec.
   407  		if err := client.CoreV1().Pods(pod.Namespace).Delete(pod.Name, metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil {
   408  			return err
   409  		}
   410  	}
   411  	return nil
   412  }
   413  
   414  func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) error {
   415  	w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion)
   416  	if err != nil {
   417  		return err
   418  	}
   419  
   420  	kind := info.Mapping.GroupVersionKind.Kind
   421  	c.Log("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout)
   422  
   423  	// What we watch for depends on the Kind.
   424  	// - For a Job, we watch for completion.
   425  	// - For all else, we watch until Ready.
   426  	// In the future, we might want to add some special logic for types
   427  	// like Ingress, Volume, etc.
   428  
   429  	ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
   430  	defer cancel()
   431  	_, err = watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) {
   432  		switch e.Type {
   433  		case watch.Added, watch.Modified:
   434  			// For things like a secret or a config map, this is the best indicator
   435  			// we get. We care mostly about jobs, where what we want to see is
   436  			// the status go into a good state. For other types, like ReplicaSet
   437  			// we don't really do anything to support these as hooks.
   438  			c.Log("Add/Modify event for %s: %v", info.Name, e.Type)
   439  			if kind == "Job" {
   440  				return c.waitForJob(e, info.Name)
   441  			}
   442  			return true, nil
   443  		case watch.Deleted:
   444  			c.Log("Deleted event for %s", info.Name)
   445  			return true, nil
   446  		case watch.Error:
   447  			// Handle error and return with an error.
   448  			c.Log("Error event for %s", info.Name)
   449  			return true, errors.Errorf("failed to deploy %s", info.Name)
   450  		default:
   451  			return false, nil
   452  		}
   453  	})
   454  	return err
   455  }
   456  
   457  // waitForJob is a helper that waits for a job to complete.
   458  //
   459  // This operates on an event returned from a watcher.
   460  func (c *Client) waitForJob(e watch.Event, name string) (bool, error) {
   461  	o, ok := e.Object.(*batch.Job)
   462  	if !ok {
   463  		return true, errors.Errorf("expected %s to be a *batch.Job, got %T", name, e.Object)
   464  	}
   465  
   466  	for _, c := range o.Status.Conditions {
   467  		if c.Type == batch.JobComplete && c.Status == "True" {
   468  			return true, nil
   469  		} else if c.Type == batch.JobFailed && c.Status == "True" {
   470  			return true, errors.Errorf("job failed: %s", c.Reason)
   471  		}
   472  	}
   473  
   474  	c.Log("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded)
   475  	return false, nil
   476  }
   477  
   478  // scrubValidationError removes kubectl info from the message.
   479  func scrubValidationError(err error) error {
   480  	if err == nil {
   481  		return nil
   482  	}
   483  	const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
   484  
   485  	if strings.Contains(err.Error(), stopValidateMessage) {
   486  		return errors.New(strings.ReplaceAll(err.Error(), "; "+stopValidateMessage, ""))
   487  	}
   488  	return err
   489  }
   490  
   491  // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase
   492  // and returns said phase (PodSucceeded or PodFailed qualify).
   493  func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) {
   494  	client, _ := c.KubernetesClientSet()
   495  	to := int64(timeout)
   496  	watcher, err := client.CoreV1().Pods(c.namespace()).Watch(metav1.ListOptions{
   497  		FieldSelector:  fmt.Sprintf("metadata.name=%s", name),
   498  		TimeoutSeconds: &to,
   499  	})
   500  
   501  	for event := range watcher.ResultChan() {
   502  		p, ok := event.Object.(*v1.Pod)
   503  		if !ok {
   504  			return v1.PodUnknown, fmt.Errorf("%s not a pod", name)
   505  		}
   506  		switch p.Status.Phase {
   507  		case v1.PodFailed:
   508  			return v1.PodFailed, nil
   509  		case v1.PodSucceeded:
   510  			return v1.PodSucceeded, nil
   511  		}
   512  	}
   513  
   514  	return v1.PodUnknown, err
   515  }