diff --git a/CHANGELOG.md b/CHANGELOG.md index 32c16bf657..de277f7bb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ - Fix DiffConfig issue when when provider's kubeconfig is set to file path (https://github.com/pulumi/pulumi-kubernetes/pull/2771) - Fix for replacement having incorrect status messages (https://github.com/pulumi/pulumi-kubernetes/pull/2810) - Use output properties for await logic (https://github.com/pulumi/pulumi-kubernetes/pull/2790) - +- Support for metadata.generateName (CSA) (https://github.com/pulumi/pulumi-kubernetes/pull/2808) + ## 4.7.1 (January 17, 2024) - Fix deployment await logic for accurate rollout detection diff --git a/provider/pkg/await/await_test.go b/provider/pkg/await/await_test.go index 6e029705e3..1a5373ad40 100644 --- a/provider/pkg/await/await_test.go +++ b/provider/pkg/await/await_test.go @@ -160,6 +160,7 @@ func Test_Creation(t *testing.T) { require.NotNil(t, actual) require.Equal(t, ns, actual.GetNamespace(), "Object should have the expected namespace") require.Equal(t, name, actual.GetName(), "Object should have the expected name") + gvr, err := clients.GVRForGVK(ctx.mapper, ctx.config.Inputs.GroupVersionKind()) require.NoError(t, err) _, err = ctx.client.Tracker().Get(gvr, ns, name) @@ -241,6 +242,15 @@ func Test_Creation(t *testing.T) { awaiter: touch, expect: []expectF{created("", "foo"), touched()}, }, + { + name: "GenerateName", + args: args{ + resType: tokens.Type("kubernetes:core/v1:Pod"), + inputs: withGenerateName(validPodUnstructured), + }, + awaiter: touch, + expect: []expectF{created("default", "foo-generated"), touched()}, + }, { name: "SkipAwait", args: args{ @@ -374,6 +384,13 @@ func withSkipAwait(obj *unstructured.Unstructured) *unstructured.Unstructured { return copy } +func withGenerateName(obj *unstructured.Unstructured) *unstructured.Unstructured { + copy := obj.DeepCopy() + copy.SetGenerateName(fmt.Sprintf("%s-", obj.GetName())) + copy.SetName("") + return copy +} + // -------------------------------------------------------------------------- // Mock implementations of Kubernetes client stuff. diff --git a/provider/pkg/clients/fake/clients.go b/provider/pkg/clients/fake/clients.go index f3f819e021..7a2104b2f0 100644 --- a/provider/pkg/clients/fake/clients.go +++ b/provider/pkg/clients/fake/clients.go @@ -16,9 +16,11 @@ package fake import ( "github.com/pulumi/pulumi-kubernetes/provider/v4/pkg/clients" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" kubeversion "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/restmapper" + kubetesting "k8s.io/client-go/testing" "k8s.io/kubectl/pkg/scheme" ) @@ -76,6 +78,7 @@ func NewSimpleDynamicClient(opts ...NewDynamicClientOption) (*clients.DynamicCli // make a fake clientset for testing purposes, backed by an testing.ObjectTracker with pre-populated objects. // see also: https://github.com/kubernetes/client-go/blob/kubernetes-1.29.0/examples/fake-client/main_test.go client := NewSimpleDynamicCLient(options.Scheme, options.Objects...) + client.PrependReactor("create", "*", AdmitCreate()) cs := &clients.DynamicClientSet{ GenericClient: client, @@ -84,3 +87,21 @@ func NewSimpleDynamicClient(opts ...NewDynamicClientOption) (*clients.DynamicCli } return cs, disco, mapper, client } + +func AdmitCreate() kubetesting.ReactionFunc { + return func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + if action, ok := action.(kubetesting.CreateAction); ok { + objMeta, err := meta.Accessor(action.GetObject()) + if err != nil { + return false, nil, err + } + + // implement GenerateName since underlying object tracker doesn't natively support this. + if objMeta.GetGenerateName() != "" && objMeta.GetName() == "" { + name := objMeta.GetGenerateName() + "generated" + objMeta.SetName(name) + } + } + return false, nil, nil + } +} diff --git a/provider/pkg/metadata/naming.go b/provider/pkg/metadata/naming.go index 2cd866c631..c71d952d02 100644 --- a/provider/pkg/metadata/naming.go +++ b/provider/pkg/metadata/naming.go @@ -24,14 +24,7 @@ import ( // All auto-named resources get the annotation `pulumi.com/autonamed` for tooling purposes. func AssignNameIfAutonamable(randomSeed []byte, obj *unstructured.Unstructured, propMap resource.PropertyMap, urn resource.URN) { contract.Assertf(urn.Name() != "", "expected non-empty name in URN: %s", urn) - // Check if the .metadata.name is set and is a computed value. If so, do not auto-name. - if md, ok := propMap["metadata"].V.(resource.PropertyMap); ok { - if name, ok := md["name"]; ok && name.IsComputed() { - return - } - } - - if obj.GetName() == "" { + if !IsNamed(obj, propMap) && !IsGenerateName(obj, propMap) { prefix := urn.Name() + "-" autoname, err := resource.NewUniqueName(randomSeed, prefix, 0, 0, nil) contract.AssertNoErrorf(err, "unexpected error while creating NewUniqueName") @@ -42,14 +35,39 @@ func AssignNameIfAutonamable(randomSeed []byte, obj *unstructured.Unstructured, // AdoptOldAutonameIfUnnamed checks if `newObj` has a name, and if not, "adopts" the name of `oldObj` // instead. If `oldObj` was autonamed, then we mark `newObj` as autonamed, too. -func AdoptOldAutonameIfUnnamed(newObj, oldObj *unstructured.Unstructured) { - contract.Assertf(oldObj.GetName() != "", "expected nonempty name for object: %s", oldObj) - if newObj.GetName() == "" && IsAutonamed(oldObj) { +// Note that autonaming is preferred over generateName for backwards compatibility. +func AdoptOldAutonameIfUnnamed(newObj, oldObj *unstructured.Unstructured, newObjMap resource.PropertyMap) { + if !IsNamed(newObj, newObjMap) && IsAutonamed(oldObj) { + contract.Assertf(oldObj.GetName() != "", "expected object name to be non-empty: %v", oldObj) newObj.SetName(oldObj.GetName()) SetAnnotationTrue(newObj, AnnotationAutonamed) } } +// IsAutonamed checks if the object is auto-named by Pulumi. func IsAutonamed(obj *unstructured.Unstructured) bool { return IsAnnotationTrue(obj, AnnotationAutonamed) } + +// IsGenerateName checks if the object is auto-named by Kubernetes. +func IsGenerateName(obj *unstructured.Unstructured, propMap resource.PropertyMap) bool { + if IsNamed(obj, propMap) { + return false + } + if md, ok := propMap["metadata"].V.(resource.PropertyMap); ok { + if name, ok := md["generateName"]; ok && name.IsComputed() { + return true + } + } + return obj.GetGenerateName() != "" +} + +// IsNamed checks if the object has an assigned name (may be a known or computed value). +func IsNamed(obj *unstructured.Unstructured, propMap resource.PropertyMap) bool { + if md, ok := propMap["metadata"].V.(resource.PropertyMap); ok { + if name, ok := md["name"]; ok && name.IsComputed() { + return true + } + } + return obj.GetName() != "" +} diff --git a/provider/pkg/metadata/naming_test.go b/provider/pkg/metadata/naming_test.go index 5179e64c50..15ffa7deca 100644 --- a/provider/pkg/metadata/naming_test.go +++ b/provider/pkg/metadata/naming_test.go @@ -15,10 +15,11 @@ package metadata import ( - "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "strings" "testing" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/stretchr/testify/assert" @@ -37,32 +38,54 @@ func TestAssignNameIfAutonamable(t *testing.T) { assert.Len(t, o1.GetName(), 12) // o2 has a name, so autonaming fails. - o2 := &unstructured.Unstructured{ - Object: map[string]any{"metadata": map[string]any{"name": "bar"}}, - } pm2 := resource.PropertyMap{ "metadata": resource.NewObjectProperty(resource.PropertyMap{ "name": resource.NewStringProperty("bar"), }), } + o2 := propMapToUnstructured(pm2) AssignNameIfAutonamable(nil, o2, pm2, resource.NewURN(tokens.QName("teststack"), tokens.PackageName("testproj"), tokens.Type(""), tokens.Type("bang:boom/fizzle:AnotherResource"), "bar")) assert.False(t, IsAutonamed(o2)) assert.Equal(t, "bar", o2.GetName()) // o3 has a computed name, so autonaming fails. - o3 := &unstructured.Unstructured{ - Object: map[string]any{"metadata": map[string]any{"name": "[Computed]"}}, - } pm3 := resource.PropertyMap{ "metadata": resource.NewObjectProperty(resource.PropertyMap{ "name": resource.MakeComputed(resource.NewStringProperty("bar")), }), } + o3 := propMapToUnstructured(pm3) AssignNameIfAutonamable(nil, o3, pm3, resource.NewURN(tokens.QName("teststack"), tokens.PackageName("testproj"), tokens.Type(""), tokens.Type("bang:boom/fizzle:MajorResource"), "foo")) assert.False(t, IsAutonamed(o3)) - assert.Equal(t, "[Computed]", o3.GetName()) + assert.Equal(t, "", o3.GetName()) + + // o4 has a generateName, so autonaming fails. + pm4 := resource.PropertyMap{ + "metadata": resource.NewObjectProperty(resource.PropertyMap{ + "generateName": resource.NewStringProperty("bar-"), + }), + } + o4 := propMapToUnstructured(pm4) + AssignNameIfAutonamable(nil, o4, pm4, resource.NewURN(tokens.QName("teststack"), tokens.PackageName("testproj"), + tokens.Type(""), tokens.Type("bang:boom/fizzle:AnotherResource"), "bar")) + assert.False(t, IsAutonamed(o4)) + assert.Equal(t, "bar-", o4.GetGenerateName()) + assert.Equal(t, "", o4.GetName()) + + // o5 has a computed generateName, so autonaming fails. + pm5 := resource.PropertyMap{ + "metadata": resource.NewObjectProperty(resource.PropertyMap{ + "generateName": resource.MakeComputed(resource.NewStringProperty("bar")), + }), + } + o5 := propMapToUnstructured(pm5) + AssignNameIfAutonamable(nil, o5, pm5, resource.NewURN(tokens.QName("teststack"), tokens.PackageName("testproj"), + tokens.Type(""), tokens.Type("bang:boom/fizzle:MajorResource"), "foo")) + assert.False(t, IsAutonamed(o5)) + assert.Equal(t, "", o5.GetGenerateName()) + assert.Equal(t, "", o5.GetName()) } func TestAdoptName(t *testing.T) { @@ -77,10 +100,13 @@ func TestAdoptName(t *testing.T) { }, }, } - new1 := &unstructured.Unstructured{ - Object: map[string]any{"metadata": map[string]any{"name": "new1"}}, + pm1 := resource.PropertyMap{ + "metadata": resource.NewObjectProperty(resource.PropertyMap{ + "name": resource.NewStringProperty("new1"), + }), } - AdoptOldAutonameIfUnnamed(new1, old1) + new1 := propMapToUnstructured(pm1) + AdoptOldAutonameIfUnnamed(new1, old1, pm1) assert.Equal(t, "old1", old1.GetName()) assert.True(t, IsAutonamed(old1)) assert.Equal(t, "new1", new1.GetName()) @@ -90,7 +116,8 @@ func TestAdoptName(t *testing.T) { new2 := &unstructured.Unstructured{ Object: map[string]any{}, } - AdoptOldAutonameIfUnnamed(new2, old1) + pm2 := resource.NewPropertyMap(struct{}{}) + AdoptOldAutonameIfUnnamed(new2, old1, pm2) assert.Equal(t, "old1", new2.GetName()) assert.True(t, IsAutonamed(new2)) @@ -98,6 +125,7 @@ func TestAdoptName(t *testing.T) { new3 := &unstructured.Unstructured{ Object: map[string]any{}, } + pm3 := resource.NewPropertyMap(struct{}{}) old2 := &unstructured.Unstructured{ Object: map[string]any{ "metadata": map[string]any{ @@ -105,7 +133,34 @@ func TestAdoptName(t *testing.T) { }, }, } - AdoptOldAutonameIfUnnamed(new3, old2) + AdoptOldAutonameIfUnnamed(new3, old2, pm3) assert.Equal(t, "", new3.GetName()) assert.False(t, IsAutonamed(new3)) + + // new4 has a computed name and therefore DOES NOT adopt old1's name. + pm4 := resource.PropertyMap{ + "metadata": resource.NewObjectProperty(resource.PropertyMap{ + "name": resource.MakeComputed(resource.NewStringProperty("new4")), + }), + } + new4 := propMapToUnstructured(pm4) + assert.Equal(t, "", new4.GetName()) + AdoptOldAutonameIfUnnamed(new4, old1, pm4) + assert.Equal(t, "", new4.GetName()) + assert.False(t, IsAutonamed(new4)) + + // new5 has a generateName and therefore DOES adopt old1's name. + pm5 := resource.PropertyMap{ + "metadata": resource.NewObjectProperty(resource.PropertyMap{ + "generateName": resource.NewStringProperty("new5-"), + }), + } + new5 := propMapToUnstructured(pm5) + AdoptOldAutonameIfUnnamed(new5, old1, pm5) + assert.Equal(t, "old1", new2.GetName()) + assert.True(t, IsAutonamed(new2)) +} + +func propMapToUnstructured(pm resource.PropertyMap) *unstructured.Unstructured { + return &unstructured.Unstructured{Object: pm.MapRepl(nil, nil)} } diff --git a/provider/pkg/provider/provider.go b/provider/pkg/provider/provider.go index d7cd21199f..920d25a99c 100644 --- a/provider/pkg/provider/provider.go +++ b/provider/pkg/provider/provider.go @@ -1294,7 +1294,7 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) ( } if !k.serverSideApplyMode && kinds.IsPatchURN(urn) { - return nil, fmt.Errorf("patch resources require Server-side Apply mode, which is enabled using the " + + return nil, fmt.Errorf("patch resources require Server-Side Apply mode, which is enabled using the " + "`enableServerSideApply` Provider config") } @@ -1334,7 +1334,7 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) ( if k.serverSideApplyMode && kinds.IsPatchURN(urn) { if len(newInputs.GetName()) == 0 { - return nil, fmt.Errorf("patch resources require the resource `.metadata.name` to be set") + return nil, fmt.Errorf("patch resources require the `.metadata.name` field to be set") } } @@ -1349,10 +1349,9 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) ( // needs to be `DeleteBeforeReplace`'d. If the resource is marked `DeleteBeforeReplace`, then // `Create` will allocate it a new name later. if len(oldInputs.Object) > 0 { - // NOTE: If old inputs exist, they have a name, either provided by the user or filled in with a - // previous run of `Check`. - contract.Assertf(oldInputs.GetName() != "", "expected object name to be nonempty: %v", oldInputs) - metadata.AdoptOldAutonameIfUnnamed(newInputs, oldInputs) + // NOTE: If old inputs exist, they MAY have a name, either provided by the user, or based on generateName, + // or filled in with a previous run of `Check`. + metadata.AdoptOldAutonameIfUnnamed(newInputs, oldInputs, news) // If the resource has existing state, we only set the "managed-by: pulumi" label if it is already present. This // avoids causing diffs for cases where the resource is being imported, or was created using SSA. The goal in @@ -1377,6 +1376,14 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) ( } } } + if metadata.IsGenerateName(newInputs, news) { + if k.serverSideApplyMode { + return nil, fmt.Errorf("the `.metadata.generateName` field is not supported in Server-Side Apply mode") + } + if k.yamlRenderMode { + return nil, fmt.Errorf("the `.metadata.generateName` field is not supported in YAML rendering mode") + } + } gvk, err := k.gvkFromURN(urn) if err != nil { @@ -1552,6 +1559,10 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p newInputs := propMapToUnstructured(newResInputs) oldInputs, oldLive := parseCheckpointObject(oldState) + if !isHelmRelease(urn) { + // "isHelmRelease" is due to https://github.com/pulumi/pulumi-kubernetes/issues/2679 + contract.Assertf(oldLive.GetName() != "", "expected live object name to be nonempty: %v", oldLive) + } oldInputs, err = normalizeInputs(oldInputs) if err != nil { @@ -1600,6 +1611,12 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p if k.serverSideApplyMode && len(oldLivePruned.GetResourceVersion()) > 0 { oldLivePruned.SetResourceVersion("") } + // If a name was specified in the new inputs, be sure that the old live object has the previous name. + // This makes it possible to update the program to set `.metadata.name` to the name that was + // made by `.metadata.generateName` without triggering replacement. + if newInputs.GetName() != "" { + oldLivePruned.SetName(oldLive.GetName()) + } var patch []byte patchBase := oldLivePruned.Object @@ -1608,15 +1625,15 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p patch, err = k.inputPatch(oldLivePruned, newInputs) if err != nil { return nil, pkgerrors.Wrapf( - err, "Failed to check for changes in resource %s/%s", newInputs.GetNamespace(), newInputs.GetName()) + err, "Failed to check for changes in resource %q", urn) } patchObj := map[string]any{} if err = json.Unmarshal(patch, &patchObj); err != nil { return nil, pkgerrors.Wrapf( - err, "Failed to check for changes in resource %s/%s because of an error serializing "+ + err, "Failed to check for changes in resource %q because of an error serializing "+ "the JSON patch describing resource changes", - newInputs.GetNamespace(), newInputs.GetName()) + urn) } hasChanges := pulumirpc.DiffResponse_DIFF_NONE @@ -1631,9 +1648,9 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p } if detailedDiff, err = convertPatchToDiff(patchObj, patchBase, newInputs.Object, oldLivePruned.Object, forceNewFields...); err != nil { return nil, pkgerrors.Wrapf( - err, "Failed to check for changes in resource %s/%s because of an error "+ + err, "Failed to check for changes in resource %q because of an error "+ "converting JSON patch describing resource changes to a diff", - newInputs.GetNamespace(), newInputs.GetName()) + urn) } // Remove any ignored changes from the computed diff. @@ -1683,7 +1700,7 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p switch newInputs.GetKind() { case "Job": // Fetch current Job status and check point-in-time readiness. Errors are ignored. - if live, err := k.readLiveObject(newInputs); err == nil { + if live, err := k.readLiveObject(oldLive); err == nil { jobChecker := checkjob.NewJobChecker() job, err := clients.FromUnstructured(live) if err == nil { @@ -1703,14 +1720,16 @@ func (k *kubeProvider) Diff(ctx context.Context, req *pulumirpc.DiffRequest) (*p deleteBeforeReplace := // 1. We know resource must be replaced. len(replaces) > 0 && - // 2. Object is NOT autonamed (i.e., user manually named it, and therefore we can't + // 2. Object is named (i.e., not using metadata.generateName). + metadata.IsNamed(newInputs, newResInputs) && + // 3. Object is NOT autonamed (i.e., user manually named it, and therefore we can't // auto-generate the name). !metadata.IsAutonamed(newInputs) && - // 3. The new, user-specified name is the same as the old name. - newInputs.GetName() == oldLivePruned.GetName() && - // 4. The resource is being deployed to the same namespace (i.e., we aren't creating the + // 4. The new, user-specified name is the same as the old name. + newInputs.GetName() == oldLive.GetName() && + // 5. The resource is being deployed to the same namespace (i.e., we aren't creating the // object in a new namespace and then deleting the old one). - newInputs.GetNamespace() == oldLivePruned.GetNamespace() + newInputs.GetNamespace() == oldLive.GetNamespace() return &pulumirpc.DiffResponse{ Changes: hasChanges, @@ -1863,22 +1882,28 @@ func (k *kubeProvider) Create( // If it's a "no match" error, this is probably a CustomResource with no corresponding // CustomResourceDefinition. This usually happens if the CRD was not created, and we // print a more useful error message in this case. + gvk, err := k.gvkFromURN(urn) + if err != nil { + return nil, err + } + gvkStr := gvk.GroupVersion().String() + "/" + gvk.Kind return nil, pkgerrors.Wrapf( - awaitErr, "creation of resource %s failed because the Kubernetes API server "+ + awaitErr, "creation of resource %q with kind %s failed because the Kubernetes API server "+ "reported that the apiVersion for this resource does not exist. "+ - "Verify that any required CRDs have been created", fqObjName(newInputs)) + "Verify that any required CRDs have been created", urn, gvkStr) } partialErr, isPartialErr := awaitErr.(await.PartialError) if !isPartialErr { // Object creation failed. return nil, pkgerrors.Wrapf( awaitErr, - "resource %s was not successfully created by the Kubernetes API server ", fqObjName(newInputs)) + "resource %q was not successfully created by the Kubernetes API server ", urn) } // Resource was created, but failed to become fully initialized. initialized = partialErr.Object() } + contract.Assertf(initialized.GetName() != "", "expected live object name to be nonempty: %v", initialized) // We need to delete the empty status field returned from the API server if we are in // preview mode. Having the status field set will cause a panic during preview if the Pulumi @@ -1905,8 +1930,8 @@ func (k *kubeProvider) Create( return nil, partialError( fqObjName(initialized), pkgerrors.Wrapf( - awaitErr, "resource %s was successfully created, but the Kubernetes API server "+ - "reported that it failed to fully initialize or become live", fqObjName(newInputs)), + awaitErr, "resource %q was successfully created, but the Kubernetes API server "+ + "reported that it failed to fully initialize or become live", urn), inputsAndComputed, nil) } @@ -2107,6 +2132,7 @@ func (k *kubeProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*p // If we get here, resource successfully registered with the API server, but failed to // initialize. } + contract.Assertf(liveObj.GetName() != "", "expected live object name to be nonempty: %v", liveObj) // Prune the live inputs to remove properties that are not present in the program inputs. liveInputs := pruneLiveState(liveObj, oldInputs) @@ -2338,22 +2364,24 @@ func (k *kubeProvider) Update( // CustomResourceDefinition. This usually happens if the CRD was not created, and we // print a more useful error message in this case. return nil, pkgerrors.Wrapf( - awaitErr, "update of resource %s failed because the Kubernetes API server "+ + awaitErr, "update of resource %q failed because the Kubernetes API server "+ "reported that the apiVersion for this resource does not exist. "+ - "Verify that any required CRDs have been created", fqObjName(newInputs)) + "Verify that any required CRDs have been created", urn) } var getErr error - initialized, getErr = k.readLiveObject(newInputs) + initialized, getErr = k.readLiveObject(oldLive) if getErr != nil { // Object update/creation failed. return nil, pkgerrors.Wrapf( - awaitErr, "update of resource %s failed because the Kubernetes API server "+ - "reported that it failed to fully initialize or become live", fqObjName(newInputs)) + awaitErr, "update of resource %q failed because the Kubernetes API server "+ + "reported that it failed to fully initialize or become live", urn) } // If we get here, resource successfully registered with the API server, but failed to // initialize. } + contract.Assertf(initialized.GetName() != "", "expected live object name to be nonempty: %v", initialized) + // Return a new "checkpoint object". obj := checkpointObject(newInputs, initialized, newResInputs, initialAPIVersion, fieldManager) inputsAndComputed, err := plugin.MarshalProperties( @@ -2374,7 +2402,7 @@ func (k *kubeProvider) Update( fqObjName(initialized), pkgerrors.Wrapf( awaitErr, "the Kubernetes API server reported that %q failed to fully initialize "+ - "or become live", fqObjName(newInputs)), + "or become live", fqObjName(initialized)), inputsAndComputed, nil) } @@ -2382,12 +2410,12 @@ func (k *kubeProvider) Update( if k.serverSideApplyMode { // For non-preview updates, drop the old fieldManager if the value changes. if !req.GetPreview() && fieldManagerOld != fieldManager { - client, err := k.clientSet.ResourceClientForObject(newInputs) + client, err := k.clientSet.ResourceClientForObject(initialized) if err != nil { return nil, err } - err = ssa.Relinquish(k.canceler.context, client, newInputs, fieldManagerOld) + err = ssa.Relinquish(k.canceler.context, client, initialized, fieldManagerOld) if err != nil { return nil, err } @@ -2597,6 +2625,7 @@ func (k *kubeProvider) gvkFromURN(urn resource.URN) (schema.GroupVersionKind, er } func (k *kubeProvider) readLiveObject(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + contract.Assertf(obj.GetName() != "", "expected object name to be nonempty: %v", obj) rc, err := k.clientSet.ResourceClientForObject(obj) if err != nil { return nil, err @@ -3228,6 +3257,7 @@ func renderYaml(resource *unstructured.Unstructured, yamlDirectory string) error // renderPathForResource determines the appropriate YAML render path depending on the resource kind. func renderPathForResource(resource *unstructured.Unstructured, yamlDirectory string) string { + contract.Assertf(resource.GetName() != "", "expected object name to be nonempty: %v", resource) crdDirectory := filepath.Join(yamlDirectory, "0-crd") manifestDirectory := filepath.Join(yamlDirectory, "1-manifest") diff --git a/tests/sdk/nodejs/autonaming/step1/Pulumi.yaml b/tests/sdk/nodejs/autonaming/step1/Pulumi.yaml index 9d6c8f4e8b..9387c9ab22 100644 --- a/tests/sdk/nodejs/autonaming/step1/Pulumi.yaml +++ b/tests/sdk/nodejs/autonaming/step1/Pulumi.yaml @@ -1,3 +1,3 @@ name: autonaming-test -description: A program that tests partial provider failure. +description: A program that tests auto-naming of Kubernetes objects. runtime: nodejs diff --git a/tests/sdk/nodejs/autonaming/step1/index.ts b/tests/sdk/nodejs/autonaming/step1/index.ts index 004912cace..573334e85c 100644 --- a/tests/sdk/nodejs/autonaming/step1/index.ts +++ b/tests/sdk/nodejs/autonaming/step1/index.ts @@ -14,14 +14,14 @@ import * as k8s from "@pulumi/kubernetes"; -export const namespace = new k8s.core.v1.Namespace("test-namespace"); +const namespace = new k8s.core.v1.Namespace("test-namespace"); // // A simple Pod definition. `.metadata.name` is not provided, so Pulumi will allocate a unique name // to the resource upon creation. // -const pod = new k8s.core.v1.Pod("autonaming-test", { +export const pod = new k8s.core.v1.Pod("autonaming-test", { metadata: { namespace: namespace.metadata.name, }, diff --git a/tests/sdk/nodejs/autonaming/step2/index.ts b/tests/sdk/nodejs/autonaming/step2/index.ts index d9e41747ab..227537c0c5 100644 --- a/tests/sdk/nodejs/autonaming/step2/index.ts +++ b/tests/sdk/nodejs/autonaming/step2/index.ts @@ -14,14 +14,14 @@ import * as k8s from "@pulumi/kubernetes"; -export const namespace = new k8s.core.v1.Namespace("test-namespace"); +const namespace = new k8s.core.v1.Namespace("test-namespace"); // // The image in the Pod's container has changed, triggering a replace. Because `.metadata.name` is // not specified, Pulumi again will provide a name upon creation of the new Pod resource. // -const pod = new k8s.core.v1.Pod("autonaming-test", { +export const pod = new k8s.core.v1.Pod("autonaming-test", { metadata: { namespace: namespace.metadata.name, }, diff --git a/tests/sdk/nodejs/autonaming/step3/index.ts b/tests/sdk/nodejs/autonaming/step3/index.ts index 5bb94dec69..8d9070895c 100644 --- a/tests/sdk/nodejs/autonaming/step3/index.ts +++ b/tests/sdk/nodejs/autonaming/step3/index.ts @@ -14,14 +14,14 @@ import * as k8s from "@pulumi/kubernetes"; -export const namespace = new k8s.core.v1.Namespace("test-namespace"); +const namespace = new k8s.core.v1.Namespace("test-namespace"); // // Only the labels have changed, so no replace is triggered. Pulumi should update the object // in-place, and the name should not be changed. // -const pod = new k8s.core.v1.Pod("autonaming-test", { +export const pod = new k8s.core.v1.Pod("autonaming-test", { metadata: { namespace: namespace.metadata.name, labels: {app: "autonaming-test"}, diff --git a/tests/sdk/nodejs/autonaming/step4/index.ts b/tests/sdk/nodejs/autonaming/step4/index.ts index e1e901df03..3da428f538 100644 --- a/tests/sdk/nodejs/autonaming/step4/index.ts +++ b/tests/sdk/nodejs/autonaming/step4/index.ts @@ -14,17 +14,17 @@ import * as k8s from "@pulumi/kubernetes"; -export const namespace = new k8s.core.v1.Namespace("test-namespace"); +const namespace = new k8s.core.v1.Namespace("test-namespace"); // -// User has now specified `.metadata.name`, so Pulumi should replace the resource, and NOT allocate -// a name to it. +// User has now specified `.metadata.generateName`, which Pulumi ignores because autonaming has already occurred, +// so no replace is triggered. Pulumi should update the object in-place, and the name should not be changed. // -const pod = new k8s.core.v1.Pod("autonaming-test", { +export const pod = new k8s.core.v1.Pod("autonaming-test", { metadata: { namespace: namespace.metadata.name, - name: "autonaming-test", + generateName: "autonaming-test-", labels: {app: "autonaming-test"}, }, spec: { diff --git a/tests/sdk/nodejs/autonaming/step5/index.ts b/tests/sdk/nodejs/autonaming/step5/index.ts new file mode 100644 index 0000000000..8fca7c00a2 --- /dev/null +++ b/tests/sdk/nodejs/autonaming/step5/index.ts @@ -0,0 +1,35 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as k8s from "@pulumi/kubernetes"; + +const namespace = new k8s.core.v1.Namespace("test-namespace"); + +// +// User has now specified `.metadata.name`, so Pulumi should replace the resource, and NOT allocate +// a name to it. +// + +export const pod = new k8s.core.v1.Pod("autonaming-test", { + metadata: { + namespace: namespace.metadata.name, + name: "autonaming-test", + labels: {app: "autonaming-test"}, + }, + spec: { + containers: [ + {name: "nginx", image: "nginx:1.15-alpine"}, + ], + }, +}); diff --git a/tests/sdk/nodejs/generatename/step1/Pulumi.yaml b/tests/sdk/nodejs/generatename/step1/Pulumi.yaml new file mode 100644 index 0000000000..bde6c38040 --- /dev/null +++ b/tests/sdk/nodejs/generatename/step1/Pulumi.yaml @@ -0,0 +1,5 @@ +name: generatename-test +description: A program that tests support for `.metadata.generateName`. +runtime: nodejs +config: + kubernetes:enableServerSideApply: false diff --git a/tests/sdk/nodejs/generatename/step1/index.ts b/tests/sdk/nodejs/generatename/step1/index.ts new file mode 100644 index 0000000000..cb59767dd3 --- /dev/null +++ b/tests/sdk/nodejs/generatename/step1/index.ts @@ -0,0 +1,37 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; + +const config = new pulumi.Config(); + +const namespace = new k8s.core.v1.Namespace("test-namespace"); + +// +// A simple Pod definition. `.metadata.name` is not provided, but `.metadata.generateName` is. +// Kubernetes will provide a unique name for the Pod using `.metadata.generateName` as a prefix. +// + +const pod = new k8s.core.v1.Pod("generatename-test", { + metadata: { + namespace: namespace.metadata.name, + generateName: "generatename-test-", + }, + spec: { + containers: [ + {name: "nginx", image: "nginx"}, + ], + }, +}); diff --git a/tests/sdk/nodejs/generatename/step1/package.json b/tests/sdk/nodejs/generatename/step1/package.json new file mode 100644 index 0000000000..779b1bb5c3 --- /dev/null +++ b/tests/sdk/nodejs/generatename/step1/package.json @@ -0,0 +1,12 @@ +{ + "name": "steps", + "version": "0.1.0", + "dependencies": { + "@pulumi/pulumi": "latest" + }, + "devDependencies": { + }, + "peerDependencies": { + "@pulumi/kubernetes": "latest" + } +} diff --git a/tests/sdk/nodejs/generatename/step1/tsconfig.json b/tests/sdk/nodejs/generatename/step1/tsconfig.json new file mode 100644 index 0000000000..5dacccbd42 --- /dev/null +++ b/tests/sdk/nodejs/generatename/step1/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "outDir": "bin", + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "stripInternal": true, + "experimentalDecorators": true, + "pretty": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "forceConsistentCasingInFileNames": true, + "strictNullChecks": true + }, + "files": [ + "index.ts" + ] +} + diff --git a/tests/sdk/nodejs/generatename/step2/index.ts b/tests/sdk/nodejs/generatename/step2/index.ts new file mode 100644 index 0000000000..c45e1ba6ec --- /dev/null +++ b/tests/sdk/nodejs/generatename/step2/index.ts @@ -0,0 +1,36 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; + +const config = new pulumi.Config(); + +const namespace = new k8s.core.v1.Namespace("test-namespace"); + +// +// The `.metadata.generateName` field has changed, but Pulumi does NOT automatically replace in that situation. +// + +const pod = new k8s.core.v1.Pod("generatename-test", { + metadata: { + namespace: namespace.metadata.name, + generateName: "generatename-test-modified-", + }, + spec: { + containers: [ + {name: "nginx", image: "nginx"}, + ], + }, +}); diff --git a/tests/sdk/nodejs/generatename/step3/index.ts b/tests/sdk/nodejs/generatename/step3/index.ts new file mode 100644 index 0000000000..2901b09bbd --- /dev/null +++ b/tests/sdk/nodejs/generatename/step3/index.ts @@ -0,0 +1,39 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; + +const config = new pulumi.Config(); + +const namespace = new k8s.core.v1.Namespace("test-namespace"); + +// +// The image in the Pod's container has changed, triggering a replace. Because `.metadata.name` is +// not specified, but `.metadata.generateName` is, Kubernetes again will provide a new name for the replacement. +// Pulumi will proceed with replace-before-delete. +// + +const pod = new k8s.core.v1.Pod("generatename-test", { + metadata: { + namespace: namespace.metadata.name, + generateName: "generatename-test-modified-", + }, + spec: { + containers: [ + {name: "nginx", image: "nginx:1.15-alpine"}, + ], + }, +}); + diff --git a/tests/sdk/nodejs/generatename/step4/index.ts b/tests/sdk/nodejs/generatename/step4/index.ts new file mode 100644 index 0000000000..b1a4c470f0 --- /dev/null +++ b/tests/sdk/nodejs/generatename/step4/index.ts @@ -0,0 +1,39 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; + +const config = new pulumi.Config(); + +const namespace = new k8s.core.v1.Namespace("test-namespace"); + +// +// Only the labels have changed, so no replace is triggered. Pulumi should update the object +// in-place, and the name should not be changed. +// + +const pod = new k8s.core.v1.Pod("generatename-test", { + metadata: { + namespace: namespace.metadata.name, + generateName: "generatename-test-modified-", + labels: {app: "generatename-test"}, + }, + spec: { + containers: [ + {name: "nginx", image: "nginx:1.15-alpine"}, + ], + }, +}); + diff --git a/tests/sdk/nodejs/generatename/step5/index.ts b/tests/sdk/nodejs/generatename/step5/index.ts new file mode 100644 index 0000000000..e2cbcb5f18 --- /dev/null +++ b/tests/sdk/nodejs/generatename/step5/index.ts @@ -0,0 +1,39 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; + +const config = new pulumi.Config(); + +const namespace = new k8s.core.v1.Namespace("test-namespace"); + +// +// The name of the pod is now explicitly set to the previously-generated name, so no replace is triggered. +// + +const pod = new k8s.core.v1.Pod("generatename-test", { + metadata: { + namespace: namespace.metadata.name, + generateName: "generatename-test-modified-", + labels: {app: "generatename-test"}, + name: config.require("podName"), + }, + spec: { + containers: [ + {name: "nginx", image: "nginx:1.15-alpine"}, + ], + }, +}); + diff --git a/tests/sdk/nodejs/generatename/step6/index.ts b/tests/sdk/nodejs/generatename/step6/index.ts new file mode 100644 index 0000000000..b9e422f324 --- /dev/null +++ b/tests/sdk/nodejs/generatename/step6/index.ts @@ -0,0 +1,40 @@ +// Copyright 2016-2019, Pulumi Corporation. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as pulumi from "@pulumi/pulumi"; +import * as k8s from "@pulumi/kubernetes"; + +const config = new pulumi.Config(); + +const namespace = new k8s.core.v1.Namespace("test-namespace"); + +// +// User has now specified `.metadata.name`, so Pulumi should replace the resource, and NOT allocate +// a name to it. Note that `.metadata.generateName` is ignored. +// + +const pod = new k8s.core.v1.Pod("generatename-test", { + metadata: { + namespace: namespace.metadata.name, + generateName: "generatename-test-modified-", + labels: {app: "generatename-test"}, + name: "generatename-test", + }, + spec: { + containers: [ + {name: "nginx", image: "nginx:1.15-alpine"}, + ], + }, +}); + diff --git a/tests/sdk/nodejs/nodejs_test.go b/tests/sdk/nodejs/nodejs_test.go index 1da97ca911..ea07920da0 100644 --- a/tests/sdk/nodejs/nodejs_test.go +++ b/tests/sdk/nodejs/nodejs_test.go @@ -18,6 +18,7 @@ package test import ( b64 "encoding/base64" "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -111,6 +112,7 @@ func TestAutonaming(t *testing.T) { var step1Name any var step2Name any var step3Name any + var step4Name any test := baseOptions.With(integration.ProgramTestOptions{ Dir: filepath.Join("autonaming", "step1"), @@ -215,6 +217,36 @@ func TestAutonaming(t *testing.T) { provRes := stackInfo.Deployment.Resources[2] assert.True(t, providers.IsProviderType(provRes.URN.Type())) + // + // Assert Pod was NOT replaced, and has the same name, previously allocated by Pulumi. + // + + pod := stackInfo.Deployment.Resources[1] + assert.Equal(t, "autonaming-test", string(pod.URN.Name())) + step4Name, _ = openapi.Pluck(pod.Outputs, "metadata", "name") + assert.True(t, strings.HasPrefix(step4Name.(string), "autonaming-test-")) + + autonamed, _ := openapi.Pluck(pod.Outputs, "metadata", "annotations", "pulumi.com/autonamed") + assert.Equal(t, "true", autonamed) + + assert.Equal(t, step3Name, step4Name) + }, + }, + { + Dir: filepath.Join("autonaming", "step5"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + assert.Equal(t, 4, len(stackInfo.Deployment.Resources)) + + tests.SortResourcesByURN(stackInfo) + + stackRes := stackInfo.Deployment.Resources[3] + assert.Equal(t, resource.RootStackType, stackRes.URN.Type()) + + provRes := stackInfo.Deployment.Resources[2] + assert.True(t, providers.IsProviderType(provRes.URN.Type())) + // // User has specified their own name for the Pod, so we replace it, and Pulumi does NOT // allocate a name on its own. @@ -234,6 +266,138 @@ func TestAutonaming(t *testing.T) { integration.ProgramTest(t, &test) } +func TestGenerateName(t *testing.T) { + var pt *integration.ProgramTester + var step1Name any + var step2Name any + var step3Name any + var step4Name any + var step5Name any + var step6Name any + + test := baseOptions.With(integration.ProgramTestOptions{ + Dir: filepath.Join("generatename", "step1"), + Quick: false, + SkipRefresh: false, + ExpectRefreshChanges: false, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + + // + // Assert pod is successfully given a unique name by Kubernetes. + // + pod := tests.SearchResourcesByName(stackInfo, "", "kubernetes:core/v1:Pod", "generatename-test") + step1Name, _ = openapi.Pluck(pod.Outputs, "metadata", "name") + assert.True(t, strings.HasPrefix(step1Name.(string), "generatename-test-")) + generateName, _ := openapi.Pluck(pod.Outputs, "metadata", "generateName") + assert.Equal(t, "generatename-test-", generateName.(string)) + _, autonamed := openapi.Pluck(pod.Outputs, "metadata", "annotations", "pulumi.com/autonamed") + assert.False(t, autonamed) + }, + Config: map[string]string{}, + + EditDirs: []integration.EditDir{ + { + Dir: filepath.Join("generatename", "step2"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + + // + // Assert pod was NOT replaced, and has the same name, previously allocated by Kubernetes. + // + pod := tests.SearchResourcesByName(stackInfo, "", "kubernetes:core/v1:Pod", "generatename-test") + step2Name, _ = openapi.Pluck(pod.Outputs, "metadata", "name") + assert.Equal(t, step1Name, step2Name) + generateName, _ := openapi.Pluck(pod.Outputs, "metadata", "generateName") + assert.Equal(t, "generatename-test-modified-", generateName.(string)) + _, autonamed := openapi.Pluck(pod.Outputs, "metadata", "annotations", "pulumi.com/autonamed") + assert.False(t, autonamed) + }, + }, + { + Dir: filepath.Join("generatename", "step3"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + + // + // Assert pod was replaced, i.e., destroyed and re-created, with allocating a new name. + // + pod := tests.SearchResourcesByName(stackInfo, "", "kubernetes:core/v1:Pod", "generatename-test") + step3Name, _ = openapi.Pluck(pod.Outputs, "metadata", "name") + assert.NotEqual(t, step2Name, step3Name) + assert.True(t, strings.HasPrefix(step3Name.(string), "generatename-test-modified-")) + _, autonamed := openapi.Pluck(pod.Outputs, "metadata", "annotations", "pulumi.com/autonamed") + assert.False(t, autonamed) + }, + }, + { + Dir: filepath.Join("generatename", "step4"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + + // + // Assert pod was NOT replaced, and has the same name, previously allocated by Kubernetes. + // + pod := tests.SearchResourcesByName(stackInfo, "", "kubernetes:core/v1:Pod", "generatename-test") + step4Name, _ = openapi.Pluck(pod.Outputs, "metadata", "name") + assert.Equal(t, step3Name, step4Name) + _, autonamed := openapi.Pluck(pod.Outputs, "metadata", "annotations", "pulumi.com/autonamed") + assert.False(t, autonamed) + + // Update the configuration for subsequent steps. + require.NoError(t, + pt.RunPulumiCommand("config", "set", "podName", step4Name.(string)), + "failed to set podName config") + }, + }, + { + Dir: filepath.Join("generatename", "step5"), + Additive: true, + ExpectNoChanges: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + + // + // User has explicitly set the name to the previously-generated name (maybe for clarity), + // and Pulumi does NOT replace the pod. + // + pod := tests.SearchResourcesByName(stackInfo, "", "kubernetes:core/v1:Pod", "generatename-test") + step5Name, _ = openapi.Pluck(pod.Outputs, "metadata", "name") + assert.Equal(t, step4Name, step5Name) + _, autonamed := openapi.Pluck(pod.Outputs, "metadata", "annotations", "pulumi.com/autonamed") + assert.False(t, autonamed) + }, + }, + { + Dir: filepath.Join("generatename", "step6"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) { + assert.NotNil(t, stackInfo.Deployment) + + // + // User has specified their own name for the Pod, so we replace it, and Pulumi/Kubernetes does NOT + // allocate a name on its own. + // + pod := tests.SearchResourcesByName(stackInfo, "", "kubernetes:core/v1:Pod", "generatename-test") + step6Name, _ = openapi.Pluck(pod.Outputs, "metadata", "name") + assert.NotEqual(t, step5Name, step6Name) + assert.Equal(t, "generatename-test", step6Name.(string)) + _, autonamed := openapi.Pluck(pod.Outputs, "metadata", "annotations", "pulumi.com/autonamed") + assert.False(t, autonamed) + }, + }, + }, + }) + pt = integration.ProgramTestManualLifeCycle(t, &test) + err := pt.TestLifeCycleInitAndDestroy() + if !errors.Is(err, integration.ErrTestFailed) { + assert.NoError(t, err) + } +} + func TestCRDs(t *testing.T) { test := baseOptions.With(integration.ProgramTestOptions{ Dir: filepath.Join("crds", "step1"),