From 367136add23cd598324fc00d0fd8b096ab235d63 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:00:03 +0100 Subject: [PATCH] terraform-provider: support importing Constellation clusters (#2702) * terraform-provider: support importing Constellation clusters * bazel: shfmt exclusion for import script * ci: fix godot check * bazel: shellcheck exclusion for import script * Update dev-docs/workflows/terraform-provider.md Co-authored-by: Adrian Stobbe * ci: fix Terraform lock exclude directories --------- Co-authored-by: Adrian Stobbe --- bazel/ci/shellcheck.sh.in | 1 + bazel/ci/shfmt.sh.in | 1 + bazel/ci/terraform.sh.in | 4 +- dev-docs/workflows/terraform-provider.md | 2 +- internal/constants/constants.go | 15 +++ .../docs/resources/cluster.md | 8 ++ .../resources/constellation_cluster/import.sh | 1 + .../internal/provider/BUILD.bazel | 31 ++--- .../internal/provider/cluster_resource.go | 78 ++++++++++++- .../provider/cluster_resource_test.go | 109 ++++++++++++++++++ 10 files changed, 222 insertions(+), 28 deletions(-) create mode 100644 terraform-provider-constellation/examples/resources/constellation_cluster/import.sh create mode 100644 terraform-provider-constellation/internal/provider/cluster_resource_test.go diff --git a/bazel/ci/shellcheck.sh.in b/bazel/ci/shellcheck.sh.in index 1ac42ba7a..e68e82a32 100644 --- a/bazel/ci/shellcheck.sh.in +++ b/bazel/ci/shellcheck.sh.in @@ -28,6 +28,7 @@ excludeDirs=( "internal/constellation/helm/charts/cilium" "build" "docs/node_modules" + "terraform-provider-constellation/examples" ) excludeFiles=( diff --git a/bazel/ci/shfmt.sh.in b/bazel/ci/shfmt.sh.in index d1fec3f75..edd6be7be 100644 --- a/bazel/ci/shfmt.sh.in +++ b/bazel/ci/shfmt.sh.in @@ -26,6 +26,7 @@ excludeDirs=( "internal/constellation/helm/charts/cilium" "build" "docs/node_modules" + "terraform-provider-constellation/examples" ) echo "The following scripts are excluded and won't be formatted with shfmt:" diff --git a/bazel/ci/terraform.sh.in b/bazel/ci/terraform.sh.in index 7f355b815..121b2313d 100644 --- a/bazel/ci/terraform.sh.in +++ b/bazel/ci/terraform.sh.in @@ -46,9 +46,7 @@ excludeDirs=( excludeLockDirs=( "build" "terraform-provider-constellation" - "terraform/aws-constellation" - "terraform/azure-constellation" - "terraform/gcp-constellation" + "terraform/legacy-module" ) excludeCheckDirs=( diff --git a/dev-docs/workflows/terraform-provider.md b/dev-docs/workflows/terraform-provider.md index 812bd68c6..a97c60d18 100644 --- a/dev-docs/workflows/terraform-provider.md +++ b/dev-docs/workflows/terraform-provider.md @@ -43,7 +43,7 @@ TF_CLI_CONFIG_FILE=config.tfrc terraform apply Terraform acceptance tests can be run hermetically through Bazel (recommended): ```bash -bazel test --test_tag_filters=integration //terraform-provider-constellation/internal/provider:provider_acc_test +bazel test --config=integration-only //terraform-provider-constellation/internal/provider:provider_test ``` The tests can also be run through Go, but the `TF_ACC` environment variable needs to be set to `1`, and the host's Terraform binary is used, which may produce inaccurate test results. diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 6fd73f6d0..40b194d2d 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -268,6 +268,21 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELcPl4Ik+qZuH4K049wksoXK/Os3Z b92PDCpM7FZAINQF88s1TZS/HmRXYk62UJ4eqPduvUnJmXhNikhLbMi6fw== -----END PUBLIC KEY----- ` + + // + // Terraform Provider. + // + + // ConstellationClusterURIScheme is the scheme used in Terraform Constellation cluster import URIs. + ConstellationClusterURIScheme = "constellation-cluster" + // KubeConfigURIKey is the key used for the KubeConfig in Terraform Constellation cluster import URIs. + KubeConfigURIKey = "kubeConfig" + // ClusterEndpointURIKey is the key used for the cluster endpoint in Terraform Constellation cluster import URIs. + ClusterEndpointURIKey = "clusterEndpoint" + // MasterSecretURIKey is the key used for the master secret in Terraform Constellation cluster import URIs. + MasterSecretURIKey = "masterSecret" + // MasterSecretSaltURIKey is the key used for the master secret salt in Terraform Constellation cluster import URIs. + MasterSecretSaltURIKey = "masterSecretSalt" ) // BinaryVersion returns the version of this Binary. diff --git a/terraform-provider-constellation/docs/resources/cluster.md b/terraform-provider-constellation/docs/resources/cluster.md index f37848e5d..a89602b26 100644 --- a/terraform-provider-constellation/docs/resources/cluster.md +++ b/terraform-provider-constellation/docs/resources/cluster.md @@ -155,3 +155,11 @@ Required: - `project_id` (String) ID of the GCP project the cluster resides in. - `service_account_key` (String) Base64-encoded private key JSON object of the service account used within the cluster. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import constellation_cluster.constellation_cluster constellation-cluster://?kubeConfig=&clusterEndpoint=&masterSecret=&masterSecretSalt= +``` diff --git a/terraform-provider-constellation/examples/resources/constellation_cluster/import.sh b/terraform-provider-constellation/examples/resources/constellation_cluster/import.sh new file mode 100644 index 000000000..1a4f42c49 --- /dev/null +++ b/terraform-provider-constellation/examples/resources/constellation_cluster/import.sh @@ -0,0 +1 @@ +terraform import constellation_cluster.constellation_cluster constellation-cluster://?kubeConfig=&clusterEndpoint=&masterSecret=&masterSecretSalt= diff --git a/terraform-provider-constellation/internal/provider/BUILD.bazel b/terraform-provider-constellation/internal/provider/BUILD.bazel index a73f918fa..ae4e401d5 100644 --- a/terraform-provider-constellation/internal/provider/BUILD.bazel +++ b/terraform-provider-constellation/internal/provider/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "//internal/cloud/azureshared", "//internal/cloud/cloudprovider", "//internal/config", + "//internal/constants", "//internal/constellation", "//internal/constellation/helm", "//internal/constellation/state", @@ -53,33 +54,14 @@ go_test( name = "provider_test", srcs = [ "attestation_data_source_test.go", + "cluster_resource_test.go", "convert_test.go", "image_data_source_test.go", "provider_test.go", ], - embed = [":provider"], - deps = [ - "//internal/attestation/idkeydigest", - "//internal/attestation/measurements", - "//internal/attestation/variant", - "//internal/config", - "@com_github_hashicorp_terraform_plugin_framework//providerserver", - "@com_github_hashicorp_terraform_plugin_go//tfprotov6", - "@com_github_hashicorp_terraform_plugin_testing//helper/resource", - "@com_github_stretchr_testify//assert", - "@com_github_stretchr_testify//require", - "@io_bazel_rules_go//go/runfiles:go_default_library", - ], -) - -go_test( - name = "provider_acc_test", - srcs = [ - "image_data_source_test.go", - "provider_test.go", - ], # keep count = 1, + # keep data = [ "//bazel/ci:com_github_hashicorp_terraform", ], @@ -98,9 +80,16 @@ go_test( # keep x_defs = {"runsUnder": "bazel"}, deps = [ + "//internal/attestation/idkeydigest", + "//internal/attestation/measurements", + "//internal/attestation/variant", + "//internal/config", "@com_github_hashicorp_terraform_plugin_framework//providerserver", "@com_github_hashicorp_terraform_plugin_go//tfprotov6", "@com_github_hashicorp_terraform_plugin_testing//helper/resource", + "@com_github_hashicorp_terraform_plugin_testing//terraform", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", "@io_bazel_rules_go//go/runfiles:go_default_library", ], ) diff --git a/terraform-provider-constellation/internal/provider/cluster_resource.go b/terraform-provider-constellation/internal/provider/cluster_resource.go index f8e811d84..7b4b38214 100644 --- a/terraform-provider-constellation/internal/provider/cluster_resource.go +++ b/terraform-provider-constellation/internal/provider/cluster_resource.go @@ -16,6 +16,7 @@ import ( "fmt" "io" "net" + "net/url" "time" "github.com/edgelesssys/constellation/v2/internal/atls" @@ -24,6 +25,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/cloud/azureshared" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constellation" "github.com/edgelesssys/constellation/v2/internal/constellation/helm" "github.com/edgelesssys/constellation/v2/internal/constellation/state" @@ -400,10 +402,80 @@ func (r *ClusterResource) Delete(ctx context.Context, req resource.DeleteRequest } // ImportState imports to the resource. -func (r *ClusterResource) ImportState(_ context.Context, _ resource.ImportStateRequest, _ *resource.ImportStateResponse) { - // TODO: Implement +func (r *ClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + expectedSchemaMsg := fmt.Sprintf( + "Expected URI of schema '%s://?%s=<...>&%s=<...>&%s=<...>&%s=<...>'", + constants.ConstellationClusterURIScheme, constants.KubeConfigURIKey, constants.ClusterEndpointURIKey, + constants.MasterSecretURIKey, constants.MasterSecretSaltURIKey) - // Take Kubeconfig, Cluster Endpoint and Master Secret and save to state + uri, err := url.Parse(req.ID) + if err != nil { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: %s.\n%s", err, expectedSchemaMsg)) + return + } + + if uri.Scheme != constants.ConstellationClusterURIScheme { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Invalid scheme '%s'.\n%s", uri.Scheme, expectedSchemaMsg)) + return + } + + // Parse query parameters + query := uri.Query() + kubeConfig := query.Get(constants.KubeConfigURIKey) + clusterEndpoint := query.Get(constants.ClusterEndpointURIKey) + masterSecret := query.Get(constants.MasterSecretURIKey) + masterSecretSalt := query.Get(constants.MasterSecretSaltURIKey) + + if kubeConfig == "" { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Missing query parameter '%s'.\n%s", constants.KubeConfigURIKey, expectedSchemaMsg)) + return + } + + if clusterEndpoint == "" { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Missing query parameter '%s'.\n%s", constants.ClusterEndpointURIKey, expectedSchemaMsg)) + return + } + + if masterSecret == "" { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Missing query parameter '%s'.\n%s", constants.MasterSecretURIKey, expectedSchemaMsg)) + return + } + + if masterSecretSalt == "" { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Missing query parameter '%s'.\n%s", constants.MasterSecretSaltURIKey, expectedSchemaMsg)) + return + } + + decodedKubeConfig, err := base64.StdEncoding.DecodeString(kubeConfig) + if err != nil { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Decoding base64-encoded kubeconfig: %s.", err)) + return + } + + // Sanity checks for master secret and master secret salt + if _, err := hex.DecodeString(masterSecret); err != nil { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Decoding hex-encoded master secret: %s.", err)) + return + } + + if _, err := hex.DecodeString(masterSecretSalt); err != nil { + resp.Diagnostics.AddError("Parsing cluster URI", + fmt.Sprintf("Parsing cluster URI: Decoding hex-encoded master secret salt: %s.", err)) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("kubeconfig"), string(decodedKubeConfig))...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("out_of_cluster_endpoint"), clusterEndpoint)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("master_secret"), masterSecret)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("master_secret_salt"), masterSecretSalt)...) } // apply applies changes to a cluster. It can be used for both creating and updating a cluster. diff --git a/terraform-provider-constellation/internal/provider/cluster_resource_test.go b/terraform-provider-constellation/internal/provider/cluster_resource_test.go new file mode 100644 index 000000000..08032e137 --- /dev/null +++ b/terraform-provider-constellation/internal/provider/cluster_resource_test.go @@ -0,0 +1,109 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stretchr/testify/assert" +) + +func TestAccClusteResourceImports(t *testing.T) { + // Set the path to the Terraform binary for acceptance testing when running under Bazel. + bazelPreCheck := func() { bazelSetTerraformBinaryPath(t) } + + testCases := map[string]resource.TestCase{ + "import success": { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: bazelPreCheck, + Steps: []resource.TestStep{ + { + Config: testingConfig + ` + resource "constellation_cluster" "test" {} + `, + ResourceName: "constellation_cluster.test", + ImportState: true, + ImportStateId: "constellation-cluster://?kubeConfig=YWJjZGU=&" + // valid base64 of "abcde" + "clusterEndpoint=b&" + + "masterSecret=de&" + + "masterSecretSalt=ad", + ImportStateCheck: func(states []*terraform.InstanceState) error { + state := states[0] + assert := assert.New(t) + assert.Equal("abcde", state.Attributes["kubeconfig"]) + assert.Equal("b", state.Attributes["out_of_cluster_endpoint"]) + assert.Equal("de", state.Attributes["master_secret"]) + assert.Equal("ad", state.Attributes["master_secret_salt"]) + return nil + }, + }, + }, + }, + "kubeconfig not base64": { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: bazelPreCheck, + Steps: []resource.TestStep{ + { + Config: testingConfig + ` + resource "constellation_cluster" "test" {} + `, + ResourceName: "constellation_cluster.test", + ImportState: true, + ImportStateId: "constellation-cluster://?kubeConfig=a&" + + "clusterEndpoint=b&" + + "masterSecret=de&" + + "masterSecretSalt=ad", + ExpectError: regexp.MustCompile(".*illegal base64 data.*"), + }, + }, + }, + "mastersecret not hex": { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: bazelPreCheck, + Steps: []resource.TestStep{ + { + Config: testingConfig + ` + resource "constellation_cluster" "test" {} + `, + ResourceName: "constellation_cluster.test", + ImportState: true, + ImportStateId: "constellation-cluster://?kubeConfig=test&" + + "clusterEndpoint=b&" + + "masterSecret=xx&" + + "masterSecretSalt=ad", + ExpectError: regexp.MustCompile(".*Decoding hex-encoded master secret.*"), + }, + }, + }, + "parameter missing": { + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + PreCheck: bazelPreCheck, + Steps: []resource.TestStep{ + { + Config: testingConfig + ` + resource "constellation_cluster" "test" {} + `, + ResourceName: "constellation_cluster.test", + ImportState: true, + ImportStateId: "constellation-cluster://?kubeConfig=test&" + + "clusterEndpoint=b&" + + "masterSecret=xx&", + ExpectError: regexp.MustCompile(".*Missing query parameter.*"), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + resource.Test(t, tc) + }) + } +}