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