diff --git a/internal/config/config.go b/internal/config/config.go index 2025bbbf9..b792987ff 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -846,7 +846,7 @@ func (c *Config) Validate(force bool) error { // Because of this we can't print the offending field name in the error message, resulting in // suboptimal UX. Adding the field name to the struct validation of Semver would make it // impossible to use Semver for other fields. - if err := validateMicroserviceVersion(constants.BinaryVersion(), c.MicroserviceVersion); err != nil { + if err := ValidateMicroserviceVersion(constants.BinaryVersion(), c.MicroserviceVersion); err != nil { msg := "microserviceVersion: " + msgFromCompatibilityError(err, constants.BinaryVersion().String(), c.MicroserviceVersion.String()) return &ValidationError{validationErrMsgs: []string{msg}} } diff --git a/internal/config/validation.go b/internal/config/validation.go index 35a55727f..5437ec5d9 100644 --- a/internal/config/validation.go +++ b/internal/config/validation.go @@ -690,7 +690,8 @@ func msgFromCompatibilityError(err error, binaryVersion, fieldValue string) stri } } -func validateMicroserviceVersion(binaryVersion, version consemver.Semver) error { +// ValidateMicroserviceVersion checks that the version of the microservice is compatible with the binary version. +func ValidateMicroserviceVersion(binaryVersion, version consemver.Semver) error { // Major versions always have to match. if binaryVersion.Major() != version.Major() { return compatibility.ErrMajorMismatch diff --git a/internal/config/validation_test.go b/internal/config/validation_test.go index f6a5bde95..0a996580e 100644 --- a/internal/config/validation_test.go +++ b/internal/config/validation_test.go @@ -75,7 +75,7 @@ func TestValidateMicroserviceVersion(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - err := validateMicroserviceVersion(tc.cli, tc.services) + err := ValidateMicroserviceVersion(tc.cli, tc.services) if tc.wantError { assert.Error(err) return diff --git a/terraform-provider-constellation/internal/provider/BUILD.bazel b/terraform-provider-constellation/internal/provider/BUILD.bazel index 54063d3f5..b6e9f732d 100644 --- a/terraform-provider-constellation/internal/provider/BUILD.bazel +++ b/terraform-provider-constellation/internal/provider/BUILD.bazel @@ -93,7 +93,11 @@ go_test( "//internal/attestation/variant", "//internal/config", "//internal/constants", + "//internal/semver", + "//terraform-provider-constellation/internal/data", + "@com_github_hashicorp_terraform_plugin_framework//attr", "@com_github_hashicorp_terraform_plugin_framework//providerserver", + "@com_github_hashicorp_terraform_plugin_framework//types/basetypes", "@com_github_hashicorp_terraform_plugin_go//tfprotov6", "@com_github_hashicorp_terraform_plugin_testing//helper/resource", "@com_github_hashicorp_terraform_plugin_testing//terraform", diff --git a/terraform-provider-constellation/internal/provider/cluster_resource.go b/terraform-provider-constellation/internal/provider/cluster_resource.go index 6e7c41bc4..6798b67b3 100644 --- a/terraform-provider-constellation/internal/provider/cluster_resource.go +++ b/terraform-provider-constellation/internal/provider/cluster_resource.go @@ -722,20 +722,11 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel, } // parse OS image version - var image imageAttribute - convertDiags = data.Image.As(ctx, &image, basetypes.ObjectAsOptions{}) + image, imageSemver, convertDiags := r.getImageVersion(ctx, data) diags.Append(convertDiags...) if diags.HasError() { return diags } - imageSemver, err := semver.New(image.Version) - if err != nil { - diags.AddAttributeError( - path.Root("image").AtName("version"), - "Invalid image version", - fmt.Sprintf("Parsing image version (%s): %s", image.Version, err)) - return diags - } // parse license ID licenseID := data.LicenseID.ValueString() @@ -948,6 +939,29 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel, return diags } +func (r *ClusterResource) getImageVersion(ctx context.Context, data *ClusterResourceModel) (imageAttribute, semver.Semver, diag.Diagnostics) { + var image imageAttribute + diags := data.Image.As(ctx, &image, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return imageAttribute{}, semver.Semver{}, diags + } + imageSemver, err := semver.New(image.Version) + if err != nil { + diags.AddAttributeError( + path.Root("image").AtName("version"), + "Invalid image version", + fmt.Sprintf("Parsing image version (%s): %s", image.Version, err)) + return imageAttribute{}, semver.Semver{}, diags + } + if err := compatibility.BinaryWith(r.providerData.Version.String(), imageSemver.String()); err != nil { + diags.AddAttributeError( + path.Root("image").AtName("version"), + "Invalid image version", + fmt.Sprintf("Image version (%s) incompatible with provider version (%s): %s", image.Version, r.providerData.Version.String(), err)) + } + return image, imageSemver, diags +} + // initRPCPayload groups the data required to run the init RPC. type initRPCPayload struct { csp cloudprovider.Provider // cloud service provider the cluster runs on. @@ -1178,6 +1192,12 @@ func (r *ClusterResource) getMicroserviceVersion(ctx context.Context, data *Clus tflog.Info(ctx, fmt.Sprintf("No Microservice version specified. Using default version %s.", r.providerData.Version)) ver = r.providerData.Version } + if err := config.ValidateMicroserviceVersion(r.providerData.Version, ver); err != nil { + diags.AddAttributeError( + path.Root("constellation_microservice_version"), + "Invalid microservice version", + fmt.Sprintf("Microservice version (%s) incompatible with provider version (%s): %s", ver, r.providerData.Version, err)) + } return ver, diags } diff --git a/terraform-provider-constellation/internal/provider/cluster_resource_test.go b/terraform-provider-constellation/internal/provider/cluster_resource_test.go index 20b7e2026..2c2a4ae97 100644 --- a/terraform-provider-constellation/internal/provider/cluster_resource_test.go +++ b/terraform-provider-constellation/internal/provider/cluster_resource_test.go @@ -7,14 +7,104 @@ SPDX-License-Identifier: AGPL-3.0-only package provider import ( + "context" "regexp" "testing" + "github.com/edgelesssys/constellation/v2/internal/semver" + "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +func TestMicroserviceConstraint(t *testing.T) { + sut := &ClusterResource{ + providerData: data.ProviderData{ + Version: semver.NewFromInt(2, 15, 0, ""), + }, + } + testCases := []struct { + name string + version string + expectedErrorCount int + }{ + { + name: "outdated by 2 minor versions is invalid", + version: "v2.13.0", + expectedErrorCount: 1, + }, + { + name: "outdated by 1 minor is allowed for upgrade", + version: "v2.14.0", + expectedErrorCount: 0, + }, + { + name: "same version is valid", + version: "v2.15.0", + expectedErrorCount: 0, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, diags := sut.getMicroserviceVersion(context.Background(), &ClusterResourceModel{ + MicroserviceVersion: basetypes.NewStringValue(tc.version), + }) + require.Equal(t, tc.expectedErrorCount, diags.ErrorsCount()) + }) + } +} + +func TestViolatedImageConstraint(t *testing.T) { + sut := &ClusterResource{ + providerData: data.ProviderData{ + Version: semver.NewFromInt(2, 15, 0, ""), + }, + } + testCases := []struct { + name string + version string + expectedErrorCount int + }{ + { + name: "outdated by 2 minor versions is invalid", + version: "v2.13.0", + expectedErrorCount: 1, + }, + { + name: "outdated by 1 minor is allowed for upgrade", + version: "v2.14.0", + expectedErrorCount: 0, + }, + { + name: "same version is valid", + version: "v2.15.0", + expectedErrorCount: 0, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + img := imageAttribute{ + Version: tc.version, + } + + input, diags := basetypes.NewObjectValueFrom(context.Background(), map[string]attr.Type{ + "version": basetypes.StringType{}, + "reference": basetypes.StringType{}, + "short_path": basetypes.StringType{}, + }, img) + require.Equal(t, 0, diags.ErrorsCount()) + _, _, diags2 := sut.getImageVersion(context.Background(), &ClusterResourceModel{ + Image: input, + }) + require.Equal(t, tc.expectedErrorCount, diags2.ErrorsCount()) + }) + } +} + func TestAccClusterResourceImports(t *testing.T) { // Set the path to the Terraform binary for acceptance testing when running under Bazel. bazelPreCheck := func() { bazelSetTerraformBinaryPath(t) }