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