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