terraform-provider: Add support for STACKIT / OpenStack

This commit is contained in:
Malte Poll 2024-03-06 20:48:40 +01:00
parent 1670d977c6
commit d69673fab7
24 changed files with 511 additions and 36 deletions

View File

@ -844,7 +844,7 @@ type applier interface {
// methods required to install/upgrade Helm charts // methods required to install/upgrade Helm charts
PrepareHelmCharts( PrepareHelmCharts(
flags helm.Options, state *state.State, serviceAccURI string, masterSecret uri.MasterSecret, openStackCfg *config.OpenStackConfig, flags helm.Options, state *state.State, serviceAccURI string, masterSecret uri.MasterSecret,
) (helm.Applier, bool, error) ) (helm.Applier, bool, error)
// methods to interact with Kubernetes // methods to interact with Kubernetes

View File

@ -554,7 +554,7 @@ func (s *stubConstellApplier) Init(context.Context, atls.Validator, *state.State
type helmApplier interface { type helmApplier interface {
PrepareHelmCharts( PrepareHelmCharts(
flags helm.Options, stateFile *state.State, serviceAccURI string, masterSecret uri.MasterSecret, openStackCfg *config.OpenStackConfig, flags helm.Options, stateFile *state.State, serviceAccURI string, masterSecret uri.MasterSecret,
) ( ) (
helm.Applier, bool, error) helm.Applier, bool, error)
} }

View File

@ -43,6 +43,18 @@ func (a *applyCmd) runHelmApply(cmd *cobra.Command, conf *config.Config, stateFi
ApplyTimeout: a.flags.helmTimeout, ApplyTimeout: a.flags.helmTimeout,
AllowDestructive: helm.DenyDestructive, AllowDestructive: helm.DenyDestructive,
} }
if conf.Provider.OpenStack != nil {
var deployYawolLoadBalancer bool
if conf.Provider.OpenStack.DeployYawolLoadBalancer != nil {
deployYawolLoadBalancer = *conf.Provider.OpenStack.DeployYawolLoadBalancer
}
options.OpenStackValues = &helm.OpenStackValues{
DeployYawolLoadBalancer: deployYawolLoadBalancer,
FloatingIPPoolID: conf.Provider.OpenStack.FloatingIPPoolID,
YawolFlavorID: conf.Provider.OpenStack.YawolFlavorID,
YawolImageID: conf.Provider.OpenStack.YawolImageID,
}
}
a.log.Debug("Getting service account URI") a.log.Debug("Getting service account URI")
serviceAccURI, err := cloudcmd.GetMarshaledServiceAccountURI(conf, a.fileHandler) serviceAccURI, err := cloudcmd.GetMarshaledServiceAccountURI(conf, a.fileHandler)
@ -51,7 +63,7 @@ func (a *applyCmd) runHelmApply(cmd *cobra.Command, conf *config.Config, stateFi
} }
a.log.Debug("Preparing Helm charts") a.log.Debug("Preparing Helm charts")
executor, includesUpgrades, err := a.applier.PrepareHelmCharts(options, stateFile, serviceAccURI, masterSecret, conf.Provider.OpenStack) executor, includesUpgrades, err := a.applier.PrepareHelmCharts(options, stateFile, serviceAccURI, masterSecret)
if errors.Is(err, helm.ErrConfirmationMissing) { if errors.Is(err, helm.ErrConfirmationMissing) {
if !a.flags.yes { if !a.flags.yes {
cmd.PrintErrln("WARNING: Upgrading cert-manager will destroy all custom resources you have manually created that are based on the current version of cert-manager.") cmd.PrintErrln("WARNING: Upgrading cert-manager will destroy all custom resources you have manually created that are based on the current version of cert-manager.")
@ -65,7 +77,7 @@ func (a *applyCmd) runHelmApply(cmd *cobra.Command, conf *config.Config, stateFi
} }
} }
options.AllowDestructive = helm.AllowDestructive options.AllowDestructive = helm.AllowDestructive
executor, includesUpgrades, err = a.applier.PrepareHelmCharts(options, stateFile, serviceAccURI, masterSecret, conf.Provider.OpenStack) executor, includesUpgrades, err = a.applier.PrepareHelmCharts(options, stateFile, serviceAccURI, masterSecret)
} }
var upgradeErr *compatibility.InvalidUpgradeError var upgradeErr *compatibility.InvalidUpgradeError
if err != nil { if err != nil {

View File

@ -279,7 +279,7 @@ type stubHelmApplier struct {
} }
func (s stubHelmApplier) PrepareHelmCharts( func (s stubHelmApplier) PrepareHelmCharts(
_ helm.Options, _ *state.State, _ string, _ uri.MasterSecret, _ *config.OpenStackConfig, _ helm.Options, _ *state.State, _ string, _ uri.MasterSecret,
) (helm.Applier, bool, error) { ) (helm.Applier, bool, error) {
return stubRunner{}, false, s.err return stubRunner{}, false, s.err
} }

View File

@ -376,9 +376,9 @@ type mockApplier struct {
} }
func (m *mockApplier) PrepareHelmCharts( func (m *mockApplier) PrepareHelmCharts(
helmOpts helm.Options, stateFile *state.State, str string, masterSecret uri.MasterSecret, openStackCfg *config.OpenStackConfig, helmOpts helm.Options, stateFile *state.State, str string, masterSecret uri.MasterSecret,
) (helm.Applier, bool, error) { ) (helm.Applier, bool, error) {
args := m.Called(helmOpts, stateFile, helmOpts, str, masterSecret, openStackCfg) args := m.Called(helmOpts, stateFile, helmOpts, str, masterSecret)
return args.Get(0).(helm.Applier), args.Bool(1), args.Error(2) return args.Get(0).(helm.Applier), args.Bool(1), args.Error(2)
} }

View File

@ -9,7 +9,6 @@ package constellation
import ( import (
"errors" "errors"
"github.com/edgelesssys/constellation/v2/internal/config"
"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"
"github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/kms/uri"
@ -17,18 +16,18 @@ import (
// PrepareHelmCharts loads Helm charts for Constellation and returns an executor to apply them. // PrepareHelmCharts loads Helm charts for Constellation and returns an executor to apply them.
func (a *Applier) PrepareHelmCharts( func (a *Applier) PrepareHelmCharts(
flags helm.Options, state *state.State, serviceAccURI string, masterSecret uri.MasterSecret, openStackCfg *config.OpenStackConfig, flags helm.Options, state *state.State, serviceAccURI string, masterSecret uri.MasterSecret,
) (helm.Applier, bool, error) { ) (helm.Applier, bool, error) {
if a.helmClient == nil { if a.helmClient == nil {
return nil, false, errors.New("helm client not initialized") return nil, false, errors.New("helm client not initialized")
} }
return a.helmClient.PrepareApply(flags, state, serviceAccURI, masterSecret, openStackCfg) return a.helmClient.PrepareApply(flags, state, serviceAccURI, masterSecret)
} }
type helmApplier interface { type helmApplier interface {
PrepareApply( PrepareApply(
flags helm.Options, stateFile *state.State, serviceAccURI string, masterSecret uri.MasterSecret, openStackCfg *config.OpenStackConfig, flags helm.Options, stateFile *state.State, serviceAccURI string, masterSecret uri.MasterSecret,
) ( ) (
helm.Applier, bool, error) helm.Applier, bool, error)
} }

View File

@ -467,7 +467,6 @@ go_library(
"//internal/cloud/gcpshared", "//internal/cloud/gcpshared",
"//internal/cloud/openstack", "//internal/cloud/openstack",
"//internal/compatibility", "//internal/compatibility",
"//internal/config",
"//internal/constants", "//internal/constants",
"//internal/constellation/helm/imageversion", "//internal/constellation/helm/imageversion",
"//internal/constellation/state", "//internal/constellation/state",

View File

@ -35,7 +35,6 @@ 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/config"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/constellation/state" "github.com/edgelesssys/constellation/v2/internal/constellation/state"
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/file"
@ -91,13 +90,14 @@ type Options struct {
MicroserviceVersion semver.Semver MicroserviceVersion semver.Semver
HelmWaitMode WaitMode HelmWaitMode WaitMode
ApplyTimeout time.Duration ApplyTimeout time.Duration
OpenStackValues *OpenStackValues
} }
// PrepareApply loads the charts and returns the executor to apply them. // PrepareApply loads the charts and returns the executor to apply them.
func (h Client) PrepareApply( func (h Client) PrepareApply(
flags Options, stateFile *state.State, serviceAccURI string, masterSecret uri.MasterSecret, openStackCfg *config.OpenStackConfig, flags Options, stateFile *state.State, serviceAccURI string, masterSecret uri.MasterSecret,
) (Applier, bool, error) { ) (Applier, bool, error) {
releases, err := h.loadReleases(flags.CSP, flags.AttestationVariant, flags.K8sVersion, masterSecret, stateFile, flags, serviceAccURI, openStackCfg) releases, err := h.loadReleases(flags.CSP, flags.AttestationVariant, flags.K8sVersion, masterSecret, stateFile, flags, serviceAccURI)
if err != nil { if err != nil {
return nil, false, fmt.Errorf("loading Helm releases: %w", err) return nil, false, fmt.Errorf("loading Helm releases: %w", err)
} }
@ -111,11 +111,11 @@ func (h Client) PrepareApply(
func (h Client) loadReleases( func (h Client) loadReleases(
csp cloudprovider.Provider, attestationVariant variant.Variant, k8sVersion versions.ValidK8sVersion, secret uri.MasterSecret, csp cloudprovider.Provider, attestationVariant variant.Variant, k8sVersion versions.ValidK8sVersion, secret uri.MasterSecret,
stateFile *state.State, flags Options, serviceAccURI string, openStackCfg *config.OpenStackConfig, stateFile *state.State, flags Options, serviceAccURI string,
) ([]release, error) { ) ([]release, error) {
helmLoader := newLoader(csp, attestationVariant, k8sVersion, stateFile, h.cliVersion) helmLoader := newLoader(csp, attestationVariant, k8sVersion, stateFile, h.cliVersion)
h.log.Debug("Created new Helm loader") h.log.Debug("Created new Helm loader")
return helmLoader.loadReleases(flags.Conformance, flags.DeployCSIDriver, flags.HelmWaitMode, secret, serviceAccURI, openStackCfg) return helmLoader.loadReleases(flags.Conformance, flags.DeployCSIDriver, flags.HelmWaitMode, secret, serviceAccURI, flags.OpenStackValues)
} }
// Applier runs the Helm actions. // Applier runs the Helm actions.

View File

@ -217,7 +217,7 @@ func TestHelmApply(t *testing.T) {
SetInfrastructure(state.Infrastructure{UID: "testuid"}). SetInfrastructure(state.Infrastructure{UID: "testuid"}).
SetClusterValues(state.ClusterValues{MeasurementSalt: []byte{0x41}}), SetClusterValues(state.ClusterValues{MeasurementSalt: []byte{0x41}}),
fakeServiceAccURI(csp), fakeServiceAccURI(csp),
uri.MasterSecret{Key: []byte("secret"), Salt: []byte("masterSalt")}, nil) uri.MasterSecret{Key: []byte("secret"), Salt: []byte("masterSalt")})
var upgradeErr *compatibility.InvalidUpgradeError var upgradeErr *compatibility.InvalidUpgradeError
if tc.expectError { if tc.expectError {
assert.Error(t, err) assert.Error(t, err)

View File

@ -21,7 +21,6 @@ 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/config"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/constellation/helm/imageversion" "github.com/edgelesssys/constellation/v2/internal/constellation/helm/imageversion"
"github.com/edgelesssys/constellation/v2/internal/constellation/state" "github.com/edgelesssys/constellation/v2/internal/constellation/state"
@ -115,9 +114,17 @@ func newLoader(csp cloudprovider.Provider, attestationVariant variant.Variant, k
// that the new release is installed after the existing one to avoid name conflicts. // that the new release is installed after the existing one to avoid name conflicts.
type releaseApplyOrder []release type releaseApplyOrder []release
// OpenStackValues are helm values for OpenStack.
type OpenStackValues struct {
DeployYawolLoadBalancer bool
FloatingIPPoolID string
YawolFlavorID string
YawolImageID string
}
// loadReleases loads the embedded helm charts and returns them as a HelmReleases object. // loadReleases loads the embedded helm charts and returns them as a HelmReleases object.
func (i *chartLoader) loadReleases(conformanceMode, deployCSIDriver bool, helmWaitMode WaitMode, masterSecret uri.MasterSecret, func (i *chartLoader) loadReleases(conformanceMode, deployCSIDriver bool, helmWaitMode WaitMode, masterSecret uri.MasterSecret,
serviceAccURI string, openStackCfg *config.OpenStackConfig, serviceAccURI string, openStackValues *OpenStackValues,
) (releaseApplyOrder, error) { ) (releaseApplyOrder, error) {
ciliumRelease, err := i.loadRelease(ciliumInfo, helmWaitMode) ciliumRelease, err := i.loadRelease(ciliumInfo, helmWaitMode)
if err != nil { if err != nil {
@ -143,7 +150,7 @@ func (i *chartLoader) loadReleases(conformanceMode, deployCSIDriver bool, helmWa
} }
svcVals, err := extraConstellationServicesValues(i.csp, i.attestationVariant, masterSecret, svcVals, err := extraConstellationServicesValues(i.csp, i.attestationVariant, masterSecret,
serviceAccURI, i.stateFile.Infrastructure, openStackCfg) serviceAccURI, i.stateFile.Infrastructure, openStackValues)
if err != nil { if err != nil {
return nil, fmt.Errorf("extending constellation-services values: %w", err) return nil, fmt.Errorf("extending constellation-services values: %w", err)
} }
@ -169,13 +176,13 @@ func (i *chartLoader) loadReleases(conformanceMode, deployCSIDriver bool, helmWa
} }
releases = append(releases, awsRelease) releases = append(releases, awsRelease)
} }
if i.csp == cloudprovider.OpenStack && openStackCfg.DeployYawolLoadBalancer != nil && *openStackCfg.DeployYawolLoadBalancer { if i.csp == cloudprovider.OpenStack && openStackValues != nil && openStackValues.DeployYawolLoadBalancer {
yawolRelease, err := i.loadRelease(yawolLBControllerInfo, WaitModeNone) yawolRelease, err := i.loadRelease(yawolLBControllerInfo, WaitModeNone)
if err != nil { if err != nil {
return nil, fmt.Errorf("loading yawol chart: %w", err) return nil, fmt.Errorf("loading yawol chart: %w", err)
} }
yawolVals, err := extraYawolValues(serviceAccURI, i.stateFile.Infrastructure, openStackCfg) yawolVals, err := extraYawolValues(serviceAccURI, i.stateFile.Infrastructure, openStackValues)
if err != nil { if err != nil {
return nil, fmt.Errorf("extending yawol chart values: %w", err) return nil, fmt.Errorf("extending yawol chart values: %w", err)
} }

View File

@ -175,6 +175,19 @@ func TestConstellationServices(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t) require := require.New(t)
var openstackValues *OpenStackValues
if tc.config.Provider.OpenStack != nil {
var deploy bool
if tc.config.Provider.OpenStack.DeployYawolLoadBalancer != nil {
deploy = *tc.config.Provider.OpenStack.DeployYawolLoadBalancer
}
openstackValues = &OpenStackValues{
DeployYawolLoadBalancer: deploy,
FloatingIPPoolID: tc.config.Provider.OpenStack.FloatingIPPoolID,
YawolFlavorID: tc.config.Provider.OpenStack.YawolFlavorID,
YawolImageID: tc.config.Provider.OpenStack.YawolImageID,
}
}
chartLoader := chartLoader{ chartLoader := chartLoader{
csp: tc.config.GetProvider(), csp: tc.config.GetProvider(),
@ -199,7 +212,7 @@ func TestConstellationServices(t *testing.T) {
UID: "uid", UID: "uid",
Azure: &state.Azure{}, Azure: &state.Azure{},
GCP: &state.GCP{}, GCP: &state.GCP{},
}, tc.config.Provider.OpenStack) }, openstackValues)
require.NoError(err) require.NoError(err)
values = mergeMaps(values, extraVals) values = mergeMaps(values, extraVals)

View File

@ -18,7 +18,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared" "github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
"github.com/edgelesssys/constellation/v2/internal/cloud/openstack" "github.com/edgelesssys/constellation/v2/internal/cloud/openstack"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/constellation/state" "github.com/edgelesssys/constellation/v2/internal/constellation/state"
"github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/kms/uri"
@ -83,7 +82,7 @@ func extraCiliumValues(provider cloudprovider.Provider, conformanceMode bool, ou
// Values set inside this function are only applied during init, not during upgrade. // Values set inside this function are only applied during init, not during upgrade.
func extraConstellationServicesValues( func extraConstellationServicesValues(
csp cloudprovider.Provider, attestationVariant variant.Variant, masterSecret uri.MasterSecret, serviceAccURI string, csp cloudprovider.Provider, attestationVariant variant.Variant, masterSecret uri.MasterSecret, serviceAccURI string,
output state.Infrastructure, openStackCfg *config.OpenStackConfig, output state.Infrastructure, openStackCfg *OpenStackValues,
) (map[string]any, error) { ) (map[string]any, error) {
extraVals := map[string]any{} extraVals := map[string]any{}
extraVals["join-service"] = map[string]any{ extraVals["join-service"] = map[string]any{
@ -152,7 +151,7 @@ func extraConstellationServicesValues(
// extraYawolValues extends the given values map by some values depending on user input. // extraYawolValues extends the given values map by some values depending on user input.
// Values set inside this function are only applied during init, not during upgrade. // Values set inside this function are only applied during init, not during upgrade.
func extraYawolValues(serviceAccURI string, output state.Infrastructure, openStackCfg *config.OpenStackConfig) (map[string]any, error) { func extraYawolValues(serviceAccURI string, output state.Infrastructure, openStackCfg *OpenStackValues) (map[string]any, error) {
extraVals := map[string]any{} extraVals := map[string]any{}
creds, err := openstack.AccountKeyFromURI(serviceAccURI) creds, err := openstack.AccountKeyFromURI(serviceAccURI)
@ -163,7 +162,7 @@ func extraYawolValues(serviceAccURI string, output state.Infrastructure, openSta
extraVals["yawol-config"] = map[string]any{ extraVals["yawol-config"] = map[string]any{
"secretData": yawolIni, "secretData": yawolIni,
} }
if openStackCfg.DeployYawolLoadBalancer != nil && *openStackCfg.DeployYawolLoadBalancer { if openStackCfg != nil && openStackCfg.DeployYawolLoadBalancer {
extraVals["yawol-controller"] = map[string]any{ extraVals["yawol-controller"] = map[string]any{
"yawolOSSecretName": "yawolkey", "yawolOSSecretName": "yawolkey",
// has to be larger than ~30s to account for slow OpenStack API calls. // has to be larger than ~30s to account for slow OpenStack API calls.

View File

@ -33,6 +33,7 @@ data "constellation_attestation" "test" {
* `azure-sev-snp` * `azure-sev-snp`
* `azure-tdx` * `azure-tdx`
* `gcp-sev-es` * `gcp-sev-es`
* `qemu-vtpm`
- `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`) - `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. 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))
@ -82,6 +83,7 @@ Read-Only:
* `azure-sev-snp` * `azure-sev-snp`
* `azure-tdx` * `azure-tdx`
* `gcp-sev-es` * `gcp-sev-es`
* `qemu-vtpm`
<a id="nestedatt--attestation--azure_firmware_signer_config"></a> <a id="nestedatt--attestation--azure_firmware_signer_config"></a>
### Nested Schema for `attestation.azure_firmware_signer_config` ### Nested Schema for `attestation.azure_firmware_signer_config`

View File

@ -32,6 +32,7 @@ data "constellation_image" "example" {
* `azure-sev-snp` * `azure-sev-snp`
* `azure-tdx` * `azure-tdx`
* `gcp-sev-es` * `gcp-sev-es`
* `qemu-vtpm`
- `csp` (String) CSP (Cloud Service Provider) to use. (e.g. `azure`) - `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. See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports.

View File

@ -86,6 +86,7 @@ See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview
- `gcp` (Attributes) GCP-specific configuration. (see [below for nested schema](#nestedatt--gcp)) - `gcp` (Attributes) GCP-specific configuration. (see [below for nested schema](#nestedatt--gcp))
- `in_cluster_endpoint` (String) The endpoint of the cluster. When not set, the out-of-cluster endpoint is used. - `in_cluster_endpoint` (String) The endpoint of the cluster. When not set, the out-of-cluster endpoint is used.
- `license_id` (String) Constellation license ID. When not set, the community license is used. - `license_id` (String) Constellation license ID. When not set, the community license is used.
- `openstack` (Attributes) OpenStack-specific configuration. (see [below for nested schema](#nestedatt--openstack))
### Read-Only ### Read-Only
@ -110,6 +111,7 @@ Required:
* `azure-sev-snp` * `azure-sev-snp`
* `azure-tdx` * `azure-tdx`
* `gcp-sev-es` * `gcp-sev-es`
* `qemu-vtpm`
Optional: Optional:
@ -211,6 +213,24 @@ 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.
<a id="nestedatt--openstack"></a>
### Nested Schema for `openstack`
Required:
- `cloud` (String) Name of the cloud in the clouds.yaml file.
- `floating_ip_pool_id` (String) Floating IP pool to use for the VMs.
- `network_id` (String) OpenStack network ID to use for the VMs.
- `subnet_id` (String) OpenStack subnet ID to use for the VMs.
Optional:
- `clouds_yaml_path` (String) Path to the clouds.yaml file.
- `deploy_yawol_load_balancer` (Boolean) Whether to deploy a YAWOL load balancer.
- `yawol_flavor_id` (String) OpenStack flavor used by the yawollet.
- `yawol_image_id` (String) OpenStack OS image used by the yawollet.
## Import ## Import
Import is supported using the following syntax: Import is supported using the following syntax:

View File

@ -0,0 +1,128 @@
terraform {
required_providers {
constellation = {
source = "edgelesssys/constellation"
version = "0.0.0" // replace with the version you want to use
}
random = {
source = "hashicorp/random"
version = "3.6.0"
}
}
}
locals {
name = "constell"
image_version = "vX.Y.Z"
kubernetes_version = "vX.Y.Z"
microservice_version = "vX.Y.Z"
csp = "stackit"
attestation_variant = "qemu-vtpm"
zone = "eu01-1"
cloud = "stackit"
clouds_yaml_path = "~/.config/openstack/clouds.yaml"
floating_ip_pool_id = "970ace5c-458f-484a-a660-0903bcfd91ad"
stackit_project_id = "" // replace with the STACKIT project id
control_plane_count = 3
worker_count = 2
instance_type = "m1a.8cd"
deploy_yawol_load_balancer = true
yawol_image_id = "bcd6c13e-75d1-4c3f-bf0f-8f83580cc1be"
yawol_flavor_id = "3b11b27e-6c73-470d-b595-1d85b95a8cdf"
master_secret = random_bytes.master_secret.hex
master_secret_salt = random_bytes.master_secret_salt.hex
measurement_salt = random_bytes.measurement_salt.hex
}
resource "random_bytes" "master_secret" {
length = 32
}
resource "random_bytes" "master_secret_salt" {
length = 32
}
resource "random_bytes" "measurement_salt" {
length = 32
}
module "stackit_infrastructure" {
// replace $VERSION with the Constellation version you want to use, e.g., v2.14.0
source = "https://github.com/edgelesssys/constellation/releases/download/$VERSION/terraform-module.zip//terraform-module/openstack"
name = local.name
node_groups = {
control_plane_default = {
role = "control-plane"
flavor_id = local.instance_type
state_disk_size = 30
state_disk_type = "storage_premium_perf6"
initial_count = local.control_plane_count
zone = local.zone
},
worker_default = {
role = "worker"
flavor_id = local.instance_type
state_disk_size = 30
state_disk_type = "storage_premium_perf6"
initial_count = local.worker_count
zone = local.zone
}
}
image_id = data.constellation_image.bar.image.reference
debug = false
cloud = local.cloud
openstack_clouds_yaml_path = local.clouds_yaml_path
floating_ip_pool_id = local.floating_ip_pool_id
stackit_project_id = local.stackit_project_id
}
data "constellation_attestation" "foo" {
csp = local.csp
attestation_variant = local.attestation_variant
image = data.constellation_image.bar.image
}
data "constellation_image" "bar" {
csp = local.csp
attestation_variant = local.attestation_variant
version = local.image_version
marketplace_image = true
}
resource "constellation_cluster" "stackit_example" {
csp = local.csp
name = module.stackit_infrastructure.name
uid = module.stackit_infrastructure.uid
image = data.constellation_image.bar.image
attestation = data.constellation_attestation.foo.attestation
kubernetes_version = local.kubernetes_version
constellation_microservice_version = local.microservice_version
init_secret = module.stackit_infrastructure.init_secret
master_secret = local.master_secret
master_secret_salt = local.master_secret_salt
measurement_salt = local.measurement_salt
out_of_cluster_endpoint = module.stackit_infrastructure.out_of_cluster_endpoint
in_cluster_endpoint = module.stackit_infrastructure.in_cluster_endpoint
api_server_cert_sans = module.stackit_infrastructure.api_server_cert_sans
openstack = {
cloud = local.cloud
clouds_yaml_path = local.clouds_yaml_path
floating_ip_pool_id = local.floating_ip_pool_id
deploy_yawol_load_balancer = local.deploy_yawol_load_balancer
yawol_image_id = local.yawol_image_id
yawol_flavor_id = local.yawol_flavor_id
network_id = module.stackit_infrastructure.network_id
subnet_id = module.stackit_infrastructure.lb_subnetwork_id
}
network_config = {
ip_cidr_node = module.stackit_infrastructure.ip_cidr_node
ip_cidr_service = "10.96.0.0/12"
}
}
output "kubeconfig" {
value = constellation_cluster.stackit_example.kubeconfig
sensitive = true
description = "KubeConfig for the Constellation cluster."
}

View File

@ -23,6 +23,8 @@ go_library(
"//internal/attestation/variant", "//internal/attestation/variant",
"//internal/cloud/azureshared", "//internal/cloud/azureshared",
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/cloud/openstack",
"//internal/cloud/openstack/clouds",
"//internal/compatibility", "//internal/compatibility",
"//internal/config", "//internal/config",
"//internal/constants", "//internal/constants",
@ -30,6 +32,7 @@ go_library(
"//internal/constellation/helm", "//internal/constellation/helm",
"//internal/constellation/kubecmd", "//internal/constellation/kubecmd",
"//internal/constellation/state", "//internal/constellation/state",
"//internal/file",
"//internal/grpc/dialer", "//internal/grpc/dialer",
"//internal/imagefetcher", "//internal/imagefetcher",
"//internal/kms/uri", "//internal/kms/uri",
@ -53,6 +56,7 @@ go_library(
"@com_github_hashicorp_terraform_plugin_framework//types/basetypes", "@com_github_hashicorp_terraform_plugin_framework//types/basetypes",
"@com_github_hashicorp_terraform_plugin_framework_validators//stringvalidator", "@com_github_hashicorp_terraform_plugin_framework_validators//stringvalidator",
"@com_github_hashicorp_terraform_plugin_log//tflog", "@com_github_hashicorp_terraform_plugin_log//tflog",
"@com_github_spf13_afero//:afero",
], ],
) )

View File

@ -110,6 +110,58 @@ func TestAccAttestationSource(t *testing.T) {
}, },
}, },
}, },
"STACKIT qemu-vtpm success": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: testingConfig + `
data "constellation_attestation" "test" {
csp = "stackit"
attestation_variant = "qemu-vtpm"
image = {
version = "v2.13.0"
reference = "v2.13.0"
short_path = "v2.13.0"
}
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.variant", "qemu-vtpm"),
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.bootloader_version", "0"), // since this is not supported on STACKIT, we expect 0
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.measurements.15.expected", "0000000000000000000000000000000000000000000000000000000000000000"),
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.measurements.15.warn_only", "false"),
),
},
},
},
"openstack qemu-vtpm success": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: testingConfig + `
data "constellation_attestation" "test" {
csp = "openstack"
attestation_variant = "qemu-vtpm"
image = {
version = "v2.13.0"
reference = "v2.13.0"
short_path = "v2.13.0"
}
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.variant", "qemu-vtpm"),
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.bootloader_version", "0"), // since this is not supported on OpenStack, we expect 0
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.measurements.15.expected", "0000000000000000000000000000000000000000000000000000000000000000"),
resource.TestCheckResourceAttr("data.constellation_attestation.test", "attestation.measurements.15.warn_only", "false"),
),
},
},
},
} }
for name, tc := range testCases { for name, tc := range testCases {

View File

@ -26,6 +26,8 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant" "github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"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"
openstackshared "github.com/edgelesssys/constellation/v2/internal/cloud/openstack"
"github.com/edgelesssys/constellation/v2/internal/cloud/openstack/clouds"
"github.com/edgelesssys/constellation/v2/internal/compatibility" "github.com/edgelesssys/constellation/v2/internal/compatibility"
"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/constants"
@ -33,6 +35,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/constellation/helm" "github.com/edgelesssys/constellation/v2/internal/constellation/helm"
"github.com/edgelesssys/constellation/v2/internal/constellation/kubecmd" "github.com/edgelesssys/constellation/v2/internal/constellation/kubecmd"
"github.com/edgelesssys/constellation/v2/internal/constellation/state" "github.com/edgelesssys/constellation/v2/internal/constellation/state"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer" "github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
"github.com/edgelesssys/constellation/v2/internal/kms/uri" "github.com/edgelesssys/constellation/v2/internal/kms/uri"
"github.com/edgelesssys/constellation/v2/internal/license" "github.com/edgelesssys/constellation/v2/internal/license"
@ -50,6 +53,7 @@ import (
"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"
"github.com/spf13/afero"
) )
var ( var (
@ -96,6 +100,7 @@ type ClusterResourceModel struct {
Attestation types.Object `tfsdk:"attestation"` Attestation types.Object `tfsdk:"attestation"`
GCP types.Object `tfsdk:"gcp"` GCP types.Object `tfsdk:"gcp"`
Azure types.Object `tfsdk:"azure"` Azure types.Object `tfsdk:"azure"`
OpenStack types.Object `tfsdk:"openstack"`
OwnerID types.String `tfsdk:"owner_id"` OwnerID types.String `tfsdk:"owner_id"`
ClusterID types.String `tfsdk:"cluster_id"` ClusterID types.String `tfsdk:"cluster_id"`
@ -129,6 +134,17 @@ type azureAttribute struct {
LoadBalancerName string `tfsdk:"load_balancer_name"` LoadBalancerName string `tfsdk:"load_balancer_name"`
} }
type openStackAttribute struct {
Cloud string `tfsdk:"cloud"`
CloudsYAMLPath string `tfsdk:"clouds_yaml_path"`
FloatingIPPoolID string `tfsdk:"floating_ip_pool_id"`
DeployYawolLoadBalancer bool `tfsdk:"deploy_yawol_load_balancer"`
YawolImageID string `tfsdk:"yawol_image_id"`
YawolFlavorID string `tfsdk:"yawol_flavor_id"`
NetworkID string `tfsdk:"network_id"`
SubnetID string `tfsdk:"subnet_id"`
}
// extraMicroservicesAttribute is the extra microservices attribute's data model. // extraMicroservicesAttribute is the extra microservices attribute's data model.
type extraMicroservicesAttribute struct { type extraMicroservicesAttribute struct {
CSIDriver bool `tfsdk:"csi_driver"` CSIDriver bool `tfsdk:"csi_driver"`
@ -333,6 +349,53 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re
}, },
}, },
}, },
"openstack": schema.SingleNestedAttribute{
MarkdownDescription: "OpenStack-specific configuration.",
Description: "OpenStack-specific configuration.",
Optional: true,
Attributes: map[string]schema.Attribute{
"cloud": schema.StringAttribute{
MarkdownDescription: "Name of the cloud in the clouds.yaml file.",
Description: "Name of the cloud in the clouds.yaml file.",
Required: true,
},
"clouds_yaml_path": schema.StringAttribute{
MarkdownDescription: "Path to the clouds.yaml file.",
Description: "Path to the clouds.yaml file.",
Optional: true,
},
"floating_ip_pool_id": schema.StringAttribute{
MarkdownDescription: "Floating IP pool to use for the VMs.",
Description: "Floating IP pool to use for the VMs.",
Required: true,
},
"deploy_yawol_load_balancer": schema.BoolAttribute{
MarkdownDescription: "Whether to deploy a YAWOL load balancer.",
Description: "Whether to deploy a YAWOL load balancer.",
Optional: true,
},
"yawol_image_id": schema.StringAttribute{
MarkdownDescription: "OpenStack OS image used by the yawollet.",
Description: "OpenStack OS image used by the yawollet.",
Optional: true,
},
"yawol_flavor_id": schema.StringAttribute{
MarkdownDescription: "OpenStack flavor used by the yawollet.",
Description: "OpenStack flavor used by the yawollet.",
Optional: true,
},
"network_id": schema.StringAttribute{
MarkdownDescription: "OpenStack network ID to use for the VMs.",
Description: "OpenStack network ID to use for the VMs.",
Required: true,
},
"subnet_id": schema.StringAttribute{
MarkdownDescription: "OpenStack subnet ID to use for the VMs.",
Description: "OpenStack subnet ID to use for the VMs.",
Required: true,
},
},
},
// Computed (output) attributes // Computed (output) attributes
"owner_id": schema.StringAttribute{ "owner_id": schema.StringAttribute{
@ -406,6 +469,26 @@ func (r *ClusterResource) ValidateConfig(ctx context.Context, req resource.Valid
"GCP configuration not allowed", "When csp is not set to 'gcp', setting the 'gcp' configuration has no effect.", "GCP configuration not allowed", "When csp is not set to 'gcp', setting the 'gcp' configuration has no effect.",
) )
} }
// OpenStack Config is required for OpenStack
if (strings.EqualFold(data.CSP.ValueString(), cloudprovider.OpenStack.String()) ||
strings.EqualFold(data.CSP.ValueString(), "stackit")) &&
data.OpenStack.IsNull() {
resp.Diagnostics.AddAttributeError(
path.Root("openstack"),
"OpenStack configuration missing", "When csp is set to 'openstack' or 'stackit', the 'openstack' configuration must be set.",
)
}
// OpenStack Config should not be set for other CSPs
if !strings.EqualFold(data.CSP.ValueString(), cloudprovider.OpenStack.String()) &&
!strings.EqualFold(data.CSP.ValueString(), "stackit") &&
!data.OpenStack.IsNull() {
resp.Diagnostics.AddAttributeWarning(
path.Root("openstack"),
"OpenStack configuration not allowed", "When csp is not set to 'openstack' or 'stackit', setting the 'openstack' configuration has no effect.",
)
}
} }
// Configure configures the resource. // Configure configures the resource.
@ -779,6 +862,7 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
serviceAccPayload := constellation.ServiceAccountPayload{} serviceAccPayload := constellation.ServiceAccountPayload{}
var gcpConfig gcpAttribute var gcpConfig gcpAttribute
var azureConfig azureAttribute var azureConfig azureAttribute
var openStackConfig openStackAttribute
switch csp { switch csp {
case cloudprovider.GCP: case cloudprovider.GCP:
convertDiags = data.GCP.As(ctx, &gcpConfig, basetypes.ObjectAsOptions{}) convertDiags = data.GCP.As(ctx, &gcpConfig, basetypes.ObjectAsOptions{})
@ -815,6 +899,33 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
PreferredAuthMethod: azureshared.AuthMethodUserAssignedIdentity, PreferredAuthMethod: azureshared.AuthMethodUserAssignedIdentity,
UamiResourceID: azureConfig.UamiResourceID, UamiResourceID: azureConfig.UamiResourceID,
} }
case cloudprovider.OpenStack:
convertDiags = data.OpenStack.As(ctx, &openStackConfig, basetypes.ObjectAsOptions{})
diags.Append(convertDiags...)
if diags.HasError() {
return diags
}
cloudsYAML, err := clouds.ReadCloudsYAML(file.NewHandler(afero.NewOsFs()), openStackConfig.CloudsYAMLPath)
if err != nil {
diags.AddError("Reading clouds.yaml", err.Error())
return diags
}
cloud, ok := cloudsYAML.Clouds[openStackConfig.Cloud]
if !ok {
diags.AddError("Reading clouds.yaml", fmt.Sprintf("Cloud %s not found in clouds.yaml", openStackConfig.Cloud))
return diags
}
serviceAccPayload.OpenStack = openstackshared.AccountKey{
AuthURL: cloud.AuthInfo.AuthURL,
Username: cloud.AuthInfo.Username,
Password: cloud.AuthInfo.Password,
ProjectID: cloud.AuthInfo.ProjectID,
ProjectName: cloud.AuthInfo.ProjectName,
UserDomainName: cloud.AuthInfo.UserDomainName,
ProjectDomainName: cloud.AuthInfo.ProjectDomainName,
RegionName: cloud.RegionName,
}
} }
serviceAccURI, err := constellation.MarshalServiceAccountURI(csp, serviceAccPayload) serviceAccURI, err := constellation.MarshalServiceAccountURI(csp, serviceAccPayload)
if err != nil { if err != nil {
@ -861,6 +972,11 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
ProjectID: gcpConfig.ProjectID, ProjectID: gcpConfig.ProjectID,
IPCidrPod: networkCfg.IPCidrPod.ValueString(), IPCidrPod: networkCfg.IPCidrPod.ValueString(),
} }
case cloudprovider.OpenStack:
stateFile.Infrastructure.OpenStack = &state.OpenStack{
NetworkID: openStackConfig.NetworkID,
SubnetID: openStackConfig.SubnetID,
}
} }
// Check license // Check license
@ -937,6 +1053,14 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
masterSecret: secrets.masterSecret, masterSecret: secrets.masterSecret,
serviceAccURI: serviceAccURI, serviceAccURI: serviceAccURI,
} }
if csp == cloudprovider.OpenStack {
payload.openStackHelmValues = &helm.OpenStackValues{
DeployYawolLoadBalancer: openStackConfig.DeployYawolLoadBalancer,
FloatingIPPoolID: openStackConfig.FloatingIPPoolID,
YawolImageID: openStackConfig.YawolImageID,
YawolFlavorID: openStackConfig.YawolFlavorID,
}
}
helmDiags := r.applyHelmCharts(ctx, applier, payload, stateFile) helmDiags := r.applyHelmCharts(ctx, applier, payload, stateFile)
diags.Append(helmDiags...) diags.Append(helmDiags...)
if diags.HasError() { if diags.HasError() {
@ -1063,6 +1187,7 @@ type applyHelmChartsPayload struct {
DeployCSIDriver bool // Whether to deploy the CSI driver. DeployCSIDriver bool // Whether to deploy the CSI driver.
masterSecret uri.MasterSecret // master secret of the cluster. masterSecret uri.MasterSecret // master secret of the cluster.
serviceAccURI string // URI of the service account used within the cluster. serviceAccURI string // URI of the service account used within the cluster.
openStackHelmValues *helm.OpenStackValues // OpenStack-specific Helm values.
} }
// applyHelmCharts applies the Helm charts to the cluster. // applyHelmCharts applies the Helm charts to the cluster.
@ -1083,10 +1208,11 @@ func (r *ClusterResource) applyHelmCharts(ctx context.Context, applier *constell
// Allow destructive changes to the cluster. // Allow destructive changes to the cluster.
// The user has previously been warned about this when planning a microservice version change. // The user has previously been warned about this when planning a microservice version change.
AllowDestructive: helm.AllowDestructive, AllowDestructive: helm.AllowDestructive,
OpenStackValues: payload.openStackHelmValues,
} }
executor, _, err := applier.PrepareHelmCharts(options, state, executor, _, err := applier.PrepareHelmCharts(options, state,
payload.serviceAccURI, payload.masterSecret, nil) payload.serviceAccURI, payload.masterSecret)
var upgradeErr *compatibility.InvalidUpgradeError var upgradeErr *compatibility.InvalidUpgradeError
if err != nil { if err != nil {
if !errors.As(err, &upgradeErr) { if !errors.As(err, &upgradeErr) {

View File

@ -489,6 +489,68 @@ func TestAccClusterResource(t *testing.T) {
}, },
}, },
}, },
"stackit config missing": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion(providerVersion),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "openstack") + fmt.Sprintf(`
resource "constellation_cluster" "test" {
csp = "stackit"
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"
}
kubernetes_version = "%s"
constellation_microservice_version = "%s"
}
`, versions.Default, providerVersion),
ExpectError: regexp.MustCompile(".*When csp is set to 'openstack' or 'stackit', the 'openstack' configuration\nmust be set.*"),
},
},
},
"openstack config missing": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactoriesWithVersion(providerVersion),
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: fullClusterTestingConfig(t, "openstack") + fmt.Sprintf(`
resource "constellation_cluster" "test" {
csp = "openstack"
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"
}
kubernetes_version = "%s"
constellation_microservice_version = "%s"
}
`, versions.Default, providerVersion),
ExpectError: regexp.MustCompile(".*When csp is set to 'openstack' or 'stackit', the 'openstack' configuration\nmust be set.*"),
},
},
},
} }
for name, tc := range testCases { for name, tc := range testCases {
@ -547,6 +609,19 @@ func fullClusterTestingConfig(t *testing.T, csp string) string {
attestation_variant = "gcp-sev-es" attestation_variant = "gcp-sev-es"
image = data.constellation_image.bar.image image = data.constellation_image.bar.image
}`, image) }`, image)
case "openstack":
return providerConfig + fmt.Sprintf(`
data "constellation_image" "bar" {
version = "%s"
attestation_variant = "qemu-vtpm"
csp = "openstack"
}
data "constellation_attestation" "foo" {
csp = "openstack"
attestation_variant = "qemu-vtpm"
image = data.constellation_image.bar.image
}`, image)
default: default:
t.Fatal("unknown csp") t.Fatal("unknown csp")
return "" return ""

View File

@ -122,6 +122,10 @@ func convertFromTfAttestationCfg(tfAttestation attestationAttribute, attestation
attestationConfig = &config.GCPSEVES{ attestationConfig = &config.GCPSEVES{
Measurements: c11nMeasurements, Measurements: c11nMeasurements,
} }
case variant.QEMUVTPM{}:
attestationConfig = &config.QEMUVTPM{
Measurements: c11nMeasurements,
}
default: default:
return nil, fmt.Errorf("unknown attestation variant: %s", attestationVariant) return nil, fmt.Errorf("unknown attestation variant: %s", attestationVariant)
} }
@ -177,7 +181,7 @@ func convertToTfAttestation(attVar variant.Variant, snpVersions attestationconfi
XFAM: hex.EncodeToString(tdxCfg.XFAM), XFAM: hex.EncodeToString(tdxCfg.XFAM),
} }
tfAttestation.TDX = tfTdxCfg tfAttestation.TDX = tfTdxCfg
case variant.GCPSEVES{}: case variant.GCPSEVES{}, variant.QEMUVTPM{}:
// no additional fields // no additional fields
default: default:
return tfAttestation, fmt.Errorf("unknown attestation variant: %s", attVar) return tfAttestation, fmt.Errorf("unknown attestation variant: %s", attVar)

View File

@ -252,9 +252,10 @@ func (d *ImageDataSource) Read(ctx context.Context, req datasource.ReadRequest,
// Save data into Terraform state // Save data into Terraform state
diags := resp.State.SetAttribute(ctx, path.Root("image"), imageAttribute{ diags := resp.State.SetAttribute(ctx, path.Root("image"), imageAttribute{
Reference: imageRef, Reference: imageRef,
Version: imageSemver, Version: imageSemver,
ShortPath: apiCompatibleVer.ShortPath(), ShortPath: apiCompatibleVer.ShortPath(),
MarketplaceImage: data.MarketplaceImage.ValueBoolPointer(),
}) })
resp.Diagnostics.Append(diags...) resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {

View File

@ -141,6 +141,38 @@ func TestAccImageDataSource(t *testing.T) {
}, },
}, },
}, },
"stackit success": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: testingConfig + `
data "constellation_image" "test" {
version = "v2.16.0"
attestation_variant = "qemu-vtpm"
csp = "stackit"
}
`,
Check: resource.TestCheckResourceAttr("data.constellation_image.test", "image.reference", "8ffc1740-1e41-4281-b872-f8088ffd7692"), // should be immutable,
},
},
},
"openstack success": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,
Steps: []resource.TestStep{
{
Config: testingConfig + `
data "constellation_image" "test" {
version = "v2.16.0"
attestation_variant = "qemu-vtpm"
csp = "openstack"
}
`,
Check: resource.TestCheckResourceAttr("data.constellation_image.test", "image.reference", "8ffc1740-1e41-4281-b872-f8088ffd7692"), // should be immutable,
},
},
},
"unknown attestation variant": { "unknown attestation variant": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck, PreCheck: bazelPreCheck,

View File

@ -31,11 +31,12 @@ func newAttestationVariantAttributeSchema(t attributeType) schema.Attribute {
" * `aws-nitro-tpm`\n" + " * `aws-nitro-tpm`\n" +
" * `azure-sev-snp`\n" + " * `azure-sev-snp`\n" +
" * `azure-tdx`\n" + " * `azure-tdx`\n" +
" * `gcp-sev-es`\n", " * `gcp-sev-es`\n" +
" * `qemu-vtpm`\n",
Required: isInput, Required: isInput,
Computed: !isInput, Computed: !isInput,
Validators: []validator.String{ Validators: []validator.String{
stringvalidator.OneOf("aws-sev-snp", "aws-nitro-tpm", "azure-sev-snp", "azure-tdx", "gcp-sev-es"), stringvalidator.OneOf("aws-sev-snp", "aws-nitro-tpm", "azure-sev-snp", "azure-tdx", "gcp-sev-es", "qemu-vtpm"),
}, },
} }
} }
@ -47,7 +48,7 @@ func newCSPAttributeSchema() schema.Attribute {
"See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports.", "See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview/clouds) that Constellation supports.",
Required: true, Required: true,
Validators: []validator.String{ Validators: []validator.String{
stringvalidator.OneOf("aws", "azure", "gcp"), stringvalidator.OneOf("aws", "azure", "gcp", "openstack", "stackit"),
}, },
} }
} }