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
- `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))
- `init_secret` (String) Secret used for initialization of 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"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ datasource.DataSource = &AttestationDataSource{}
var (
// Ensure provider defined types fully satisfy framework interfaces.
_ datasource.DataSource = &AttestationDataSource{}
_ datasource.DataSourceWithValidateConfig = &AttestationDataSource{}
_ datasource.DataSourceWithConfigure = &AttestationDataSource{}
)
// NewAttestationDataSource creates a new attestation data source.
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.
func (d *AttestationDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
var data AttestationDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

View File

@ -17,6 +17,8 @@ import (
"io"
"net"
"net/url"
"regexp"
"strings"
"time"
"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/versions"
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/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"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/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure provider defined types fully satisfy framework interfaces.
var (
_ resource.Resource = &ClusterResource{}
_ resource.ResourceWithImportState = &ClusterResource{}
_ resource.ResourceWithModifyPlan = &ClusterResource{}
// Ensure provider defined types fully satisfy framework interfaces.
_ resource.Resource = &ClusterResource{}
_ resource.ResourceWithImportState = &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.
@ -140,11 +149,7 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Description: "Name used in the cluster's named resources / cluster name.",
Required: true, // TODO: Make optional and default to Constell.
},
"csp": schema.StringAttribute{
MarkdownDescription: "The Cloud Service Provider (CSP) the cluster should run on.",
Description: "The Cloud Service Provider (CSP) the cluster should run on.",
Required: true,
},
"csp": newCSPAttributeSchema(),
"uid": schema.StringAttribute{
MarkdownDescription: "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.",
Description: "CIDR range of the cluster's node network.",
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(cidrRegex, "Node IP CIDR must be a valid CIDR range."),
},
},
"ip_cidr_pod": schema.StringAttribute{
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.",
Optional: true,
Validators: []validator.String{
stringvalidator.RegexMatches(cidrRegex, "Pod IP CIDR must be a valid CIDR range."),
},
},
"ip_cidr_service": schema.StringAttribute{
MarkdownDescription: "CIDR range of the cluster's service network.",
Description: "CIDR range of the cluster's service network.",
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.",
Description: "Hex-encoded 32-byte master secret for the cluster.",
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{
MarkdownDescription: "Hex-encoded 32-byte master secret salt for the cluster.",
Description: "Hex-encoded 32-byte master secret salt for the cluster.",
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{
MarkdownDescription: "Hex-encoded 32-byte measurement salt for the cluster.",
Description: "Hex-encoded 32-byte measurement salt for the cluster.",
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{
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.",
Description: "Base64-encoded private key JSON object of the service account used within the cluster.",
Required: true,
Validators: []validator.String{
stringvalidator.RegexMatches(base64Regex, "Service account key must be a base64-encoded JSON object."),
},
},
"project_id": schema.StringAttribute{
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.
func (r *ClusterResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// 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
var networkCfg networkConfigAttribute
convertDiags = data.NetworkConfig.As(ctx, &networkCfg, basetypes.ObjectAsOptions{
UnhandledNullAsEmpty: true, // we want to allow null values, as some of the field's subfields are optional.
})
diags.Append(convertDiags...)
networkCfg, getDiags := r.getNetworkConfig(ctx, data)
diags.Append(getDiags...)
if diags.HasError() {
return diags
}
@ -1050,6 +1137,15 @@ func (r *ClusterResource) getMicroserviceVersion(ctx context.Context, data *Clus
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
// Constellation's logger.
type tfContextLogger struct {

View File

@ -15,7 +15,7 @@ import (
"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.
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 (
"context"
"errors"
"fmt"
"regexp"
"strings"
@ -16,6 +17,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"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/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
@ -26,10 +28,12 @@ import (
var (
// Ensure provider defined types fully satisfy framework interfaces.
_ datasource.DataSource = &ImageDataSource{}
caseInsensitiveCommunityGalleriesRegexp = regexp.MustCompile(`(?i)\/communitygalleries\/`)
caseInsensitiveImagesRegExp = regexp.MustCompile(`(?i)\/images\/`)
caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`)
_ datasource.DataSource = &ImageDataSource{}
_ datasource.DataSourceWithValidateConfig = &ImageDataSource{}
_ datasource.DataSourceWithConfigure = &ImageDataSource{}
caseInsensitiveCommunityGalleriesRegexp = regexp.MustCompile(`(?i)\/communitygalleries\/`)
caseInsensitiveImagesRegExp = regexp.MustCompile(`(?i)\/images\/`)
caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`)
)
// NewImageDataSource creates a new data source for fetching Constellation OS images
@ -103,19 +107,48 @@ func (d *ImageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
// ValidateConfig validates the configuration for the image data source.
func (d *ImageDataSource) ValidateConfig(ctx context.Context, req datasource.ValidateConfigRequest, resp *datasource.ValidateConfigResponse) {
var data ImageDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Region must be set for AWS
if data.CSP.Equal(types.StringValue("aws")) && data.Region.IsNull() {
resp.Diagnostics.AddAttributeError(
path.Root("region"),
"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,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
// Read testing
{
Config: testingConfig + `
data "constellation_image" "test" {
@ -61,7 +60,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
// Read testing
{
Config: testingConfig + `
data "constellation_image" "test" {
@ -78,7 +76,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
// Read testing
{
Config: testingConfig + `
data "constellation_image" "test" {
@ -96,7 +93,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
// Read testing
{
Config: testingConfig + `
data "constellation_image" "test" {
@ -115,7 +111,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
// Read testing
{
Config: testingConfig + `
data "constellation_image" "test" {
@ -132,7 +127,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
// Read testing
{
Config: testingConfig + `
data "constellation_image" "test" {
@ -149,7 +143,6 @@ func TestAccImageDataSource(t *testing.T) {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
// Read testing
{
Config: testingConfig + `
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 {