2022-09-05 09:06:08 +02:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
2022-03-22 16:03:15 +01:00
package cmd
import (
"errors"
"fmt"
"io/fs"
2023-10-31 12:46:40 +01:00
"os"
"path/filepath"
2022-03-22 16:03:15 +01:00
2022-09-21 13:47:57 +02:00
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
2023-09-25 16:19:43 +02:00
"github.com/edgelesssys/constellation/v2/cli/internal/state"
2023-06-07 16:16:32 +02:00
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
2023-06-27 18:24:35 +02:00
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
2023-06-09 15:41:02 +02:00
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
2022-11-15 15:40:49 +01:00
"github.com/edgelesssys/constellation/v2/internal/config"
2022-09-21 13:47:57 +02:00
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
2023-06-27 18:24:35 +02:00
"github.com/edgelesssys/constellation/v2/internal/semver"
2022-04-13 13:01:38 +02:00
"github.com/spf13/afero"
2022-03-22 16:03:15 +01:00
"github.com/spf13/cobra"
2023-10-16 15:05:29 +02:00
"github.com/spf13/pflag"
2022-03-22 16:03:15 +01:00
)
2022-06-08 08:14:28 +02:00
// NewCreateCmd returns a new cobra.Command for the create command.
func NewCreateCmd ( ) * cobra . Command {
2022-03-22 16:03:15 +01:00
cmd := & cobra . Command {
2022-09-07 15:38:29 +02:00
Use : "create" ,
2022-05-06 17:51:41 +02:00
Short : "Create instances on a cloud platform for your Constellation cluster" ,
2022-05-04 09:13:46 +02:00
Long : "Create instances on a cloud platform for your Constellation cluster." ,
2022-12-07 11:48:54 +01:00
Args : cobra . ExactArgs ( 0 ) ,
RunE : runCreate ,
2022-03-22 16:03:15 +01:00
}
2022-05-09 17:02:47 +02:00
cmd . Flags ( ) . BoolP ( "yes" , "y" , false , "create the cluster without further confirmation" )
2022-03-22 16:03:15 +01:00
return cmd
}
2023-10-16 15:05:29 +02:00
// createFlags contains the parsed flags of the create command.
type createFlags struct {
rootFlags
yes bool
}
// parse parses the flags of the create command.
func ( f * createFlags ) parse ( flags * pflag . FlagSet ) error {
if err := f . rootFlags . parse ( flags ) ; err != nil {
return err
}
yes , err := flags . GetBool ( "yes" )
if err != nil {
return fmt . Errorf ( "getting 'yes' flag: %w" , err )
}
f . yes = yes
return nil
}
2023-01-04 09:46:29 +00:00
type createCmd struct {
2023-10-16 15:05:29 +02:00
log debugLog
flags createFlags
2023-01-04 09:46:29 +00:00
}
2023-03-20 11:03:36 +01:00
func runCreate ( cmd * cobra . Command , _ [ ] string ) error {
2023-01-04 09:46:29 +00:00
log , err := newCLILogger ( cmd )
if err != nil {
return fmt . Errorf ( "creating logger: %w" , err )
}
defer log . Sync ( )
2023-01-18 13:10:24 +01:00
spinner , err := newSpinnerOrStderr ( cmd )
2023-01-04 11:00:07 +01:00
if err != nil {
return fmt . Errorf ( "creating spinner: %w" , err )
}
2022-10-07 19:35:07 +02:00
defer spinner . Stop ( )
2023-01-04 11:00:07 +01:00
fileHandler := file . NewHandler ( afero . NewOsFs ( ) )
2023-01-04 09:46:29 +00:00
c := & createCmd { log : log }
2023-10-16 15:05:29 +02:00
if err := c . flags . parse ( cmd . Flags ( ) ) ; err != nil {
return err
}
c . log . Debugf ( "Using flags: %+v" , c . flags )
2023-10-31 12:46:40 +01:00
applier , removeInstaller , err := cloudcmd . NewApplier (
cmd . Context ( ) ,
spinner ,
constants . TerraformWorkingDir ,
filepath . Join ( constants . UpgradeDir , "create" ) , // Not used by create
c . flags . tfLogLevel ,
fileHandler ,
)
if err != nil {
return err
}
defer removeInstaller ( )
2023-06-07 16:16:32 +02:00
fetcher := attestationconfigapi . NewFetcher ( )
2023-10-31 12:46:40 +01:00
return c . create ( cmd , applier , fileHandler , spinner , fetcher )
2022-04-13 13:01:38 +02:00
}
2023-10-31 12:46:40 +01:00
func ( c * createCmd ) create ( cmd * cobra . Command , applier cloudApplier , fileHandler file . Handler , spinner spinnerInterf , fetcher attestationconfigapi . Fetcher ) ( retErr error ) {
2023-08-08 15:42:06 +02:00
if err := c . checkDirClean ( fileHandler ) ; err != nil {
2022-04-13 13:01:38 +02:00
return err
}
2023-10-16 15:05:29 +02:00
c . log . Debugf ( "Loading configuration file from %q" , c . flags . pathPrefixer . PrefixPrintablePath ( constants . ConfigFilename ) )
conf , err := config . New ( fileHandler , constants . ConfigFilename , fetcher , c . flags . force )
2023-03-31 19:19:10 +02:00
c . log . Debugf ( "Configuration file loaded: %+v" , conf )
2023-02-07 12:56:25 +01:00
var configValidationErr * config . ValidationError
if errors . As ( err , & configValidationErr ) {
cmd . PrintErrln ( configValidationErr . LongMessage ( ) )
}
2022-04-13 13:01:38 +02:00
if err != nil {
2023-02-07 12:56:25 +01:00
return err
2022-04-13 13:01:38 +02:00
}
2023-10-16 15:05:29 +02:00
if ! c . flags . force {
2023-07-25 14:20:25 +02:00
if err := validateCLIandConstellationVersionAreEqual ( constants . BinaryVersion ( ) , conf . Image , conf . MicroserviceVersion ) ; err != nil {
2023-06-27 18:24:35 +02:00
return err
}
}
2022-04-13 13:01:38 +02:00
2023-01-04 09:46:29 +00:00
c . log . Debugf ( "Checking configuration for warnings" )
2022-09-06 13:05:49 +02:00
var printedAWarning bool
2022-11-22 18:47:08 +01:00
if ! conf . IsReleaseImage ( ) {
2022-11-10 10:27:24 +01:00
cmd . PrintErrln ( "Configured image doesn't look like a released production image. Double check image before deploying to production." )
2022-09-06 13:05:49 +02:00
printedAWarning = true
}
2023-06-19 16:51:39 +02:00
if conf . IsNamedLikeDebugImage ( ) && ! conf . IsDebugCluster ( ) {
cmd . PrintErrln ( "WARNING: A debug image is used but debugCluster is false." )
printedAWarning = true
}
2022-11-15 15:40:49 +01:00
if conf . IsDebugCluster ( ) {
2022-11-10 10:27:24 +01: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 13:05:49 +02:00
printedAWarning = true
2022-08-16 15:53:54 +02:00
}
2023-05-03 11:11:53 +02:00
if conf . GetAttestationConfig ( ) . GetVariant ( ) . Equal ( variant . AzureTrustedLaunch { } ) {
2022-11-10 10:27:24 +01:00
cmd . PrintErrln ( "Disabling Confidential VMs is insecure. Use only for evaluation purposes." )
2022-09-06 13:05:49 +02:00
printedAWarning = true
2022-08-25 15:24:31 +02:00
}
2022-09-06 13:05:49 +02:00
// Print an extra new line later to separate warnings from the prompt message of the create command
if printedAWarning {
2022-11-10 10:27:24 +01:00
cmd . PrintErrln ( "" )
2022-09-06 13:05:49 +02:00
}
2023-08-02 10:36:55 +02:00
controlPlaneGroup , ok := conf . NodeGroups [ constants . DefaultControlPlaneGroupName ]
if ! ok {
return fmt . Errorf ( "default control-plane node group %q not found in configuration" , constants . DefaultControlPlaneGroupName )
}
workerGroup , ok := conf . NodeGroups [ constants . DefaultWorkerGroupName ]
if ! ok {
return fmt . Errorf ( "default worker node group %q not found in configuration" , constants . DefaultWorkerGroupName )
}
otherGroupNames := make ( [ ] string , 0 , len ( conf . NodeGroups ) - 2 )
for groupName := range conf . NodeGroups {
if groupName != constants . DefaultControlPlaneGroupName && groupName != constants . DefaultWorkerGroupName {
otherGroupNames = append ( otherGroupNames , groupName )
}
}
if len ( otherGroupNames ) > 0 {
c . log . Debugf ( "Creating %d additional node groups: %v" , len ( otherGroupNames ) , otherGroupNames )
2022-08-31 17:35:33 +02:00
}
2023-10-16 15:05:29 +02:00
if ! c . flags . yes {
2022-04-13 13:01:38 +02:00
// Ask user to confirm action.
2022-05-04 09:13:46 +02:00
cmd . Printf ( "The following Constellation cluster will be created:\n" )
2023-08-02 10:36:55 +02:00
cmd . Printf ( " %d control-plane node%s of type %s will be created.\n" , controlPlaneGroup . InitialCount , isPlural ( controlPlaneGroup . InitialCount ) , controlPlaneGroup . InstanceType )
cmd . Printf ( " %d worker node%s of type %s will be created.\n" , workerGroup . InitialCount , isPlural ( workerGroup . InitialCount ) , workerGroup . InstanceType )
for _ , groupName := range otherGroupNames {
group := conf . NodeGroups [ groupName ]
cmd . Printf ( " group %s with %d node%s of type %s will be created.\n" , groupName , group . InitialCount , isPlural ( group . InitialCount ) , group . InstanceType )
}
2022-05-04 09:13:46 +02:00
ok , err := askToConfirm ( cmd , "Do you want to create this cluster?" )
2022-04-13 13:01:38 +02:00
if err != nil {
return err
}
if ! ok {
2022-05-04 09:13:46 +02:00
cmd . Println ( "The creation of the cluster was aborted." )
2022-04-13 13:01:38 +02:00
return nil
}
}
2022-10-07 19:35:07 +02:00
spinner . Start ( "Creating" , false )
2023-10-31 12:46:40 +01:00
if _ , err := applier . Plan ( cmd . Context ( ) , conf ) ; err != nil {
return fmt . Errorf ( "planning infrastructure creation: %w" , err )
2023-04-14 14:15:07 +02:00
}
2023-10-31 12:46:40 +01:00
infraState , err := applier . Apply ( cmd . Context ( ) , conf . GetProvider ( ) , cloudcmd . WithRollbackOnError )
2022-10-04 19:17:05 +03:00
spinner . Stop ( )
2022-04-13 13:01:38 +02:00
if err != nil {
2023-10-26 10:55:50 +02:00
return err
2022-04-13 13:01:38 +02:00
}
2023-02-03 10:05:42 +00:00
c . log . Debugf ( "Successfully created the cloud resources for the cluster" )
2022-04-13 13:01:38 +02:00
2023-10-17 14:37:09 +02:00
stateFile , err := state . CreateOrRead ( fileHandler , constants . StateFilename )
2023-10-16 20:18:59 +02:00
if err != nil {
return fmt . Errorf ( "reading state file: %w" , err )
}
stateFile = stateFile . SetInfrastructure ( infraState )
if err := stateFile . WriteToFile ( fileHandler , constants . StateFilename ) ; err != nil {
2023-10-09 13:04:29 +02:00
return fmt . Errorf ( "writing state file: %w" , err )
2023-09-25 16:19:43 +02:00
}
2022-07-29 10:01:10 +02:00
2022-05-04 09:13:46 +02:00
cmd . Println ( "Your Constellation cluster was created successfully." )
2022-04-13 13:01:38 +02:00
return nil
}
2022-03-22 16:03:15 +01:00
// checkDirClean checks if files of a previous Constellation are left in the current working dir.
2023-08-08 15:42:06 +02:00
func ( c * createCmd ) checkDirClean ( fileHandler file . Handler ) error {
2023-01-04 09:46:29 +00:00
c . log . Debugf ( "Checking admin configuration file" )
2022-04-06 10:36:58 +02:00
if _ , err := fileHandler . Stat ( constants . AdminConfFilename ) ; ! errors . Is ( err , fs . ErrNotExist ) {
2023-10-16 15:05:29 +02:00
return fmt . Errorf (
"file '%s' already exists in working directory, run 'constellation terminate' before creating a new one" ,
c . flags . pathPrefixer . PrefixPrintablePath ( constants . AdminConfFilename ) ,
)
2022-03-22 16:03:15 +01:00
}
2023-01-04 09:46:29 +00:00
c . log . Debugf ( "Checking master secrets file" )
2022-04-06 10:36:58 +02:00
if _ , err := fileHandler . Stat ( constants . MasterSecretFilename ) ; ! errors . Is ( err , fs . ErrNotExist ) {
2023-10-16 15:05:29 +02: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" ,
c . flags . pathPrefixer . PrefixPrintablePath ( constants . MasterSecretFilename ) ,
)
2022-03-22 16:03:15 +01:00
}
2023-10-31 12:46:40 +01:00
c . log . Debugf ( "Checking terraform working directory" )
if clean , err := fileHandler . IsEmpty ( constants . TerraformWorkingDir ) ; err != nil && ! errors . Is ( err , os . ErrNotExist ) {
return fmt . Errorf ( "checking if terraform working directory is empty: %w" , err )
} else if err == nil && ! clean {
2023-10-26 10:55:50 +02:00
return fmt . Errorf (
2023-10-31 12:46:40 +01:00
"directory '%s' already exists and is not empty, run 'constellation terminate' before creating a new one" ,
2023-10-26 10:55:50 +02:00
c . flags . pathPrefixer . PrefixPrintablePath ( constants . TerraformWorkingDir ) ,
)
}
2022-03-22 16:03:15 +01:00
return nil
}
2022-04-13 13:01:38 +02:00
2023-02-17 08:15:17 +01:00
func isPlural ( count int ) string {
if count == 1 {
return ""
}
return "s"
}
2023-06-27 18:24:35 +02:00
// validateCLIandConstellationVersionAreEqual checks if the image and microservice version are equal (down to patch level) to the CLI version.
2023-07-25 14:20:25 +02:00
func validateCLIandConstellationVersionAreEqual ( cliVersion semver . Semver , imageVersion string , microserviceVersion semver . Semver ) error {
2023-06-27 18:24:35 +02:00
parsedImageVersion , err := versionsapi . NewVersionFromShortPath ( imageVersion , versionsapi . VersionKindImage )
if err != nil {
return fmt . Errorf ( "parsing image version: %w" , err )
}
2023-08-01 16:48:13 +02:00
semImage , err := semver . New ( parsedImageVersion . Version ( ) )
2023-06-27 18:24:35 +02:00
if err != nil {
return fmt . Errorf ( "parsing image semantical version: %w" , err )
}
2023-07-25 14:20:25 +02:00
if ! cliVersion . MajorMinorEqual ( semImage ) {
return fmt . Errorf ( "image version %q does not match the major and minor version of the cli version %q" , semImage . String ( ) , cliVersion . String ( ) )
2023-06-27 18:24:35 +02:00
}
2023-07-25 14:20:25 +02:00
if cliVersion . Compare ( microserviceVersion ) != 0 {
return fmt . Errorf ( "cli version %q does not match microservice version %q" , cliVersion . String ( ) , microserviceVersion . String ( ) )
2023-06-27 18:24:35 +02:00
}
return nil
}