github.com/umeshredd/helm@v3.0.0-alpha.1+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  		RequireNamespace().
   118  		Flatten()
   119  }
   120  
   121  func (c *Client) validator() resource.ContentValidator {
   122  	schema, err := c.Factory.Validator(true)
   123  	if err != nil {
   124  		c.Log("warning: failed to load schema: %s", err)
   125  	}
   126  	return schema
   127  }
   128  
   129  // BuildUnstructured validates for Kubernetes objects and returns unstructured infos.
   130  func (c *Client) BuildUnstructured(reader io.Reader) (Result, error) {
   131  	result, err := c.newBuilder().
   132  		Unstructured().
   133  		Stream(reader, "").
   134  		Do().Infos()
   135  	return result, scrubValidationError(err)
   136  }
   137  
   138  // Build validates for Kubernetes objects and returns resource Infos from a io.Reader.
   139  func (c *Client) Build(reader io.Reader) (Result, error) {
   140  	result, err := c.newBuilder().
   141  		WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
   142  		Schema(c.validator()).
   143  		Stream(reader, "").
   144  		Do().
   145  		Infos()
   146  	return result, scrubValidationError(err)
   147  }
   148  
   149  // Update reads in the current configuration and a target configuration from io.reader
   150  // and creates resources that don't already exists, updates resources that have been modified
   151  // in the target configuration and deletes resources from the current configuration that are
   152  // not present in the target configuration.
   153  //
   154  // Namespace will set the namespaces.
   155  func (c *Client) Update(originalReader, targetReader io.Reader, force, recreate bool) error {
   156  	original, err := c.BuildUnstructured(originalReader)
   157  	if err != nil {
   158  		return errors.Wrap(err, "failed decoding reader into objects")
   159  	}
   160  
   161  	c.Log("building resources from updated manifest")
   162  	target, err := c.BuildUnstructured(targetReader)
   163  	if err != nil {
   164  		return errors.Wrap(err, "failed decoding reader into objects")
   165  	}
   166  
   167  	updateErrors := []string{}
   168  
   169  	c.Log("checking %d resources for changes", len(target))
   170  	err = target.Visit(func(info *resource.Info, err error) error {
   171  		if err != nil {
   172  			return err
   173  		}
   174  
   175  		helper := resource.NewHelper(info.Client, info.Mapping)
   176  		if _, err := helper.Get(info.Namespace, info.Name, info.Export); err != nil {
   177  			if !apierrors.IsNotFound(err) {
   178  				return errors.Wrap(err, "could not get information about the resource")
   179  			}
   180  
   181  			// Since the resource does not exist, create it.
   182  			if err := createResource(info); err != nil {
   183  				return errors.Wrap(err, "failed to create resource")
   184  			}
   185  
   186  			kind := info.Mapping.GroupVersionKind.Kind
   187  			c.Log("Created a new %s called %q\n", kind, info.Name)
   188  			return nil
   189  		}
   190  
   191  		originalInfo := original.Get(info)
   192  		if originalInfo == nil {
   193  			kind := info.Mapping.GroupVersionKind.Kind
   194  			return errors.Errorf("no %s with the name %q found", kind, info.Name)
   195  		}
   196  
   197  		if err := updateResource(c, info, originalInfo.Object, force, recreate); err != nil {
   198  			c.Log("error updating the resource %q:\n\t %v", info.Name, err)
   199  			updateErrors = append(updateErrors, err.Error())
   200  		}
   201  
   202  		return nil
   203  	})
   204  
   205  	switch {
   206  	case err != nil:
   207  		return err
   208  	case len(updateErrors) != 0:
   209  		return errors.Errorf(strings.Join(updateErrors, " && "))
   210  	}
   211  
   212  	for _, info := range original.Difference(target) {
   213  		c.Log("Deleting %q in %s...", info.Name, info.Namespace)
   214  		if err := deleteResource(info); err != nil {
   215  			c.Log("Failed to delete %q, err: %s", info.Name, err)
   216  		}
   217  	}
   218  	return nil
   219  }
   220  
   221  // Delete deletes Kubernetes resources from an io.reader.
   222  //
   223  // Namespace will set the namespace.
   224  func (c *Client) Delete(reader io.Reader) error {
   225  	infos, err := c.BuildUnstructured(reader)
   226  	if err != nil {
   227  		return err
   228  	}
   229  	return perform(infos, func(info *resource.Info) error {
   230  		c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
   231  		err := deleteResource(info)
   232  		return c.skipIfNotFound(err)
   233  	})
   234  }
   235  
   236  func (c *Client) skipIfNotFound(err error) error {
   237  	if apierrors.IsNotFound(err) {
   238  		c.Log("%v", err)
   239  		return nil
   240  	}
   241  	return err
   242  }
   243  
   244  func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
   245  	return func(info *resource.Info) error {
   246  		return c.watchUntilReady(t, info)
   247  	}
   248  }
   249  
   250  // WatchUntilReady watches the resource given in the reader, and waits until it is ready.
   251  //
   252  // This function is mainly for hook implementations. It watches for a resource to
   253  // hit a particular milestone. The milestone depends on the Kind.
   254  //
   255  // For most kinds, it checks to see if the resource is marked as Added or Modified
   256  // by the Kubernetes event stream. For some kinds, it does more:
   257  //
   258  // - Jobs: A job is marked "Ready" when it has successfully completed. This is
   259  //   ascertained by watching the Status fields in a job's output.
   260  //
   261  // Handling for other kinds will be added as necessary.
   262  func (c *Client) WatchUntilReady(reader io.Reader, timeout time.Duration) error {
   263  	infos, err := c.Build(reader)
   264  	if err != nil {
   265  		return err
   266  	}
   267  	// For jobs, there's also the option to do poll c.Jobs(namespace).Get():
   268  	// https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300
   269  	return perform(infos, c.watchTimeout(timeout))
   270  }
   271  
   272  func perform(infos Result, fn func(*resource.Info) error) error {
   273  	if len(infos) == 0 {
   274  		return ErrNoObjectsVisited
   275  	}
   276  
   277  	for _, info := range infos {
   278  		if err := fn(info); err != nil {
   279  			return err
   280  		}
   281  	}
   282  	return nil
   283  }
   284  
   285  func createResource(info *resource.Info) error {
   286  	obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	return info.Refresh(obj, true)
   291  }
   292  
   293  func deleteResource(info *resource.Info) error {
   294  	policy := metav1.DeletePropagationBackground
   295  	opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
   296  	_, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, opts)
   297  	return err
   298  }
   299  
   300  func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) {
   301  	oldData, err := json.Marshal(current)
   302  	if err != nil {
   303  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration")
   304  	}
   305  	newData, err := json.Marshal(target.Object)
   306  	if err != nil {
   307  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration")
   308  	}
   309  
   310  	// While different objects need different merge types, the parent function
   311  	// that calls this does not try to create a patch when the data (first
   312  	// returned object) is nil. We can skip calculating the merge type as
   313  	// the returned merge type is ignored.
   314  	if apiequality.Semantic.DeepEqual(oldData, newData) {
   315  		return nil, types.StrategicMergePatchType, nil
   316  	}
   317  
   318  	// Get a versioned object
   319  	versionedObject := asVersioned(target)
   320  
   321  	// Unstructured objects, such as CRDs, may not have an not registered error
   322  	// returned from ConvertToVersion. Anything that's unstructured should
   323  	// use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
   324  	// on objects like CRDs.
   325  	if _, ok := versionedObject.(runtime.Unstructured); ok {
   326  		// fall back to generic JSON merge patch
   327  		patch, err := jsonpatch.CreateMergePatch(oldData, newData)
   328  		return patch, types.MergePatchType, err
   329  	}
   330  	patch, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, versionedObject)
   331  	return patch, types.StrategicMergePatchType, err
   332  }
   333  
   334  func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force, recreate bool) error {
   335  	patch, patchType, err := createPatch(target, currentObj)
   336  	if err != nil {
   337  		return errors.Wrap(err, "failed to create patch")
   338  	}
   339  	if patch == nil {
   340  		c.Log("Looks like there are no changes for %s %q", target.Mapping.GroupVersionKind.Kind, target.Name)
   341  		// This needs to happen to make sure that tiller has the latest info from the API
   342  		// Otherwise there will be no labels and other functions that use labels will panic
   343  		if err := target.Get(); err != nil {
   344  			return errors.Wrap(err, "error trying to refresh resource information")
   345  		}
   346  	} else {
   347  		// send patch to server
   348  		helper := resource.NewHelper(target.Client, target.Mapping)
   349  
   350  		obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
   351  		if err != nil {
   352  			kind := target.Mapping.GroupVersionKind.Kind
   353  			log.Printf("Cannot patch %s: %q (%v)", kind, target.Name, err)
   354  
   355  			if force {
   356  				// Attempt to delete...
   357  				if err := deleteResource(target); err != nil {
   358  					return err
   359  				}
   360  				log.Printf("Deleted %s: %q", kind, target.Name)
   361  
   362  				// ... and recreate
   363  				if err := createResource(target); err != nil {
   364  					return errors.Wrap(err, "failed to recreate resource")
   365  				}
   366  				log.Printf("Created a new %s called %q\n", kind, target.Name)
   367  
   368  				// No need to refresh the target, as we recreated the resource based
   369  				// on it. In addition, it might not exist yet and a call to `Refresh`
   370  				// may fail.
   371  			} else {
   372  				log.Print("Use --force to force recreation of the resource")
   373  				return err
   374  			}
   375  		} else {
   376  			// When patch succeeds without needing to recreate, refresh target.
   377  			target.Refresh(obj, true)
   378  		}
   379  	}
   380  
   381  	if !recreate {
   382  		return nil
   383  	}
   384  
   385  	versioned := asVersioned(target)
   386  	selector, err := selectorsForObject(versioned)
   387  	if err != nil {
   388  		return nil
   389  	}
   390  
   391  	client, err := c.KubernetesClientSet()
   392  	if err != nil {
   393  		return err
   394  	}
   395  
   396  	pods, err := client.CoreV1().Pods(target.Namespace).List(metav1.ListOptions{
   397  		LabelSelector: selector.String(),
   398  	})
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	// Restart pods
   404  	for _, pod := range pods.Items {
   405  		c.Log("Restarting pod: %v/%v", pod.Namespace, pod.Name)
   406  
   407  		// Delete each pod for get them restarted with changed spec.
   408  		if err := client.CoreV1().Pods(pod.Namespace).Delete(pod.Name, metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil {
   409  			return err
   410  		}
   411  	}
   412  	return nil
   413  }
   414  
   415  func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) error {
   416  	w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion)
   417  	if err != nil {
   418  		return err
   419  	}
   420  
   421  	kind := info.Mapping.GroupVersionKind.Kind
   422  	c.Log("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout)
   423  
   424  	// What we watch for depends on the Kind.
   425  	// - For a Job, we watch for completion.
   426  	// - For all else, we watch until Ready.
   427  	// In the future, we might want to add some special logic for types
   428  	// like Ingress, Volume, etc.
   429  
   430  	ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
   431  	defer cancel()
   432  	_, err = watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) {
   433  		switch e.Type {
   434  		case watch.Added, watch.Modified:
   435  			// For things like a secret or a config map, this is the best indicator
   436  			// we get. We care mostly about jobs, where what we want to see is
   437  			// the status go into a good state. For other types, like ReplicaSet
   438  			// we don't really do anything to support these as hooks.
   439  			c.Log("Add/Modify event for %s: %v", info.Name, e.Type)
   440  			if kind == "Job" {
   441  				return c.waitForJob(e, info.Name)
   442  			}
   443  			return true, nil
   444  		case watch.Deleted:
   445  			c.Log("Deleted event for %s", info.Name)
   446  			return true, nil
   447  		case watch.Error:
   448  			// Handle error and return with an error.
   449  			c.Log("Error event for %s", info.Name)
   450  			return true, errors.Errorf("failed to deploy %s", info.Name)
   451  		default:
   452  			return false, nil
   453  		}
   454  	})
   455  	return err
   456  }
   457  
   458  // waitForJob is a helper that waits for a job to complete.
   459  //
   460  // This operates on an event returned from a watcher.
   461  func (c *Client) waitForJob(e watch.Event, name string) (bool, error) {
   462  	o, ok := e.Object.(*batch.Job)
   463  	if !ok {
   464  		return true, errors.Errorf("expected %s to be a *batch.Job, got %T", name, e.Object)
   465  	}
   466  
   467  	for _, c := range o.Status.Conditions {
   468  		if c.Type == batch.JobComplete && c.Status == "True" {
   469  			return true, nil
   470  		} else if c.Type == batch.JobFailed && c.Status == "True" {
   471  			return true, errors.Errorf("job failed: %s", c.Reason)
   472  		}
   473  	}
   474  
   475  	c.Log("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded)
   476  	return false, nil
   477  }
   478  
   479  // scrubValidationError removes kubectl info from the message.
   480  func scrubValidationError(err error) error {
   481  	if err == nil {
   482  		return nil
   483  	}
   484  	const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
   485  
   486  	if strings.Contains(err.Error(), stopValidateMessage) {
   487  		return errors.New(strings.ReplaceAll(err.Error(), "; "+stopValidateMessage, ""))
   488  	}
   489  	return err
   490  }
   491  
   492  // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase
   493  // and returns said phase (PodSucceeded or PodFailed qualify).
   494  func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) {
   495  	client, _ := c.KubernetesClientSet()
   496  	to := int64(timeout)
   497  	watcher, err := client.CoreV1().Pods(c.namespace()).Watch(metav1.ListOptions{
   498  		FieldSelector:  fmt.Sprintf("metadata.name=%s", name),
   499  		TimeoutSeconds: &to,
   500  	})
   501  
   502  	for event := range watcher.ResultChan() {
   503  		p, ok := event.Object.(*v1.Pod)
   504  		if !ok {
   505  			return v1.PodUnknown, fmt.Errorf("%s not a pod", name)
   506  		}
   507  		switch p.Status.Phase {
   508  		case v1.PodFailed:
   509  			return v1.PodFailed, nil
   510  		case v1.PodSucceeded:
   511  			return v1.PodSucceeded, nil
   512  		}
   513  	}
   514  
   515  	return v1.PodUnknown, err
   516  }