2022-09-05 03:06:08 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
2022-03-22 11:03:15 -04:00
package cmd
import (
"errors"
"fmt"
"io/fs"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
2022-11-16 10:33:51 -05:00
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
2023-06-07 10:16:32 -04:00
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
2023-06-27 12:24:35 -04:00
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
2023-06-09 09:41:02 -04:00
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
2022-11-15 09:40:49 -05:00
"github.com/edgelesssys/constellation/v2/internal/config"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
2023-06-27 12:24:35 -04:00
"github.com/edgelesssys/constellation/v2/internal/semver"
2022-04-13 07:01:38 -04:00
"github.com/spf13/afero"
2022-03-22 11:03:15 -04:00
"github.com/spf13/cobra"
)
2022-06-08 02:14:28 -04:00
// NewCreateCmd returns a new cobra.Command for the create command.
func NewCreateCmd ( ) * cobra . Command {
2022-03-22 11:03:15 -04:00
cmd := & cobra . Command {
2022-09-07 09:38:29 -04:00
Use : "create" ,
2022-05-06 11:51:41 -04:00
Short : "Create instances on a cloud platform for your Constellation cluster" ,
2022-05-04 03:13:46 -04:00
Long : "Create instances on a cloud platform for your Constellation cluster." ,
2022-12-07 05:48:54 -05:00
Args : cobra . ExactArgs ( 0 ) ,
RunE : runCreate ,
2022-03-22 11:03:15 -04:00
}
2022-05-09 11:02:47 -04:00
cmd . Flags ( ) . BoolP ( "yes" , "y" , false , "create the cluster without further confirmation" )
2022-05-04 12:41:24 -04:00
cmd . Flags ( ) . IntP ( "control-plane-nodes" , "c" , 0 , "number of control-plane nodes (required)" )
2022-05-04 02:50:50 -04:00
must ( cobra . MarkFlagRequired ( cmd . Flags ( ) , "control-plane-nodes" ) )
2022-05-04 12:41:24 -04:00
cmd . Flags ( ) . IntP ( "worker-nodes" , "w" , 0 , "number of worker nodes (required)" )
2022-05-04 02:50:50 -04:00
must ( cobra . MarkFlagRequired ( cmd . Flags ( ) , "worker-nodes" ) )
2022-03-22 11:03:15 -04:00
return cmd
}
2023-01-04 04:46:29 -05:00
type createCmd struct {
log debugLog
}
2023-03-20 06:03:36 -04:00
func runCreate ( cmd * cobra . Command , _ [ ] string ) error {
2023-01-04 04:46:29 -05:00
log , err := newCLILogger ( cmd )
if err != nil {
return fmt . Errorf ( "creating logger: %w" , err )
}
defer log . Sync ( )
2023-01-18 07:10:24 -05:00
spinner , err := newSpinnerOrStderr ( cmd )
2023-01-04 05:00:07 -05:00
if err != nil {
return fmt . Errorf ( "creating spinner: %w" , err )
}
2022-10-07 13:35:07 -04:00
defer spinner . Stop ( )
2023-01-04 05:00:07 -05:00
fileHandler := file . NewHandler ( afero . NewOsFs ( ) )
2022-10-21 08:26:42 -04:00
creator := cloudcmd . NewCreator ( spinner )
2023-01-04 04:46:29 -05:00
c := & createCmd { log : log }
2023-06-07 10:16:32 -04:00
fetcher := attestationconfigapi . NewFetcher ( )
2023-06-01 07:55:46 -04:00
return c . create ( cmd , creator , fileHandler , spinner , fetcher )
2022-04-13 07:01:38 -04:00
}
2023-06-07 10:16:32 -04:00
func ( c * createCmd ) create ( cmd * cobra . Command , creator cloudCreator , fileHandler file . Handler , spinner spinnerInterf , fetcher attestationconfigapi . Fetcher ) ( retErr error ) {
2023-01-04 04:46:29 -05:00
flags , err := c . parseCreateFlags ( cmd )
2022-04-13 07:01:38 -04:00
if err != nil {
return err
}
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Using flags: %+v" , flags )
if err := c . checkDirClean ( fileHandler ) ; err != nil {
2022-04-13 07:01:38 -04:00
return err
}
2023-01-18 07:10:24 -05:00
c . log . Debugf ( "Loading configuration file from %q" , flags . configPath )
2023-06-01 07:55:46 -04:00
conf , err := config . New ( fileHandler , flags . configPath , fetcher , flags . force )
2023-03-31 13:19:10 -04:00
c . log . Debugf ( "Configuration file loaded: %+v" , conf )
2023-02-07 06:56:25 -05:00
var configValidationErr * config . ValidationError
if errors . As ( err , & configValidationErr ) {
cmd . PrintErrln ( configValidationErr . LongMessage ( ) )
}
2022-04-13 07:01:38 -04:00
if err != nil {
2023-02-07 06:56:25 -05:00
return err
2022-04-13 07:01:38 -04:00
}
2023-06-27 12:24:35 -04:00
if ! flags . force {
if err := validateCLIandConstellationVersionAreEqual ( constants . VersionInfo ( ) , conf . Image , conf . MicroserviceVersion ) ; err != nil {
return err
}
}
2022-04-13 07:01:38 -04:00
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Checking configuration for warnings" )
2022-09-06 07:05:49 -04:00
var printedAWarning bool
2022-11-22 12:47:08 -05:00
if ! conf . IsReleaseImage ( ) {
2022-11-10 04:27:24 -05:00
cmd . PrintErrln ( "Configured image doesn't look like a released production image. Double check image before deploying to production." )
2022-09-06 07:05:49 -04:00
printedAWarning = true
}
2023-06-19 10:51:39 -04:00
if conf . IsNamedLikeDebugImage ( ) && ! conf . IsDebugCluster ( ) {
cmd . PrintErrln ( "WARNING: A debug image is used but debugCluster is false." )
printedAWarning = true
}
2022-11-15 09:40:49 -05:00
if conf . IsDebugCluster ( ) {
2022-11-10 04:27:24 -05:00
cmd . PrintErrln ( "WARNING: Creating a debug cluster. This cluster is not secure and should only be used for debugging purposes." )
cmd . PrintErrln ( "DO NOT USE THIS CLUSTER IN PRODUCTION." )
2022-09-06 07:05:49 -04:00
printedAWarning = true
2022-08-16 09:53:54 -04:00
}
2023-05-03 05:11:53 -04:00
if conf . GetAttestationConfig ( ) . GetVariant ( ) . Equal ( variant . AzureTrustedLaunch { } ) {
2022-11-10 04:27:24 -05:00
cmd . PrintErrln ( "Disabling Confidential VMs is insecure. Use only for evaluation purposes." )
2022-09-06 07:05:49 -04:00
printedAWarning = true
2022-08-25 09:24:31 -04:00
}
2022-09-06 07:05:49 -04:00
// Print an extra new line later to separate warnings from the prompt message of the create command
if printedAWarning {
2022-11-10 04:27:24 -05:00
cmd . PrintErrln ( "" )
2022-09-06 07:05:49 -04:00
}
2022-11-15 09:40:49 -05:00
provider := conf . GetProvider ( )
2022-08-31 11:35:33 -04:00
var instanceType string
switch provider {
2022-10-21 06:24:18 -04:00
case cloudprovider . AWS :
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Configuring instance type for AWS" )
2022-11-15 09:40:49 -05:00
instanceType = conf . Provider . AWS . InstanceType
2022-08-31 11:35:33 -04:00
case cloudprovider . Azure :
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Configuring instance type for Azure" )
2022-11-15 09:40:49 -05:00
instanceType = conf . Provider . Azure . InstanceType
2022-08-31 11:35:33 -04:00
case cloudprovider . GCP :
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Configuring instance type for GCP" )
2022-11-15 09:40:49 -05:00
instanceType = conf . Provider . GCP . InstanceType
2023-02-27 12:19:52 -05:00
case cloudprovider . OpenStack :
c . log . Debugf ( "Configuring instance type for OpenStack" )
instanceType = conf . Provider . OpenStack . FlavorID
2022-09-26 09:52:31 -04:00
case cloudprovider . QEMU :
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Configuring instance type for QEMU" )
2022-11-15 09:40:49 -05:00
cpus := conf . Provider . QEMU . VCPUs
2022-09-26 09:52:31 -04:00
instanceType = fmt . Sprintf ( "%d-vCPU" , cpus )
2022-08-31 11:35:33 -04:00
}
2023-01-18 07:10:24 -05:00
c . log . Debugf ( "Configured with instance type %q" , instanceType )
2022-08-31 11:35:33 -04:00
2022-04-13 07:01:38 -04:00
if ! flags . yes {
// Ask user to confirm action.
2022-05-04 03:13:46 -04:00
cmd . Printf ( "The following Constellation cluster will be created:\n" )
2023-02-17 02:15:17 -05:00
cmd . Printf ( "%d control-plane node%s of type %s will be created.\n" , flags . controllerCount , isPlural ( flags . controllerCount ) , instanceType )
cmd . Printf ( "%d worker node%s of type %s will be created.\n" , flags . workerCount , isPlural ( flags . workerCount ) , instanceType )
2022-05-04 03:13:46 -04:00
ok , err := askToConfirm ( cmd , "Do you want to create this cluster?" )
2022-04-13 07:01:38 -04:00
if err != nil {
return err
}
if ! ok {
2022-05-04 03:13:46 -04:00
cmd . Println ( "The creation of the cluster was aborted." )
2022-04-13 07:01:38 -04:00
return nil
}
}
2022-10-07 13:35:07 -04:00
spinner . Start ( "Creating" , false )
2023-04-14 08:15:07 -04:00
opts := cloudcmd . CreateOptions {
Provider : provider ,
Config : conf ,
InsType : instanceType ,
ControlPlaneCount : flags . controllerCount ,
WorkerCount : flags . workerCount ,
TFLogLevel : flags . tfLogLevel ,
}
idFile , err := creator . Create ( cmd . Context ( ) , opts )
2022-10-04 12:17:05 -04:00
spinner . Stop ( )
2022-04-13 07:01:38 -04:00
if err != nil {
2022-11-16 10:33:51 -05:00
return translateCreateErrors ( cmd , err )
2022-04-13 07:01:38 -04:00
}
2023-02-03 05:05:42 -05:00
c . log . Debugf ( "Successfully created the cloud resources for the cluster" )
2022-04-13 07:01:38 -04:00
2022-10-11 06:24:33 -04:00
if err := fileHandler . WriteJSON ( constants . ClusterIDsFileName , idFile , file . OptNone ) ; err != nil {
2022-07-29 04:01:10 -04:00
return err
}
2022-05-04 03:13:46 -04:00
cmd . Println ( "Your Constellation cluster was created successfully." )
2022-04-13 07:01:38 -04:00
return nil
}
// parseCreateFlags parses the flags of the create command.
2023-01-04 04:46:29 -05:00
func ( c * createCmd ) parseCreateFlags ( cmd * cobra . Command ) ( createFlags , error ) {
2022-05-04 02:50:50 -04:00
controllerCount , err := cmd . Flags ( ) . GetInt ( "control-plane-nodes" )
if err != nil {
2022-06-09 10:10:42 -04:00
return createFlags { } , fmt . Errorf ( "parsing number of control-plane nodes: %w" , err )
2022-05-04 02:50:50 -04:00
}
2023-01-18 07:10:24 -05:00
c . log . Debugf ( "Control-plane nodes flag is %d" , controllerCount )
2022-05-04 02:50:50 -04:00
if controllerCount < constants . MinControllerCount {
return createFlags { } , fmt . Errorf ( "number of control-plane nodes must be at least %d" , constants . MinControllerCount )
}
workerCount , err := cmd . Flags ( ) . GetInt ( "worker-nodes" )
if err != nil {
2022-06-09 10:10:42 -04:00
return createFlags { } , fmt . Errorf ( "parsing number of worker nodes: %w" , err )
2022-05-04 02:50:50 -04:00
}
2023-01-18 07:10:24 -05:00
c . log . Debugf ( "Worker nodes flag is %d" , workerCount )
2022-05-04 02:50:50 -04:00
if workerCount < constants . MinWorkerCount {
return createFlags { } , fmt . Errorf ( "number of worker nodes must be at least %d" , constants . MinWorkerCount )
}
2022-04-13 07:01:38 -04:00
yes , err := cmd . Flags ( ) . GetBool ( "yes" )
if err != nil {
2023-04-14 08:15:07 -04:00
return createFlags { } , fmt . Errorf ( "parsing yes bool: %w" , err )
2022-04-13 07:01:38 -04:00
}
2023-01-18 07:10:24 -05:00
c . log . Debugf ( "Yes flag is %t" , yes )
2022-05-04 02:50:50 -04:00
2022-05-13 05:56:43 -04:00
configPath , err := cmd . Flags ( ) . GetString ( "config" )
2022-04-13 07:01:38 -04:00
if err != nil {
2022-06-09 10:10:42 -04:00
return createFlags { } , fmt . Errorf ( "parsing config path argument: %w" , err )
2022-04-13 07:01:38 -04:00
}
2023-01-18 07:10:24 -05:00
c . log . Debugf ( "Configuration path flag is %q" , configPath )
2022-04-13 07:01:38 -04:00
2023-01-31 05:45:31 -05:00
force , err := cmd . Flags ( ) . GetBool ( "force" )
if err != nil {
return createFlags { } , fmt . Errorf ( "parsing force argument: %w" , err )
}
c . log . Debugf ( "force flag is %t" , force )
2023-04-14 08:15:07 -04:00
logLevelString , err := cmd . Flags ( ) . GetString ( "tf-log" )
if err != nil {
return createFlags { } , fmt . Errorf ( "parsing tf-log string: %w" , err )
}
logLevel , err := terraform . ParseLogLevel ( logLevelString )
if err != nil {
return createFlags { } , fmt . Errorf ( "parsing Terraform log level %s: %w" , logLevelString , err )
}
c . log . Debugf ( "Terraform logs will be written into %s at level %s" , constants . TerraformLogFile , logLevel . String ( ) )
2022-04-13 07:01:38 -04:00
return createFlags {
2022-05-04 02:50:50 -04:00
controllerCount : controllerCount ,
workerCount : workerCount ,
2022-05-13 05:56:43 -04:00
configPath : configPath ,
2023-04-14 08:15:07 -04:00
tfLogLevel : logLevel ,
2023-01-31 05:45:31 -05:00
force : force ,
2022-05-04 02:50:50 -04:00
yes : yes ,
2022-04-13 07:01:38 -04:00
} , nil
}
// createFlags contains the parsed flags of the create command.
type createFlags struct {
2022-05-04 02:50:50 -04:00
controllerCount int
workerCount int
2022-05-13 05:56:43 -04:00
configPath string
2023-04-14 08:15:07 -04:00
tfLogLevel terraform . LogLevel
2023-01-31 05:45:31 -05:00
force bool
2022-05-04 02:50:50 -04:00
yes bool
}
2022-03-22 11:03:15 -04:00
// checkDirClean checks if files of a previous Constellation are left in the current working dir.
2023-01-04 04:46:29 -05:00
func ( c * createCmd ) checkDirClean ( fileHandler file . Handler ) error {
c . log . Debugf ( "Checking admin configuration file" )
2022-04-06 04:36:58 -04:00
if _ , err := fileHandler . Stat ( constants . AdminConfFilename ) ; ! errors . Is ( err , fs . ErrNotExist ) {
return fmt . Errorf ( "file '%s' already exists in working directory, run 'constellation terminate' before creating a new one" , constants . AdminConfFilename )
2022-03-22 11:03:15 -04:00
}
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Checking master secrets file" )
2022-04-06 04:36:58 -04:00
if _ , err := fileHandler . Stat ( constants . MasterSecretFilename ) ; ! errors . Is ( err , fs . ErrNotExist ) {
2022-06-09 10:10:42 -04:00
return fmt . Errorf ( "file '%s' already exists in working directory. Constellation won't overwrite previous master secrets. Move it somewhere or delete it before creating a new cluster" , constants . MasterSecretFilename )
2022-03-22 11:03:15 -04:00
}
2023-01-04 04:46:29 -05:00
c . log . Debugf ( "Checking cluster IDs file" )
2022-07-29 04:01:10 -04:00
if _ , err := fileHandler . Stat ( constants . ClusterIDsFileName ) ; ! errors . Is ( err , fs . ErrNotExist ) {
return fmt . Errorf ( "file '%s' already exists in working directory. Constellation won't overwrite previous cluster IDs. Move it somewhere or delete it before creating a new cluster" , constants . ClusterIDsFileName )
}
2022-03-22 11:03:15 -04:00
return nil
}
2022-04-13 07:01:38 -04:00
2022-11-16 10:33:51 -05:00
func translateCreateErrors ( cmd * cobra . Command , err error ) error {
switch {
case errors . Is ( err , terraform . ErrTerraformWorkspaceDifferentFiles ) :
cmd . PrintErrln ( "\nYour current working directory contains an existing Terraform workspace which does not match the expected state." )
cmd . PrintErrln ( "This can be due to a mix up between providers, versions or an otherwise corrupted workspace." )
cmd . PrintErrln ( "Before creating a new cluster, try \"constellation terminate\"." )
cmd . PrintErrf ( "If this does not work, either move or delete the directory %q.\n" , constants . TerraformWorkingDir )
cmd . PrintErrln ( "Please only delete the directory if you made sure that all created cloud resources have been terminated." )
return err
case errors . Is ( err , terraform . ErrTerraformWorkspaceExistsWithDifferentVariables ) :
cmd . PrintErrln ( "\nYour current working directory contains an existing Terraform workspace which was initiated with different input variables." )
cmd . PrintErrln ( "This can be the case if you have tried to create a cluster before with different options which did not complete, or the workspace is corrupted." )
cmd . PrintErrln ( "Before creating a new cluster, try \"constellation terminate\"." )
cmd . PrintErrf ( "If this does not work, either move or delete the directory %q.\n" , constants . TerraformWorkingDir )
cmd . PrintErrln ( "Please only delete the directory if you made sure that all created cloud resources have been terminated." )
return err
default :
return err
}
}
2023-02-17 02:15:17 -05:00
func isPlural ( count int ) string {
if count == 1 {
return ""
}
return "s"
}
2022-06-08 02:14:28 -04:00
func must ( err error ) {
if err != nil {
panic ( err )
}
}
2023-06-27 12:24:35 -04:00
// validateCLIandConstellationVersionAreEqual checks if the image and microservice version are equal (down to patch level) to the CLI version.
func validateCLIandConstellationVersionAreEqual ( cliVersion , imageVersion , microserviceVersion string ) error {
parsedImageVersion , err := versionsapi . NewVersionFromShortPath ( imageVersion , versionsapi . VersionKindImage )
if err != nil {
return fmt . Errorf ( "parsing image version: %w" , err )
}
semImage , err := semver . New ( parsedImageVersion . Version )
if err != nil {
return fmt . Errorf ( "parsing image semantical version: %w" , err )
}
semMicro , err := semver . New ( microserviceVersion )
if err != nil {
return fmt . Errorf ( "parsing microservice version: %w" , err )
}
semCLI , err := semver . New ( cliVersion )
if err != nil {
return fmt . Errorf ( "parsing binary version: %w" , err )
}
if ! semCLI . MajorMinorEqual ( semImage ) {
return fmt . Errorf ( "image version %q does not match the major and minor version of the cli version %q" , semImage . String ( ) , semCLI . String ( ) )
}
if semCLI . Compare ( semMicro ) != 0 {
return fmt . Errorf ( "cli version %q does not match microservice version %q" , semCLI . String ( ) , semMicro . String ( ) )
}
return nil
}