mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-06-21 12:44:20 -04:00
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:
parent
d08e75bf9c
commit
367136add2
10 changed files with 222 additions and 28 deletions
|
@ -28,6 +28,7 @@ excludeDirs=(
|
||||||
"internal/constellation/helm/charts/cilium"
|
"internal/constellation/helm/charts/cilium"
|
||||||
"build"
|
"build"
|
||||||
"docs/node_modules"
|
"docs/node_modules"
|
||||||
|
"terraform-provider-constellation/examples"
|
||||||
)
|
)
|
||||||
|
|
||||||
excludeFiles=(
|
excludeFiles=(
|
||||||
|
|
|
@ -26,6 +26,7 @@ excludeDirs=(
|
||||||
"internal/constellation/helm/charts/cilium"
|
"internal/constellation/helm/charts/cilium"
|
||||||
"build"
|
"build"
|
||||||
"docs/node_modules"
|
"docs/node_modules"
|
||||||
|
"terraform-provider-constellation/examples"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "The following scripts are excluded and won't be formatted with shfmt:"
|
echo "The following scripts are excluded and won't be formatted with shfmt:"
|
||||||
|
|
|
@ -46,9 +46,7 @@ excludeDirs=(
|
||||||
excludeLockDirs=(
|
excludeLockDirs=(
|
||||||
"build"
|
"build"
|
||||||
"terraform-provider-constellation"
|
"terraform-provider-constellation"
|
||||||
"terraform/aws-constellation"
|
"terraform/legacy-module"
|
||||||
"terraform/azure-constellation"
|
|
||||||
"terraform/gcp-constellation"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
excludeCheckDirs=(
|
excludeCheckDirs=(
|
||||||
|
|
|
@ -43,7 +43,7 @@ TF_CLI_CONFIG_FILE=config.tfrc terraform apply
|
||||||
Terraform acceptance tests can be run hermetically through Bazel (recommended):
|
Terraform acceptance tests can be run hermetically through Bazel (recommended):
|
||||||
|
|
||||||
```bash
|
```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.
|
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.
|
||||||
|
|
|
@ -268,6 +268,21 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELcPl4Ik+qZuH4K049wksoXK/Os3Z
|
||||||
b92PDCpM7FZAINQF88s1TZS/HmRXYk62UJ4eqPduvUnJmXhNikhLbMi6fw==
|
b92PDCpM7FZAINQF88s1TZS/HmRXYk62UJ4eqPduvUnJmXhNikhLbMi6fw==
|
||||||
-----END PUBLIC KEY-----
|
-----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.
|
// BinaryVersion returns the version of this Binary.
|
||||||
|
|
|
@ -155,3 +155,11 @@ Required:
|
||||||
|
|
||||||
- `project_id` (String) ID of the GCP project the cluster resides in.
|
- `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.
|
- `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>
|
||||||
|
```
|
||||||
|
|
|
@ -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>
|
|
@ -23,6 +23,7 @@ go_library(
|
||||||
"//internal/cloud/azureshared",
|
"//internal/cloud/azureshared",
|
||||||
"//internal/cloud/cloudprovider",
|
"//internal/cloud/cloudprovider",
|
||||||
"//internal/config",
|
"//internal/config",
|
||||||
|
"//internal/constants",
|
||||||
"//internal/constellation",
|
"//internal/constellation",
|
||||||
"//internal/constellation/helm",
|
"//internal/constellation/helm",
|
||||||
"//internal/constellation/state",
|
"//internal/constellation/state",
|
||||||
|
@ -53,33 +54,14 @@ go_test(
|
||||||
name = "provider_test",
|
name = "provider_test",
|
||||||
srcs = [
|
srcs = [
|
||||||
"attestation_data_source_test.go",
|
"attestation_data_source_test.go",
|
||||||
|
"cluster_resource_test.go",
|
||||||
"convert_test.go",
|
"convert_test.go",
|
||||||
"image_data_source_test.go",
|
"image_data_source_test.go",
|
||||||
"provider_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
|
# keep
|
||||||
count = 1,
|
count = 1,
|
||||||
|
# keep
|
||||||
data = [
|
data = [
|
||||||
"//bazel/ci:com_github_hashicorp_terraform",
|
"//bazel/ci:com_github_hashicorp_terraform",
|
||||||
],
|
],
|
||||||
|
@ -98,9 +80,16 @@ go_test(
|
||||||
# keep
|
# keep
|
||||||
x_defs = {"runsUnder": "bazel"},
|
x_defs = {"runsUnder": "bazel"},
|
||||||
deps = [
|
deps = [
|
||||||
|
"//internal/attestation/idkeydigest",
|
||||||
|
"//internal/attestation/measurements",
|
||||||
|
"//internal/attestation/variant",
|
||||||
|
"//internal/config",
|
||||||
"@com_github_hashicorp_terraform_plugin_framework//providerserver",
|
"@com_github_hashicorp_terraform_plugin_framework//providerserver",
|
||||||
"@com_github_hashicorp_terraform_plugin_go//tfprotov6",
|
"@com_github_hashicorp_terraform_plugin_go//tfprotov6",
|
||||||
"@com_github_hashicorp_terraform_plugin_testing//helper/resource",
|
"@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",
|
"@io_bazel_rules_go//go/runfiles:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/atls"
|
"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/azureshared"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
"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"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constellation/helm"
|
"github.com/edgelesssys/constellation/v2/internal/constellation/helm"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
|
"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.
|
// ImportState imports to the resource.
|
||||||
func (r *ClusterResource) ImportState(_ context.Context, _ resource.ImportStateRequest, _ *resource.ImportStateResponse) {
|
func (r *ClusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||||
// TODO: Implement
|
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.
|
// apply applies changes to a cluster. It can be used for both creating and updating a cluster.
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue