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