Skip to content

Commit

Permalink
Add kubernetes_resource data source
Browse files Browse the repository at this point in the history
  • Loading branch information
jrhouston committed Feb 1, 2022
1 parent 45b1a78 commit 7d25484
Show file tree
Hide file tree
Showing 9 changed files with 450 additions and 9 deletions.
207 changes: 207 additions & 0 deletions manifest/provider/datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package provider

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-provider-kubernetes/manifest/morph"
"github.com/hashicorp/terraform-provider-kubernetes/manifest/payload"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// ReadDataSource function
func (s *RawProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) {
s.logger.Trace("[ReadDataSource][Request]\n%s\n", dump(*req))

resp := &tfprotov5.ReadDataSourceResponse{}

execDiag := s.canExecute()
if len(execDiag) > 0 {
resp.Diagnostics = append(resp.Diagnostics, execDiag...)
return resp, nil
}

rt, err := GetDataSourceType(req.TypeName)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to determine data source type",
Detail: err.Error(),
})
return resp, nil
}

config, err := req.Config.Unmarshal(rt)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to unmarshal data source configuration",
Detail: err.Error(),
})
return resp, nil
}

var dsConfig map[string]tftypes.Value
err = config.As(&dsConfig)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to extract attributes from data source configuration",
Detail: err.Error(),
})
return resp, nil
}

rm, err := s.getRestMapper()
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to get RESTMapper client",
Detail: err.Error(),
})
return resp, nil
}

client, err := s.getDynamicClient()
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "failed to get Dynamic client",
Detail: err.Error(),
})
return resp, nil
}

var apiVersion, kind string
dsConfig["api_version"].As(&apiVersion)
dsConfig["kind"].As(&kind)

gvr, err := getGVR(apiVersion, kind, rm)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to determine resource GroupVersion",
Detail: err.Error(),
})
return resp, nil
}

gvk := gvr.GroupVersion().WithKind(kind)
ns, err := IsResourceNamespaced(gvk, rm)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed determine if resource is namespaced",
Detail: err.Error(),
})
return resp, nil
}
rcl := client.Resource(gvr)

objectType, err := s.TFTypeFromOpenAPI(ctx, gvk, false)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to save resource state",
Detail: err.Error(),
})
return resp, nil
}

var metadataBlock []tftypes.Value
dsConfig["metadata"].As(&metadataBlock)

var metadata map[string]tftypes.Value
metadataBlock[0].As(&metadata)

var name string
metadata["name"].As(&name)

var res *unstructured.Unstructured
if ns {
var namespace string
metadata["namespace"].As(&namespace)
if namespace == "" {
namespace = "default"
}
res, err = rcl.Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
} else {
res, err = rcl.Get(ctx, name, metav1.GetOptions{})
}
if err != nil {
if apierrors.IsNotFound(err) {
return resp, nil
}
d := tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: fmt.Sprintf("Failed to get data source"),
Detail: err.Error(),
}
resp.Diagnostics = append(resp.Diagnostics, &d)
return resp, nil
}

fo := RemoveServerSideFields(res.Object)
nobj, err := payload.ToTFValue(fo, objectType, tftypes.NewAttributePath())
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to convert API response to Terraform value type",
Detail: err.Error(),
})
return resp, nil
}

nobj, err = morph.DeepUnknown(objectType, nobj, tftypes.NewAttributePath())
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to save resource state",
Detail: err.Error(),
})
return resp, nil
}
rawState := make(map[string]tftypes.Value)
err = config.As(&rawState)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to save resource state",
Detail: err.Error(),
})
return resp, nil
}
rawState["object"] = morph.UnknownToNull(nobj)

v := tftypes.NewValue(rt, rawState)
state, err := tfprotov5.NewDynamicValue(v.Type(), v)
if err != nil {
resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{
Severity: tfprotov5.DiagnosticSeverityError,
Summary: "Failed to save resource state",
Detail: err.Error(),
})
return resp, nil
}
resp.State = &state
return resp, nil
}

func getGVR(apiVersion, kind string, m meta.RESTMapper) (schema.GroupVersionResource, error) {
gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil {
return schema.GroupVersionResource{}, err
}
mapping, err := m.RESTMapping(gv.WithKind(kind).GroupKind(), gv.Version)
if err != nil {
return schema.GroupVersionResource{}, err
}
return mapping.Resource, err
}
6 changes: 4 additions & 2 deletions manifest/provider/getproviderschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ func (s *RawProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov
cfgSchema := GetProviderConfigSchema()

resSchema := GetProviderResourceSchema()
dsSchema := GetProviderDataSourceSchema()

return &tfprotov5.GetProviderSchemaResponse{
Provider: cfgSchema,
ResourceSchemas: resSchema,
Provider: cfgSchema,
ResourceSchemas: resSchema,
DataSourceSchemas: dsSchema,
}, nil
}
67 changes: 67 additions & 0 deletions manifest/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ func GetResourceType(name string) (tftypes.Type, error) {
return GetObjectTypeFromSchema(rsch), nil
}

// GetDataSourceType returns the tftypes.Type of a datasource of type 'name'
func GetDataSourceType(name string) (tftypes.Type, error) {
sch := GetProviderDataSourceSchema()
rsch, ok := sch[name]
if !ok {
return tftypes.DynamicPseudoType, fmt.Errorf("unknown data source %q: cannot find schema", name)
}
return GetObjectTypeFromSchema(rsch), nil
}

// GetProviderResourceSchema contains the definitions of all supported resources
func GetProviderResourceSchema() map[string]*tfprotov5.Schema {
waitForType := tftypes.Object{
Expand Down Expand Up @@ -147,3 +157,60 @@ func GetProviderResourceSchema() map[string]*tfprotov5.Schema {
},
}
}

// GetProviderDataSourceSchema contains the definitions of all supported data sources
func GetProviderDataSourceSchema() map[string]*tfprotov5.Schema {
return map[string]*tfprotov5.Schema{
"kubernetes_resource": {
Version: 1,
Block: &tfprotov5.SchemaBlock{
Attributes: []*tfprotov5.SchemaAttribute{
{
Name: "api_version",
Type: tftypes.String,
Required: true,
Description: "The resource apiVersion.",
},
{
Name: "kind",
Type: tftypes.String,
Required: true,
Description: "The resource kind.",
},
{
Name: "object",
Type: tftypes.DynamicPseudoType,
Optional: true,
Computed: true,
Description: "The response from the API server.",
},
},
BlockTypes: []*tfprotov5.SchemaNestedBlock{
{
TypeName: "metadata",
Nesting: tfprotov5.SchemaNestedBlockNestingModeList,
MinItems: 1,
MaxItems: 1,
Block: &tfprotov5.SchemaBlock{
Description: "Metadata for the resource",
Attributes: []*tfprotov5.SchemaAttribute{
{
Name: "name",
Type: tftypes.String,
Required: true,
Description: "The resource name.",
},
{
Name: "namespace",
Type: tftypes.String,
Optional: true,
Description: "The resource namespace.",
},
},
},
},
},
},
},
}
}
7 changes: 0 additions & 7 deletions manifest/provider/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,6 @@ func (s *RawProviderServer) UpgradeResourceState(ctx context.Context, req *tfpro
return resp, nil
}

// ReadDataSource function
func (s *RawProviderServer) ReadDataSource(ctx context.Context, req *tfprotov5.ReadDataSourceRequest) (*tfprotov5.ReadDataSourceResponse, error) {
s.logger.Trace("[ReadDataSource][Request]\n%s\n", dump(*req))

return nil, status.Errorf(codes.Unimplemented, "method ReadDataSource not implemented")
}

// StopProvider function
func (s *RawProviderServer) StopProvider(ctx context.Context, req *tfprotov5.StopProviderRequest) (*tfprotov5.StopProviderResponse, error) {
s.logger.Trace("[StopProvider][Request]\n%s\n", dump(*req))
Expand Down
73 changes: 73 additions & 0 deletions manifest/test/acceptance/datasource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// +build acceptance

package acceptance

import (
"context"
"testing"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/terraform-provider-kubernetes/manifest/provider"
"github.com/hashicorp/terraform-provider-kubernetes/manifest/test/helper/kubernetes"

tfstatehelper "github.com/hashicorp/terraform-provider-kubernetes/manifest/test/helper/state"
)

func TestDataSourceKubernetesResource_ConfigMap(t *testing.T) {
name := randName()
name2 := randName()
namespace := randName()

k8shelper.CreateNamespace(t, namespace)
defer k8shelper.DeleteResource(t, namespace, kubernetes.NewGroupVersionResource("v1", "namespaces"))

// STEP 1: Create a ConfigMap to use as a data source
tf := tfhelper.RequireNewWorkingDir(t)
tf.SetReattachInfo(reattachInfo)
defer func() {
tf.RequireDestroy(t)
tf.Close()
k8shelper.AssertNamespacedResourceDoesNotExist(t, "v1", "configmaps", namespace, name)
}()

tfvars := TFVARS{
"name": name,
"name2": name2,
"namespace": namespace,
}
tfconfig := loadTerraformConfig(t, "datasource/step1.tf", tfvars)
tf.RequireSetConfig(t, tfconfig)
tf.RequireInit(t)
tf.RequireApply(t)

k8shelper.AssertNamespacedResourceExists(t, "v1", "configmaps", namespace, name)

// STEP 2: Create another ConfigMap using the ConfigMap from step 1 as a data source
reattachInfo2, err := provider.ServeTest(context.TODO(), hclog.Default())
if err != nil {
t.Errorf("Failed to create additional provider instance: %q", err)
}
step2 := tfhelper.RequireNewWorkingDir(t)
step2.SetReattachInfo(reattachInfo2)
defer func() {
step2.RequireDestroy(t)
step2.Close()
k8shelper.AssertNamespacedResourceDoesNotExist(t, "v1", "configmaps", namespace, name2)
}()

tfconfig = loadTerraformConfig(t, "datasource/step2.tf", tfvars)
step2.RequireSetConfig(t, string(tfconfig))
step2.RequireInit(t)
step2.RequireApply(t)

tfstate := tfstatehelper.NewHelper(step2.RequireState(t))

// check the data source
tfstate.AssertAttributeValues(t, tfstatehelper.AttributeValues{
"data.kubernetes_resource.test_config.object.data.TEST": "hello world",
})
// check the resource was created with the correct value
tfstate.AssertAttributeValues(t, tfstatehelper.AttributeValues{
"kubernetes_manifest.test_config2.object.data.TEST": "hello world",
})
}
Loading

0 comments on commit 7d25484

Please sign in to comment.