From d4f44c8937ac10a5ba473dd3dc9a497ec6b26ab0 Mon Sep 17 00:00:00 2001 From: Pat Gavlin Date: Tue, 5 Mar 2019 10:51:01 -0800 Subject: [PATCH 1/3] Adopt changes to Read and Diff 1. Return resource inputs as well as resource state from Read() 2. Return a list of properties that changed from Diff() We implement the former by reading the last applied configuration from the `kubectl.kubernetes.io/last-applied-configuration` annotation in the live object state. If this key is not present, no inputs are populated and the old inputs are retained. These changes also update the provider to set this field during `Create` and `Update`. We implement the latter by scanning the JSON diff and recording the names of the top-level properties that changed. The engine uses this information to filter diffs to only those that are semantically meaningful. These changes required a couple of bugfixes: - Old names are only adopted if the old resource was auto-named. This ensures that a name must be specified when importing a resource that was not autonamed. - URN to GVK conversion was fixed for resources in the "core" group. These resources have no group part in the GVK. Parsing was also simplified through the use of pulumi/pulumi's token manipulation functions. - When reading a resource, the GVK for the resource to read is now pulled from the URN if it is absent from the inputs. --- CHANGELOG.md | 3 +- go.sum | 8 +- pkg/metadata/naming.go | 10 +- pkg/metadata/naming_test.go | 22 ++++- pkg/provider/provider.go | 182 ++++++++++++++++++++++++++++-------- 5 files changed, 173 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c24512bf..521c9df7ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -251,7 +251,8 @@ As such, we are rev'ing the minor version of the package from 0.16 to 0.17. Rec ### Improvements -- None +- The Kubernetes provider now supports the internal features necessary for the Pulumi engine to detect diffs between + the actual and desired state of a resource after a `pulumi refresh` (https://github.com/pulumi/pulumi-kubernetes/pull/477) ### Bug fixes diff --git a/go.sum b/go.sum index 2a1211bc1f..4c068c47ef 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgo cloud.google.com/go v0.37.2 h1:4y4L7BdHenTfZL0HervofNTHh9Ad6mNX72cQvl+5eH0= cloud.google.com/go v0.37.2/go.mod h1:H8IAquKe2L30IxoupDgqTaQvKSwF/c8prYHynGIWQbA= contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= +contrib.go.opencensus.io/exporter/ocagent v0.4.2 h1:EjvhWhqxJpIUEBcTJoUDUyScfZ/30ehPEvDmvj9v4DA= contrib.go.opencensus.io/exporter/ocagent v0.4.2/go.mod h1:YuG83h+XWwqWjvCqn7vK4KSyLKhThY3+gNGQ37iS2V0= contrib.go.opencensus.io/exporter/ocagent v0.4.12 h1:jGFvw3l57ViIVEPKKEUXPcLYIXJmQxLUh6ey1eJhwyc= contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA= @@ -30,6 +31,7 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7O github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v11.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v11.1.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v11.3.2+incompatible h1:2bRmoaLvtIXW5uWpZVoIkc0C1z7c84rVGnP+3mpyCRg= github.com/Azure/go-autorest v11.3.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v12.0.0+incompatible h1:N+VqClcomLGD/sHb3smbSYYtNMgKpVV3Cd5r5i8z6bQ= github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= @@ -104,6 +106,7 @@ github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXN github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.1.0-0.20181214143942-ba49f56771b8/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.1.0 h1:VwZ9smxzX8u14/125wHIX7ARV+YhR+L4JADswwxWK0Y= github.com/census-instrumentation/opencensus-proto v0.1.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXGM30YZL1WW/M337pXml+GrcZ4= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -359,7 +362,6 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kevinburke/go-bindata v3.11.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM= github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8= @@ -415,6 +417,7 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mna/pigeon v1.0.1-0.20180808201053-bb0192cfc2ae/go.mod h1:Iym28+kJVnC1hfQvv5MUtI6AiFFzvQjHcvI4RFTG/04= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da h1:ZQGIPjr1iTtUPXZFk8WShqb5G+Qg65VHFLtSvmHh+Mw= github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -576,11 +579,8 @@ github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnW github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= go.etcd.io/etcd v3.3.11+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.mongodb.org/mongo-driver v1.0.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= diff --git a/pkg/metadata/naming.go b/pkg/metadata/naming.go index f345a135da..2166d9c40d 100644 --- a/pkg/metadata/naming.go +++ b/pkg/metadata/naming.go @@ -36,15 +36,13 @@ func AssignNameIfAutonamable(obj *unstructured.Unstructured, base tokens.QName) } } -// AdoptOldNameIfUnnamed checks if `newObj` has a name, and if not, "adopts" the name of `oldObj` +// 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 AdoptOldNameIfUnnamed(newObj, oldObj *unstructured.Unstructured) { +func AdoptOldAutonameIfUnnamed(newObj, oldObj *unstructured.Unstructured) { contract.Assert(oldObj.GetName() != "") - if newObj.GetName() == "" { + if newObj.GetName() == "" && IsAutonamed(oldObj) { newObj.SetName(oldObj.GetName()) - if IsAutonamed(oldObj) { - SetAnnotationTrue(newObj, AnnotationAutonamed) - } + SetAnnotationTrue(newObj, AnnotationAutonamed) } } diff --git a/pkg/metadata/naming_test.go b/pkg/metadata/naming_test.go index ef9d3c9d4c..cab3c8c203 100644 --- a/pkg/metadata/naming_test.go +++ b/pkg/metadata/naming_test.go @@ -48,12 +48,13 @@ func TestAdoptName(t *testing.T) { // NOTE: annotations needs to be a `map[string]interface{}` rather than `map[string]string` // or the k8s utility functions fail. "annotations": map[string]interface{}{AnnotationAutonamed: "true"}, - }}, + }, + }, } new1 := &unstructured.Unstructured{ Object: map[string]interface{}{"metadata": map[string]interface{}{"name": "new1"}}, } - AdoptOldNameIfUnnamed(new1, old1) + AdoptOldAutonameIfUnnamed(new1, old1) assert.Equal(t, "old1", old1.GetName()) assert.True(t, IsAutonamed(old1)) assert.Equal(t, "new1", new1.GetName()) @@ -63,7 +64,22 @@ func TestAdoptName(t *testing.T) { new2 := &unstructured.Unstructured{ Object: map[string]interface{}{}, } - AdoptOldNameIfUnnamed(new2, old1) + AdoptOldAutonameIfUnnamed(new2, old1) assert.Equal(t, "old1", new2.GetName()) assert.True(t, IsAutonamed(new2)) + + // old2 is not autonamed, so new3 DOES NOT adopt old2's name. + new3 := &unstructured.Unstructured{ + Object: map[string]interface{}{}, + } + old2 := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "old1", + }, + }, + } + AdoptOldAutonameIfUnnamed(new3, old2) + assert.Equal(t, "", new3.GetName()) + assert.False(t, IsAutonamed(new3)) } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 5f52f23679..bb3693937f 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -60,8 +60,8 @@ import ( // -------------------------------------------------------------------------- const ( - gvkDelimiter = ":" invokeKubectlReplace = "kubernetes:kubernetes:kubectlReplace" + lastAppliedConfigKey = "kubectl.kubernetes.io/last-applied-configuration" ) type cancellationContext struct { @@ -86,7 +86,7 @@ type kubeProvider struct { canceler *cancellationContext name string version string - providerPrefix string + providerPackage string opts kubeOpts defaultNamespace string @@ -99,11 +99,11 @@ func makeKubeProvider( host *provider.HostClient, name, version string, ) (pulumirpc.ResourceProviderServer, error) { return &kubeProvider{ - host: host, - canceler: makeCancellationContext(), - name: name, - version: version, - providerPrefix: name + gvkDelimiter, + host: host, + canceler: makeCancellationContext(), + name: name, + version: version, + providerPackage: name, }, nil } @@ -392,7 +392,7 @@ func (k *kubeProvider) Check(ctx context.Context, req *pulumirpc.CheckRequest) ( // NOTE: If old inputs exist, they have a name, either provided by the user or filled in with a // previous run of `Check`. contract.Assert(oldInputs.GetName() != "") - metadata.AdoptOldNameIfUnnamed(newInputs, oldInputs) + metadata.AdoptOldAutonameIfUnnamed(newInputs, oldInputs) } else { metadata.AssignNameIfAutonamable(newInputs, urn.Name()) } @@ -542,6 +542,19 @@ func (k *kubeProvider) Diff( oldInputs.SetGroupVersionKind(gvk) } + // We do not expose the "last applied config" field to the user via `Check`, so we want to ignore any diffs in the + // same. We achieve that by simply copying this field from the old state. + if oldAnnotations := oldLiveState.GetAnnotations(); oldAnnotations != nil { + if lastAppliedConfig, ok := oldAnnotations[lastAppliedConfigKey]; ok { + annotations := newInputs.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + annotations[lastAppliedConfigKey] = lastAppliedConfig + newInputs.SetAnnotations(annotations) + } + } + patch, _, err := openapi.PatchForResourceUpdate( k.clientSet.DiscoveryClientCached, oldInputs, newInputs, oldLiveState) if err != nil { @@ -623,6 +636,11 @@ func (k *kubeProvider) Create( } newInputs := propMapToUnstructured(newResInputs) + annotatedInputs, err := withLastAppliedConfig(newInputs) + if err != nil { + return nil, err + } + config := await.CreateConfig{ ProviderConfig: await.ProviderConfig{ Context: k.canceler.context, @@ -631,7 +649,7 @@ func (k *kubeProvider) Create( ClientSet: k.clientSet, DedupLogger: logging.NewLogger(k.canceler.context, k.host, urn), }, - Inputs: newInputs, + Inputs: annotatedInputs, } initialized, awaitErr := await.Creation(config) @@ -665,7 +683,7 @@ func (k *kubeProvider) Create( if awaitErr != nil { // Resource was created but failed to initialize. Return live version of object so it can be // checkpointed. - return nil, partialError(FqObjName(initialized), awaitErr, inputsAndComputed) + return nil, partialError(FqObjName(initialized), awaitErr, inputsAndComputed, nil) } // Invalidate the client cache if this was a CRD. This will require subsequent CR creations to @@ -714,16 +732,27 @@ func (k *kubeProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*p oldInputs, newInputs := parseCheckpointObject(oldState) if oldInputs.GroupVersionKind().Empty() { - oldInputs.SetGroupVersionKind(newInputs.GroupVersionKind()) + if newInputs.GroupVersionKind().Empty() { + gvk, err := k.gvkFromURN(urn) + if err != nil { + return nil, err + } + oldInputs.SetGroupVersionKind(gvk) + } else { + oldInputs.SetGroupVersionKind(newInputs.GroupVersionKind()) + } } - _, name := ParseFqName(req.GetId()) + namespace, name := ParseFqName(req.GetId()) if name == "" { return nil, fmt.Errorf("failed to parse resource name from request ID: %s", req.GetId()) } if oldInputs.GetName() == "" { oldInputs.SetName(name) } + if oldInputs.GetNamespace() == "" { + oldInputs.SetNamespace(namespace) + } config := await.ReadConfig{ ProviderConfig: await.ProviderConfig{ @@ -768,6 +797,9 @@ func (k *kubeProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*p // initialize. } + // Attempt to parse the inputs for this object. If parsing was unsuccessful, retain the old inputs. + liveInputs := parseLiveInputs(liveObj, oldInputs) + // TODO(lblackstone): not sure why this is needed id := FqObjName(liveObj) if reqID := req.GetId(); len(reqID) > 0 { @@ -775,9 +807,17 @@ func (k *kubeProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*p } // Return a new "checkpoint object". - inputsAndComputed, err := plugin.MarshalProperties( - checkpointObject(oldInputs, liveObj), plugin.MarshalOptions{ - Label: fmt.Sprintf("%s.inputsAndComputed", label), KeepUnknowns: true, SkipNulls: true, + state, err := plugin.MarshalProperties( + checkpointObject(liveInputs, liveObj), plugin.MarshalOptions{ + Label: fmt.Sprintf("%s.state", label), KeepUnknowns: true, SkipNulls: true, + }) + if err != nil { + return nil, err + } + + inputs, err := plugin.MarshalProperties( + resource.NewPropertyMapFromMap(liveInputs.Object), plugin.MarshalOptions{ + Label: label + ".inputs", KeepUnknowns: true, SkipNulls: true, }) if err != nil { return nil, err @@ -786,11 +826,11 @@ func (k *kubeProvider) Read(ctx context.Context, req *pulumirpc.ReadRequest) (*p if readErr != nil { // Resource was created but failed to initialize. Return live version of object so it can be // checkpointed. - glog.V(3).Infof("%v", partialError(id, readErr, inputsAndComputed)) - return nil, partialError(id, readErr, inputsAndComputed) + glog.V(3).Infof("%v", partialError(id, readErr, state, inputs)) + return nil, partialError(id, readErr, state, inputs) } - return &pulumirpc.ReadResponse{Id: id, Properties: inputsAndComputed}, nil + return &pulumirpc.ReadResponse{Id: id, Properties: state, Inputs: inputs}, nil } // Update updates an existing resource with new values. Currently this client supports the @@ -881,6 +921,11 @@ func (k *kubeProvider) Update( } newInputs := propMapToUnstructured(newResInputs) + annotatedInputs, err := withLastAppliedConfig(newInputs) + if err != nil { + return nil, err + } + config := await.UpdateConfig{ ProviderConfig: await.ProviderConfig{ Context: k.canceler.context, @@ -890,7 +935,7 @@ func (k *kubeProvider) Update( DedupLogger: logging.NewLogger(k.canceler.context, k.host, urn), }, Previous: oldInputs, - Inputs: newInputs, + Inputs: annotatedInputs, } // Apply update. initialized, awaitErr := await.Update(config) @@ -926,7 +971,7 @@ func (k *kubeProvider) Update( if awaitErr != nil { // Resource was updated/created but failed to initialize. Return live version of object so it // can be checkpointed. - return nil, partialError(FqObjName(initialized), awaitErr, inputsAndComputed) + return nil, partialError(FqObjName(initialized), awaitErr, inputsAndComputed, nil) } return &pulumirpc.UpdateResponse{Properties: inputsAndComputed}, nil @@ -991,7 +1036,7 @@ func (k *kubeProvider) Delete( // Resource delete was issued, but failed to complete. Return live version of object so it can be // checkpointed. - return nil, partialError(FqObjName(lastKnownState), awaitErr, inputsAndComputed) + return nil, partialError(FqObjName(lastKnownState), awaitErr, inputsAndComputed, nil) } return &pbempty.Empty{}, nil @@ -1025,27 +1070,24 @@ func (k *kubeProvider) label() string { } func (k *kubeProvider) gvkFromURN(urn resource.URN) (schema.GroupVersionKind, error) { - // Strip prefix. - s := string(urn.Type()) - contract.Assertf(strings.HasPrefix(s, k.providerPrefix), - "Expected prefix: %q, Kubernetes GVK is: %q", k.providerPrefix, string(urn)) - s = s[len(k.providerPrefix):] + contract.Assertf(string(urn.Type().Package()) == k.providerPackage, "Kubernetes GVK is: %q", string(urn)) // Emit GVK. - gvk := strings.Split(s, gvkDelimiter) - gv := strings.Split(gvk[0], "/") - if len(gvk) < 2 { - return schema.GroupVersionKind{}, - fmt.Errorf("GVK must have both an apiVersion and a Kind: %q", s) - } else if len(gv) != 2 { + kind := string(urn.Type().Name()) + gv := strings.Split(string(urn.Type().Module().Name()), "/") + if len(gv) != 2 { return schema.GroupVersionKind{}, - fmt.Errorf("apiVersion does not have both a group and a version: %q", s) + fmt.Errorf("apiVersion does not have both a group and a version: %q", urn.Type().Module().Name()) + } + group, version := gv[0], gv[1] + if group == "core" { + group = "" } return schema.GroupVersionKind{ - Group: gv[0], - Version: gv[1], - Kind: gvk[1], + Group: group, + Version: version, + Kind: kind, }, nil } @@ -1064,6 +1106,26 @@ func propMapToUnstructured(pm resource.PropertyMap) *unstructured.Unstructured { return &unstructured.Unstructured{Object: pm.Mappable()} } +func withLastAppliedConfig(config *unstructured.Unstructured) (*unstructured.Unstructured, error) { + // Serialize the inputs and add the last-applied-configuration annotation. + marshaled, err := config.MarshalJSON() + if err != nil { + return nil, err + } + + // Deep copy the config before returning. + config = config.DeepCopy() + + annotations := config.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) + } + + annotations[lastAppliedConfigKey] = string(marshaled) + config.SetAnnotations(annotations) + return config, nil +} + func checkpointObject(inputs, live *unstructured.Unstructured) resource.PropertyMap { object := resource.NewPropertyMapFromMap(live.Object) object["__inputs"] = resource.NewObjectProperty(resource.NewPropertyMapFromMap(inputs.Object)) @@ -1101,15 +1163,16 @@ func parseCheckpointObject(obj resource.PropertyMap) (oldInputs, live *unstructu // partialError creates an error for resources that did not complete an operation in progress. // The last known state of the object is included in the error so that it can be checkpointed. -func partialError(id string, err error, inputsAndComputed *structpb.Struct) error { +func partialError(id string, err error, state *structpb.Struct, inputs *structpb.Struct) error { reasons := []string{err.Error()} if aggregate, isAggregate := err.(await.AggregatedError); isAggregate { reasons = append(reasons, aggregate.SubErrors()...) } detail := pulumirpc.ErrorResourceInitFailed{ Id: id, - Properties: inputsAndComputed, + Properties: state, Reasons: reasons, + Inputs: inputs, } return rpcerror.WithDetails(rpcerror.New(codes.Unknown, err.Error()), &detail) } @@ -1125,3 +1188,46 @@ func canonicalNamespace(ns string) string { // deleteResponse causes the resource to be deleted from the state. var deleteResponse = &pulumirpc.ReadResponse{Id: "", Properties: nil} + +// parseLastAppliedConfig attempts to find and parse an annotation that records the last applied configuration for the +// given live object state. +func parseLastAppliedConfig(live *unstructured.Unstructured) *unstructured.Unstructured { + // If `kubectl.kubernetes.io/last-applied-configuration` metadata anotation is present, parse it into a real object + // and use it as the current set of live inputs. Otherwise, return nil. + annotations := live.GetAnnotations() + if annotations == nil { + return nil + } + lastAppliedConfig, ok := annotations[lastAppliedConfigKey] + if !ok { + return nil + } + + liveInputs := &unstructured.Unstructured{} + if err := liveInputs.UnmarshalJSON([]byte(lastAppliedConfig)); err != nil { + return nil + } + return liveInputs +} + +// parseLiveInputs attempts to parse the provider inputs that produced the given live object out of the object's state. +// This is used by Read. +func parseLiveInputs(live, oldInputs *unstructured.Unstructured) *unstructured.Unstructured { + // First try to find and parse a `kubectl.kubernetes.io/last-applied-configuration` metadata anotation. If that + // succeeds, we are done. + if inputs := parseLastAppliedConfig(live); inputs != nil { + return inputs + } + + // If no such annotation was present--or if parsing failed--either retain the old inputs if they exist, or + // attempt to propagate the live object's GVK, any Pulumi-generated autoname and its annotation, and return + // the result. + if oldInputs != nil && len(oldInputs.Object) > 0 { + return oldInputs + } + + inputs := &unstructured.Unstructured{Object: map[string]interface{}{}} + inputs.SetGroupVersionKind(live.GroupVersionKind()) + metadata.AdoptOldAutonameIfUnnamed(inputs, live) + return inputs +} From 0b901433822d9d76bcca466fc33a40469fc33d8a Mon Sep 17 00:00:00 2001 From: Pat Gavlin Date: Wed, 26 Jun 2019 15:17:59 -0700 Subject: [PATCH 2/3] Fix the CHANGELOG --- CHANGELOG.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 521c9df7ef..fed6141074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### Improvements - Unify diff behavior between `Diff` and `Update`. This should result in better detection of state drift as well as behavior that is more consistent with respect to `kubectl`. +- The Kubernetes provider now supports the internal features necessary for the Pulumi engine to detect diffs between the actual and desired state of a resource after a `pulumi refresh` (https://github.com/pulumi/pulumi-kubernetes/pull/477). +- The Kubernetes provider now sets the `"kubectl.kubernetes.io/last-applied-configuration"` annotation to the last deployed configuration for a resource. This enables better interoperability with `kubectl`. ## 0.25.0 (June 19, 2019) @@ -249,11 +251,6 @@ As such, we are rev'ing the minor version of the package from 0.16 to 0.17. Rec - None -### Improvements - -- The Kubernetes provider now supports the internal features necessary for the Pulumi engine to detect diffs between - the actual and desired state of a resource after a `pulumi refresh` (https://github.com/pulumi/pulumi-kubernetes/pull/477) - ### Bug fixes - Move mocha dependencies to devDependencies (https://github.com/pulumi/pulumi-kubernetes/pull/441) From 180f4b1b3a64d923d561f9f4506a062de0d185e8 Mon Sep 17 00:00:00 2001 From: Pat Gavlin Date: Wed, 26 Jun 2019 15:36:38 -0700 Subject: [PATCH 3/3] Restore a removed piece of the CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fed6141074..8a27d33d41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -251,6 +251,10 @@ As such, we are rev'ing the minor version of the package from 0.16 to 0.17. Rec - None +### Improvements + +- None + ### Bug fixes - Move mocha dependencies to devDependencies (https://github.com/pulumi/pulumi-kubernetes/pull/441)