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