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  }