github.com/canthefason/helm@v2.2.1-0.20170221172616-16b043b8d505+incompatible/pkg/kube/client.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 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 "k8s.io/helm/pkg/kube" 18 19 import ( 20 "bytes" 21 "encoding/json" 22 goerrors "errors" 23 "fmt" 24 "io" 25 "log" 26 "strings" 27 "time" 28 29 jsonpatch "github.com/evanphx/json-patch" 30 "k8s.io/kubernetes/pkg/api" 31 "k8s.io/kubernetes/pkg/api/errors" 32 "k8s.io/kubernetes/pkg/api/meta" 33 "k8s.io/kubernetes/pkg/api/v1" 34 apps "k8s.io/kubernetes/pkg/apis/apps/v1beta1" 35 batchinternal "k8s.io/kubernetes/pkg/apis/batch" 36 batch "k8s.io/kubernetes/pkg/apis/batch/v1" 37 extensionsinternal "k8s.io/kubernetes/pkg/apis/extensions" 38 extensions "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" 39 "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" 40 conditions "k8s.io/kubernetes/pkg/client/unversioned" 41 "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" 42 "k8s.io/kubernetes/pkg/fields" 43 "k8s.io/kubernetes/pkg/kubectl" 44 cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 45 "k8s.io/kubernetes/pkg/kubectl/resource" 46 "k8s.io/kubernetes/pkg/labels" 47 "k8s.io/kubernetes/pkg/runtime" 48 "k8s.io/kubernetes/pkg/util/strategicpatch" 49 "k8s.io/kubernetes/pkg/util/wait" 50 "k8s.io/kubernetes/pkg/watch" 51 ) 52 53 // ErrNoObjectsVisited indicates that during a visit operation, no matching objects were found. 54 var ErrNoObjectsVisited = goerrors.New("no objects visited") 55 56 // Client represents a client capable of communicating with the Kubernetes API. 57 type Client struct { 58 cmdutil.Factory 59 // SchemaCacheDir is the path for loading cached schema. 60 SchemaCacheDir string 61 } 62 63 // New create a new Client 64 func New(config clientcmd.ClientConfig) *Client { 65 return &Client{ 66 Factory: cmdutil.NewFactory(config), 67 SchemaCacheDir: clientcmd.RecommendedSchemaFile, 68 } 69 } 70 71 // ResourceActorFunc performs an action on a single resource. 72 type ResourceActorFunc func(*resource.Info) error 73 74 // Create creates kubernetes resources from an io.reader 75 // 76 // Namespace will set the namespace 77 func (c *Client) Create(namespace string, reader io.Reader, timeout int64, shouldWait bool) error { 78 client, err := c.ClientSet() 79 if err != nil { 80 return err 81 } 82 if err := ensureNamespace(client, namespace); err != nil { 83 return err 84 } 85 infos, buildErr := c.BuildUnstructured(namespace, reader) 86 if buildErr != nil { 87 return buildErr 88 } 89 if err := perform(c, namespace, infos, createResource); err != nil { 90 return err 91 } 92 if shouldWait { 93 return c.waitForResources(time.Duration(timeout)*time.Second, infos) 94 } 95 return nil 96 } 97 98 func (c *Client) newBuilder(namespace string, reader io.Reader) *resource.Result { 99 schema, err := c.Validator(true, c.SchemaCacheDir) 100 if err != nil { 101 log.Printf("warning: failed to load schema: %s", err) 102 } 103 return c.NewBuilder(). 104 ContinueOnError(). 105 Schema(schema). 106 NamespaceParam(namespace). 107 DefaultNamespace(). 108 Stream(reader, ""). 109 Flatten(). 110 Do() 111 } 112 113 // BuildUnstructured validates for Kubernetes objects and returns unstructured infos. 114 func (c *Client) BuildUnstructured(namespace string, reader io.Reader) (Result, error) { 115 schema, err := c.Validator(true, c.SchemaCacheDir) 116 if err != nil { 117 log.Printf("warning: failed to load schema: %s", err) 118 } 119 120 mapper, typer, err := c.UnstructuredObject() 121 if err != nil { 122 log.Printf("failed to load mapper: %s", err) 123 return nil, err 124 } 125 var result Result 126 result, err = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(c.UnstructuredClientForMapping), runtime.UnstructuredJSONScheme). 127 ContinueOnError(). 128 Schema(schema). 129 NamespaceParam(namespace). 130 DefaultNamespace(). 131 Stream(reader, ""). 132 Flatten(). 133 Do().Infos() 134 return result, scrubValidationError(err) 135 } 136 137 // Build validates for Kubernetes objects and returns resource Infos from a io.Reader. 138 func (c *Client) Build(namespace string, reader io.Reader) (Result, error) { 139 var result Result 140 result, err := c.newBuilder(namespace, reader).Infos() 141 return result, scrubValidationError(err) 142 } 143 144 // Get gets kubernetes resources as pretty printed string 145 // 146 // Namespace will set the namespace 147 func (c *Client) Get(namespace string, reader io.Reader) (string, error) { 148 // Since we don't know what order the objects come in, let's group them by the types, so 149 // that when we print them, they come looking good (headers apply to subgroups, etc.) 150 objs := make(map[string][]runtime.Object) 151 infos, err := c.BuildUnstructured(namespace, reader) 152 if err != nil { 153 return "", err 154 } 155 err = perform(c, namespace, infos, func(info *resource.Info) error { 156 log.Printf("Doing get for: '%s'", info.Name) 157 obj, err := resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name, info.Export) 158 if err != nil { 159 return err 160 } 161 // We need to grab the ObjectReference so we can correctly group the objects. 162 or, err := api.GetReference(obj) 163 if err != nil { 164 log.Printf("FAILED GetReference for: %#v\n%v", obj, err) 165 return err 166 } 167 168 // Use APIVersion/Kind as grouping mechanism. I'm not sure if you can have multiple 169 // versions per cluster, but this certainly won't hurt anything, so let's be safe. 170 objType := or.APIVersion + "/" + or.Kind 171 objs[objType] = append(objs[objType], obj) 172 return nil 173 }) 174 if err != nil { 175 return "", err 176 } 177 178 // Ok, now we have all the objects grouped by types (say, by v1/Pod, v1/Service, etc.), so 179 // spin through them and print them. Printer is cool since it prints the header only when 180 // an object type changes, so we can just rely on that. Problem is it doesn't seem to keep 181 // track of tab widths 182 buf := new(bytes.Buffer) 183 p := kubectl.NewHumanReadablePrinter(kubectl.PrintOptions{}) 184 for t, ot := range objs { 185 if _, err = buf.WriteString("==> " + t + "\n"); err != nil { 186 return "", err 187 } 188 for _, o := range ot { 189 if err := p.PrintObj(o, buf); err != nil { 190 log.Printf("failed to print object type '%s', object: '%s' :\n %v", t, o, err) 191 return "", err 192 } 193 } 194 if _, err := buf.WriteString("\n"); err != nil { 195 return "", err 196 } 197 } 198 return buf.String(), nil 199 } 200 201 // Update reads in the current configuration and a target configuration from io.reader 202 // and creates resources that don't already exists, updates resources that have been modified 203 // in the target configuration and deletes resources from the current configuration that are 204 // not present in the target configuration 205 // 206 // Namespace will set the namespaces 207 func (c *Client) Update(namespace string, originalReader, targetReader io.Reader, recreate bool, timeout int64, shouldWait bool) error { 208 original, err := c.BuildUnstructured(namespace, originalReader) 209 if err != nil { 210 return fmt.Errorf("failed decoding reader into objects: %s", err) 211 } 212 213 target, err := c.BuildUnstructured(namespace, targetReader) 214 if err != nil { 215 return fmt.Errorf("failed decoding reader into objects: %s", err) 216 } 217 218 updateErrors := []string{} 219 220 err = target.Visit(func(info *resource.Info, err error) error { 221 if err != nil { 222 return err 223 } 224 225 helper := resource.NewHelper(info.Client, info.Mapping) 226 if _, err := helper.Get(info.Namespace, info.Name, info.Export); err != nil { 227 if !errors.IsNotFound(err) { 228 return fmt.Errorf("Could not get information about the resource: err: %s", err) 229 } 230 231 // Since the resource does not exist, create it. 232 if err := createResource(info); err != nil { 233 return fmt.Errorf("failed to create resource: %s", err) 234 } 235 236 kind := info.Mapping.GroupVersionKind.Kind 237 log.Printf("Created a new %s called %s\n", kind, info.Name) 238 return nil 239 } 240 241 originalInfo := original.Get(info) 242 if originalInfo == nil { 243 return fmt.Errorf("no resource with the name %s found", info.Name) 244 } 245 246 if err := updateResource(c, info, originalInfo.Object, recreate); err != nil { 247 log.Printf("error updating the resource %s:\n\t %v", info.Name, err) 248 updateErrors = append(updateErrors, err.Error()) 249 } 250 251 return nil 252 }) 253 254 switch { 255 case err != nil: 256 return err 257 case len(updateErrors) != 0: 258 return fmt.Errorf(strings.Join(updateErrors, " && ")) 259 } 260 261 for _, info := range original.Difference(target) { 262 log.Printf("Deleting %s in %s...", info.Name, info.Namespace) 263 if err := deleteResource(c, info); err != nil { 264 log.Printf("Failed to delete %s, err: %s", info.Name, err) 265 } 266 } 267 if shouldWait { 268 return c.waitForResources(time.Duration(timeout)*time.Second, target) 269 } 270 return nil 271 } 272 273 // Delete deletes kubernetes resources from an io.reader 274 // 275 // Namespace will set the namespace 276 func (c *Client) Delete(namespace string, reader io.Reader) error { 277 infos, err := c.BuildUnstructured(namespace, reader) 278 if err != nil { 279 return err 280 } 281 return perform(c, namespace, infos, func(info *resource.Info) error { 282 log.Printf("Starting delete for %s %s", info.Name, info.Mapping.GroupVersionKind.Kind) 283 err := deleteResource(c, info) 284 return skipIfNotFound(err) 285 }) 286 } 287 288 func skipIfNotFound(err error) error { 289 if errors.IsNotFound(err) { 290 log.Printf("%v", err) 291 return nil 292 } 293 return err 294 } 295 296 func watchTimeout(t time.Duration) ResourceActorFunc { 297 return func(info *resource.Info) error { 298 return watchUntilReady(t, info) 299 } 300 } 301 302 // WatchUntilReady watches the resource given in the reader, and waits until it is ready. 303 // 304 // This function is mainly for hook implementations. It watches for a resource to 305 // hit a particular milestone. The milestone depends on the Kind. 306 // 307 // For most kinds, it checks to see if the resource is marked as Added or Modified 308 // by the Kubernetes event stream. For some kinds, it does more: 309 // 310 // - Jobs: A job is marked "Ready" when it has successfully completed. This is 311 // ascertained by watching the Status fields in a job's output. 312 // 313 // Handling for other kinds will be added as necessary. 314 func (c *Client) WatchUntilReady(namespace string, reader io.Reader, timeout int64, shouldWait bool) error { 315 infos, err := c.Build(namespace, reader) 316 if err != nil { 317 return err 318 } 319 // For jobs, there's also the option to do poll c.Jobs(namespace).Get(): 320 // https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300 321 return perform(c, namespace, infos, watchTimeout(time.Duration(timeout)*time.Second)) 322 } 323 324 func perform(c *Client, namespace string, infos Result, fn ResourceActorFunc) error { 325 if len(infos) == 0 { 326 return ErrNoObjectsVisited 327 } 328 329 for _, info := range infos { 330 if err := fn(info); err != nil { 331 return err 332 } 333 } 334 return nil 335 } 336 337 func createResource(info *resource.Info) error { 338 obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) 339 if err != nil { 340 return err 341 } 342 return info.Refresh(obj, true) 343 } 344 345 func deleteResource(c *Client, info *resource.Info) error { 346 reaper, err := c.Reaper(info.Mapping) 347 if err != nil { 348 // If there is no reaper for this resources, delete it. 349 if kubectl.IsNoSuchReaperError(err) { 350 return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) 351 } 352 return err 353 } 354 log.Printf("Using reaper for deleting %s", info.Name) 355 return reaper.Stop(info.Namespace, info.Name, 0, nil) 356 } 357 358 func createPatch(mapping *meta.RESTMapping, target, current runtime.Object) ([]byte, api.PatchType, error) { 359 oldData, err := json.Marshal(current) 360 if err != nil { 361 return nil, api.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %s", err) 362 } 363 newData, err := json.Marshal(target) 364 if err != nil { 365 return nil, api.StrategicMergePatchType, fmt.Errorf("serializing target configuration: %s", err) 366 } 367 368 if api.Semantic.DeepEqual(oldData, newData) { 369 return nil, api.StrategicMergePatchType, nil 370 } 371 372 // Get a versioned object 373 versionedObject, err := api.Scheme.New(mapping.GroupVersionKind) 374 switch { 375 case runtime.IsNotRegisteredError(err): 376 // fall back to generic JSON merge patch 377 patch, err := jsonpatch.CreateMergePatch(oldData, newData) 378 return patch, api.MergePatchType, err 379 case err != nil: 380 return nil, api.StrategicMergePatchType, fmt.Errorf("failed to get versionedObject: %s", err) 381 default: 382 log.Printf("generating strategic merge patch for %T", target) 383 patch, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, versionedObject) 384 return patch, api.StrategicMergePatchType, err 385 } 386 } 387 388 func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, recreate bool) error { 389 patch, patchType, err := createPatch(target.Mapping, target.Object, currentObj) 390 if err != nil { 391 return fmt.Errorf("failed to create patch: %s", err) 392 } 393 if patch == nil { 394 log.Printf("Looks like there are no changes for %s", target.Name) 395 return nil 396 } 397 398 // send patch to server 399 helper := resource.NewHelper(target.Client, target.Mapping) 400 obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch) 401 if err != nil { 402 return err 403 } 404 405 target.Refresh(obj, true) 406 407 if !recreate { 408 return nil 409 } 410 411 selector, err := getSelectorFromObject(currentObj) 412 if err != nil { 413 return nil 414 } 415 client, _ := c.ClientSet() 416 return recreatePods(client, target.Namespace, selector) 417 } 418 419 func getSelectorFromObject(obj runtime.Object) (map[string]string, error) { 420 switch typed := obj.(type) { 421 case *v1.ReplicationController: 422 return typed.Spec.Selector, nil 423 case *extensions.ReplicaSet: 424 return typed.Spec.Selector.MatchLabels, nil 425 case *extensions.Deployment: 426 return typed.Spec.Selector.MatchLabels, nil 427 case *extensions.DaemonSet: 428 return typed.Spec.Selector.MatchLabels, nil 429 case *batch.Job: 430 return typed.Spec.Selector.MatchLabels, nil 431 case *apps.StatefulSet: 432 return typed.Spec.Selector.MatchLabels, nil 433 default: 434 return nil, fmt.Errorf("Unsupported kind when getting selector: %v", obj) 435 } 436 } 437 438 func recreatePods(client *internalclientset.Clientset, namespace string, selector map[string]string) error { 439 pods, err := client.Pods(namespace).List(api.ListOptions{ 440 FieldSelector: fields.Everything(), 441 LabelSelector: labels.Set(selector).AsSelector(), 442 }) 443 if err != nil { 444 return err 445 } 446 447 // Restart pods 448 for _, pod := range pods.Items { 449 log.Printf("Restarting pod: %v/%v", pod.Namespace, pod.Name) 450 451 // Delete each pod for get them restarted with changed spec. 452 if err := client.Pods(pod.Namespace).Delete(pod.Name, api.NewPreconditionDeleteOptions(string(pod.UID))); err != nil { 453 return err 454 } 455 } 456 return nil 457 } 458 459 func watchUntilReady(timeout time.Duration, info *resource.Info) error { 460 w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) 461 if err != nil { 462 return err 463 } 464 465 kind := info.Mapping.GroupVersionKind.Kind 466 log.Printf("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout) 467 468 // What we watch for depends on the Kind. 469 // - For a Job, we watch for completion. 470 // - For all else, we watch until Ready. 471 // In the future, we might want to add some special logic for types 472 // like Ingress, Volume, etc. 473 474 _, err = watch.Until(timeout, w, func(e watch.Event) (bool, error) { 475 switch e.Type { 476 case watch.Added, watch.Modified: 477 // For things like a secret or a config map, this is the best indicator 478 // we get. We care mostly about jobs, where what we want to see is 479 // the status go into a good state. For other types, like ReplicaSet 480 // we don't really do anything to support these as hooks. 481 log.Printf("Add/Modify event for %s: %v", info.Name, e.Type) 482 if kind == "Job" { 483 return waitForJob(e, info.Name) 484 } 485 return true, nil 486 case watch.Deleted: 487 log.Printf("Deleted event for %s", info.Name) 488 return true, nil 489 case watch.Error: 490 // Handle error and return with an error. 491 log.Printf("Error event for %s", info.Name) 492 return true, fmt.Errorf("Failed to deploy %s", info.Name) 493 default: 494 return false, nil 495 } 496 }) 497 return err 498 } 499 500 func podsReady(pods []api.Pod) bool { 501 for _, pod := range pods { 502 if !api.IsPodReady(&pod) { 503 return false 504 } 505 } 506 return true 507 } 508 509 func servicesReady(svc []api.Service) bool { 510 for _, s := range svc { 511 if !api.IsServiceIPSet(&s) { 512 return false 513 } 514 // This checks if the service has a LoadBalancer and that balancer has an Ingress defined 515 if s.Spec.Type == api.ServiceTypeLoadBalancer && s.Status.LoadBalancer.Ingress == nil { 516 return false 517 } 518 } 519 return true 520 } 521 522 func volumesReady(vols []api.PersistentVolumeClaim) bool { 523 for _, v := range vols { 524 if v.Status.Phase != api.ClaimBound { 525 return false 526 } 527 } 528 return true 529 } 530 531 func getPods(client *internalclientset.Clientset, namespace string, selector map[string]string) ([]api.Pod, error) { 532 list, err := client.Pods(namespace).List(api.ListOptions{ 533 FieldSelector: fields.Everything(), 534 LabelSelector: labels.Set(selector).AsSelector(), 535 }) 536 return list.Items, err 537 } 538 539 // waitForResources polls to get the current status of all pods, PVCs, and Services 540 // until all are ready or a timeout is reached 541 func (c *Client) waitForResources(timeout time.Duration, created Result) error { 542 log.Printf("beginning wait for resources with timeout of %v", timeout) 543 client, _ := c.ClientSet() 544 return wait.Poll(2*time.Second, timeout, func() (bool, error) { 545 pods := []api.Pod{} 546 services := []api.Service{} 547 pvc := []api.PersistentVolumeClaim{} 548 for _, v := range created { 549 switch value := v.Object.(type) { 550 case (*api.ReplicationController): 551 list, err := getPods(client, value.Namespace, value.Spec.Selector) 552 if err != nil { 553 return false, err 554 } 555 pods = append(pods, list...) 556 case (*api.Pod): 557 pod, err := client.Pods(value.Namespace).Get(value.Name) 558 if err != nil { 559 return false, err 560 } 561 pods = append(pods, *pod) 562 case (*extensionsinternal.Deployment): 563 // Get the RS children first 564 rs, err := client.ReplicaSets(value.Namespace).List(api.ListOptions{ 565 FieldSelector: fields.Everything(), 566 LabelSelector: labels.Set(value.Spec.Selector.MatchLabels).AsSelector(), 567 }) 568 if err != nil { 569 return false, err 570 } 571 for _, r := range rs.Items { 572 list, err := getPods(client, value.Namespace, r.Spec.Selector.MatchLabels) 573 if err != nil { 574 return false, err 575 } 576 pods = append(pods, list...) 577 } 578 case (*extensionsinternal.DaemonSet): 579 list, err := getPods(client, value.Namespace, value.Spec.Selector.MatchLabels) 580 if err != nil { 581 return false, err 582 } 583 pods = append(pods, list...) 584 case (*apps.StatefulSet): 585 list, err := getPods(client, value.Namespace, value.Spec.Selector.MatchLabels) 586 if err != nil { 587 return false, err 588 } 589 pods = append(pods, list...) 590 case (*extensionsinternal.ReplicaSet): 591 list, err := getPods(client, value.Namespace, value.Spec.Selector.MatchLabels) 592 if err != nil { 593 return false, err 594 } 595 pods = append(pods, list...) 596 case (*api.PersistentVolumeClaim): 597 claim, err := client.PersistentVolumeClaims(value.Namespace).Get(value.Name) 598 if err != nil { 599 return false, err 600 } 601 pvc = append(pvc, *claim) 602 case (*api.Service): 603 svc, err := client.Services(value.Namespace).Get(value.Name) 604 if err != nil { 605 return false, err 606 } 607 services = append(services, *svc) 608 } 609 } 610 return podsReady(pods) && servicesReady(services) && volumesReady(pvc), nil 611 }) 612 } 613 614 // waitForJob is a helper that waits for a job to complete. 615 // 616 // This operates on an event returned from a watcher. 617 func waitForJob(e watch.Event, name string) (bool, error) { 618 o, ok := e.Object.(*batchinternal.Job) 619 if !ok { 620 return true, fmt.Errorf("Expected %s to be a *batch.Job, got %T", name, e.Object) 621 } 622 623 for _, c := range o.Status.Conditions { 624 if c.Type == batchinternal.JobComplete && c.Status == api.ConditionTrue { 625 return true, nil 626 } else if c.Type == batchinternal.JobFailed && c.Status == api.ConditionTrue { 627 return true, fmt.Errorf("Job failed: %s", c.Reason) 628 } 629 } 630 631 log.Printf("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded) 632 return false, nil 633 } 634 635 // scrubValidationError removes kubectl info from the message 636 func scrubValidationError(err error) error { 637 if err == nil { 638 return nil 639 } 640 const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false" 641 642 if strings.Contains(err.Error(), stopValidateMessage) { 643 return goerrors.New(strings.Replace(err.Error(), "; "+stopValidateMessage, "", -1)) 644 } 645 return err 646 } 647 648 // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase 649 // and returns said phase (PodSucceeded or PodFailed qualify) 650 func (c *Client) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (api.PodPhase, error) { 651 infos, err := c.Build(namespace, reader) 652 if err != nil { 653 return api.PodUnknown, err 654 } 655 info := infos[0] 656 657 kind := info.Mapping.GroupVersionKind.Kind 658 if kind != "Pod" { 659 return api.PodUnknown, fmt.Errorf("%s is not a Pod", info.Name) 660 } 661 662 if err := watchPodUntilComplete(timeout, info); err != nil { 663 return api.PodUnknown, err 664 } 665 666 if err := info.Get(); err != nil { 667 return api.PodUnknown, err 668 } 669 status := info.Object.(*api.Pod).Status.Phase 670 671 return status, nil 672 } 673 674 func watchPodUntilComplete(timeout time.Duration, info *resource.Info) error { 675 w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) 676 if err != nil { 677 return err 678 } 679 680 log.Printf("Watching pod %s for completion with timeout of %v", info.Name, timeout) 681 _, err = watch.Until(timeout, w, func(e watch.Event) (bool, error) { 682 return conditions.PodCompleted(e) 683 }) 684 685 return err 686 }