Skip to content

Commit 37a1dc2

Browse files
authored
Add resource identity to kubernetes_manifest (#2737)
1 parent 96684d8 commit 37a1dc2

File tree

9 files changed

+199
-22
lines changed

9 files changed

+199
-22
lines changed

.changelog/2737.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
Add ResourceIdentity support to kubernetes_manifest
3+
```

manifest/provider/apply.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import (
2020
"k8s.io/client-go/dynamic"
2121
)
2222

23-
var defaultCreateTimeout = "10m"
24-
var defaultUpdateTimeout = "10m"
25-
var defaultDeleteTimeout = "10m"
23+
var (
24+
defaultCreateTimeout = "10m"
25+
defaultUpdateTimeout = "10m"
26+
defaultDeleteTimeout = "10m"
27+
)
2628

2729
// ApplyResourceChange function
2830
func (s *RawProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov5.ApplyResourceChangeRequest) (*tfprotov5.ApplyResourceChangeResponse, error) {
@@ -482,6 +484,15 @@ func (s *RawProviderServer) ApplyResourceChange(ctx context.Context, req *tfprot
482484
return resp, err
483485
}
484486
resp.NewState = &newResState
487+
488+
// set resource identity data
489+
idData, err := createIdentityData(result)
490+
if err != nil {
491+
return resp, err
492+
}
493+
resp.NewIdentity = &tfprotov5.ResourceIdentityData{
494+
IdentityData: &idData,
495+
}
485496
case applyPlannedState.IsNull():
486497
// Delete the resource
487498
priorStateVal := make(map[string]tftypes.Value)

manifest/provider/import.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1717
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"k8s.io/apimachinery/pkg/runtime/schema"
1819
)
1920

2021
// ImportResourceState function
@@ -49,7 +50,14 @@ func (s *RawProviderServer) ImportResourceState(ctx context.Context, req *tfprot
4950
return resp, nil
5051
}
5152

52-
gvk, name, namespace, err := util.ParseResourceID(req.ID)
53+
var gvk schema.GroupVersionKind
54+
var name, namespace string
55+
var err error
56+
if req.Identity != nil {
57+
gvk, name, namespace, err = parseResourceIdentityData(req.Identity)
58+
} else {
59+
gvk, name, namespace, err = util.ParseResourceID(req.ID)
60+
}
5361
if err != nil {
5462
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
5563
Severity: tfprotov5.DiagnosticSeverityError,
@@ -58,6 +66,7 @@ func (s *RawProviderServer) ImportResourceState(ctx context.Context, req *tfprot
5866
})
5967
}
6068
s.logger.Trace("[ImportResourceState]", "[ID]", gvk, name, namespace)
69+
6170
rt, err := GetResourceType(req.TypeName)
6271
if err != nil {
6372
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
@@ -196,16 +205,24 @@ func (s *RawProviderServer) ImportResourceState(ctx context.Context, req *tfprot
196205
Detail: err.Error(),
197206
})
198207
}
208+
idData, err := createIdentityData(ro)
209+
if err != nil {
210+
return resp, err
211+
}
199212
nr := &tfprotov5.ImportedResource{
200213
TypeName: req.TypeName,
201214
State: &impState,
202215
Private: fb,
216+
Identity: &tfprotov5.ResourceIdentityData{
217+
IdentityData: &idData,
218+
},
203219
}
204220
resp.ImportedResources = append(resp.ImportedResources, nr)
205221
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
206222
Severity: tfprotov5.DiagnosticSeverityWarning,
207223
Summary: "Apply needed after 'import'",
208224
Detail: "Please run apply after a successful import to realign the resource state to the configuration in Terraform.",
209225
})
226+
210227
return resp, nil
211228
}

manifest/provider/read.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,5 +194,15 @@ func (s *RawProviderServer) ReadResource(ctx context.Context, req *tfprotov5.Rea
194194
return resp, err
195195
}
196196
resp.NewState = &newState
197+
198+
// set resource identity data
199+
idData, err := createIdentityData(ro)
200+
if err != nil {
201+
return resp, err
202+
}
203+
resp.NewIdentity = &tfprotov5.ResourceIdentityData{
204+
IdentityData: &idData,
205+
}
206+
197207
return resp, nil
198208
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
11+
"github.com/hashicorp/terraform-plugin-go/tftypes"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
"k8s.io/apimachinery/pkg/runtime/schema"
14+
)
15+
16+
func (s *RawProviderServer) GetResourceIdentitySchemas(ctx context.Context, req *tfprotov5.GetResourceIdentitySchemasRequest) (*tfprotov5.GetResourceIdentitySchemasResponse, error) {
17+
s.logger.Trace("[GetResourceIdentitySchemas][Request]\n%s\n", dump(*req))
18+
resp := &tfprotov5.GetResourceIdentitySchemasResponse{
19+
IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{
20+
"kubernetes_manifest": {
21+
Version: 1,
22+
IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{
23+
{Name: "api_version", RequiredForImport: true, Type: tftypes.String},
24+
{Name: "kind", RequiredForImport: true, Type: tftypes.String},
25+
{Name: "name", RequiredForImport: true, Type: tftypes.String},
26+
{Name: "namespace", OptionalForImport: true, Type: tftypes.String},
27+
},
28+
},
29+
},
30+
}
31+
return resp, nil
32+
}
33+
34+
func (s *RawProviderServer) UpgradeResourceIdentity(ctx context.Context, req *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) {
35+
s.logger.Trace("[UpgradeResourceIdentity][Request]\n%s\n", dump(*req))
36+
resp := &tfprotov5.UpgradeResourceIdentityResponse{}
37+
return resp, nil
38+
}
39+
40+
func parseResourceIdentityData(rid *tfprotov5.ResourceIdentityData) (schema.GroupVersionKind, string, string, error) {
41+
namespace := "default"
42+
var apiVersion, kind, name string
43+
44+
iddata, err := rid.IdentityData.Unmarshal(getIdentityType())
45+
if err != nil {
46+
return schema.GroupVersionKind{}, "", "",
47+
fmt.Errorf("could not unmarshal identity data: %v", err.Error())
48+
}
49+
50+
var idvals map[string]tftypes.Value
51+
iddata.As(&idvals)
52+
53+
idvals["api_version"].As(&apiVersion)
54+
idvals["kind"].As(&kind)
55+
idvals["namespace"].As(&namespace)
56+
idvals["name"].As(&name)
57+
58+
gvk := schema.FromAPIVersionAndKind(apiVersion, kind)
59+
return gvk, name, namespace, nil
60+
}
61+
62+
func getIdentityType() tftypes.Type {
63+
return tftypes.Object{
64+
AttributeTypes: map[string]tftypes.Type{
65+
"namespace": tftypes.String,
66+
"name": tftypes.String,
67+
"api_version": tftypes.String,
68+
"kind": tftypes.String,
69+
},
70+
}
71+
}
72+
73+
func createIdentityData(obj *unstructured.Unstructured) (tfprotov5.DynamicValue, error) {
74+
idVal := tftypes.NewValue(getIdentityType(), map[string]tftypes.Value{
75+
"namespace": tftypes.NewValue(tftypes.String, obj.GetNamespace()),
76+
"name": tftypes.NewValue(tftypes.String, obj.GetName()),
77+
"api_version": tftypes.NewValue(tftypes.String, obj.GetAPIVersion()),
78+
"kind": tftypes.NewValue(tftypes.String, obj.GetKind()),
79+
})
80+
return tfprotov5.NewDynamicValue(idVal.Type(), idVal)
81+
}

manifest/provider/server.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ func init() {
2323
install.Install(scheme.Scheme)
2424
}
2525

26-
var _ tfprotov5.ProviderServer = &RawProviderServer{}
27-
var _ tfprotov5.ResourceServer = &RawProviderServer{}
28-
var _ tfprotov5.DataSourceServer = &RawProviderServer{}
26+
var (
27+
_ tfprotov5.ProviderServer = &RawProviderServer{}
28+
_ tfprotov5.ResourceServer = &RawProviderServer{}
29+
_ tfprotov5.DataSourceServer = &RawProviderServer{}
30+
)
2931

3032
// RawProviderServer implements the ProviderServer interface as exported from ProtoBuf.
3133
type RawProviderServer struct {
@@ -136,15 +138,3 @@ func (s *RawProviderServer) ValidateEphemeralResourceConfig(ctx context.Context,
136138
resp := &tfprotov5.ValidateEphemeralResourceConfigResponse{}
137139
return resp, nil
138140
}
139-
140-
func (s *RawProviderServer) GetResourceIdentitySchemas(ctx context.Context, req *tfprotov5.GetResourceIdentitySchemasRequest) (*tfprotov5.GetResourceIdentitySchemasResponse, error) {
141-
s.logger.Trace("[GetResourceIdentitySchemas][Request]\n%s\n", dump(*req))
142-
resp := &tfprotov5.GetResourceIdentitySchemasResponse{}
143-
return resp, nil
144-
}
145-
146-
func (s *RawProviderServer) UpgradeResourceIdentity(ctx context.Context, req *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) {
147-
s.logger.Trace("[UpgradeResourceIdentity][Request]\n%s\n", dump(*req))
148-
resp := &tfprotov5.UpgradeResourceIdentityResponse{}
149-
return resp, nil
150-
}

manifest/test/acceptance/configmap_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"testing"
1212

1313
"github.com/hashicorp/go-hclog"
14+
version "github.com/hashicorp/go-version"
1415
"github.com/hashicorp/terraform-provider-kubernetes/manifest/provider"
1516
"github.com/hashicorp/terraform-provider-kubernetes/manifest/test/helper/kubernetes"
1617
tfstatehelper "github.com/hashicorp/terraform-provider-kubernetes/manifest/test/helper/state"
@@ -87,4 +88,18 @@ func TestKubernetesManifest_ConfigMap(t *testing.T) {
8788
tfstate.AssertAttributeNotEmpty(t, "kubernetes_manifest.test.object.metadata.labels.test")
8889

8990
tfstate.AssertAttributeDoesNotExist(t, "kubernetes_manifest.test.spec")
91+
92+
tfversion, err := tf.Version(ctx)
93+
if err != nil {
94+
t.Fatalf("Failed to retrieve terraform version: %v", err)
95+
}
96+
constraint, _ := version.NewConstraint(">= 1.12.0")
97+
if constraint.Check(tfversion) {
98+
tfstate.AssertIdentityValueEqual(t, "kubernetes_manifest.test", "api_version", "v1")
99+
tfstate.AssertIdentityValueEqual(t, "kubernetes_manifest.test", "kind", "ConfigMap")
100+
tfstate.AssertIdentityValueEqual(t, "kubernetes_manifest.test", "name", name)
101+
tfstate.AssertIdentityValueEqual(t, "kubernetes_manifest.test", "namespace", namespace)
102+
} else {
103+
t.Logf("Skipping identity assertions because terraform version %s is less than 1.12.0", tfversion)
104+
}
90105
}

manifest/test/helper/state/state_helper.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Helper struct {
2626
func NewHelper(tfstate *tfjson.State) *Helper {
2727
return &Helper{tfstate}
2828
}
29+
2930
func (s *Helper) ResourceExists(t *testing.T, resourceAddress string) bool {
3031
t.Helper()
3132
_, err := getAttributesValuesFromResource(s, resourceAddress)
@@ -42,6 +43,16 @@ func getAttributesValuesFromResource(state *Helper, address string) (interface{}
4243
return nil, fmt.Errorf("Could not find resource %q in state", address)
4344
}
4445

46+
// getIdentityValues pulls out the getIdentityValues field from the resource at the given address
47+
func getIdentityValuesFromResource(state *Helper, address string) (map[string]any, error) {
48+
for _, r := range state.Values.RootModule.Resources {
49+
if r.Address == address {
50+
return r.IdentityValues, nil
51+
}
52+
}
53+
return nil, fmt.Errorf("Could not find resource %q in state", address)
54+
}
55+
4556
// getOutputValues gets the given output name value from the state
4657
func getOutputValues(state *Helper, name string) (interface{}, error) {
4758
for n, v := range state.Values.Outputs {
@@ -95,10 +106,10 @@ func parseStateAddress(address string) (string, string) {
95106
switch parts[0] {
96107
case "data":
97108
resourceAddress = strings.Join(parts[0:3], ".")
98-
attributeAddress = strings.Join(parts[3:len(parts)], ".")
109+
attributeAddress = strings.Join(parts[3:], ".")
99110
default:
100111
resourceAddress = strings.Join(parts[0:2], ".")
101-
attributeAddress = strings.Join(parts[2:len(parts)], ".")
112+
attributeAddress = strings.Join(parts[2:], ".")
102113
}
103114

104115
return resourceAddress, attributeAddress
@@ -123,6 +134,22 @@ func (s *Helper) GetAttributeValue(t *testing.T, address string) interface{} {
123134
return value
124135
}
125136

137+
// GetIdentityValue will get the identity value at the given resource address from the state
138+
func (s *Helper) GetIdentityValue(t *testing.T, address string, identitykey string) interface{} {
139+
t.Helper()
140+
141+
identityvals, err := getIdentityValuesFromResource(s, address)
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
146+
val, ok := identityvals[identitykey]
147+
if !ok {
148+
t.Fatalf("resource identity value %q does not exist for %q", identitykey, address)
149+
}
150+
return val
151+
}
152+
126153
// GetOutputValue gets the given output name value from the state
127154
func (s *Helper) GetOutputValue(t *testing.T, name string) interface{} {
128155
t.Helper()
@@ -157,6 +184,14 @@ func (s *Helper) AssertAttributeEqual(t *testing.T, address string, expectedValu
157184
fmt.Sprintf("Address: %q", address))
158185
}
159186

187+
// AssertIdentityValueEqual will fail the test if the identity value does not equal expectedValue
188+
func (s *Helper) AssertIdentityValueEqual(t *testing.T, address string, identitykey string, expectedValue interface{}) {
189+
t.Helper()
190+
191+
assert.EqualValues(t, expectedValue, s.GetIdentityValue(t, address, identitykey),
192+
fmt.Sprintf("Resource: %q, Identity key: %q", address, identitykey))
193+
}
194+
160195
// AssertAttributeNotEqual will fail the test if the attribute is equal to expectedValue
161196
func (s *Helper) AssertAttributeNotEqual(t *testing.T, address string, expectedValue interface{}) {
162197
t.Helper()

manifest/test/plugintest/working_dir.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"os"
1313
"path/filepath"
1414

15+
"github.com/hashicorp/go-version"
16+
1517
"github.com/hashicorp/terraform-exec/tfexec"
1618
tfjson "github.com/hashicorp/terraform-json"
1719
"github.com/hashicorp/terraform-provider-kubernetes/manifest/test/logging"
@@ -84,7 +86,7 @@ func (wd *WorkingDir) SetConfig(ctx context.Context, cfg string) error {
8486
if err := os.Remove(rmFilename); err != nil && !os.IsNotExist(err) {
8587
return fmt.Errorf("unable to remove %q: %w", rmFilename, err)
8688
}
87-
err := os.WriteFile(outFilename, bCfg, 0700)
89+
err := os.WriteFile(outFilename, bCfg, 0o700)
8890
if err != nil {
8991
return err
9092
}
@@ -326,3 +328,16 @@ func (wd *WorkingDir) Schemas(ctx context.Context) (*tfjson.ProviderSchemas, err
326328

327329
return providerSchemas, err
328330
}
331+
332+
// Version returns the current version of Terraform
333+
//
334+
// If the version cannot be read, Version returns an error.
335+
func (wd *WorkingDir) Version(ctx context.Context) (*version.Version, error) {
336+
logging.HelperResourceTrace(ctx, "Calling Terraform CLI providers version command")
337+
338+
version, _, err := wd.tf.Version(context.Background(), false)
339+
340+
logging.HelperResourceTrace(ctx, "Called Terraform CLI providers version command")
341+
342+
return version, err
343+
}

0 commit comments

Comments
 (0)