terraform-provider: add input validation (#2744)

* terraform-provider: add validation for `constellation_image`

* terraform-provider: add validation for `constellation_cluster`

* image: accept short path versions

* terraform-provider: correct error statement

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>

* terraform-provider: remove superfluous log statements

* terraform-provider: fix error assertion casing

* terraform-provider: remove superfluous semver check

* Update terraform-provider-constellation/internal/provider/shared_attributes.go

Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>

---------

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
Co-authored-by: Adrian Stobbe <stobbe.adrian@gmail.com>
This commit is contained in:
Moritz Sanft 2023-12-20 15:56:48 +01:00 committed by GitHub
parent db65f5116d
commit 82e2875927
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 472 additions and 34 deletions

View File

@ -63,7 +63,8 @@ resource "constellation_cluster" "azure_example" {
### Required ### Required
- `attestation` (Attributes) Attestation comprises the measurements and SEV-SNP specific parameters. The output of the [constellation_attestation](../data-sources/attestation.md) data source provides sensible defaults. (see [below for nested schema](#nestedatt--attestation)) - `attestation` (Attributes) Attestation comprises the measurements and SEV-SNP specific parameters. The output of the [constellation_attestation](../data-sources/attestation.md) data source provides sensible defaults. (see [below for nested schema](#nestedatt--attestation))
- `csp` (String) The Cloud Service Provider (CSP) the cluster should run on. - `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`)
See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports.
- `image` (Attributes) Constellation OS Image to use on the nodes. (see [below for nested schema](#nestedatt--image)) - `image` (Attributes) Constellation OS Image to use on the nodes. (see [below for nested schema](#nestedatt--image))
- `init_secret` (String) Secret used for initialization of the cluster. - `init_secret` (String) Secret used for initialization of the cluster.
- `master_secret` (String) Hex-encoded 32-byte master secret for the cluster. - `master_secret` (String) Hex-encoded 32-byte master secret for the cluster.

View File

@ -26,8 +26,12 @@ import (
"github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-log/tflog"
) )
var (
// Ensure provider defined types fully satisfy framework interfaces. // Ensure provider defined types fully satisfy framework interfaces.
var _ datasource.DataSource = &AttestationDataSource{} _ datasource.DataSource = &AttestationDataSource{}
_ datasource.DataSourceWithValidateConfig = &AttestationDataSource{}
_ datasource.DataSourceWithConfigure = &AttestationDataSource{}
)
// NewAttestationDataSource creates a new attestation data source. // NewAttestationDataSource creates a new attestation data source.
func NewAttestationDataSource() datasource.DataSource { func NewAttestationDataSource() datasource.DataSource {
@ -109,9 +113,7 @@ func (d *AttestationDataSource) Schema(_ context.Context, _ datasource.SchemaReq
// ValidateConfig validates the configuration for the image data source. // ValidateConfig validates the configuration for the image data source.
func (d *AttestationDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { func (d *AttestationDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
var data AttestationDataSourceModel var data AttestationDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
} }

View File

@ -17,6 +17,8 @@ import (
"io" "io"
"net" "net"
"net/url" "net/url"
"regexp"
"strings"
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/atls" "github.com/edgelesssys/constellation/v2/internal/atls"
@ -36,22 +38,29 @@ import (
"github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/semver"
"github.com/edgelesssys/constellation/v2/internal/versions" "github.com/edgelesssys/constellation/v2/internal/versions"
datastruct "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data" datastruct "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-log/tflog"
) )
// Ensure provider defined types fully satisfy framework interfaces.
var ( var (
// Ensure provider defined types fully satisfy framework interfaces.
_ resource.Resource = &ClusterResource{} _ resource.Resource = &ClusterResource{}
_ resource.ResourceWithImportState = &ClusterResource{} _ resource.ResourceWithImportState = &ClusterResource{}
_ resource.ResourceWithModifyPlan = &ClusterResource{} _ resource.ResourceWithModifyPlan = &ClusterResource{}
_ resource.ResourceWithValidateConfig = &ClusterResource{}
cidrRegex = regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$`)
hexRegex = regexp.MustCompile(`^[0-9a-fA-F]+$`)
base64Regex = regexp.MustCompile(`^[-A-Za-z0-9+/]*={0,3}$`)
) )
// NewClusterResource creates a new cluster resource. // NewClusterResource creates a new cluster resource.
@ -140,11 +149,7 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Description: "Name used in the cluster's named resources / cluster name.", Description: "Name used in the cluster's named resources / cluster name.",
Required: true, // TODO: Make optional and default to Constell. Required: true, // TODO: Make optional and default to Constell.
}, },
"csp": schema.StringAttribute{ "csp": newCSPAttributeSchema(),
MarkdownDescription: "The Cloud Service Provider (CSP) the cluster should run on.",
Description: "The Cloud Service Provider (CSP) the cluster should run on.",
Required: true,
},
"uid": schema.StringAttribute{ "uid": schema.StringAttribute{
MarkdownDescription: "The UID of the cluster.", MarkdownDescription: "The UID of the cluster.",
Description: "The UID of the cluster.", Description: "The UID of the cluster.",
@ -199,16 +204,25 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
MarkdownDescription: "CIDR range of the cluster's node network.", MarkdownDescription: "CIDR range of the cluster's node network.",
Description: "CIDR range of the cluster's node network.", Description: "CIDR range of the cluster's node network.",
Required: true, Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(cidrRegex, "Node IP CIDR must be a valid CIDR range."),
},
}, },
"ip_cidr_pod": schema.StringAttribute{ "ip_cidr_pod": schema.StringAttribute{
MarkdownDescription: "CIDR range of the cluster's pod network. Only required for clusters running on GCP.", MarkdownDescription: "CIDR range of the cluster's pod network. Only required for clusters running on GCP.",
Description: "CIDR range of the cluster's pod network. Only required for clusters running on GCP.", Description: "CIDR range of the cluster's pod network. Only required for clusters running on GCP.",
Optional: true, Optional: true,
Validators: []validator.String{
stringvalidator.RegexMatches(cidrRegex, "Pod IP CIDR must be a valid CIDR range."),
},
}, },
"ip_cidr_service": schema.StringAttribute{ "ip_cidr_service": schema.StringAttribute{
MarkdownDescription: "CIDR range of the cluster's service network.", MarkdownDescription: "CIDR range of the cluster's service network.",
Description: "CIDR range of the cluster's service network.", Description: "CIDR range of the cluster's service network.",
Required: true, Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(cidrRegex, "Service IP CIDR must be a valid CIDR range."),
},
}, },
}, },
}, },
@ -216,16 +230,28 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
MarkdownDescription: "Hex-encoded 32-byte master secret for the cluster.", MarkdownDescription: "Hex-encoded 32-byte master secret for the cluster.",
Description: "Hex-encoded 32-byte master secret for the cluster.", Description: "Hex-encoded 32-byte master secret for the cluster.",
Required: true, Required: true,
Validators: []validator.String{
stringvalidator.LengthBetween(64, 64),
stringvalidator.RegexMatches(hexRegex, "Master secret must be a hex-encoded 32-byte value."),
},
}, },
"master_secret_salt": schema.StringAttribute{ "master_secret_salt": schema.StringAttribute{
MarkdownDescription: "Hex-encoded 32-byte master secret salt for the cluster.", MarkdownDescription: "Hex-encoded 32-byte master secret salt for the cluster.",
Description: "Hex-encoded 32-byte master secret salt for the cluster.", Description: "Hex-encoded 32-byte master secret salt for the cluster.",
Required: true, Required: true,
Validators: []validator.String{
stringvalidator.LengthBetween(64, 64),
stringvalidator.RegexMatches(hexRegex, "Master secret salt must be a hex-encoded 32-byte value."),
},
}, },
"measurement_salt": schema.StringAttribute{ "measurement_salt": schema.StringAttribute{
MarkdownDescription: "Hex-encoded 32-byte measurement salt for the cluster.", MarkdownDescription: "Hex-encoded 32-byte measurement salt for the cluster.",
Description: "Hex-encoded 32-byte measurement salt for the cluster.", Description: "Hex-encoded 32-byte measurement salt for the cluster.",
Required: true, Required: true,
Validators: []validator.String{
stringvalidator.LengthBetween(64, 64),
stringvalidator.RegexMatches(hexRegex, "Measurement salt must be a hex-encoded 32-byte value."),
},
}, },
"init_secret": schema.StringAttribute{ "init_secret": schema.StringAttribute{
MarkdownDescription: "Secret used for initialization of the cluster.", MarkdownDescription: "Secret used for initialization of the cluster.",
@ -242,6 +268,9 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
MarkdownDescription: "Base64-encoded private key JSON object of the service account used within the cluster.", MarkdownDescription: "Base64-encoded private key JSON object of the service account used within the cluster.",
Description: "Base64-encoded private key JSON object of the service account used within the cluster.", Description: "Base64-encoded private key JSON object of the service account used within the cluster.",
Required: true, Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(base64Regex, "Service account key must be a base64-encoded JSON object."),
},
}, },
"project_id": schema.StringAttribute{ "project_id": schema.StringAttribute{
MarkdownDescription: "ID of the GCP project the cluster resides in.", MarkdownDescription: "ID of the GCP project the cluster resides in.",
@ -331,6 +360,67 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
} }
} }
// ValidateConfig validates the configuration for the resource.
func (r *ClusterResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data ClusterResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Azure Config is required for Azure
if strings.EqualFold(data.CSP.ValueString(), cloudprovider.Azure.String()) && data.Azure.IsNull() {
resp.Diagnostics.AddAttributeError(
path.Root("azure"),
"Azure configuration missing", "When csp is set to 'azure', the 'azure' configuration must be set.",
)
}
// Azure Config should not be set for other CSPs
if !strings.EqualFold(data.CSP.ValueString(), cloudprovider.Azure.String()) && !data.Azure.IsNull() {
resp.Diagnostics.AddAttributeWarning(
path.Root("azure"),
"Azure configuration not allowed", "When csp is not set to 'azure', setting the 'azure' configuration has no effect.",
)
}
// GCP Config is required for GCP
if strings.EqualFold(data.CSP.ValueString(), cloudprovider.GCP.String()) && data.GCP.IsNull() {
resp.Diagnostics.AddAttributeError(
path.Root("gcp"),
"GCP configuration missing", "When csp is set to 'gcp', the 'gcp' configuration must be set.",
)
}
// GCP Config should not be set for other CSPs
if !strings.EqualFold(data.CSP.ValueString(), cloudprovider.GCP.String()) && !data.GCP.IsNull() {
resp.Diagnostics.AddAttributeWarning(
path.Root("gcp"),
"GCP configuration not allowed", "When csp is not set to 'gcp', setting the 'gcp' configuration has no effect.",
)
}
networkCfg, diags := r.getNetworkConfig(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Pod IP CIDR is required for GCP
if strings.EqualFold(data.CSP.ValueString(), cloudprovider.GCP.String()) && networkCfg.IPCidrPod == "" {
resp.Diagnostics.AddAttributeError(
path.Root("network_config").AtName("ip_cidr_pod"),
"Pod IP CIDR missing", "When csp is set to 'gcp', 'ip_cidr_pod' must be set.",
)
}
// Pod IP CIDR should not be set for other CSPs
if !strings.EqualFold(data.CSP.ValueString(), cloudprovider.GCP.String()) && networkCfg.IPCidrPod != "" {
resp.Diagnostics.AddAttributeWarning(
path.Root("network_config").AtName("ip_cidr_pod"),
"Pod IP CIDR not allowed", "When csp is not set to 'gcp', setting 'ip_cidr_pod' has no effect.",
)
}
}
// Configure configures the resource. // Configure configures the resource.
func (r *ClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { func (r *ClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured. // Prevent panic if the provider has not been configured.
@ -578,11 +668,8 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
} }
// parse network config // parse network config
var networkCfg networkConfigAttribute networkCfg, getDiags := r.getNetworkConfig(ctx, data)
convertDiags = data.NetworkConfig.As(ctx, &networkCfg, basetypes.ObjectAsOptions{ diags.Append(getDiags...)
UnhandledNullAsEmpty: true, // we want to allow null values, as some of the field's subfields are optional.
})
diags.Append(convertDiags...)
if diags.HasError() { if diags.HasError() {
return diags return diags
} }
@ -1050,6 +1137,15 @@ func (r *ClusterResource) getMicroserviceVersion(ctx context.Context, data *Clus
return ver, diags return ver, diags
} }
// getNetworkConfig returns the network config from the Terraform state.
func (r *ClusterResource) getNetworkConfig(ctx context.Context, data *ClusterResourceModel) (networkConfigAttribute, diag.Diagnostics) {
var networkCfg networkConfigAttribute
diags := data.NetworkConfig.As(ctx, &networkCfg, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true, // we want to allow null values, as some of the field's subfields are optional.
})
return networkCfg, diags
}
// tfContextLogger is a logging adapter between the tflog package and // tfContextLogger is a logging adapter between the tflog package and
// Constellation's logger. // Constellation's logger.
type tfContextLogger struct { type tfContextLogger struct {

View File

@ -15,7 +15,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestAccClusteResourceImports(t *testing.T) { func TestAccClusterResourceImports(t *testing.T) {
// Set the path to the Terraform binary for acceptance testing when running under Bazel. // Set the path to the Terraform binary for acceptance testing when running under Bazel.
bazelPreCheck := func() { bazelSetTerraformBinaryPath(t) } bazelPreCheck := func() { bazelSetTerraformBinaryPath(t) }
@ -107,3 +107,300 @@ func TestAccClusteResourceImports(t *testing.T) {
}) })
} }
} }
func TestAccClusterResource(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{
"master secret not hex": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "aws") + `
resource "constellation_cluster" "test" {
csp = "aws"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "xxx"
master_secret_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
measurement_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0.0/24"
ip_cidr_service = "0.0.0.0/24"
}
}
`,
ExpectError: regexp.MustCompile(".*Master secret must be a hex-encoded 32-byte.*"),
},
},
},
"master secret salt not hex": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "aws") + `
resource "constellation_cluster" "test" {
csp = "aws"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
master_secret_salt = "xxx"
measurement_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0.0/24"
ip_cidr_service = "0.0.0.0/24"
}
}
`,
ExpectError: regexp.MustCompile(".*Master secret salt must be a hex-encoded 32-byte.*"),
},
},
},
"measurement salt not hex": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "aws") + `
resource "constellation_cluster" "test" {
csp = "aws"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
master_secret_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
measurement_salt = "xxx"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0.0/24"
ip_cidr_service = "0.0.0.0/24"
}
}
`,
ExpectError: regexp.MustCompile(".*Measurement salt must be a hex-encoded 32-byte.*"),
},
},
},
"invalid node ip cidr": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "aws") + `
resource "constellation_cluster" "test" {
csp = "aws"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
master_secret_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
measurement_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0x.0/xxx"
ip_cidr_service = "0.0.0.0/24"
}
}
`,
ExpectError: regexp.MustCompile(".*Node IP CIDR must be a valid CIDR.*"),
},
},
},
"invalid service ip cidr": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "aws") + `
resource "constellation_cluster" "test" {
csp = "aws"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
master_secret_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
measurement_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0.0/24"
ip_cidr_service = "0.0.0x.0/xxx"
}
}
`,
ExpectError: regexp.MustCompile(".*Service IP CIDR must be a valid CIDR.*"),
},
},
},
"azure config missing": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "azure") + `
resource "constellation_cluster" "test" {
csp = "azure"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
master_secret_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
measurement_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0.0/24"
ip_cidr_service = "0.0.0.0/24"
}
}
`,
ExpectError: regexp.MustCompile(".*When csp is set to 'azure', the 'azure' configuration must be set.*"),
},
},
},
"gcp config missing": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "gcp") + `
resource "constellation_cluster" "test" {
csp = "gcp"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
master_secret_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
measurement_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0.0/24"
ip_cidr_service = "0.0.0.0/24"
ip_cidr_pod = "0.0.0.0/24"
}
}
`,
ExpectError: regexp.MustCompile(".*When csp is set to 'gcp', the 'gcp' configuration must be set.*"),
},
},
},
"gcp pod ip cidr missing": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion("v2.13.0"),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "gcp") + `
resource "constellation_cluster" "test" {
csp = "gcp"
name = "constell"
uid = "test"
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
init_secret = "deadbeef"
master_secret = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
master_secret_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
measurement_salt = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
out_of_cluster_endpoint = "192.0.2.1"
in_cluster_endpoint = "192.0.2.1"
network_config = {
ip_cidr_node = "0.0.0.0/24"
ip_cidr_service = "0.0.0.0/24"
}
gcp = {
project_id = "test"
service_account_key = "eyJ0ZXN0IjogInRlc3QifQ=="
}
}
`,
ExpectError: regexp.MustCompile(".*When csp is set to 'gcp', 'ip_cidr_pod' must be set.*"),
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
resource.Test(t, tc)
})
}
}
func fullClusterTestingConfig(t *testing.T, csp string) string {
t.Helper()
providerConfig := `
provider "constellation" {}
`
switch csp {
case "aws":
return providerConfig + `
data "constellation_image" "bar" {
version = "v2.13.0"
attestation_variant = "aws-sev-snp"
csp = "aws"
region = "us-east-2"
}
data "constellation_attestation" "foo" {
csp = "aws"
attestation_variant = "aws-sev-snp"
image = data.constellation_image.bar.image
}`
case "azure":
return providerConfig + `
data "constellation_image" "bar" {
version = "v2.13.0"
attestation_variant = "azure-sev-snp"
csp = "azure"
}
data "constellation_attestation" "foo" {
csp = "azure"
attestation_variant = "azure-sev-snp"
image = data.constellation_image.bar.image
}`
case "gcp":
return providerConfig + `
data "constellation_image" "bar" {
version = "v2.13.0"
attestation_variant = "gcp-sev-es"
csp = "gcp"
}
data "constellation_attestation" "foo" {
csp = "gcp"
attestation_variant = "gcp-sev-es"
image = data.constellation_image.bar.image
}`
default:
t.Fatal("unknown csp")
return ""
}
}

View File

@ -8,6 +8,7 @@ package provider
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@ -16,6 +17,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/imagefetcher" "github.com/edgelesssys/constellation/v2/internal/imagefetcher"
"github.com/edgelesssys/constellation/v2/internal/semver"
"github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data" "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data"
"github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
@ -27,6 +29,8 @@ import (
var ( var (
// Ensure provider defined types fully satisfy framework interfaces. // Ensure provider defined types fully satisfy framework interfaces.
_ datasource.DataSource = &ImageDataSource{} _ datasource.DataSource = &ImageDataSource{}
_ datasource.DataSourceWithValidateConfig = &ImageDataSource{}
_ datasource.DataSourceWithConfigure = &ImageDataSource{}
caseInsensitiveCommunityGalleriesRegexp = regexp.MustCompile(`(?i)\/communitygalleries\/`) caseInsensitiveCommunityGalleriesRegexp = regexp.MustCompile(`(?i)\/communitygalleries\/`)
caseInsensitiveImagesRegExp = regexp.MustCompile(`(?i)\/images\/`) caseInsensitiveImagesRegExp = regexp.MustCompile(`(?i)\/images\/`)
caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`) caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`)
@ -103,19 +107,48 @@ func (d *ImageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
// ValidateConfig validates the configuration for the image data source. // ValidateConfig validates the configuration for the image data source.
func (d *ImageDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) { func (d *ImageDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
var data ImageDataSourceModel var data ImageDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
} }
// Region must be set for AWS
if data.CSP.Equal(types.StringValue("aws")) && data.Region.IsNull() { if data.CSP.Equal(types.StringValue("aws")) && data.Region.IsNull() {
resp.Diagnostics.AddAttributeError( resp.Diagnostics.AddAttributeError(
path.Root("region"), path.Root("region"),
"Region must be set for AWS", "When csp is set to 'aws', 'region' must be specified.", "Region must be set for AWS", "When csp is set to 'aws', 'region' must be specified.",
) )
return }
// Setting Region for non-AWS CSPs has no effect
if !data.CSP.Equal(types.StringValue("aws")) && !data.Region.IsNull() {
resp.Diagnostics.AddAttributeWarning(
path.Root("region"),
"Region should only be set for AWS", "When another CSP than AWS is used, setting 'region' has no effect.",
)
}
// Marketplace image is only supported for Azure
if !data.CSP.Equal(types.StringValue("azure")) && !data.MarketplaceImage.IsNull() {
resp.Diagnostics.AddAttributeWarning(
path.Root("marketplace_image"),
"Marketplace images are currently only supported on Azure", "When another CSP than Azure is used, setting 'marketplace_image' has no effect.",
)
}
// Version should be a valid semver or short path, if set
if !data.Version.IsNull() {
_, semverErr := semver.New(data.Version.ValueString())
_, shortpathErr := versionsapi.NewVersionFromShortPath(data.Version.ValueString(), versionsapi.VersionKindImage)
if semverErr != nil && shortpathErr != nil {
resp.Diagnostics.AddAttributeError(
path.Root("version"),
"Invalid Version",
fmt.Sprintf("When parsing the version (%s), an error occurred: %s", data.Version.ValueString(), errors.Join(semverErr, shortpathErr)),
)
}
} }
} }

View File

@ -42,7 +42,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Read testing
{ {
Config: testingConfig + ` Config: testingConfig + `
data "constellation_image" "test" { data "constellation_image" "test" {
@ -61,7 +60,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Read testing
{ {
Config: testingConfig + ` Config: testingConfig + `
data "constellation_image" "test" { data "constellation_image" "test" {
@ -78,7 +76,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Read testing
{ {
Config: testingConfig + ` Config: testingConfig + `
data "constellation_image" "test" { data "constellation_image" "test" {
@ -96,7 +93,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Read testing
{ {
Config: testingConfig + ` Config: testingConfig + `
data "constellation_image" "test" { data "constellation_image" "test" {
@ -115,7 +111,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Read testing
{ {
Config: testingConfig + ` Config: testingConfig + `
data "constellation_image" "test" { data "constellation_image" "test" {
@ -132,7 +127,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Read testing
{ {
Config: testingConfig + ` Config: testingConfig + `
data "constellation_image" "test" { data "constellation_image" "test" {
@ -149,7 +143,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// Read testing
{ {
Config: testingConfig + ` Config: testingConfig + `
data "constellation_image" "test" { data "constellation_image" "test" {
@ -162,6 +155,22 @@ func TestAccImageDataSource(t *testing.T) {
}, },
}, },
}, },
"invalid version": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: testingConfig + `
data "constellation_image" "test" {
version = "xxx"
attestation_variant = "azure-sev-snp"
csp = "azure"
}
`,
ExpectError: regexp.MustCompile(".*Invalid Version.*"),
},
},
},
} }
for name, tc := range testCases { for name, tc := range testCases {