github.com/azure-devops-engineer/helm@v3.0.0-alpha.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 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 Flatten() 118 } 119 120 func (c *Client) validator() resource.ContentValidator { 121 schema, err := c.Factory.Validator(true) 122 if err != nil { 123 c.Log("warning: failed to load schema: %s", err) 124 } 125 return schema 126 } 127 128 // BuildUnstructured validates for Kubernetes objects and returns unstructured infos. 129 func (c *Client) BuildUnstructured(reader io.Reader) (Result, error) { 130 result, err := c.newBuilder(). 131 Unstructured(). 132 Stream(reader, ""). 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(reader io.Reader) (Result, error) { 139 result, err := c.newBuilder(). 140 WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). 141 Schema(c.validator()). 142 Stream(reader, ""). 143 Do(). 144 Infos() 145 return result, scrubValidationError(err) 146 } 147 148 // Update reads in the current configuration and a target configuration from io.reader 149 // and creates resources that don't already exists, updates resources that have been modified 150 // in the target configuration and deletes resources from the current configuration that are 151 // not present in the target configuration. 152 // 153 // Namespace will set the namespaces. 154 func (c *Client) Update(originalReader, targetReader io.Reader, force, recreate bool) error { 155 original, err := c.BuildUnstructured(originalReader) 156 if err != nil { 157 return errors.Wrap(err, "failed decoding reader into objects") 158 } 159 160 c.Log("building resources from updated manifest") 161 target, err := c.BuildUnstructured(targetReader) 162 if err != nil { 163 return errors.Wrap(err, "failed decoding reader into objects") 164 } 165 166 updateErrors := []string{} 167 168 c.Log("checking %d resources for changes", len(target)) 169 err = target.Visit(func(info *resource.Info, err error) error { 170 if err != nil { 171 return err 172 } 173 174 helper := resource.NewHelper(info.Client, info.Mapping) 175 if _, err := helper.Get(info.Namespace, info.Name, info.Export); err != nil { 176 if !apierrors.IsNotFound(err) { 177 return errors.Wrap(err, "could not get information about the resource") 178 } 179 180 // Since the resource does not exist, create it. 181 if err := createResource(info); err != nil { 182 return errors.Wrap(err, "failed to create resource") 183 } 184 185 kind := info.Mapping.GroupVersionKind.Kind 186 c.Log("Created a new %s called %q\n", kind, info.Name) 187 return nil 188 } 189 190 originalInfo := original.Get(info) 191 if originalInfo == nil { 192 kind := info.Mapping.GroupVersionKind.Kind 193 return errors.Errorf("no %s with the name %q found", kind, info.Name) 194 } 195 196 if err := updateResource(c, info, originalInfo.Object, force, recreate); err != nil { 197 c.Log("error updating the resource %q:\n\t %v", info.Name, err) 198 updateErrors = append(updateErrors, err.Error()) 199 } 200 201 return nil 202 }) 203 204 switch { 205 case err != nil: 206 return err 207 case len(updateErrors) != 0: 208 return errors.Errorf(strings.Join(updateErrors, " && ")) 209 } 210 211 for _, info := range original.Difference(target) { 212 c.Log("Deleting %q in %s...", info.Name, info.Namespace) 213 if err := deleteResource(info); err != nil { 214 c.Log("Failed to delete %q, err: %s", info.Name, err) 215 } 216 } 217 return nil 218 } 219 220 // Delete deletes Kubernetes resources from an io.reader. 221 // 222 // Namespace will set the namespace. 223 func (c *Client) Delete(reader io.Reader) error { 224 infos, err := c.BuildUnstructured(reader) 225 if err != nil { 226 return err 227 } 228 return perform(infos, func(info *resource.Info) error { 229 c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind) 230 err := deleteResource(info) 231 return c.skipIfNotFound(err) 232 }) 233 } 234 235 func (c *Client) skipIfNotFound(err error) error { 236 if apierrors.IsNotFound(err) { 237 c.Log("%v", err) 238 return nil 239 } 240 return err 241 } 242 243 func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error { 244 return func(info *resource.Info) error { 245 return c.watchUntilReady(t, info) 246 } 247 } 248 249 // WatchUntilReady watches the resource given in the reader, and waits until it is ready. 250 // 251 // This function is mainly for hook implementations. It watches for a resource to 252 // hit a particular milestone. The milestone depends on the Kind. 253 // 254 // For most kinds, it checks to see if the resource is marked as Added or Modified 255 // by the Kubernetes event stream. For some kinds, it does more: 256 // 257 // - Jobs: A job is marked "Ready" when it has successfully completed. This is 258 // ascertained by watching the Status fields in a job's output. 259 // 260 // Handling for other kinds will be added as necessary. 261 func (c *Client) WatchUntilReady(reader io.Reader, timeout time.Duration) error { 262 infos, err := c.Build(reader) 263 if err != nil { 264 return err 265 } 266 // For jobs, there's also the option to do poll c.Jobs(namespace).Get(): 267 // https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300 268 return perform(infos, c.watchTimeout(timeout)) 269 } 270 271 func perform(infos Result, fn func(*resource.Info) error) error { 272 if len(infos) == 0 { 273 return ErrNoObjectsVisited 274 } 275 276 for _, info := range infos { 277 if err := fn(info); err != nil { 278 return err 279 } 280 } 281 return nil 282 } 283 284 func createResource(info *resource.Info) error { 285 obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object, nil) 286 if err != nil { 287 return err 288 } 289 return info.Refresh(obj, true) 290 } 291 292 func deleteResource(info *resource.Info) error { 293 policy := metav1.DeletePropagationBackground 294 opts := &metav1.DeleteOptions{PropagationPolicy: &policy} 295 _, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, opts) 296 return err 297 } 298 299 func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) { 300 oldData, err := json.Marshal(current) 301 if err != nil { 302 return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration") 303 } 304 newData, err := json.Marshal(target.Object) 305 if err != nil { 306 return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration") 307 } 308 309 // While different objects need different merge types, the parent function 310 // that calls this does not try to create a patch when the data (first 311 // returned object) is nil. We can skip calculating the merge type as 312 // the returned merge type is ignored. 313 if apiequality.Semantic.DeepEqual(oldData, newData) { 314 return nil, types.StrategicMergePatchType, nil 315 } 316 317 // Get a versioned object 318 versionedObject := asVersioned(target) 319 320 // Unstructured objects, such as CRDs, may not have an not registered error 321 // returned from ConvertToVersion. Anything that's unstructured should 322 // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported 323 // on objects like CRDs. 324 if _, ok := versionedObject.(runtime.Unstructured); ok { 325 // fall back to generic JSON merge patch 326 patch, err := jsonpatch.CreateMergePatch(oldData, newData) 327 return patch, types.MergePatchType, err 328 } 329 patch, err := strategicpatch.CreateTwoWayMergePatch(oldData, newData, versionedObject) 330 return patch, types.StrategicMergePatchType, err 331 } 332 333 func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force, recreate bool) error { 334 patch, patchType, err := createPatch(target, currentObj) 335 if err != nil { 336 return errors.Wrap(err, "failed to create patch") 337 } 338 if patch == nil { 339 c.Log("Looks like there are no changes for %s %q", target.Mapping.GroupVersionKind.Kind, target.Name) 340 // This needs to happen to make sure that tiller has the latest info from the API 341 // Otherwise there will be no labels and other functions that use labels will panic 342 if err := target.Get(); err != nil { 343 return errors.Wrap(err, "error trying to refresh resource information") 344 } 345 } else { 346 // send patch to server 347 helper := resource.NewHelper(target.Client, target.Mapping) 348 349 obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil) 350 if err != nil { 351 kind := target.Mapping.GroupVersionKind.Kind 352 log.Printf("Cannot patch %s: %q (%v)", kind, target.Name, err) 353 354 if force { 355 // Attempt to delete... 356 if err := deleteResource(target); err != nil { 357 return err 358 } 359 log.Printf("Deleted %s: %q", kind, target.Name) 360 361 // ... and recreate 362 if err := createResource(target); err != nil { 363 return errors.Wrap(err, "failed to recreate resource") 364 } 365 log.Printf("Created a new %s called %q\n", kind, target.Name) 366 367 // No need to refresh the target, as we recreated the resource based 368 // on it. In addition, it might not exist yet and a call to `Refresh` 369 // may fail. 370 } else { 371 log.Print("Use --force to force recreation of the resource") 372 return err 373 } 374 } else { 375 // When patch succeeds without needing to recreate, refresh target. 376 target.Refresh(obj, true) 377 } 378 } 379 380 if !recreate { 381 return nil 382 } 383 384 versioned := asVersioned(target) 385 selector, err := selectorsForObject(versioned) 386 if err != nil { 387 return nil 388 } 389 390 client, err := c.KubernetesClientSet() 391 if err != nil { 392 return err 393 } 394 395 pods, err := client.CoreV1().Pods(target.Namespace).List(metav1.ListOptions{ 396 LabelSelector: selector.String(), 397 }) 398 if err != nil { 399 return err 400 } 401 402 // Restart pods 403 for _, pod := range pods.Items { 404 c.Log("Restarting pod: %v/%v", pod.Namespace, pod.Name) 405 406 // Delete each pod for get them restarted with changed spec. 407 if err := client.CoreV1().Pods(pod.Namespace).Delete(pod.Name, metav1.NewPreconditionDeleteOptions(string(pod.UID))); err != nil { 408 return err 409 } 410 } 411 return nil 412 } 413 414 func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) error { 415 w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) 416 if err != nil { 417 return err 418 } 419 420 kind := info.Mapping.GroupVersionKind.Kind 421 c.Log("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout) 422 423 // What we watch for depends on the Kind. 424 // - For a Job, we watch for completion. 425 // - For all else, we watch until Ready. 426 // In the future, we might want to add some special logic for types 427 // like Ingress, Volume, etc. 428 429 ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout) 430 defer cancel() 431 _, err = watchtools.UntilWithoutRetry(ctx, w, func(e watch.Event) (bool, error) { 432 switch e.Type { 433 case watch.Added, watch.Modified: 434 // For things like a secret or a config map, this is the best indicator 435 // we get. We care mostly about jobs, where what we want to see is 436 // the status go into a good state. For other types, like ReplicaSet 437 // we don't really do anything to support these as hooks. 438 c.Log("Add/Modify event for %s: %v", info.Name, e.Type) 439 if kind == "Job" { 440 return c.waitForJob(e, info.Name) 441 } 442 return true, nil 443 case watch.Deleted: 444 c.Log("Deleted event for %s", info.Name) 445 return true, nil 446 case watch.Error: 447 // Handle error and return with an error. 448 c.Log("Error event for %s", info.Name) 449 return true, errors.Errorf("failed to deploy %s", info.Name) 450 default: 451 return false, nil 452 } 453 }) 454 return err 455 } 456 457 // waitForJob is a helper that waits for a job to complete. 458 // 459 // This operates on an event returned from a watcher. 460 func (c *Client) waitForJob(e watch.Event, name string) (bool, error) { 461 o, ok := e.Object.(*batch.Job) 462 if !ok { 463 return true, errors.Errorf("expected %s to be a *batch.Job, got %T", name, e.Object) 464 } 465 466 for _, c := range o.Status.Conditions { 467 if c.Type == batch.JobComplete && c.Status == "True" { 468 return true, nil 469 } else if c.Type == batch.JobFailed && c.Status == "True" { 470 return true, errors.Errorf("job failed: %s", c.Reason) 471 } 472 } 473 474 c.Log("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded) 475 return false, nil 476 } 477 478 // scrubValidationError removes kubectl info from the message. 479 func scrubValidationError(err error) error { 480 if err == nil { 481 return nil 482 } 483 const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false" 484 485 if strings.Contains(err.Error(), stopValidateMessage) { 486 return errors.New(strings.ReplaceAll(err.Error(), "; "+stopValidateMessage, "")) 487 } 488 return err 489 } 490 491 // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase 492 // and returns said phase (PodSucceeded or PodFailed qualify). 493 func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) { 494 client, _ := c.KubernetesClientSet() 495 to := int64(timeout) 496 watcher, err := client.CoreV1().Pods(c.namespace()).Watch(metav1.ListOptions{ 497 FieldSelector: fmt.Sprintf("metadata.name=%s", name), 498 TimeoutSeconds: &to, 499 }) 500 501 for event := range watcher.ResultChan() { 502 p, ok := event.Object.(*v1.Pod) 503 if !ok { 504 return v1.PodUnknown, fmt.Errorf("%s not a pod", name) 505 } 506 switch p.Status.Phase { 507 case v1.PodFailed: 508 return v1.PodFailed, nil 509 case v1.PodSucceeded: 510 return v1.PodSucceeded, nil 511 } 512 } 513 514 return v1.PodUnknown, err 515 }