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