github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/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 "github.com/stefanmcshane/helm/pkg/kube"
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	jsonpatch "github.com/evanphx/json-patch"
    31  	"github.com/pkg/errors"
    32  	batch "k8s.io/api/batch/v1"
    33  	v1 "k8s.io/api/core/v1"
    34  	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    35  	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    36  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    37  
    38  	"k8s.io/apimachinery/pkg/api/meta"
    39  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    40  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    41  	"k8s.io/apimachinery/pkg/fields"
    42  	"k8s.io/apimachinery/pkg/runtime"
    43  	"k8s.io/apimachinery/pkg/types"
    44  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    45  	"k8s.io/apimachinery/pkg/watch"
    46  	"k8s.io/cli-runtime/pkg/genericclioptions"
    47  	"k8s.io/cli-runtime/pkg/resource"
    48  	"k8s.io/client-go/kubernetes"
    49  	"k8s.io/client-go/kubernetes/scheme"
    50  	cachetools "k8s.io/client-go/tools/cache"
    51  	watchtools "k8s.io/client-go/tools/watch"
    52  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    53  )
    54  
    55  // ErrNoObjectsVisited indicates that during a visit operation, no matching objects were found.
    56  var ErrNoObjectsVisited = errors.New("no objects visited")
    57  
    58  var metadataAccessor = meta.NewAccessor()
    59  
    60  // ManagedFieldsManager is the name of the manager of Kubernetes managedFields
    61  // first introduced in Kubernetes 1.18
    62  var ManagedFieldsManager string
    63  
    64  // Client represents a client capable of communicating with the Kubernetes API.
    65  type Client struct {
    66  	Factory Factory
    67  	Log     func(string, ...interface{})
    68  	// Namespace allows to bypass the kubeconfig file for the choice of the namespace
    69  	Namespace string
    70  
    71  	kubeClient *kubernetes.Clientset
    72  }
    73  
    74  var addToScheme sync.Once
    75  
    76  // New creates a new Client.
    77  func New(getter genericclioptions.RESTClientGetter) *Client {
    78  	if getter == nil {
    79  		getter = genericclioptions.NewConfigFlags(true)
    80  	}
    81  	// Add CRDs to the scheme. They are missing by default.
    82  	addToScheme.Do(func() {
    83  		if err := apiextv1.AddToScheme(scheme.Scheme); err != nil {
    84  			// This should never happen.
    85  			panic(err)
    86  		}
    87  		if err := apiextv1beta1.AddToScheme(scheme.Scheme); err != nil {
    88  			panic(err)
    89  		}
    90  	})
    91  	return &Client{
    92  		Factory: cmdutil.NewFactory(getter),
    93  		Log:     nopLogger,
    94  	}
    95  }
    96  
    97  var nopLogger = func(_ string, _ ...interface{}) {}
    98  
    99  // getKubeClient get or create a new KubernetesClientSet
   100  func (c *Client) getKubeClient() (*kubernetes.Clientset, error) {
   101  	var err error
   102  	if c.kubeClient == nil {
   103  		c.kubeClient, err = c.Factory.KubernetesClientSet()
   104  	}
   105  
   106  	return c.kubeClient, err
   107  }
   108  
   109  // IsReachable tests connectivity to the cluster.
   110  func (c *Client) IsReachable() error {
   111  	client, err := c.getKubeClient()
   112  	if err == genericclioptions.ErrEmptyConfig {
   113  		// re-replace kubernetes ErrEmptyConfig error with a friendy error
   114  		// moar workarounds for Kubernetes API breaking.
   115  		return errors.New("Kubernetes cluster unreachable")
   116  	}
   117  	if err != nil {
   118  		return errors.Wrap(err, "Kubernetes cluster unreachable")
   119  	}
   120  	if _, err := client.ServerVersion(); err != nil {
   121  		return errors.Wrap(err, "Kubernetes cluster unreachable")
   122  	}
   123  	return nil
   124  }
   125  
   126  // Create creates Kubernetes resources specified in the resource list.
   127  func (c *Client) Create(resources ResourceList) (*Result, error) {
   128  	c.Log("creating %d resource(s)", len(resources))
   129  	if err := perform(resources, createResource); err != nil {
   130  		return nil, err
   131  	}
   132  	return &Result{Created: resources}, nil
   133  }
   134  
   135  // Wait waits up to the given timeout for the specified resources to be ready.
   136  func (c *Client) Wait(resources ResourceList, timeout time.Duration) error {
   137  	cs, err := c.getKubeClient()
   138  	if err != nil {
   139  		return err
   140  	}
   141  	checker := NewReadyChecker(cs, c.Log, PausedAsReady(true))
   142  	w := waiter{
   143  		c:       checker,
   144  		log:     c.Log,
   145  		timeout: timeout,
   146  	}
   147  	return w.waitForResources(resources)
   148  }
   149  
   150  // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs.
   151  func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) error {
   152  	cs, err := c.getKubeClient()
   153  	if err != nil {
   154  		return err
   155  	}
   156  	checker := NewReadyChecker(cs, c.Log, PausedAsReady(true), CheckJobs(true))
   157  	w := waiter{
   158  		c:       checker,
   159  		log:     c.Log,
   160  		timeout: timeout,
   161  	}
   162  	return w.waitForResources(resources)
   163  }
   164  
   165  // WaitForDelete wait up to the given timeout for the specified resources to be deleted.
   166  func (c *Client) WaitForDelete(resources ResourceList, timeout time.Duration) error {
   167  	w := waiter{
   168  		log:     c.Log,
   169  		timeout: timeout,
   170  	}
   171  	return w.waitForDeletedResources(resources)
   172  }
   173  
   174  func (c *Client) namespace() string {
   175  	if c.Namespace != "" {
   176  		return c.Namespace
   177  	}
   178  	if ns, _, err := c.Factory.ToRawKubeConfigLoader().Namespace(); err == nil {
   179  		return ns
   180  	}
   181  	return v1.NamespaceDefault
   182  }
   183  
   184  // newBuilder returns a new resource builder for structured api objects.
   185  func (c *Client) newBuilder() *resource.Builder {
   186  	return c.Factory.NewBuilder().
   187  		ContinueOnError().
   188  		NamespaceParam(c.namespace()).
   189  		DefaultNamespace().
   190  		Flatten()
   191  }
   192  
   193  // Build validates for Kubernetes objects and returns unstructured infos.
   194  func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) {
   195  	validationDirective := metav1.FieldValidationIgnore
   196  	if validate {
   197  		validationDirective = metav1.FieldValidationStrict
   198  	}
   199  
   200  	dynamicClient, err := c.Factory.DynamicClient()
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	verifier := resource.NewQueryParamVerifier(dynamicClient, c.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation)
   206  	schema, err := c.Factory.Validator(validationDirective, verifier)
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	result, err := c.newBuilder().
   211  		Unstructured().
   212  		Schema(schema).
   213  		Stream(reader, "").
   214  		Do().Infos()
   215  	return result, scrubValidationError(err)
   216  }
   217  
   218  // Update takes the current list of objects and target list of objects and
   219  // creates resources that don't already exist, updates resources that have been
   220  // modified in the target configuration, and deletes resources from the current
   221  // configuration that are not present in the target configuration. If an error
   222  // occurs, a Result will still be returned with the error, containing all
   223  // resource updates, creations, and deletions that were attempted. These can be
   224  // used for cleanup or other logging purposes.
   225  func (c *Client) Update(original, target ResourceList, force bool) (*Result, error) {
   226  	updateErrors := []string{}
   227  	res := &Result{}
   228  
   229  	c.Log("checking %d resources for changes", len(target))
   230  	err := target.Visit(func(info *resource.Info, err error) error {
   231  		if err != nil {
   232  			return err
   233  		}
   234  
   235  		helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager())
   236  		if _, err := helper.Get(info.Namespace, info.Name); err != nil {
   237  			if !apierrors.IsNotFound(err) {
   238  				return errors.Wrap(err, "could not get information about the resource")
   239  			}
   240  
   241  			// Append the created resource to the results, even if something fails
   242  			res.Created = append(res.Created, info)
   243  
   244  			// Since the resource does not exist, create it.
   245  			if err := createResource(info); err != nil {
   246  				return errors.Wrap(err, "failed to create resource")
   247  			}
   248  
   249  			kind := info.Mapping.GroupVersionKind.Kind
   250  			c.Log("Created a new %s called %q in %s\n", kind, info.Name, info.Namespace)
   251  			return nil
   252  		}
   253  
   254  		originalInfo := original.Get(info)
   255  		if originalInfo == nil {
   256  			kind := info.Mapping.GroupVersionKind.Kind
   257  			return errors.Errorf("no %s with the name %q found", kind, info.Name)
   258  		}
   259  
   260  		if err := updateResource(c, info, originalInfo.Object, force); err != nil {
   261  			c.Log("error updating the resource %q:\n\t %v", info.Name, err)
   262  			updateErrors = append(updateErrors, err.Error())
   263  		}
   264  		// Because we check for errors later, append the info regardless
   265  		res.Updated = append(res.Updated, info)
   266  
   267  		return nil
   268  	})
   269  
   270  	switch {
   271  	case err != nil:
   272  		return res, err
   273  	case len(updateErrors) != 0:
   274  		return res, errors.Errorf(strings.Join(updateErrors, " && "))
   275  	}
   276  
   277  	for _, info := range original.Difference(target) {
   278  		c.Log("Deleting %s %q in namespace %s...", info.Mapping.GroupVersionKind.Kind, info.Name, info.Namespace)
   279  
   280  		if err := info.Get(); err != nil {
   281  			c.Log("Unable to get obj %q, err: %s", info.Name, err)
   282  			continue
   283  		}
   284  		annotations, err := metadataAccessor.Annotations(info.Object)
   285  		if err != nil {
   286  			c.Log("Unable to get annotations on %q, err: %s", info.Name, err)
   287  		}
   288  		if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy {
   289  			c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy)
   290  			continue
   291  		}
   292  		if err := deleteResource(info); err != nil {
   293  			c.Log("Failed to delete %q, err: %s", info.ObjectName(), err)
   294  			continue
   295  		}
   296  		res.Deleted = append(res.Deleted, info)
   297  	}
   298  	return res, nil
   299  }
   300  
   301  // Delete deletes Kubernetes resources specified in the resources list. It will
   302  // attempt to delete all resources even if one or more fail and collect any
   303  // errors. All successfully deleted items will be returned in the `Deleted`
   304  // ResourceList that is part of the result.
   305  func (c *Client) Delete(resources ResourceList) (*Result, []error) {
   306  	var errs []error
   307  	res := &Result{}
   308  	mtx := sync.Mutex{}
   309  	err := perform(resources, func(info *resource.Info) error {
   310  		c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind)
   311  		err := deleteResource(info)
   312  		if err == nil || apierrors.IsNotFound(err) {
   313  			if err != nil {
   314  				c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err)
   315  			}
   316  			mtx.Lock()
   317  			defer mtx.Unlock()
   318  			res.Deleted = append(res.Deleted, info)
   319  			return nil
   320  		}
   321  		mtx.Lock()
   322  		defer mtx.Unlock()
   323  		// Collect the error and continue on
   324  		errs = append(errs, err)
   325  		return nil
   326  	})
   327  	if err != nil {
   328  		// Rewrite the message from "no objects visited" if that is what we got
   329  		// back
   330  		if err == ErrNoObjectsVisited {
   331  			err = errors.New("object not found, skipping delete")
   332  		}
   333  		errs = append(errs, err)
   334  	}
   335  	if errs != nil {
   336  		return nil, errs
   337  	}
   338  	return res, nil
   339  }
   340  
   341  func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
   342  	return func(info *resource.Info) error {
   343  		return c.watchUntilReady(t, info)
   344  	}
   345  }
   346  
   347  // WatchUntilReady watches the resources given and waits until it is ready.
   348  //
   349  // This method is mainly for hook implementations. It watches for a resource to
   350  // hit a particular milestone. The milestone depends on the Kind.
   351  //
   352  // For most kinds, it checks to see if the resource is marked as Added or Modified
   353  // by the Kubernetes event stream. For some kinds, it does more:
   354  //
   355  // - Jobs: A job is marked "Ready" when it has successfully completed. This is
   356  //   ascertained by watching the Status fields in a job's output.
   357  // - Pods: A pod is marked "Ready" when it has successfully completed. This is
   358  //   ascertained by watching the status.phase field in a pod's output.
   359  //
   360  // Handling for other kinds will be added as necessary.
   361  func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration) error {
   362  	// For jobs, there's also the option to do poll c.Jobs(namespace).Get():
   363  	// https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300
   364  	return perform(resources, c.watchTimeout(timeout))
   365  }
   366  
   367  func perform(infos ResourceList, fn func(*resource.Info) error) error {
   368  	if len(infos) == 0 {
   369  		return ErrNoObjectsVisited
   370  	}
   371  
   372  	errs := make(chan error)
   373  	go batchPerform(infos, fn, errs)
   374  
   375  	for range infos {
   376  		err := <-errs
   377  		if err != nil {
   378  			return err
   379  		}
   380  	}
   381  	return nil
   382  }
   383  
   384  // getManagedFieldsManager returns the manager string. If one was set it will be returned.
   385  // Otherwise, one is calculated based on the name of the binary.
   386  func getManagedFieldsManager() string {
   387  
   388  	// When a manager is explicitly set use it
   389  	if ManagedFieldsManager != "" {
   390  		return ManagedFieldsManager
   391  	}
   392  
   393  	// When no manager is set and no calling application can be found it is unknown
   394  	if len(os.Args[0]) == 0 {
   395  		return "unknown"
   396  	}
   397  
   398  	// When there is an application that can be determined and no set manager
   399  	// use the base name. This is one of the ways Kubernetes libs handle figuring
   400  	// names out.
   401  	return filepath.Base(os.Args[0])
   402  }
   403  
   404  func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) {
   405  	var kind string
   406  	var wg sync.WaitGroup
   407  	for _, info := range infos {
   408  		currentKind := info.Object.GetObjectKind().GroupVersionKind().Kind
   409  		if kind != currentKind {
   410  			wg.Wait()
   411  			kind = currentKind
   412  		}
   413  		wg.Add(1)
   414  		go func(i *resource.Info) {
   415  			errs <- fn(i)
   416  			wg.Done()
   417  		}(info)
   418  	}
   419  }
   420  
   421  func createResource(info *resource.Info) error {
   422  	obj, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).Create(info.Namespace, true, info.Object)
   423  	if err != nil {
   424  		return err
   425  	}
   426  	return info.Refresh(obj, true)
   427  }
   428  
   429  func deleteResource(info *resource.Info) error {
   430  	policy := metav1.DeletePropagationBackground
   431  	opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
   432  	_, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts)
   433  	return err
   434  }
   435  
   436  func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) {
   437  	oldData, err := json.Marshal(current)
   438  	if err != nil {
   439  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration")
   440  	}
   441  	newData, err := json.Marshal(target.Object)
   442  	if err != nil {
   443  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration")
   444  	}
   445  
   446  	// Fetch the current object for the three way merge
   447  	helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
   448  	currentObj, err := helper.Get(target.Namespace, target.Name)
   449  	if err != nil && !apierrors.IsNotFound(err) {
   450  		return nil, types.StrategicMergePatchType, errors.Wrapf(err, "unable to get data for current object %s/%s", target.Namespace, target.Name)
   451  	}
   452  
   453  	// Even if currentObj is nil (because it was not found), it will marshal just fine
   454  	currentData, err := json.Marshal(currentObj)
   455  	if err != nil {
   456  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing live configuration")
   457  	}
   458  
   459  	// Get a versioned object
   460  	versionedObject := AsVersioned(target)
   461  
   462  	// Unstructured objects, such as CRDs, may not have an not registered error
   463  	// returned from ConvertToVersion. Anything that's unstructured should
   464  	// use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
   465  	// on objects like CRDs.
   466  	_, isUnstructured := versionedObject.(runtime.Unstructured)
   467  
   468  	// On newer K8s versions, CRDs aren't unstructured but has this dedicated type
   469  	_, isCRD := versionedObject.(*apiextv1beta1.CustomResourceDefinition)
   470  
   471  	if isUnstructured || isCRD {
   472  		// fall back to generic JSON merge patch
   473  		patch, err := jsonpatch.CreateMergePatch(oldData, newData)
   474  		return patch, types.MergePatchType, err
   475  	}
   476  
   477  	patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
   478  	if err != nil {
   479  		return nil, types.StrategicMergePatchType, errors.Wrap(err, "unable to create patch metadata from object")
   480  	}
   481  
   482  	patch, err := strategicpatch.CreateThreeWayMergePatch(oldData, newData, currentData, patchMeta, true)
   483  	return patch, types.StrategicMergePatchType, err
   484  }
   485  
   486  func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error {
   487  	var (
   488  		obj    runtime.Object
   489  		helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
   490  		kind   = target.Mapping.GroupVersionKind.Kind
   491  	)
   492  
   493  	// if --force is applied, attempt to replace the existing resource with the new object.
   494  	if force {
   495  		var err error
   496  		obj, err = helper.Replace(target.Namespace, target.Name, true, target.Object)
   497  		if err != nil {
   498  			return errors.Wrap(err, "failed to replace object")
   499  		}
   500  		c.Log("Replaced %q with kind %s for kind %s", target.Name, currentObj.GetObjectKind().GroupVersionKind().Kind, kind)
   501  	} else {
   502  		patch, patchType, err := createPatch(target, currentObj)
   503  		if err != nil {
   504  			return errors.Wrap(err, "failed to create patch")
   505  		}
   506  
   507  		if patch == nil || string(patch) == "{}" {
   508  			c.Log("Looks like there are no changes for %s %q", kind, target.Name)
   509  			// This needs to happen to make sure that Helm has the latest info from the API
   510  			// Otherwise there will be no labels and other functions that use labels will panic
   511  			if err := target.Get(); err != nil {
   512  				return errors.Wrap(err, "failed to refresh resource information")
   513  			}
   514  			return nil
   515  		}
   516  		// send patch to server
   517  		c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace)
   518  		obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
   519  		if err != nil {
   520  			return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind)
   521  		}
   522  	}
   523  
   524  	target.Refresh(obj, true)
   525  	return nil
   526  }
   527  
   528  func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) error {
   529  	kind := info.Mapping.GroupVersionKind.Kind
   530  	switch kind {
   531  	case "Job", "Pod":
   532  	default:
   533  		return nil
   534  	}
   535  
   536  	c.Log("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout)
   537  
   538  	// Use a selector on the name of the resource. This should be unique for the
   539  	// given version and kind
   540  	selector, err := fields.ParseSelector(fmt.Sprintf("metadata.name=%s", info.Name))
   541  	if err != nil {
   542  		return err
   543  	}
   544  	lw := cachetools.NewListWatchFromClient(info.Client, info.Mapping.Resource.Resource, info.Namespace, selector)
   545  
   546  	// What we watch for depends on the Kind.
   547  	// - For a Job, we watch for completion.
   548  	// - For all else, we watch until Ready.
   549  	// In the future, we might want to add some special logic for types
   550  	// like Ingress, Volume, etc.
   551  
   552  	ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
   553  	defer cancel()
   554  	_, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, nil, func(e watch.Event) (bool, error) {
   555  		// Make sure the incoming object is versioned as we use unstructured
   556  		// objects when we build manifests
   557  		obj := convertWithMapper(e.Object, info.Mapping)
   558  		switch e.Type {
   559  		case watch.Added, watch.Modified:
   560  			// For things like a secret or a config map, this is the best indicator
   561  			// we get. We care mostly about jobs, where what we want to see is
   562  			// the status go into a good state. For other types, like ReplicaSet
   563  			// we don't really do anything to support these as hooks.
   564  			c.Log("Add/Modify event for %s: %v", info.Name, e.Type)
   565  			switch kind {
   566  			case "Job":
   567  				return c.waitForJob(obj, info.Name)
   568  			case "Pod":
   569  				return c.waitForPodSuccess(obj, info.Name)
   570  			}
   571  			return true, nil
   572  		case watch.Deleted:
   573  			c.Log("Deleted event for %s", info.Name)
   574  			return true, nil
   575  		case watch.Error:
   576  			// Handle error and return with an error.
   577  			c.Log("Error event for %s", info.Name)
   578  			return true, errors.Errorf("failed to deploy %s", info.Name)
   579  		default:
   580  			return false, nil
   581  		}
   582  	})
   583  	return err
   584  }
   585  
   586  // waitForJob is a helper that waits for a job to complete.
   587  //
   588  // This operates on an event returned from a watcher.
   589  func (c *Client) waitForJob(obj runtime.Object, name string) (bool, error) {
   590  	o, ok := obj.(*batch.Job)
   591  	if !ok {
   592  		return true, errors.Errorf("expected %s to be a *batch.Job, got %T", name, obj)
   593  	}
   594  
   595  	for _, c := range o.Status.Conditions {
   596  		if c.Type == batch.JobComplete && c.Status == "True" {
   597  			return true, nil
   598  		} else if c.Type == batch.JobFailed && c.Status == "True" {
   599  			return true, errors.Errorf("job failed: %s", c.Reason)
   600  		}
   601  	}
   602  
   603  	c.Log("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded)
   604  	return false, nil
   605  }
   606  
   607  // waitForPodSuccess is a helper that waits for a pod to complete.
   608  //
   609  // This operates on an event returned from a watcher.
   610  func (c *Client) waitForPodSuccess(obj runtime.Object, name string) (bool, error) {
   611  	o, ok := obj.(*v1.Pod)
   612  	if !ok {
   613  		return true, errors.Errorf("expected %s to be a *v1.Pod, got %T", name, obj)
   614  	}
   615  
   616  	switch o.Status.Phase {
   617  	case v1.PodSucceeded:
   618  		c.Log("Pod %s succeeded", o.Name)
   619  		return true, nil
   620  	case v1.PodFailed:
   621  		return true, errors.Errorf("pod %s failed", o.Name)
   622  	case v1.PodPending:
   623  		c.Log("Pod %s pending", o.Name)
   624  	case v1.PodRunning:
   625  		c.Log("Pod %s running", o.Name)
   626  	}
   627  
   628  	return false, nil
   629  }
   630  
   631  // scrubValidationError removes kubectl info from the message.
   632  func scrubValidationError(err error) error {
   633  	if err == nil {
   634  		return nil
   635  	}
   636  	const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false"
   637  
   638  	if strings.Contains(err.Error(), stopValidateMessage) {
   639  		return errors.New(strings.ReplaceAll(err.Error(), "; "+stopValidateMessage, ""))
   640  	}
   641  	return err
   642  }
   643  
   644  // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase
   645  // and returns said phase (PodSucceeded or PodFailed qualify).
   646  func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) {
   647  	client, err := c.getKubeClient()
   648  	if err != nil {
   649  		return v1.PodUnknown, err
   650  	}
   651  	to := int64(timeout)
   652  	watcher, err := client.CoreV1().Pods(c.namespace()).Watch(context.Background(), metav1.ListOptions{
   653  		FieldSelector:  fmt.Sprintf("metadata.name=%s", name),
   654  		TimeoutSeconds: &to,
   655  	})
   656  	if err != nil {
   657  		return v1.PodUnknown, err
   658  	}
   659  
   660  	for event := range watcher.ResultChan() {
   661  		p, ok := event.Object.(*v1.Pod)
   662  		if !ok {
   663  			return v1.PodUnknown, fmt.Errorf("%s not a pod", name)
   664  		}
   665  		switch p.Status.Phase {
   666  		case v1.PodFailed:
   667  			return v1.PodFailed, nil
   668  		case v1.PodSucceeded:
   669  			return v1.PodSucceeded, nil
   670  		}
   671  	}
   672  
   673  	return v1.PodUnknown, err
   674  }