2023-07-21 04:04:29 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package cloudcmd
import (
2023-10-31 07:46:40 -04:00
"context"
"errors"
2023-07-21 04:04:29 -04:00
"fmt"
2023-10-31 07:46:40 -04:00
"net/url"
"os"
2023-08-23 04:35:42 -04:00
"path/filepath"
2023-10-31 07:46:40 -04:00
"regexp"
"runtime"
2023-08-02 04:41:26 -04:00
"strings"
2023-07-21 04:04:29 -04:00
2023-10-31 07:46:40 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/libvirt"
2023-07-21 04:04:29 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
2023-08-23 04:35:42 -04:00
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
2023-12-08 08:40:31 -05:00
"github.com/edgelesssys/constellation/v2/internal/mpimage"
2023-07-21 04:04:29 -04:00
"github.com/edgelesssys/constellation/v2/internal/role"
)
2023-10-31 07:46:40 -04:00
// The azurerm Terraform provider enforces its own convention of case sensitivity for Azure URIs which Azure's API itself does not enforce or, even worse, actually returns.
// These regular expression are used to make sure that the URIs we pass to Terraform are in the format that the provider expects.
var (
caseInsensitiveSubscriptionsRegexp = regexp . MustCompile ( ` (?i)\/subscriptions\/ ` )
caseInsensitiveResourceGroupRegexp = regexp . MustCompile ( ` (?i)\/resourcegroups\/ ` )
caseInsensitiveProvidersRegexp = regexp . MustCompile ( ` (?i)\/providers\/ ` )
caseInsensitiveUserAssignedIdentitiesRegexp = regexp . MustCompile ( ` (?i)\/userassignedidentities\/ ` )
caseInsensitiveMicrosoftManagedIdentity = regexp . MustCompile ( ` (?i)\/microsoft.managedidentity\/ ` )
caseInsensitiveCommunityGalleriesRegexp = regexp . MustCompile ( ` (?i)\/communitygalleries\/ ` )
caseInsensitiveImagesRegExp = regexp . MustCompile ( ` (?i)\/images\/ ` )
caseInsensitiveVersionsRegExp = regexp . MustCompile ( ` (?i)\/versions\/ ` )
)
2023-08-23 04:35:42 -04:00
// TerraformIAMUpgradeVars returns variables required to execute IAM upgrades with Terraform.
func TerraformIAMUpgradeVars ( conf * config . Config , fileHandler file . Handler ) ( terraform . Variables , error ) {
// Load the tfvars of the existing IAM workspace.
// Ideally we would only load values from the config file, but this currently does not hold all values required.
// This should be refactored in the future.
oldVarBytes , err := fileHandler . Read ( filepath . Join ( constants . TerraformIAMWorkingDir , "terraform.tfvars" ) )
if err != nil {
return nil , fmt . Errorf ( "reading existing IAM workspace: %w" , err )
}
var vars terraform . Variables
switch conf . GetProvider ( ) {
case cloudprovider . AWS :
var oldVars terraform . AWSIAMVariables
if err := terraform . VariablesFromBytes ( oldVarBytes , & oldVars ) ; err != nil {
return nil , fmt . Errorf ( "parsing existing IAM workspace: %w" , err )
}
vars = awsTerraformIAMVars ( conf , oldVars )
case cloudprovider . Azure :
var oldVars terraform . AzureIAMVariables
if err := terraform . VariablesFromBytes ( oldVarBytes , & oldVars ) ; err != nil {
return nil , fmt . Errorf ( "parsing existing IAM workspace: %w" , err )
}
vars = azureTerraformIAMVars ( conf , oldVars )
case cloudprovider . GCP :
var oldVars terraform . GCPIAMVariables
if err := terraform . VariablesFromBytes ( oldVarBytes , & oldVars ) ; err != nil {
return nil , fmt . Errorf ( "parsing existing IAM workspace: %w" , err )
}
vars = gcpTerraformIAMVars ( conf , oldVars )
default :
return nil , fmt . Errorf ( "unsupported provider: %s" , conf . GetProvider ( ) )
}
return vars , nil
2023-07-21 04:04:29 -04:00
}
// awsTerraformVars provides variables required to execute the Terraform scripts.
// It should be the only place to declare the AWS variables.
2023-08-02 04:41:26 -04:00
func awsTerraformVars ( conf * config . Config , imageRef string ) * terraform . AWSClusterVariables {
nodeGroups := make ( map [ string ] terraform . AWSNodeGroup )
for groupName , group := range conf . NodeGroups {
nodeGroups [ groupName ] = terraform . AWSNodeGroup {
Role : role . FromString ( group . Role ) . TFString ( ) ,
StateDiskSizeGB : group . StateDiskSizeGB ,
InitialCount : group . InitialCount ,
Zone : group . Zone ,
InstanceType : group . InstanceType ,
DiskType : group . StateDiskType ,
}
}
2024-02-06 06:13:59 -05:00
2023-07-21 04:04:29 -04:00
return & terraform . AWSClusterVariables {
2023-08-02 04:41:26 -04:00
Name : conf . Name ,
NodeGroups : nodeGroups ,
2023-07-21 04:04:29 -04:00
Region : conf . Provider . AWS . Region ,
Zone : conf . Provider . AWS . Zone ,
2023-12-15 04:36:58 -05:00
ImageID : imageRef ,
2023-07-21 04:04:29 -04:00
IAMProfileControlPlane : conf . Provider . AWS . IAMProfileControlPlane ,
IAMProfileWorkerNodes : conf . Provider . AWS . IAMProfileWorkerNodes ,
Debug : conf . IsDebugCluster ( ) ,
EnableSNP : conf . GetAttestationConfig ( ) . GetVariant ( ) . Equal ( variant . AWSSEVSNP { } ) ,
2023-07-21 10:43:51 -04:00
CustomEndpoint : conf . CustomEndpoint ,
2023-10-17 09:46:15 -04:00
InternalLoadBalancer : conf . InternalLoadBalancer ,
2023-07-21 04:04:29 -04:00
}
}
2023-08-23 04:35:42 -04:00
func awsTerraformIAMVars ( conf * config . Config , oldVars terraform . AWSIAMVariables ) * terraform . AWSIAMVariables {
return & terraform . AWSIAMVariables {
Region : conf . Provider . AWS . Region ,
Prefix : oldVars . Prefix ,
}
}
2023-10-31 07:46:40 -04:00
func normalizeAzureURIs ( vars * terraform . AzureClusterVariables ) * terraform . AzureClusterVariables {
vars . UserAssignedIdentity = caseInsensitiveSubscriptionsRegexp . ReplaceAllString ( vars . UserAssignedIdentity , "/subscriptions/" )
vars . UserAssignedIdentity = caseInsensitiveResourceGroupRegexp . ReplaceAllString ( vars . UserAssignedIdentity , "/resourceGroups/" )
vars . UserAssignedIdentity = caseInsensitiveProvidersRegexp . ReplaceAllString ( vars . UserAssignedIdentity , "/providers/" )
vars . UserAssignedIdentity = caseInsensitiveUserAssignedIdentitiesRegexp . ReplaceAllString ( vars . UserAssignedIdentity , "/userAssignedIdentities/" )
vars . UserAssignedIdentity = caseInsensitiveMicrosoftManagedIdentity . ReplaceAllString ( vars . UserAssignedIdentity , "/Microsoft.ManagedIdentity/" )
vars . ImageID = caseInsensitiveCommunityGalleriesRegexp . ReplaceAllString ( vars . ImageID , "/communityGalleries/" )
vars . ImageID = caseInsensitiveImagesRegExp . ReplaceAllString ( vars . ImageID , "/images/" )
vars . ImageID = caseInsensitiveVersionsRegExp . ReplaceAllString ( vars . ImageID , "/versions/" )
return vars
}
2023-07-21 04:04:29 -04:00
// azureTerraformVars provides variables required to execute the Terraform scripts.
// It should be the only place to declare the Azure variables.
2023-12-08 08:40:31 -05:00
func azureTerraformVars ( conf * config . Config , imageRef string ) ( * terraform . AzureClusterVariables , error ) {
2023-08-02 04:41:26 -04:00
nodeGroups := make ( map [ string ] terraform . AzureNodeGroup )
for groupName , group := range conf . NodeGroups {
zones := strings . Split ( group . Zone , "," )
if len ( zones ) == 0 || ( len ( zones ) == 1 && zones [ 0 ] == "" ) {
zones = nil
}
nodeGroups [ groupName ] = terraform . AzureNodeGroup {
Role : role . FromString ( group . Role ) . TFString ( ) ,
InitialCount : group . InitialCount ,
InstanceType : group . InstanceType ,
DiskSizeGB : group . StateDiskSizeGB ,
DiskType : group . StateDiskType ,
Zones : zones ,
}
}
2023-07-21 04:04:29 -04:00
vars := & terraform . AzureClusterVariables {
2023-08-02 04:41:26 -04:00
Name : conf . Name ,
NodeGroups : nodeGroups ,
2023-07-21 04:04:29 -04:00
Location : conf . Provider . Azure . Location ,
CreateMAA : toPtr ( conf . GetAttestationConfig ( ) . GetVariant ( ) . Equal ( variant . AzureSEVSNP { } ) ) ,
Debug : toPtr ( conf . IsDebugCluster ( ) ) ,
2024-01-24 09:10:15 -05:00
ConfidentialVM : toPtr ( ! conf . GetAttestationConfig ( ) . GetVariant ( ) . Equal ( variant . AzureTrustedLaunch { } ) ) ,
2023-07-21 04:04:29 -04:00
SecureBoot : conf . Provider . Azure . SecureBoot ,
UserAssignedIdentity : conf . Provider . Azure . UserAssignedIdentity ,
ResourceGroup : conf . Provider . Azure . ResourceGroup ,
2023-07-21 10:43:51 -04:00
CustomEndpoint : conf . CustomEndpoint ,
2023-10-17 09:46:15 -04:00
InternalLoadBalancer : conf . InternalLoadBalancer ,
2023-12-11 02:15:47 -05:00
MarketplaceImage : nil ,
2023-07-21 04:04:29 -04:00
}
2023-12-08 08:40:31 -05:00
if conf . UseMarketplaceImage ( ) {
image , err := mpimage . NewFromURI ( imageRef )
if err != nil {
return nil , fmt . Errorf ( "parsing marketplace image URI: %w" , err )
}
azureImage , ok := image . ( mpimage . AzureMarketplaceImage )
if ! ok {
return nil , fmt . Errorf ( "expected Azure marketplace image, got %T" , image )
}
// If a marketplace image is used, only the marketplace reference is required.
2023-12-11 02:15:47 -05:00
vars . MarketplaceImage = & terraform . AzureMarketplaceImageVariables {
2023-12-08 08:40:31 -05:00
Publisher : azureImage . Publisher ,
Product : azureImage . Offer ,
Name : azureImage . SKU ,
Version : azureImage . Version ,
}
} else {
// If not, we need to specify the exact CommunityGalleries/.. image reference.
vars . ImageID = imageRef
}
2023-07-21 04:04:29 -04:00
vars = normalizeAzureURIs ( vars )
2023-12-08 08:40:31 -05:00
return vars , nil
2023-07-21 04:04:29 -04:00
}
2023-08-23 04:35:42 -04:00
func azureTerraformIAMVars ( conf * config . Config , oldVars terraform . AzureIAMVariables ) * terraform . AzureIAMVariables {
return & terraform . AzureIAMVariables {
2023-12-15 04:36:58 -05:00
Location : conf . Provider . Azure . Location ,
2023-08-23 04:35:42 -04:00
ServicePrincipal : oldVars . ServicePrincipal ,
ResourceGroup : conf . Provider . Azure . ResourceGroup ,
}
}
2023-07-21 04:04:29 -04:00
// gcpTerraformVars provides variables required to execute the Terraform scripts.
// It should be the only place to declare the GCP variables.
2023-08-02 04:41:26 -04:00
func gcpTerraformVars ( conf * config . Config , imageRef string ) * terraform . GCPClusterVariables {
nodeGroups := make ( map [ string ] terraform . GCPNodeGroup )
for groupName , group := range conf . NodeGroups {
nodeGroups [ groupName ] = terraform . GCPNodeGroup {
Role : role . FromString ( group . Role ) . TFString ( ) ,
StateDiskSizeGB : group . StateDiskSizeGB ,
InitialCount : group . InitialCount ,
Zone : group . Zone ,
InstanceType : group . InstanceType ,
DiskType : group . StateDiskType ,
}
}
2023-07-21 04:04:29 -04:00
return & terraform . GCPClusterVariables {
2023-10-17 09:46:15 -04:00
Name : conf . Name ,
NodeGroups : nodeGroups ,
Project : conf . Provider . GCP . Project ,
Region : conf . Provider . GCP . Region ,
Zone : conf . Provider . GCP . Zone ,
ImageID : imageRef ,
Debug : conf . IsDebugCluster ( ) ,
CustomEndpoint : conf . CustomEndpoint ,
InternalLoadBalancer : conf . InternalLoadBalancer ,
2023-07-21 04:04:29 -04:00
}
}
2023-08-23 04:35:42 -04:00
func gcpTerraformIAMVars ( conf * config . Config , oldVars terraform . GCPIAMVariables ) * terraform . GCPIAMVariables {
return & terraform . GCPIAMVariables {
Project : conf . Provider . GCP . Project ,
Region : conf . Provider . GCP . Region ,
Zone : conf . Provider . GCP . Zone ,
ServiceAccountID : oldVars . ServiceAccountID ,
}
}
2023-07-21 04:04:29 -04:00
// openStackTerraformVars provides variables required to execute the Terraform scripts.
// It should be the only place to declare the OpenStack variables.
2023-10-31 07:46:40 -04:00
func openStackTerraformVars ( conf * config . Config , imageRef string ) ( * terraform . OpenStackClusterVariables , error ) {
if _ , hasOSAuthURL := os . LookupEnv ( "OS_AUTH_URL" ) ; ! hasOSAuthURL && conf . Provider . OpenStack . Cloud == "" {
return nil , errors . New (
"neither environment variable OS_AUTH_URL nor cloud name for \"clouds.yaml\" is set. OpenStack authentication requires a set of " +
"OS_* environment variables that are typically sourced into the current shell with an openrc file " +
"or a cloud name for \"clouds.yaml\". " +
"See https://docs.openstack.org/openstacksdk/latest/user/config/configuration.html for more information" ,
)
}
2023-08-02 04:41:26 -04:00
nodeGroups := make ( map [ string ] terraform . OpenStackNodeGroup )
for groupName , group := range conf . NodeGroups {
nodeGroups [ groupName ] = terraform . OpenStackNodeGroup {
Role : role . FromString ( group . Role ) . TFString ( ) ,
StateDiskSizeGB : group . StateDiskSizeGB ,
InitialCount : group . InitialCount ,
FlavorID : group . InstanceType ,
Zone : group . Zone ,
StateDiskType : group . StateDiskType ,
}
}
2023-07-21 04:04:29 -04:00
return & terraform . OpenStackClusterVariables {
Name : conf . Name ,
Cloud : toPtr ( conf . Provider . OpenStack . Cloud ) ,
FloatingIPPoolID : conf . Provider . OpenStack . FloatingIPPoolID ,
ImageURL : imageRef ,
DirectDownload : * conf . Provider . OpenStack . DirectDownload ,
OpenstackUserDomainName : conf . Provider . OpenStack . UserDomainName ,
OpenstackUsername : conf . Provider . OpenStack . Username ,
OpenstackPassword : conf . Provider . OpenStack . Password ,
Debug : conf . IsDebugCluster ( ) ,
2023-08-02 04:41:26 -04:00
NodeGroups : nodeGroups ,
CustomEndpoint : conf . CustomEndpoint ,
2023-10-17 09:46:15 -04:00
InternalLoadBalancer : conf . InternalLoadBalancer ,
2023-10-31 07:46:40 -04:00
} , nil
2023-07-21 04:04:29 -04:00
}
// qemuTerraformVars provides variables required to execute the Terraform scripts.
// It should be the only place to declare the QEMU variables.
2023-10-31 07:46:40 -04:00
func qemuTerraformVars (
ctx context . Context , conf * config . Config , imageRef string ,
lv libvirtRunner , downloader rawDownloader ,
) ( * terraform . QEMUVariables , error ) {
if runtime . GOARCH != "amd64" || runtime . GOOS != "linux" {
return nil , fmt . Errorf ( "creation of a QEMU based Constellation is not supported for %s/%s" , runtime . GOOS , runtime . GOARCH )
}
imagePath , err := downloader . Download ( ctx , nil , false , imageRef , conf . Image )
if err != nil {
return nil , fmt . Errorf ( "download raw image: %w" , err )
}
libvirtURI := conf . Provider . QEMU . LibvirtURI
libvirtSocketPath := "."
switch {
// if no libvirt URI is specified, start a libvirt container
case libvirtURI == "" :
if err := lv . Start ( ctx , conf . Name , conf . Provider . QEMU . LibvirtContainerImage ) ; err != nil {
return nil , fmt . Errorf ( "start libvirt container: %w" , err )
}
libvirtURI = libvirt . LibvirtTCPConnectURI
// socket for system URI should be in /var/run/libvirt/libvirt-sock
case libvirtURI == "qemu:///system" :
libvirtSocketPath = "/var/run/libvirt/libvirt-sock"
// socket for session URI should be in /run/user/<uid>/libvirt/libvirt-sock
case libvirtURI == "qemu:///session" :
libvirtSocketPath = fmt . Sprintf ( "/run/user/%d/libvirt/libvirt-sock" , os . Getuid ( ) )
// if a unix socket is specified we need to parse the URI to get the socket path
case strings . HasPrefix ( libvirtURI , "qemu+unix://" ) :
unixURI , err := url . Parse ( strings . TrimPrefix ( libvirtURI , "qemu+unix://" ) )
if err != nil {
return nil , err
}
libvirtSocketPath = unixURI . Query ( ) . Get ( "socket" )
if libvirtSocketPath == "" {
return nil , fmt . Errorf ( "socket path not specified in qemu+unix URI: %s" , libvirtURI )
}
}
metadataLibvirtURI := libvirtURI
if libvirtSocketPath != "." {
metadataLibvirtURI = "qemu:///system"
}
var firmware * string
if conf . Provider . QEMU . Firmware != "" {
firmware = & conf . Provider . QEMU . Firmware
}
2023-08-02 04:41:26 -04:00
nodeGroups := make ( map [ string ] terraform . QEMUNodeGroup )
for groupName , group := range conf . NodeGroups {
nodeGroups [ groupName ] = terraform . QEMUNodeGroup {
Role : role . FromString ( group . Role ) . TFString ( ) ,
InitialCount : group . InitialCount ,
DiskSize : group . StateDiskSizeGB ,
CPUCount : conf . Provider . QEMU . VCPUs ,
MemorySize : conf . Provider . QEMU . Memory ,
}
}
2023-07-21 04:04:29 -04:00
return & terraform . QEMUVariables {
Name : conf . Name ,
LibvirtURI : libvirtURI ,
LibvirtSocketPath : libvirtSocketPath ,
// TODO(malt3): auto select boot mode based on attestation variant.
// requires image info v2.
2023-08-02 04:41:26 -04:00
BootMode : "uefi" ,
2023-10-31 07:46:40 -04:00
ImagePath : imagePath ,
2023-08-02 04:41:26 -04:00
ImageFormat : conf . Provider . QEMU . ImageFormat ,
NodeGroups : nodeGroups ,
2023-07-21 04:04:29 -04:00
Machine : "q35" , // TODO(elchead): make configurable AB#3225
MetadataAPIImage : conf . Provider . QEMU . MetadataAPIImage ,
MetadataLibvirtURI : metadataLibvirtURI ,
NVRAM : conf . Provider . QEMU . NVRAM ,
2023-10-31 07:46:40 -04:00
Firmware : firmware ,
2023-07-21 04:04:29 -04:00
// TODO(malt3) enable once we have a way to auto-select values for these
// requires image info v2.
// BzImagePath: placeholder,
// InitrdPath: placeholder,
// KernelCmdline: placeholder,
2023-10-31 07:46:40 -04:00
} , nil
}
func toPtr [ T any ] ( v T ) * T {
return & v
2023-07-21 04:04:29 -04:00
}