From 78e778b19e5761c2a530917bd5bba9b7abb6fabf Mon Sep 17 00:00:00 2001 From: Krishnan Anantheswaran Date: Sat, 6 Jul 2019 16:31:25 -0700 Subject: [PATCH] add support for objects with generated names, fixes #29 (#42) Enable support for objects (like jobs and one-off pods) to be created with generateName set instead of a fixed name. This enables the following: * show -O displays prefix- as the object name for dynamic objects * duplicate checks do not consider dynamic objects * the real name of the object is updated on sync to ensure that an object just created is not garbage collected * GC will delete previously created objects with generated names. There is not yet a way to customize this. * diffs will correctly show a new object being created and existing object(s) deleted This commit also fixes a nasty regression in the previous commit wherein deletion wouldn't honor component name filters. --- Makefile | 7 ++-- examples/test-app/components/test-job.yaml | 13 ++++++++ internal/commands/apply.go | 23 +++++++++++-- internal/commands/apply_test.go | 4 ++- internal/commands/component_test.go | 3 +- internal/commands/delete.go | 4 ++- internal/commands/delete_test.go | 16 +++++++++ internal/commands/diff.go | 31 ++++++++++++----- internal/commands/diff_test.go | 3 ++ internal/commands/filter.go | 17 ++++++---- internal/commands/show.go | 5 +-- internal/commands/utils_test.go | 18 +++++----- internal/eval/object-extract.go | 11 ++++-- internal/model/app_test.go | 16 +++++---- internal/model/k8s.go | 25 +++++++++++--- internal/remote/client.go | 39 ++++++++++++++-------- internal/remote/collection.go | 14 +++++--- 17 files changed, 188 insertions(+), 61 deletions(-) create mode 100644 examples/test-app/components/test-job.yaml diff --git a/Makefile b/Makefile index bb72fbbe..8db3b195 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,8 @@ LD_FLAGS += -X "$(LD_FLAGS_PKG).version=$(VERSION)" LD_FLAGS += -X "$(LD_FLAGS_PKG).commit=$(SHORT_COMMIT)" LD_FLAGS += -X "$(LD_FLAGS_PKG).goVersion=$(GO_VERSION)" -DEP_FLAGS ?= "" -LINT_FLAGS ?= "" +DEP_FLAGS ?= +LINT_FLAGS ?= .PHONY: all all: get build lint test @@ -26,11 +26,12 @@ build: .PHONY: test test: - go test -v ./... + go test -race ./... .PHONY: lint lint: go list ./... | grep -v vendor | xargs go vet + go list ./... | grep -v vendor | xargs golint golangci-lint run $(LINT_FLAGS) . .PHONY: install-ci diff --git a/examples/test-app/components/test-job.yaml b/examples/test-app/components/test-job.yaml new file mode 100644 index 00000000..2e2d39e7 --- /dev/null +++ b/examples/test-app/components/test-job.yaml @@ -0,0 +1,13 @@ +apiVersion: batch/v1 +kind: Job +metadata: + generateName: tj- +spec: + template: + spec: + containers: + - name: pi + image: perl + command: ["perl"] + args: ["-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never diff --git a/internal/commands/apply.go b/internal/commands/apply.go index eb127be2..d0cf6e56 100644 --- a/internal/commands/apply.go +++ b/internal/commands/apply.go @@ -56,6 +56,15 @@ type applyCommandConfig struct { filterFunc func() (filterParams, error) } +type nameWrap struct { + name string + model.K8sLocalObject +} + +func (nw nameWrap) GetName() string { + return nw.name +} + func doApply(args []string, config applyCommandConfig) error { if len(args) != 1 { return newUsageError("exactly one environment required") @@ -88,11 +97,17 @@ func doApply(args []string, config applyCommandConfig) error { // prepare for GC with object list of deletions var lister lister = &stubLister{} var all []model.K8sLocalObject + var retainObjects []model.K8sLocalObject if config.gc { all, err = allObjects(config.Config, env) if err != nil { return err } + for _, o := range all { + if o.GetName() != "" { + retainObjects = append(retainObjects, o) + } + } var scope remote.ListQueryScope lister, scope, err = newRemoteLister(client, all, config.app.DefaultNamespace(env)) if err != nil { @@ -117,11 +132,15 @@ func doApply(args []string, config applyCommandConfig) error { var stats applyStats for _, ob := range objects { - name := client.DisplayName(ob) res, err := client.Sync(ob, opts) if err != nil { return err } + if res.GeneratedName != "" { + ob = nameWrap{name: res.GeneratedName, K8sLocalObject: ob} + retainObjects = append(retainObjects, ob) + } + name := client.DisplayName(ob) stats.update(name, res) show := res.Type != remote.SyncObjectsIdentical || config.Verbosity() > 0 if show { @@ -131,7 +150,7 @@ func doApply(args []string, config applyCommandConfig) error { } // process deletions - deletions, err := lister.deletions(all, fp.Includes) + deletions, err := lister.deletions(retainObjects, fp.Includes) if err != nil { return err } diff --git a/internal/commands/apply_test.go b/internal/commands/apply_test.go index f285a849..5fc4b8e1 100644 --- a/internal/commands/apply_test.go +++ b/internal/commands/apply_test.go @@ -43,6 +43,8 @@ func TestApplyBasic(t *testing.T) { return &remote.SyncResult{Type: remote.SyncCreated, Details: "some yaml"}, nil case obj.GetName() == "svc2-deploy": return &remote.SyncResult{Type: remote.SyncObjectsIdentical, Details: "sync skipped"}, nil + case obj.GetName() == "": + return &remote.SyncResult{Type: remote.SyncCreated, GeneratedName: obj.GetGenerateName() + "1234", Details: "created"}, nil default: return &remote.SyncResult{Type: remote.SyncObjectsIdentical, Details: "sync skipped"}, nil } @@ -58,7 +60,7 @@ func TestApplyBasic(t *testing.T) { a.EqualValues(remote.SyncOptions{}, captured) a.True(stats["same"].(float64) > 0) a.EqualValues(8, stats["same"]) - a.EqualValues([]interface{}{"Secret:bar-system:svc2-secret"}, stats["created"]) + a.EqualValues([]interface{}{"Secret:bar-system:svc2-secret", "Job::tj-1234"}, stats["created"]) a.EqualValues([]interface{}{"ConfigMap:bar-system:svc2-cm"}, stats["updated"]) a.EqualValues([]interface{}{"Deployment:bar-system:svc2-previous-deploy"}, stats["deleted"]) s.assertErrorLineMatch(regexp.MustCompile(`sync ConfigMap:bar-system:svc2-cm`)) diff --git a/internal/commands/component_test.go b/internal/commands/component_test.go index 5a220f37..ae67a11b 100644 --- a/internal/commands/component_test.go +++ b/internal/commands/component_test.go @@ -32,10 +32,11 @@ func TestComponentListBasic(t *testing.T) { require.Nil(t, err) lines := strings.Split(strings.Trim(s.stdout(), "\n"), "\n") a := assert.New(t) - a.Equal(3, len(lines)) + a.Equal(4, len(lines)) s.assertOutputLineMatch(regexp.MustCompile(`COMPONENT\s+FILE`)) s.assertOutputLineMatch(regexp.MustCompile(`cluster-objects\s+components/cluster-objects.yaml`)) s.assertOutputLineMatch(regexp.MustCompile(`service2\s+components/service2.jsonnet`)) + s.assertOutputLineMatch(regexp.MustCompile(`test-job\s+components/test-job.yaml`)) } func TestComponentListYAML(t *testing.T) { diff --git a/internal/commands/delete.go b/internal/commands/delete.go index 3d3a50f5..4962d290 100644 --- a/internal/commands/delete.go +++ b/internal/commands/delete.go @@ -58,7 +58,9 @@ func doDelete(args []string, config deleteCommandConfig) error { return err } for _, o := range objects { - deletions = append(deletions, o) + if o.GetName() != "" { + deletions = append(deletions, o) + } } } else { all, err := allObjects(config.Config, env) diff --git a/internal/commands/delete_test.go b/internal/commands/delete_test.go index 91078f17..84199f54 100644 --- a/internal/commands/delete_test.go +++ b/internal/commands/delete_test.go @@ -41,6 +41,22 @@ func TestDeleteRemote(t *testing.T) { a.EqualValues([]interface{}{"Deployment:bar-system:svc2-previous-deploy", "Deployment:bar-system:svc2-deploy"}, stats["deleted"]) } +func TestDeleteRemoteComponentFilter(t *testing.T) { + s := newScaffold(t) + defer s.reset() + d := &dg{cmValue: "baz", secretValue: "baz"} + s.client.getFunc = d.get + s.client.listFunc = stdLister + s.client.deleteFunc = func(obj model.K8sMeta, dryRun bool) (*remote.SyncResult, error) { + return &remote.SyncResult{Type: remote.SyncDeleted}, nil + } + err := s.executeCommand("delete", "dev", "-c", "service2") + require.Nil(t, err) + stats := s.outputStats() + a := assert.New(t) + a.EqualValues([]interface{}{"Deployment:bar-system:svc2-previous-deploy"}, stats["deleted"]) +} + func TestDeleteLocal(t *testing.T) { s := newScaffold(t) defer s.reset() diff --git a/internal/commands/diff.go b/internal/commands/diff.go index d60a6a0d..37c4dd1e 100644 --- a/internal/commands/diff.go +++ b/internal/commands/diff.go @@ -185,7 +185,11 @@ func (d *differ) writeDiff(name string, left, right namedUn) (finalErr error) { if err != nil { return err } - rightContent = addLeader(rightContent, "object doesn't exist on the server") + leaderComment := "object doesn't exist on the server" + if right.obj.GetName() == "" { + leaderComment += " (generated name)" + } + rightContent = addLeader(rightContent, leaderComment) b, err := diff.Strings("", rightContent, fileOpts) if err != nil { return err @@ -214,11 +218,16 @@ func (d *differ) writeDiff(name string, left, right namedUn) (finalErr error) { func (d *differ) diff(ob model.K8sMeta) error { name, leftName, rightName := d.names(ob) - remoteObject, err := d.client.Get(ob) - if err != nil && err != remote.ErrNotFound { - d.stats.errors(name) - sio.Errorf("error fetching %s, %v\n", name, err) - return err + var remoteObject *unstructured.Unstructured + var err error + + if ob.GetName() != "" { + remoteObject, err = d.client.Get(ob) + if err != nil && err != remote.ErrNotFound { + d.stats.errors(name) + sio.Errorf("error fetching %s, %v\n", name, err) + return err + } } fixup := func(u *unstructured.Unstructured) *unstructured.Unstructured { @@ -233,7 +242,7 @@ func (d *differ) diff(ob model.K8sMeta) error { } var left, right *unstructured.Unstructured - if err == nil { + if remoteObject != nil { var source string left, source = remote.GetPristineVersionForDiff(remoteObject) leftName += " (source: " + source + ")" @@ -287,11 +296,17 @@ func doDiff(args []string, config diffCommandConfig) error { var lister lister = &stubLister{} var all []model.K8sLocalObject + var retainObjects []model.K8sLocalObject if config.showDeletions { all, err = allObjects(config.Config, env) if err != nil { return err } + for _, o := range all { + if o.GetName() != "" { + retainObjects = append(retainObjects, o) + } + } var scope remote.ListQueryScope lister, scope, err = newRemoteLister(client, all, config.app.DefaultNamespace(env)) if err != nil { @@ -328,7 +343,7 @@ func doDiff(args []string, config diffCommandConfig) error { var listErr error if dErr == nil { - extra, err := lister.deletions(all, fp.Includes) + extra, err := lister.deletions(retainObjects, fp.Includes) if err != nil { listErr = err } else { diff --git a/internal/commands/diff_test.go b/internal/commands/diff_test.go index d1610e79..04a3ed0f 100644 --- a/internal/commands/diff_test.go +++ b/internal/commands/diff_test.go @@ -47,6 +47,9 @@ func TestDiffBasic(t *testing.T) { a.True(regexp.MustCompile(`\d+ object\(s\) different`).MatchString(err.Error())) a.EqualValues([]interface{}{"ConfigMap:bar-system:svc2-cm", "Secret:bar-system:svc2-secret"}, stats["changes"]) a.EqualValues([]interface{}{"Deployment:bar-system:svc2-previous-deploy"}, stats["deletions"]) + adds, ok := stats["additions"].([]interface{}) + require.True(t, ok) + a.Contains(adds, "Job::tj-") secretValue := base64.StdEncoding.EncodeToString([]byte("baz")) redactedValue := base64.RawStdEncoding.EncodeToString([]byte("redacted.")) a.Contains(s.stdout(), redactedValue) diff --git a/internal/commands/filter.go b/internal/commands/filter.go index a02f54b0..e45f9511 100644 --- a/internal/commands/filter.go +++ b/internal/commands/filter.go @@ -52,17 +52,19 @@ func addFilterParams(cmd *cobra.Command, includeKindFilters bool) func() (filter cmd.Flags().StringArrayVarP(&kindExcludes, "exclude-kind", "K", nil, "exclude objects with this kind") } return func() (filterParams, error) { - if len(includes) > 0 && len(excludes) > 0 { - return filterParams{}, newUsageError("cannot include as well as exclude components, specify one or the other") - } of, err := model.NewKindFilter(kindIncludes, kindExcludes) if err != nil { return filterParams{}, newUsageError(err.Error()) } + cf, err := model.NewComponentFilter(includes, excludes) + if err != nil { + return filterParams{}, newUsageError(err.Error()) + } return filterParams{ - includes: includes, - excludes: excludes, - kindFilter: of, + includes: includes, + excludes: excludes, + kindFilter: of, + componentFilter: cf, }, nil } } @@ -92,6 +94,9 @@ func checkDuplicates(objects []model.K8sLocalObject, kf keyFunc) error { } objectsByKey := map[string]model.K8sLocalObject{} for _, o := range objects { + if o.GetName() == "" { // generated name + continue + } key := kf(o) if prev, ok := objectsByKey[key]; ok { return fmt.Errorf("duplicate objects %s and %s", displayName(prev), displayName(o)) diff --git a/internal/commands/show.go b/internal/commands/show.go index 4874ebe7..1a150a41 100644 --- a/internal/commands/show.go +++ b/internal/commands/show.go @@ -39,7 +39,7 @@ func (n *metaOnly) MarshalJSON() ([]byte, error) { "component": n.Component(), "environment": n.Environment(), "kind": gvk.Kind, - "name": n.GetName(), + "name": model.NameForDisplay(n), } if n.GetNamespace() != "" { m["namespace"] = n.GetNamespace() @@ -51,7 +51,8 @@ func showNames(objects []model.K8sLocalObject, formatSpecified bool, format stri if !formatSpecified { // render as table fmt.Fprintf(w, "%-30s %-30s %-40s %s\n", "COMPONENT", "KIND", "NAME", "NAMESPACE") for _, o := range objects { - fmt.Fprintf(w, "%-30s %-30s %-40s %s\n", o.Component(), o.GroupVersionKind().Kind, o.GetName(), o.GetNamespace()) + name := model.NameForDisplay(o) + fmt.Fprintf(w, "%-30s %-30s %-40s %s\n", o.Component(), o.GroupVersionKind().Kind, name, o.GetNamespace()) } return nil } diff --git a/internal/commands/utils_test.go b/internal/commands/utils_test.go index 8025b5cd..e121f34f 100644 --- a/internal/commands/utils_test.go +++ b/internal/commands/utils_test.go @@ -62,10 +62,11 @@ type basicObject struct { env string } -func (b *basicObject) Application() string { return b.app } -func (b *basicObject) Tag() string { return b.tag } -func (b *basicObject) Component() string { return b.component } -func (b *basicObject) Environment() string { return b.env } +func (b *basicObject) Application() string { return b.app } +func (b *basicObject) Tag() string { return b.tag } +func (b *basicObject) Component() string { return b.component } +func (b *basicObject) Environment() string { return b.env } +func (b *basicObject) GetGenerateName() string { return "" } type coll struct { data map[objectKey]model.K8sQbecMeta @@ -114,7 +115,7 @@ type client struct { } func (c *client) DisplayName(o model.K8sMeta) string { - return fmt.Sprintf("%s:%s:%s", o.GetKind(), o.GetNamespace(), o.GetName()) + return fmt.Sprintf("%s:%s:%s", o.GetKind(), o.GetNamespace(), model.NameForDisplay(o)) } func (c *client) IsNamespaced(kind schema.GroupVersionKind) (bool, error) { @@ -220,14 +221,15 @@ func (s *scaffold) jsonOutput(data interface{}) error { return json.Unmarshal(s.outCapture.Bytes(), &data) } -func (s *scaffold) executeCommand(args ...string) error { +func (s *scaffold) executeCommand(args ...string) (err error) { s.cmd.SetArgs(args) defer func() { if os.Getenv("QBEC_VERBOSE") != "" { l := log.New(os.Stderr, "", 0) l.Println("Command:", args) - l.Println("Output:\n" + s.stdout()) - l.Println("Error:\n" + s.stderr()) + l.Println("Stdout:\n" + s.stdout()) + l.Println("Stderr:\n" + s.stderr()) + l.Println("Err:", err) } }() return s.cmd.Execute() diff --git a/internal/eval/object-extract.go b/internal/eval/object-extract.go index f5b5c0a4..7b1c0774 100644 --- a/internal/eval/object-extract.go +++ b/internal/eval/object-extract.go @@ -26,8 +26,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -var _ = unstructured.Unstructured{} - func tolerantJSON(data interface{}) string { b, err := json.MarshalIndent(data, "", " ") if err != nil { @@ -78,6 +76,15 @@ func (w *walker) walkObjects(path string, component string, data interface{}) ([ } ret = append(ret, objects...) } else { + u := unstructured.Unstructured{Object: t} + name := u.GetName() + genName := u.GetGenerateName() + if name == "" && genName == "" { + return nil, fmt.Errorf("object (%v) did not have a name at path %q, (json=\n%s)", + reflect.TypeOf(data), + path, + tolerantJSON(data)) + } ret = append(ret, model.NewK8sLocalObject(t, w.app, w.tag, component, w.env)) } return ret, nil diff --git a/internal/model/app_test.go b/internal/model/app_test.go index 4e5d42b5..7cbbe3f6 100644 --- a/internal/model/app_test.go +++ b/internal/model/app_test.go @@ -50,34 +50,38 @@ func TestAppSimple(t *testing.T) { a.Equal(2, len(app.inner.Spec.Environments)) a.Contains(app.inner.Spec.Environments, "dev") a.Contains(app.inner.Spec.Environments, "prod") - a.Equal(3, len(app.allComponents)) - a.Equal(2, len(app.defaultComponents)) + a.Equal(4, len(app.allComponents)) + a.Equal(3, len(app.defaultComponents)) a.Contains(app.allComponents, "service2") a.NotContains(app.defaultComponents, "service2") comps, err := app.ComponentsForEnvironment("_", nil, nil) require.Nil(t, err) - require.Equal(t, 2, len(comps)) + require.Equal(t, 3, len(comps)) a.Equal("cluster-objects", comps[0].Name) a.Equal("service1", comps[1].Name) + a.Equal("test-job", comps[2].Name) comps, err = app.ComponentsForEnvironment("dev", nil, nil) require.Nil(t, err) - require.Equal(t, 2, len(comps)) + require.Equal(t, 3, len(comps)) a.Equal("cluster-objects", comps[0].Name) a.Equal("service2", comps[1].Name) + a.Equal("test-job", comps[2].Name) comps, err = app.ComponentsForEnvironment("prod", nil, nil) require.Nil(t, err) - require.Equal(t, 3, len(comps)) + require.Equal(t, 4, len(comps)) a.Equal("cluster-objects", comps[0].Name) a.Equal("service1", comps[1].Name) a.Equal("service2", comps[2].Name) + a.Equal("test-job", comps[3].Name) comps, err = app.ComponentsForEnvironment("dev", nil, []string{"service2"}) require.Nil(t, err) - require.Equal(t, 1, len(comps)) + require.Equal(t, 2, len(comps)) a.Equal("cluster-objects", comps[0].Name) + a.Equal("test-job", comps[1].Name) comps, err = app.ComponentsForEnvironment("dev", []string{"service2"}, nil) require.Nil(t, err) diff --git a/internal/model/k8s.go b/internal/model/k8s.go index 331b79b5..fe338210 100644 --- a/internal/model/k8s.go +++ b/internal/model/k8s.go @@ -34,6 +34,17 @@ type K8sMeta interface { GroupVersionKind() schema.GroupVersionKind GetNamespace() string GetName() string + GetGenerateName() string +} + +// NameForDisplay returns the local name of the metadata object, taking +// generated names into account. +func NameForDisplay(m K8sMeta) string { + name := m.GetName() + if name != "" { + return name + } + return m.GetGenerateName() + "" } // QbecMeta provides qbec metadata. @@ -79,18 +90,24 @@ func (k *ko) String() string { return fmt.Sprintf("%s:%s:%s", k.GroupVersionKind(), k.GetNamespace(), k.GetName()) } +func toUnstructured(data map[string]interface{}) *unstructured.Unstructured { + base := &unstructured.Unstructured{Object: data} + if base.GetName() != "" && base.GetGenerateName() != "" { // if a name is specified for the object, nuke its generated name since it won't be used + base.SetGenerateName("") + } + return base +} + // NewK8sObject wraps a K8sObject implementation around the unstructured object data specified as a bag // of attributes. func NewK8sObject(data map[string]interface{}) K8sObject { - base := &unstructured.Unstructured{Object: data} - ret := &ko{Unstructured: base} - return ret + return &ko{Unstructured: toUnstructured(data)} } // NewK8sLocalObject wraps a K8sLocalObject implementation around the unstructured object data specified as a bag // of attributes for the supplied application, component and environment. func NewK8sLocalObject(data map[string]interface{}, app, tag, component, env string) K8sLocalObject { - base := &unstructured.Unstructured{Object: data} + base := toUnstructured(data) ret := &ko{Unstructured: base, app: app, tag: tag, comp: component, env: env} labels := base.GetLabels() if labels == nil { diff --git a/internal/remote/client.go b/internal/remote/client.go index a02e4f54..1da1d84b 100644 --- a/internal/remote/client.go +++ b/internal/remote/client.go @@ -141,7 +141,7 @@ func (c *Client) DisplayName(o model.K8sMeta) string { displayName := func() string { ns := c.objectNamespace(o) - name := o.GetName() + name := model.NameForDisplay(o) if ns == "" { return name } @@ -284,12 +284,13 @@ func (c *Client) ListObjects(scope ListQueryConfig) (Collection, error) { } type updateResult struct { - SkipReason string `json:"skip,omitempty"` - Operation string `json:"operation,omitempty"` - Source string `json:"source,omitempty"` - Kind types.PatchType `json:"kind,omitempty"` - DisplayPatch string `json:"patch,omitempty"` - patch []byte + SkipReason string `json:"skip,omitempty"` + Operation string `json:"operation,omitempty"` + Source string `json:"source,omitempty"` + Kind types.PatchType `json:"kind,omitempty"` + DisplayPatch string `json:"patch,omitempty"` + GeneratedName string `json:"generatedName,omitempty"` + patch []byte } func (u *updateResult) String() string { @@ -314,8 +315,9 @@ func (u *updateResult) toSyncResult() *SyncResult { } case u.Operation == opCreate: return &SyncResult{ - Type: SyncCreated, - Details: u.String(), + Type: SyncCreated, + GeneratedName: u.GeneratedName, // only set when name actually generated + Details: u.String(), } case u.Operation == opUpdate: return &SyncResult{ @@ -343,8 +345,9 @@ const ( // SyncResult is the result of a sync operation. There is no difference in the output for a real versus // a dry-run. type SyncResult struct { - Type SyncResultType // the result type - Details string // additional details that are safe to print to console (e.g. no secrets) + Type SyncResultType // the result type + GeneratedName string // the actual name of an object that has generateName set + Details string // additional details that are safe to print to console (e.g. no secrets) } func extractCustomTypes(obj model.K8sObject) (schema.GroupVersionKind, error) { @@ -419,8 +422,15 @@ func (c *Client) Sync(original model.K8sLocalObject, opts SyncOptions) (_ *SyncR func (c *Client) doSync(original model.K8sLocalObject, opts SyncOptions, internal internalSyncOptions) (*updateResult, error) { gvk := original.GroupVersionKind() - remObj, objErr := c.Get(original) + var remObj *unstructured.Unstructured + var objErr error + if original.GetName() != "" { + remObj, objErr = c.Get(original) + } switch { + // empty name, always create + case original.GetName() == "": + break // ignore object not found errors case objErr == ErrNotFound: break @@ -582,10 +592,13 @@ func (c *Client) maybeCreate(obj model.K8sLocalObject, opts SyncOptions) (*updat if err != nil { return nil, errors.Wrap(err, "get resource interface") } - _, err = ri.Create(obj.ToUnstructured()) + out, err := ri.Create(obj.ToUnstructured()) if err != nil { return nil, err } + if obj.GetName() == "" { + result.GeneratedName = out.GetName() + } return result, nil } diff --git a/internal/remote/collection.go b/internal/remote/collection.go index 6bdc2406..f144589a 100644 --- a/internal/remote/collection.go +++ b/internal/remote/collection.go @@ -17,6 +17,8 @@ package remote import ( + "fmt" + "github.com/splunk/qbec/internal/model" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -40,10 +42,11 @@ type basicObject struct { env string } -func (b *basicObject) Application() string { return b.app } -func (b *basicObject) Tag() string { return b.tag } -func (b *basicObject) Component() string { return b.component } -func (b *basicObject) Environment() string { return b.env } +func (b *basicObject) Application() string { return b.app } +func (b *basicObject) Tag() string { return b.tag } +func (b *basicObject) Component() string { return b.component } +func (b *basicObject) Environment() string { return b.env } +func (b *basicObject) GetGenerateName() string { return "" } type collectMetadata interface { objectNamespace(obj model.K8sMeta) string @@ -74,6 +77,9 @@ func (c *collection) add(object model.K8sQbecMeta) error { if err != nil { return err } + if object.GetName() == "" { + return fmt.Errorf("internal error: object %v did not have a name", object) + } ns := c.meta.objectNamespace(object) key := objectKey{ gvk: canonicalGVK,