github.com/felipejfc/helm@v2.1.2+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 goerrors "errors" 22 "fmt" 23 "io" 24 "log" 25 "strings" 26 "time" 27 28 "k8s.io/kubernetes/pkg/api" 29 "k8s.io/kubernetes/pkg/api/errors" 30 "k8s.io/kubernetes/pkg/apimachinery/registered" 31 "k8s.io/kubernetes/pkg/apis/batch" 32 "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" 33 "k8s.io/kubernetes/pkg/kubectl" 34 cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 35 "k8s.io/kubernetes/pkg/kubectl/resource" 36 "k8s.io/kubernetes/pkg/runtime" 37 "k8s.io/kubernetes/pkg/util/strategicpatch" 38 "k8s.io/kubernetes/pkg/watch" 39 ) 40 41 // ErrNoObjectsVisited indicates that during a visit operation, no matching objects were found. 42 var ErrNoObjectsVisited = goerrors.New("no objects visited") 43 44 // Client represents a client capable of communicating with the Kubernetes API. 45 type Client struct { 46 cmdutil.Factory 47 // Validate idicates whether to load a schema for validation. 48 Validate bool 49 // SchemaCacheDir is the path for loading cached schema. 50 SchemaCacheDir string 51 } 52 53 // New create a new Client 54 func New(config clientcmd.ClientConfig) *Client { 55 return &Client{ 56 Factory: cmdutil.NewFactory(config), 57 Validate: true, 58 SchemaCacheDir: clientcmd.RecommendedSchemaFile, 59 } 60 } 61 62 // ResourceActorFunc performs an action on a single resource. 63 type ResourceActorFunc func(*resource.Info) error 64 65 // ErrAlreadyExists can be returned where there are no changes 66 type ErrAlreadyExists struct { 67 errorMsg string 68 } 69 70 func (e ErrAlreadyExists) Error() string { 71 return fmt.Sprintf("Looks like there are no changes for %s", e.errorMsg) 72 } 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) 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 return perform(c, namespace, reader, createResource) 86 } 87 88 func (c *Client) newBuilder(namespace string, reader io.Reader) *resource.Builder { 89 schema, err := c.Validator(c.Validate, c.SchemaCacheDir) 90 if err != nil { 91 log.Printf("warning: failed to load schema: %s", err) 92 } 93 return c.NewBuilder(). 94 ContinueOnError(). 95 Schema(schema). 96 NamespaceParam(namespace). 97 DefaultNamespace(). 98 Stream(reader, ""). 99 Flatten() 100 } 101 102 // Get gets kubernetes resources as pretty printed string 103 // 104 // Namespace will set the namespace 105 func (c *Client) Get(namespace string, reader io.Reader) (string, error) { 106 // Since we don't know what order the objects come in, let's group them by the types, so 107 // that when we print them, they come looking good (headers apply to subgroups, etc.) 108 objs := make(map[string][]runtime.Object) 109 err := perform(c, namespace, reader, func(info *resource.Info) error { 110 log.Printf("Doing get for: '%s'", info.Name) 111 obj, err := resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name, info.Export) 112 if err != nil { 113 return err 114 } 115 // We need to grab the ObjectReference so we can correctly group the objects. 116 or, err := api.GetReference(obj) 117 if err != nil { 118 log.Printf("FAILED GetReference for: %#v\n%v", obj, err) 119 return err 120 } 121 122 // Use APIVersion/Kind as grouping mechanism. I'm not sure if you can have multiple 123 // versions per cluster, but this certainly won't hurt anything, so let's be safe. 124 objType := or.APIVersion + "/" + or.Kind 125 objs[objType] = append(objs[objType], obj) 126 return nil 127 }) 128 129 // Ok, now we have all the objects grouped by types (say, by v1/Pod, v1/Service, etc.), so 130 // spin through them and print them. Printer is cool since it prints the header only when 131 // an object type changes, so we can just rely on that. Problem is it doesn't seem to keep 132 // track of tab widths 133 buf := new(bytes.Buffer) 134 p := kubectl.NewHumanReadablePrinter(kubectl.PrintOptions{}) 135 for t, ot := range objs { 136 _, err = buf.WriteString("==> " + t + "\n") 137 if err != nil { 138 return "", err 139 } 140 for _, o := range ot { 141 err = p.PrintObj(o, buf) 142 if err != nil { 143 log.Printf("failed to print object type '%s', object: '%s' :\n %v", t, o, err) 144 return "", err 145 } 146 } 147 _, err := buf.WriteString("\n") 148 if err != nil { 149 return "", err 150 } 151 } 152 return buf.String(), err 153 } 154 155 // Update reads in the current configuration and a target configuration from io.reader 156 // and creates resources that don't already exists, updates resources that have been modified 157 // in the target configuration and deletes resources from the current configuration that are 158 // not present in the target configuration 159 // 160 // Namespace will set the namespaces 161 func (c *Client) Update(namespace string, currentReader, targetReader io.Reader) error { 162 currentInfos, err := c.newBuilder(namespace, currentReader).Do().Infos() 163 if err != nil { 164 return fmt.Errorf("failed decoding reader into objects: %s", err) 165 } 166 167 target := c.newBuilder(namespace, targetReader).Do() 168 if target.Err() != nil { 169 return fmt.Errorf("failed decoding reader into objects: %s", target.Err()) 170 } 171 172 targetInfos := []*resource.Info{} 173 updateErrors := []string{} 174 175 err = target.Visit(func(info *resource.Info, err error) error { 176 targetInfos = append(targetInfos, info) 177 if err != nil { 178 return err 179 } 180 181 helper := resource.NewHelper(info.Client, info.Mapping) 182 if _, err := helper.Get(info.Namespace, info.Name, info.Export); err != nil { 183 if !errors.IsNotFound(err) { 184 return fmt.Errorf("Could not get information about the resource: err: %s", err) 185 } 186 187 // Since the resource does not exist, create it. 188 if err := createResource(info); err != nil { 189 return fmt.Errorf("failed to create resource: %s", err) 190 } 191 192 kind := info.Mapping.GroupVersionKind.Kind 193 log.Printf("Created a new %s called %s\n", kind, info.Name) 194 return nil 195 } 196 197 currentObj, err := getCurrentObject(info, currentInfos) 198 if err != nil { 199 return err 200 } 201 202 if err := updateResource(info, currentObj); err != nil { 203 if alreadyExistErr, ok := err.(ErrAlreadyExists); ok { 204 log.Printf(alreadyExistErr.errorMsg) 205 } else { 206 log.Printf("error updating the resource %s:\n\t %v", info.Name, err) 207 updateErrors = append(updateErrors, err.Error()) 208 } 209 } 210 211 return nil 212 }) 213 214 if err != nil { 215 return err 216 } else if len(updateErrors) != 0 { 217 return fmt.Errorf(strings.Join(updateErrors, " && ")) 218 } 219 deleteUnwantedResources(currentInfos, targetInfos) 220 return nil 221 } 222 223 // Delete deletes kubernetes resources from an io.reader 224 // 225 // Namespace will set the namespace 226 func (c *Client) Delete(namespace string, reader io.Reader) error { 227 return perform(c, namespace, reader, func(info *resource.Info) error { 228 log.Printf("Starting delete for %s %s", info.Name, info.Mapping.GroupVersionKind.Kind) 229 230 reaper, err := c.Reaper(info.Mapping) 231 if err != nil { 232 // If there is no reaper for this resources, delete it. 233 if kubectl.IsNoSuchReaperError(err) { 234 err := resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) 235 return skipIfNotFound(err) 236 } 237 238 return err 239 } 240 241 log.Printf("Using reaper for deleting %s", info.Name) 242 err = reaper.Stop(info.Namespace, info.Name, 0, nil) 243 return skipIfNotFound(err) 244 }) 245 } 246 247 func skipIfNotFound(err error) error { 248 if err != nil && errors.IsNotFound(err) { 249 log.Printf("%v", err) 250 return nil 251 } 252 return err 253 } 254 255 // WatchUntilReady watches the resource given in the reader, and waits until it is ready. 256 // 257 // This function is mainly for hook implementations. It watches for a resource to 258 // hit a particular milestone. The milestone depends on the Kind. 259 // 260 // For most kinds, it checks to see if the resource is marked as Added or Modified 261 // by the Kubernetes event stream. For some kinds, it does more: 262 // 263 // - Jobs: A job is marked "Ready" when it has successfully completed. This is 264 // ascertained by watching the Status fields in a job's output. 265 // 266 // Handling for other kinds will be added as necessary. 267 func (c *Client) WatchUntilReady(namespace string, reader io.Reader) error { 268 // For jobs, there's also the option to do poll c.Jobs(namespace).Get(): 269 // https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300 270 return perform(c, namespace, reader, watchUntilReady) 271 } 272 273 func perform(c *Client, namespace string, reader io.Reader, fn ResourceActorFunc) error { 274 infos, err := c.newBuilder(namespace, reader).Do().Infos() 275 switch { 276 case err != nil: 277 return scrubValidationError(err) 278 case len(infos) == 0: 279 return ErrNoObjectsVisited 280 } 281 for _, info := range infos { 282 if err := fn(info); err != nil { 283 return err 284 } 285 } 286 return nil 287 } 288 289 func createResource(info *resource.Info) error { 290 _, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object) 291 return err 292 } 293 294 func deleteResource(info *resource.Info) error { 295 return resource.NewHelper(info.Client, info.Mapping).Delete(info.Namespace, info.Name) 296 } 297 298 func updateResource(target *resource.Info, currentObj runtime.Object) error { 299 encoder := api.Codecs.LegacyCodec(registered.EnabledVersions()...) 300 original, err := runtime.Encode(encoder, currentObj) 301 if err != nil { 302 return err 303 } 304 305 modified, err := runtime.Encode(encoder, target.Object) 306 if err != nil { 307 return err 308 } 309 310 if api.Semantic.DeepEqual(original, modified) { 311 return ErrAlreadyExists{target.Name} 312 } 313 314 patch, err := strategicpatch.CreateTwoWayMergePatch(original, modified, currentObj) 315 if err != nil { 316 return err 317 } 318 319 // send patch to server 320 helper := resource.NewHelper(target.Client, target.Mapping) 321 _, err = helper.Patch(target.Namespace, target.Name, api.StrategicMergePatchType, patch) 322 return err 323 } 324 325 func watchUntilReady(info *resource.Info) error { 326 w, err := resource.NewHelper(info.Client, info.Mapping).WatchSingle(info.Namespace, info.Name, info.ResourceVersion) 327 if err != nil { 328 return err 329 } 330 331 kind := info.Mapping.GroupVersionKind.Kind 332 log.Printf("Watching for changes to %s %s", kind, info.Name) 333 timeout := time.Minute * 5 334 335 // What we watch for depends on the Kind. 336 // - For a Job, we watch for completion. 337 // - For all else, we watch until Ready. 338 // In the future, we might want to add some special logic for types 339 // like Ingress, Volume, etc. 340 341 _, err = watch.Until(timeout, w, func(e watch.Event) (bool, error) { 342 switch e.Type { 343 case watch.Added, watch.Modified: 344 // For things like a secret or a config map, this is the best indicator 345 // we get. We care mostly about jobs, where what we want to see is 346 // the status go into a good state. For other types, like ReplicaSet 347 // we don't really do anything to support these as hooks. 348 log.Printf("Add/Modify event for %s: %v", info.Name, e.Type) 349 if kind == "Job" { 350 return waitForJob(e, info.Name) 351 } 352 return true, nil 353 case watch.Deleted: 354 log.Printf("Deleted event for %s", info.Name) 355 return true, nil 356 case watch.Error: 357 // Handle error and return with an error. 358 log.Printf("Error event for %s", info.Name) 359 return true, fmt.Errorf("Failed to deploy %s", info.Name) 360 default: 361 return false, nil 362 } 363 }) 364 return err 365 } 366 367 // waitForJob is a helper that waits for a job to complete. 368 // 369 // This operates on an event returned from a watcher. 370 func waitForJob(e watch.Event, name string) (bool, error) { 371 o, ok := e.Object.(*batch.Job) 372 if !ok { 373 return true, fmt.Errorf("Expected %s to be a *batch.Job, got %T", name, o) 374 } 375 376 for _, c := range o.Status.Conditions { 377 if c.Type == batch.JobComplete && c.Status == api.ConditionTrue { 378 return true, nil 379 } else if c.Type == batch.JobFailed && c.Status == api.ConditionTrue { 380 return true, fmt.Errorf("Job failed: %s", c.Reason) 381 } 382 } 383 384 log.Printf("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded) 385 return false, nil 386 } 387 388 func deleteUnwantedResources(currentInfos, targetInfos []*resource.Info) { 389 for _, cInfo := range currentInfos { 390 if _, ok := findMatchingInfo(cInfo, targetInfos); !ok { 391 log.Printf("Deleting %s...", cInfo.Name) 392 if err := deleteResource(cInfo); err != nil { 393 log.Printf("Failed to delete %s, err: %s", cInfo.Name, err) 394 } 395 } 396 } 397 } 398 399 func getCurrentObject(target *resource.Info, infos []*resource.Info) (runtime.Object, error) { 400 if found, ok := findMatchingInfo(target, infos); ok { 401 return found.Mapping.ConvertToVersion(found.Object, found.Mapping.GroupVersionKind.GroupVersion()) 402 } 403 return nil, fmt.Errorf("no resource with the name %s found", target.Name) 404 } 405 406 // isMatchingInfo returns true if infos match on Name and Kind. 407 func isMatchingInfo(a, b *resource.Info) bool { 408 return a.Name == b.Name && a.Mapping.GroupVersionKind.Kind == b.Mapping.GroupVersionKind.Kind 409 } 410 411 // findMatchingInfo returns the first object that matches target. 412 func findMatchingInfo(target *resource.Info, infos []*resource.Info) (*resource.Info, bool) { 413 for _, info := range infos { 414 if isMatchingInfo(target, info) { 415 return info, true 416 } 417 } 418 return nil, false 419 } 420 421 // scrubValidationError removes kubectl info from the message 422 func scrubValidationError(err error) error { 423 const stopValidateMessage = "if you choose to ignore these errors, turn validation off with --validate=false" 424 425 if strings.Contains(err.Error(), stopValidateMessage) { 426 return goerrors.New(strings.Replace(err.Error(), "; "+stopValidateMessage, "", -1)) 427 } 428 return err 429 }