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"
2023-08-08 09:42:06 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/cmd/pathprefix"
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-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-03-22 11:03:15 -04:00
return cmd
}
2023-01-04 04:46:29 -05:00
type createCmd struct {
log debugLog
2023-08-08 09:42:06 -04:00
pf pathprefix . PathPrefixer
2023-01-04 04:46:29 -05:00
}
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 )
2023-08-08 09:42:06 -04:00
if err := c . checkDirClean ( fileHandler ) ; err != nil {
2022-04-13 07:01:38 -04:00
return err
}
2023-08-16 09:38:40 -04:00
c . log . Debugf ( "Loading configuration file from %q" , c . pf . PrefixPrintablePath ( constants . ConfigFilename ) )
2023-08-04 07:53:51 -04:00
conf , err := config . New ( fileHandler , constants . ConfigFilename , 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 {
2023-07-25 08:20:25 -04:00
if err := validateCLIandConstellationVersionAreEqual ( constants . BinaryVersion ( ) , conf . Image , conf . MicroserviceVersion ) ; err != nil {
2023-06-27 12:24:35 -04:00
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 ( )
2023-08-02 04:36:55 -04: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 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-08-02 04:36:55 -04: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 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 {
2023-08-04 07:53:51 -04:00
Provider : provider ,
Config : conf ,
TFLogLevel : flags . tfLogLevel ,
TFWorkspace : constants . TerraformWorkingDir ,
2023-04-14 08:15:07 -04:00
}
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 {
2023-08-08 09:42:06 -04:00
return translateCreateErrors ( cmd , c . pf , 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
2023-08-04 07:53:51 -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-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
2023-08-08 09:42:06 -04:00
workDir , err := cmd . Flags ( ) . GetString ( "workspace" )
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-08-08 09:42:06 -04:00
c . log . Debugf ( "Workspace set to %q" , workDir )
c . pf = pathprefix . New ( workDir )
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 )
}
2023-08-16 09:38:40 -04:00
c . log . Debugf ( "Terraform logs will be written into %s at level %s" , c . pf . PrefixPrintablePath ( constants . TerraformLogFile ) , logLevel . String ( ) )
2023-04-14 08:15:07 -04:00
2022-04-13 07:01:38 -04:00
return createFlags {
2023-08-02 04:36:55 -04:00
tfLogLevel : logLevel ,
force : force ,
yes : yes ,
2022-04-13 07:01:38 -04:00
} , nil
}
// createFlags contains the parsed flags of the create command.
type createFlags struct {
2023-08-02 04:36:55 -04:00
tfLogLevel terraform . LogLevel
force bool
yes bool
2022-05-04 02:50:50 -04:00
}
2022-03-22 11:03:15 -04:00
// checkDirClean checks if files of a previous Constellation are left in the current working dir.
2023-08-08 09:42:06 -04:00
func ( c * createCmd ) checkDirClean ( fileHandler file . Handler ) error {
2023-01-04 04:46:29 -05:00
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 ) {
2023-08-16 09:38:40 -04:00
return fmt . Errorf ( "file '%s' already exists in working directory, run 'constellation terminate' before creating a new one" , c . pf . PrefixPrintablePath ( 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 ) {
2023-08-16 09:38:40 -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" , c . pf . PrefixPrintablePath ( 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" )
2023-08-04 07:53:51 -04:00
if _ , err := fileHandler . Stat ( constants . ClusterIDsFilename ) ; ! errors . Is ( err , fs . ErrNotExist ) {
2023-08-16 09:38:40 -04:00
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" , c . pf . PrefixPrintablePath ( constants . ClusterIDsFilename ) )
2022-07-29 04:01:10 -04:00
}
2022-03-22 11:03:15 -04:00
return nil
}
2022-04-13 07:01:38 -04:00
2023-08-08 09:42:06 -04:00
func translateCreateErrors ( cmd * cobra . Command , pf pathprefix . PathPrefixer , err error ) error {
2022-11-16 10:33:51 -05:00
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\"." )
2023-08-16 09:38:40 -04:00
cmd . PrintErrf ( "If this does not work, either move or delete the directory %q.\n" , pf . PrefixPrintablePath ( constants . TerraformWorkingDir ) )
2022-11-16 10:33:51 -05:00
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\"." )
2023-08-16 09:38:40 -04:00
cmd . PrintErrf ( "If this does not work, either move or delete the directory %q.\n" , pf . PrefixPrintablePath ( constants . TerraformWorkingDir ) )
2022-11-16 10:33:51 -05:00
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.
2023-07-25 08:20:25 -04:00
func validateCLIandConstellationVersionAreEqual ( cliVersion semver . Semver , imageVersion string , microserviceVersion semver . Semver ) error {
2023-06-27 12:24:35 -04:00
parsedImageVersion , err := versionsapi . NewVersionFromShortPath ( imageVersion , versionsapi . VersionKindImage )
if err != nil {
return fmt . Errorf ( "parsing image version: %w" , err )
}
2023-08-01 10:48:13 -04:00
semImage , err := semver . New ( parsedImageVersion . Version ( ) )
2023-06-27 12:24:35 -04:00
if err != nil {
return fmt . Errorf ( "parsing image semantical version: %w" , err )
}
2023-07-25 08:20:25 -04: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 12:24:35 -04:00
}
2023-07-25 08:20:25 -04: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 12:24:35 -04:00
}
return nil
}