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 <stobbe.adrian@gmail.com>

* ci: fix Terraform lock exclude directories

---------

Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>
This commit is contained in:
Moritz Sanft 2023-12-12 16:00:03 +01:00 committed by GitHub
parent d08e75bf9c
commit 367136add2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 222 additions and 28 deletions

View File

@ -28,6 +28,7 @@ excludeDirs=(
"internal/constellation/helm/charts/cilium"
"build"
"docs/node_modules"
"terraform-provider-constellation/examples"
)
excludeFiles=(

View File

@ -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:"

View File

@ -46,9 +46,7 @@ excludeDirs=(
excludeLockDirs=(
"build"
"terraform-provider-constellation"
"terraform/aws-constellation"
"terraform/azure-constellation"
"terraform/gcp-constellation"
"terraform/legacy-module"
)
excludeCheckDirs=(

View File

@ -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.

View File

@ -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.

View File

@ -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=<base64-encoded-kubeconfig>&clusterEndpoint=<cluster-endpoint>&masterSecret=<hex-encoded-mastersecret>&masterSecretSalt=<hex-encoded-mastersecret-salt>
```

View File

@ -0,0 +1 @@
terraform import constellation_cluster.constellation_cluster constellation-cluster://?kubeConfig=<base64-encoded-kubeconfig>&clusterEndpoint=<cluster-endpoint>&masterSecret=<hex-encoded-mastersecret>&masterSecretSalt=<hex-encoded-mastersecret-salt>

View File

@ -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",
],
)

View File

@ -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.

View File

@ -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)
})
}
}