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
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)
// methods to interact with Kubernetes

View File

@ -554,7 +554,7 @@ func (s *stubConstellApplier) Init(context.Context, atls.Validator, *state.State
type helmApplier interface {
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)
}

View File

@ -43,6 +43,18 @@ func (a *applyCmd) runHelmApply(cmd *cobra.Command, conf *config.Config, stateFi
ApplyTimeout: a.flags.helmTimeout,
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")
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")
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 !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.")
@ -65,7 +77,7 @@ func (a *applyCmd) runHelmApply(cmd *cobra.Command, conf *config.Config, stateFi
}
}
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
if err != nil {

View File

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

View File

@ -376,9 +376,9 @@ type mockApplier struct {
}
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) {
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)
}

View File

@ -9,7 +9,6 @@ package constellation
import (
"errors"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constellation/helm"
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
"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.
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) {
if a.helmClient == nil {
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 {
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)
}

View File

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

View File

@ -35,7 +35,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"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/constellation/state"
"github.com/edgelesssys/constellation/v2/internal/file"
@ -91,13 +90,14 @@ type Options struct {
MicroserviceVersion semver.Semver
HelmWaitMode WaitMode
ApplyTimeout time.Duration
OpenStackValues *OpenStackValues
}
// PrepareApply loads the charts and returns the executor to apply them.
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) {
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 {
return nil, false, fmt.Errorf("loading Helm releases: %w", err)
}
@ -111,11 +111,11 @@ func (h Client) PrepareApply(
func (h Client) loadReleases(
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) {
helmLoader := newLoader(csp, attestationVariant, k8sVersion, stateFile, h.cliVersion)
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.

View File

@ -217,7 +217,7 @@ func TestHelmApply(t *testing.T) {
SetInfrastructure(state.Infrastructure{UID: "testuid"}).
SetClusterValues(state.ClusterValues{MeasurementSalt: []byte{0x41}}),
fakeServiceAccURI(csp),
uri.MasterSecret{Key: []byte("secret"), Salt: []byte("masterSalt")}, nil)
uri.MasterSecret{Key: []byte("secret"), Salt: []byte("masterSalt")})
var upgradeErr *compatibility.InvalidUpgradeError
if tc.expectError {
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/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/constellation/helm/imageversion"
"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.
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.
func (i *chartLoader) loadReleases(conformanceMode, deployCSIDriver bool, helmWaitMode WaitMode, masterSecret uri.MasterSecret,
serviceAccURI string, openStackCfg *config.OpenStackConfig,
serviceAccURI string, openStackValues *OpenStackValues,
) (releaseApplyOrder, error) {
ciliumRelease, err := i.loadRelease(ciliumInfo, helmWaitMode)
if err != nil {
@ -143,7 +150,7 @@ func (i *chartLoader) loadReleases(conformanceMode, deployCSIDriver bool, helmWa
}
svcVals, err := extraConstellationServicesValues(i.csp, i.attestationVariant, masterSecret,
serviceAccURI, i.stateFile.Infrastructure, openStackCfg)
serviceAccURI, i.stateFile.Infrastructure, openStackValues)
if err != nil {
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)
}
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)
if err != nil {
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 {
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) {
assert := assert.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{
csp: tc.config.GetProvider(),
@ -199,7 +212,7 @@ func TestConstellationServices(t *testing.T) {
UID: "uid",
Azure: &state.Azure{},
GCP: &state.GCP{},
}, tc.config.Provider.OpenStack)
}, openstackValues)
require.NoError(err)
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/gcpshared"
"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/constellation/state"
"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.
func extraConstellationServicesValues(
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) {
extraVals := 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.
// 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{}
creds, err := openstack.AccountKeyFromURI(serviceAccURI)
@ -163,7 +162,7 @@ func extraYawolValues(serviceAccURI string, output state.Infrastructure, openSta
extraVals["yawol-config"] = map[string]any{
"secretData": yawolIni,
}
if openStackCfg.DeployYawolLoadBalancer != nil && *openStackCfg.DeployYawolLoadBalancer {
if openStackCfg != nil && openStackCfg.DeployYawolLoadBalancer {
extraVals["yawol-controller"] = map[string]any{
"yawolOSSecretName": "yawolkey",
// 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-tdx`
* `gcp-sev-es`
* `qemu-vtpm`
- `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))
@ -82,6 +83,7 @@ Read-Only:
* `azure-sev-snp`
* `azure-tdx`
* `gcp-sev-es`
* `qemu-vtpm`
<a id="nestedatt--attestation--azure_firmware_signer_config"></a>
### Nested Schema for `attestation.azure_firmware_signer_config`

View File

@ -32,6 +32,7 @@ data "constellation_image" "example" {
* `azure-sev-snp`
* `azure-tdx`
* `gcp-sev-es`
* `qemu-vtpm`
- `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.

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))
- `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.
- `openstack` (Attributes) OpenStack-specific configuration. (see [below for nested schema](#nestedatt--openstack))
### Read-Only
@ -110,6 +111,7 @@ Required:
* `azure-sev-snp`
* `azure-tdx`
* `gcp-sev-es`
* `qemu-vtpm`
Optional:
@ -211,6 +213,24 @@ Required:
- `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.
<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 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/cloud/azureshared",
"//internal/cloud/cloudprovider",
"//internal/cloud/openstack",
"//internal/cloud/openstack/clouds",
"//internal/compatibility",
"//internal/config",
"//internal/constants",
@ -30,6 +32,7 @@ go_library(
"//internal/constellation/helm",
"//internal/constellation/kubecmd",
"//internal/constellation/state",
"//internal/file",
"//internal/grpc/dialer",
"//internal/imagefetcher",
"//internal/kms/uri",
@ -53,6 +56,7 @@ go_library(
"@com_github_hashicorp_terraform_plugin_framework//types/basetypes",
"@com_github_hashicorp_terraform_plugin_framework_validators//stringvalidator",
"@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 {

View File

@ -26,6 +26,8 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/azureshared"
"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/config"
"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/kubecmd"
"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/kms/uri"
"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/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/spf13/afero"
)
var (
@ -96,6 +100,7 @@ type ClusterResourceModel struct {
Attestation types.Object `tfsdk:"attestation"`
GCP types.Object `tfsdk:"gcp"`
Azure types.Object `tfsdk:"azure"`
OpenStack types.Object `tfsdk:"openstack"`
OwnerID types.String `tfsdk:"owner_id"`
ClusterID types.String `tfsdk:"cluster_id"`
@ -129,6 +134,17 @@ type azureAttribute struct {
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.
type extraMicroservicesAttribute struct {
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
"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.",
)
}
// 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.
@ -779,6 +862,7 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
serviceAccPayload := constellation.ServiceAccountPayload{}
var gcpConfig gcpAttribute
var azureConfig azureAttribute
var openStackConfig openStackAttribute
switch csp {
case cloudprovider.GCP:
convertDiags = data.GCP.As(ctx, &gcpConfig, basetypes.ObjectAsOptions{})
@ -815,6 +899,33 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
PreferredAuthMethod: azureshared.AuthMethodUserAssignedIdentity,
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)
if err != nil {
@ -861,6 +972,11 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
ProjectID: gcpConfig.ProjectID,
IPCidrPod: networkCfg.IPCidrPod.ValueString(),
}
case cloudprovider.OpenStack:
stateFile.Infrastructure.OpenStack = &state.OpenStack{
NetworkID: openStackConfig.NetworkID,
SubnetID: openStackConfig.SubnetID,
}
}
// Check license
@ -937,6 +1053,14 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
masterSecret: secrets.masterSecret,
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)
diags.Append(helmDiags...)
if diags.HasError() {
@ -1063,6 +1187,7 @@ type applyHelmChartsPayload struct {
DeployCSIDriver bool // Whether to deploy the CSI driver.
masterSecret uri.MasterSecret // master secret of 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.
@ -1083,10 +1208,11 @@ func (r *ClusterResource) applyHelmCharts(ctx context.Context, applier *constell
// Allow destructive changes to the cluster.
// The user has previously been warned about this when planning a microservice version change.
AllowDestructive: helm.AllowDestructive,
OpenStackValues: payload.openStackHelmValues,
}
executor, _, err := applier.PrepareHelmCharts(options, state,
payload.serviceAccURI, payload.masterSecret, nil)
payload.serviceAccURI, payload.masterSecret)
var upgradeErr *compatibility.InvalidUpgradeError
if err != nil {
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 {
@ -547,6 +609,19 @@ func fullClusterTestingConfig(t *testing.T, csp string) string {
attestation_variant = "gcp-sev-es"
image = data.constellation_image.bar.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:
t.Fatal("unknown csp")
return ""

View File

@ -122,6 +122,10 @@ func convertFromTfAttestationCfg(tfAttestation attestationAttribute, attestation
attestationConfig = &config.GCPSEVES{
Measurements: c11nMeasurements,
}
case variant.QEMUVTPM{}:
attestationConfig = &config.QEMUVTPM{
Measurements: c11nMeasurements,
}
default:
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),
}
tfAttestation.TDX = tfTdxCfg
case variant.GCPSEVES{}:
case variant.GCPSEVES{}, variant.QEMUVTPM{}:
// no additional fields
default:
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
diags := resp.State.SetAttribute(ctx, path.Root("image"), imageAttribute{
Reference: imageRef,
Version: imageSemver,
ShortPath: apiCompatibleVer.ShortPath(),
Reference: imageRef,
Version: imageSemver,
ShortPath: apiCompatibleVer.ShortPath(),
MarketplaceImage: data.MarketplaceImage.ValueBoolPointer(),
})
resp.Diagnostics.Append(diags...)
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": {
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
PreCheck: bazelPreCheck,

View File

@ -31,11 +31,12 @@ func newAttestationVariantAttributeSchema(t attributeType) schema.Attribute {
" * `aws-nitro-tpm`\n" +
" * `azure-sev-snp`\n" +
" * `azure-tdx`\n" +
" * `gcp-sev-es`\n",
" * `gcp-sev-es`\n" +
" * `qemu-vtpm`\n",
Required: isInput,
Computed: !isInput,
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.",
Required: true,
Validators: []validator.String{
stringvalidator.OneOf("aws", "azure", "gcp"),
stringvalidator.OneOf("aws", "azure", "gcp", "openstack", "stackit"),
},
}
}