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 }