From 7bb36f51ba229e05bd22aabecc1b8d4a8e3d0549 Mon Sep 17 00:00:00 2001
From: Angus Lees <gus@inodes.org>
Date: Wed, 2 Aug 2017 16:56:54 +1000
Subject: [PATCH] Move all the non-command-line bulk of update into a library
 package

This will make it possible to inject fake dependencies, once the fake
dynamic client makes it into a client-go release.

(other commands will be moved similarly in future PRs)
---
 Makefile                            |   2 +-
 cmd/update.go                       | 232 ++-----------------------
 pkg/kubecfg/update.go               | 254 ++++++++++++++++++++++++++++
 {cmd => pkg/kubecfg}/update_test.go |   2 +-
 4 files changed, 266 insertions(+), 224 deletions(-)
 create mode 100644 pkg/kubecfg/update.go
 rename {cmd => pkg/kubecfg}/update_test.go (99%)

diff --git a/Makefile b/Makefile
index 5e01d173..ea646f52 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ GOFMT = gofmt
 
 JSONNET_FILES = lib/kubecfg_test.jsonnet examples/guestbook.jsonnet
 # TODO: Simplify this once ./... ignores ./vendor
-GO_PACKAGES = ./cmd/... ./utils/...
+GO_PACKAGES = ./cmd/... ./utils/... ./pkg/...
 
 all: kubecfg
 
diff --git a/cmd/update.go b/cmd/update.go
index a69038f9..c48696fe 100644
--- a/cmd/update.go
+++ b/cmd/update.go
@@ -16,24 +16,9 @@
 package cmd
 
 import (
-	"encoding/json"
-	"fmt"
-	"sort"
-
-	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
-	"k8s.io/apimachinery/pkg/api/errors"
-	"k8s.io/apimachinery/pkg/api/meta"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/diff"
-	"k8s.io/apimachinery/pkg/util/sets"
-	"k8s.io/client-go/discovery"
-	"k8s.io/client-go/dynamic"
 
-	"github.com/ksonnet/kubecfg/utils"
+	"github.com/ksonnet/kubecfg/pkg/kubecfg"
 )
 
 const (
@@ -71,33 +56,35 @@ var updateCmd = &cobra.Command{
 	Short: "Update Kubernetes resources with local config",
 	RunE: func(cmd *cobra.Command, args []string) error {
 		flags := cmd.Flags()
+		var err error
+		c := kubecfg.UpdateCmd{}
 
-		create, err := flags.GetBool(flagCreate)
+		c.Create, err = flags.GetBool(flagCreate)
 		if err != nil {
 			return err
 		}
 
-		gcTag, err := flags.GetString(flagGcTag)
+		c.GcTag, err = flags.GetString(flagGcTag)
 		if err != nil {
 			return err
 		}
 
-		skipGc, err := flags.GetBool(flagSkipGc)
+		c.SkipGc, err = flags.GetBool(flagSkipGc)
 		if err != nil {
 			return err
 		}
 
-		dryRun, err := flags.GetBool(flagDryRun)
+		c.DryRun, err = flags.GetBool(flagDryRun)
 		if err != nil {
 			return err
 		}
 
-		clientpool, disco, err := restClientPool(cmd)
+		c.ClientPool, c.Discovery, err = restClientPool(cmd)
 		if err != nil {
 			return err
 		}
 
-		defaultNs, _, err := clientConfig.Namespace()
+		c.DefaultNamespace, _, err = clientConfig.Namespace()
 		if err != nil {
 			return err
 		}
@@ -107,205 +94,6 @@ var updateCmd = &cobra.Command{
 			return err
 		}
 
-		dryRunText := ""
-		if dryRun {
-			dryRunText = " (dry-run)"
-		}
-
-		sort.Sort(utils.DependencyOrder(objs))
-
-		seenUids := sets.NewString()
-
-		for _, obj := range objs {
-			if gcTag != "" {
-				utils.SetMetaDataAnnotation(obj, AnnotationGcTag, gcTag)
-			}
-
-			desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(disco, obj), utils.FqName(obj))
-			log.Info("Updating ", desc, dryRunText)
-
-			rc, err := utils.ClientForResource(clientpool, disco, obj, defaultNs)
-			if err != nil {
-				return err
-			}
-
-			asPatch, err := json.Marshal(obj)
-			if err != nil {
-				return err
-			}
-			var newobj metav1.Object
-			if !dryRun {
-				newobj, err = rc.Patch(obj.GetName(), types.MergePatchType, asPatch)
-				log.Debug("Patch(%s) returned (%v, %v)", obj.GetName(), newobj, err)
-			} else {
-				newobj, err = rc.Get(obj.GetName())
-			}
-			if create && errors.IsNotFound(err) {
-				log.Info(" Creating non-existent ", desc, dryRunText)
-				if !dryRun {
-					newobj, err = rc.Create(obj)
-					log.Debug("Create(%s) returned (%v, %v)", obj.GetName(), newobj, err)
-				} else {
-					newobj = obj
-					err = nil
-				}
-			}
-			if err != nil {
-				// TODO: retry
-				return fmt.Errorf("Error updating %s: %s", desc, err)
-			}
-
-			log.Debug("Updated object: ", diff.ObjectDiff(obj, newobj))
-
-			// Some objects appear under multiple kinds
-			// (eg: Deployment is both extensions/v1beta1
-			// and apps/v1beta1).  UID is the only stable
-			// identifier that links these two views of
-			// the same object.
-			seenUids.Insert(string(newobj.GetUID()))
-		}
-
-		if gcTag != "" && !skipGc {
-			version, err := utils.FetchVersion(disco)
-			if err != nil {
-				return err
-			}
-
-			err = walkObjects(clientpool, disco, metav1.ListOptions{}, func(o runtime.Object) error {
-				meta, err := meta.Accessor(o)
-				if err != nil {
-					return err
-				}
-				gvk := o.GetObjectKind().GroupVersionKind()
-				desc := fmt.Sprintf("%s %s (%s)", utils.ResourceNameFor(disco, o), utils.FqName(meta), gvk.GroupVersion())
-				log.Debugf("Considering %v for gc", desc)
-				if eligibleForGc(meta, gcTag) && !seenUids.Has(string(meta.GetUID())) {
-					log.Info("Garbage collecting ", desc, dryRunText)
-					if !dryRun {
-						err := gcDelete(clientpool, disco, &version, o)
-						if err != nil {
-							return err
-						}
-					}
-				}
-				return nil
-			})
-			if err != nil {
-				return err
-			}
-		}
-
-		return nil
+		return c.Run(objs)
 	},
 }
-
-func stringListContains(list []string, value string) bool {
-	for _, item := range list {
-		if item == value {
-			return true
-		}
-	}
-	return false
-}
-
-func gcDelete(clientpool dynamic.ClientPool, disco discovery.DiscoveryInterface, version *utils.ServerVersion, o runtime.Object) error {
-	obj, err := meta.Accessor(o)
-	if err != nil {
-		return fmt.Errorf("Unexpected object type: %s", err)
-	}
-
-	uid := obj.GetUID()
-	desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(disco, o), utils.FqName(obj))
-
-	deleteOpts := metav1.DeleteOptions{
-		Preconditions: &metav1.Preconditions{UID: &uid},
-	}
-	if version.Compare(1, 6) < 0 {
-		// 1.5.x option
-		boolFalse := false
-		deleteOpts.OrphanDependents = &boolFalse
-	} else {
-		// 1.6.x option (NB: Background is broken)
-		fg := metav1.DeletePropagationForeground
-		deleteOpts.PropagationPolicy = &fg
-	}
-
-	c, err := utils.ClientForResource(clientpool, disco, o, metav1.NamespaceNone)
-	if err != nil {
-		return err
-	}
-
-	err = c.Delete(obj.GetName(), &deleteOpts)
-	if err != nil && (errors.IsNotFound(err) || errors.IsConflict(err)) {
-		// We lost a race with something else changing the object
-		log.Debugf("Ignoring error while deleting %s: %s", desc, err)
-		err = nil
-	}
-	if err != nil {
-		return fmt.Errorf("Error deleting %s: %s", desc, err)
-	}
-
-	return nil
-}
-
-func walkObjects(pool dynamic.ClientPool, disco discovery.DiscoveryInterface, listopts metav1.ListOptions, callback func(runtime.Object) error) error {
-	rsrclists, err := disco.ServerResources()
-	if err != nil {
-		return err
-	}
-	for _, rsrclist := range rsrclists {
-		gv, err := schema.ParseGroupVersion(rsrclist.GroupVersion)
-		if err != nil {
-			return err
-		}
-		for _, rsrc := range rsrclist.APIResources {
-			gvk := gv.WithKind(rsrc.Kind)
-
-			if !stringListContains(rsrc.Verbs, "list") {
-				log.Debugf("Don't know how to list %v, skipping", rsrc)
-				continue
-			}
-			client, err := pool.ClientForGroupVersionKind(gvk)
-			if err != nil {
-				return err
-			}
-
-			var ns string
-			if rsrc.Namespaced {
-				ns = metav1.NamespaceAll
-			} else {
-				ns = metav1.NamespaceNone
-			}
-
-			rc := client.Resource(&rsrc, ns)
-			log.Debugf("Listing %s", gvk)
-			obj, err := rc.List(listopts)
-			if err != nil {
-				return err
-			}
-			if err = meta.EachListItem(obj, callback); err != nil {
-				return err
-			}
-		}
-	}
-	return nil
-}
-
-func eligibleForGc(obj metav1.Object, gcTag string) bool {
-	for _, ref := range obj.GetOwnerReferences() {
-		if ref.Controller != nil && *ref.Controller {
-			// Has a controller ref
-			return false
-		}
-	}
-
-	a := obj.GetAnnotations()
-
-	strategy, ok := a[AnnotationGcStrategy]
-	if !ok {
-		strategy = GcStrategyAuto
-	}
-
-	return a[AnnotationGcTag] == gcTag &&
-		strategy == GcStrategyAuto
-}
diff --git a/pkg/kubecfg/update.go b/pkg/kubecfg/update.go
new file mode 100644
index 00000000..9a95c84e
--- /dev/null
+++ b/pkg/kubecfg/update.go
@@ -0,0 +1,254 @@
+package kubecfg
+
+import (
+	"encoding/json"
+	"fmt"
+	"sort"
+
+	log "github.com/sirupsen/logrus"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/diff"
+	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/dynamic"
+
+	"github.com/ksonnet/kubecfg/utils"
+)
+
+const (
+	// AnnotationGcTag annotation that triggers
+	// garbage collection. Objects with value equal to
+	// command-line flag that are *not* in config will be deleted.
+	AnnotationGcTag = "kubecfg.ksonnet.io/garbage-collect-tag"
+
+	// AnnotationGcStrategy controls gc logic.  Current values:
+	// `auto` (default if absent) - do garbage collection
+	// `ignore` - never garbage collect this object
+	AnnotationGcStrategy = "kubecfg.ksonnet.io/garbage-collect-strategy"
+
+	// GcStrategyAuto is the default automatic gc logic
+	GcStrategyAuto = "auto"
+	// GcStrategyIgnore means this object should be ignored by garbage collection
+	GcStrategyIgnore = "ignore"
+)
+
+// UpdateCmd represents the update subcommand
+type UpdateCmd struct {
+	ClientPool       dynamic.ClientPool
+	Discovery        discovery.DiscoveryInterface
+	DefaultNamespace string
+
+	Create bool
+	GcTag  string
+	SkipGc bool
+	DryRun bool
+}
+
+func (c UpdateCmd) Run(objs []*unstructured.Unstructured) error {
+	dryRunText := ""
+	if c.DryRun {
+		dryRunText = " (dry-run)"
+	}
+
+	sort.Sort(utils.DependencyOrder(objs))
+
+	seenUids := sets.NewString()
+
+	for _, obj := range objs {
+		if c.GcTag != "" {
+			utils.SetMetaDataAnnotation(obj, AnnotationGcTag, c.GcTag)
+		}
+
+		desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(c.Discovery, obj), utils.FqName(obj))
+		log.Info("Updating ", desc, dryRunText)
+
+		rc, err := utils.ClientForResource(c.ClientPool, c.Discovery, obj, c.DefaultNamespace)
+		if err != nil {
+			return err
+		}
+
+		asPatch, err := json.Marshal(obj)
+		if err != nil {
+			return err
+		}
+		var newobj metav1.Object
+		if !c.DryRun {
+			newobj, err = rc.Patch(obj.GetName(), types.MergePatchType, asPatch)
+			log.Debug("Patch(%s) returned (%v, %v)", obj.GetName(), newobj, err)
+		} else {
+			newobj, err = rc.Get(obj.GetName())
+		}
+		if c.Create && errors.IsNotFound(err) {
+			log.Info(" Creating non-existent ", desc, dryRunText)
+			if !c.DryRun {
+				newobj, err = rc.Create(obj)
+				log.Debug("Create(%s) returned (%v, %v)", obj.GetName(), newobj, err)
+			} else {
+				newobj = obj
+				err = nil
+			}
+		}
+		if err != nil {
+			// TODO: retry
+			return fmt.Errorf("Error updating %s: %s", desc, err)
+		}
+
+		log.Debug("Updated object: ", diff.ObjectDiff(obj, newobj))
+
+		// Some objects appear under multiple kinds
+		// (eg: Deployment is both extensions/v1beta1
+		// and apps/v1beta1).  UID is the only stable
+		// identifier that links these two views of
+		// the same object.
+		seenUids.Insert(string(newobj.GetUID()))
+	}
+
+	if c.GcTag != "" && !c.SkipGc {
+		version, err := utils.FetchVersion(c.Discovery)
+		if err != nil {
+			return err
+		}
+
+		err = walkObjects(c.ClientPool, c.Discovery, metav1.ListOptions{}, func(o runtime.Object) error {
+			meta, err := meta.Accessor(o)
+			if err != nil {
+				return err
+			}
+			gvk := o.GetObjectKind().GroupVersionKind()
+			desc := fmt.Sprintf("%s %s (%s)", utils.ResourceNameFor(c.Discovery, o), utils.FqName(meta), gvk.GroupVersion())
+			log.Debugf("Considering %v for gc", desc)
+			if eligibleForGc(meta, c.GcTag) && !seenUids.Has(string(meta.GetUID())) {
+				log.Info("Garbage collecting ", desc, dryRunText)
+				if !c.DryRun {
+					err := gcDelete(c.ClientPool, c.Discovery, &version, o)
+					if err != nil {
+						return err
+					}
+				}
+			}
+			return nil
+		})
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func stringListContains(list []string, value string) bool {
+	for _, item := range list {
+		if item == value {
+			return true
+		}
+	}
+	return false
+}
+
+func gcDelete(clientpool dynamic.ClientPool, disco discovery.DiscoveryInterface, version *utils.ServerVersion, o runtime.Object) error {
+	obj, err := meta.Accessor(o)
+	if err != nil {
+		return fmt.Errorf("Unexpected object type: %s", err)
+	}
+
+	uid := obj.GetUID()
+	desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(disco, o), utils.FqName(obj))
+
+	deleteOpts := metav1.DeleteOptions{
+		Preconditions: &metav1.Preconditions{UID: &uid},
+	}
+	if version.Compare(1, 6) < 0 {
+		// 1.5.x option
+		boolFalse := false
+		deleteOpts.OrphanDependents = &boolFalse
+	} else {
+		// 1.6.x option (NB: Background is broken)
+		fg := metav1.DeletePropagationForeground
+		deleteOpts.PropagationPolicy = &fg
+	}
+
+	c, err := utils.ClientForResource(clientpool, disco, o, metav1.NamespaceNone)
+	if err != nil {
+		return err
+	}
+
+	err = c.Delete(obj.GetName(), &deleteOpts)
+	if err != nil && (errors.IsNotFound(err) || errors.IsConflict(err)) {
+		// We lost a race with something else changing the object
+		log.Debugf("Ignoring error while deleting %s: %s", desc, err)
+		err = nil
+	}
+	if err != nil {
+		return fmt.Errorf("Error deleting %s: %s", desc, err)
+	}
+
+	return nil
+}
+
+func walkObjects(pool dynamic.ClientPool, disco discovery.DiscoveryInterface, listopts metav1.ListOptions, callback func(runtime.Object) error) error {
+	rsrclists, err := disco.ServerResources()
+	if err != nil {
+		return err
+	}
+	for _, rsrclist := range rsrclists {
+		gv, err := schema.ParseGroupVersion(rsrclist.GroupVersion)
+		if err != nil {
+			return err
+		}
+		for _, rsrc := range rsrclist.APIResources {
+			gvk := gv.WithKind(rsrc.Kind)
+
+			if !stringListContains(rsrc.Verbs, "list") {
+				log.Debugf("Don't know how to list %v, skipping", rsrc)
+				continue
+			}
+			client, err := pool.ClientForGroupVersionKind(gvk)
+			if err != nil {
+				return err
+			}
+
+			var ns string
+			if rsrc.Namespaced {
+				ns = metav1.NamespaceAll
+			} else {
+				ns = metav1.NamespaceNone
+			}
+
+			rc := client.Resource(&rsrc, ns)
+			log.Debugf("Listing %s", gvk)
+			obj, err := rc.List(listopts)
+			if err != nil {
+				return err
+			}
+			if err = meta.EachListItem(obj, callback); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func eligibleForGc(obj metav1.Object, gcTag string) bool {
+	for _, ref := range obj.GetOwnerReferences() {
+		if ref.Controller != nil && *ref.Controller {
+			// Has a controller ref
+			return false
+		}
+	}
+
+	a := obj.GetAnnotations()
+
+	strategy, ok := a[AnnotationGcStrategy]
+	if !ok {
+		strategy = GcStrategyAuto
+	}
+
+	return a[AnnotationGcTag] == gcTag &&
+		strategy == GcStrategyAuto
+}
diff --git a/cmd/update_test.go b/pkg/kubecfg/update_test.go
similarity index 99%
rename from cmd/update_test.go
rename to pkg/kubecfg/update_test.go
index 70d596a5..9a785a72 100644
--- a/cmd/update_test.go
+++ b/pkg/kubecfg/update_test.go
@@ -1,4 +1,4 @@
-package cmd
+package kubecfg
 
 import (
 	"testing"
-- 
GitLab