2022-03-22 11:03:15 -04:00
package cmd
import (
2022-06-21 11:59:12 -04:00
"context"
2022-03-22 11:03:15 -04:00
"encoding/base64"
"errors"
"fmt"
"io"
"io/fs"
"net"
2022-05-13 07:10:27 -04:00
"strconv"
2022-04-05 03:13:09 -04:00
"text/tabwriter"
2022-06-21 11:59:12 -04:00
"time"
2022-03-22 11:03:15 -04:00
2022-06-29 09:26:29 -04:00
"github.com/edgelesssys/constellation/bootstrapper/initproto"
2022-06-07 10:30:41 -04:00
"github.com/edgelesssys/constellation/cli/internal/azure"
2022-06-08 02:26:08 -04:00
"github.com/edgelesssys/constellation/cli/internal/cloudcmd"
2022-06-07 08:52:47 -04:00
"github.com/edgelesssys/constellation/cli/internal/gcp"
2022-06-07 05:08:44 -04:00
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
2022-06-08 02:17:52 -04:00
"github.com/edgelesssys/constellation/internal/cloud/cloudtypes"
2022-03-22 11:03:15 -04:00
"github.com/edgelesssys/constellation/internal/config"
2022-04-06 04:36:58 -04:00
"github.com/edgelesssys/constellation/internal/constants"
2022-07-26 04:58:39 -04:00
"github.com/edgelesssys/constellation/internal/crypto"
2022-05-16 11:32:00 -04:00
"github.com/edgelesssys/constellation/internal/deploy/ssh"
"github.com/edgelesssys/constellation/internal/file"
2022-06-21 11:59:12 -04:00
"github.com/edgelesssys/constellation/internal/grpc/dialer"
2022-07-21 09:20:12 -04:00
grpcRetry "github.com/edgelesssys/constellation/internal/grpc/retry"
"github.com/edgelesssys/constellation/internal/retry"
2022-03-22 11:03:15 -04:00
"github.com/edgelesssys/constellation/internal/state"
2022-06-29 10:17:23 -04:00
kms "github.com/edgelesssys/constellation/kms/setup"
2022-04-05 03:11:45 -04:00
"github.com/spf13/afero"
"github.com/spf13/cobra"
2022-06-21 11:59:12 -04:00
"google.golang.org/grpc"
2022-03-22 11:03:15 -04:00
)
2022-06-08 02:14:28 -04:00
// NewInitCmd returns a new cobra.Command for the init command.
func NewInitCmd ( ) * cobra . Command {
2022-03-22 11:03:15 -04:00
cmd := & cobra . Command {
Use : "init" ,
2022-05-06 11:51:41 -04:00
Short : "Initialize the Constellation cluster" ,
2022-05-04 03:13:46 -04:00
Long : "Initialize the Constellation cluster. Start your confidential Kubernetes." ,
2022-03-22 11:03:15 -04:00
ValidArgsFunction : initCompletion ,
Args : cobra . ExactArgs ( 0 ) ,
RunE : runInitialize ,
}
2022-05-09 11:02:47 -04:00
cmd . Flags ( ) . String ( "master-secret" , "" , "path to base64-encoded master secret" )
2022-05-06 11:51:41 -04:00
cmd . Flags ( ) . Bool ( "autoscale" , false , "enable Kubernetes cluster-autoscaler" )
2022-03-22 11:03:15 -04:00
return cmd
}
// runInitialize runs the initialize command.
func runInitialize ( cmd * cobra . Command , args [ ] string ) error {
fileHandler := file . NewHandler ( afero . NewOsFs ( ) )
2022-04-13 07:01:38 -04:00
serviceAccountCreator := cloudcmd . NewServiceAccountCreator ( )
2022-06-21 11:59:12 -04:00
dialer := dialer . New ( nil , nil , & net . Dialer { } )
return initialize ( cmd , dialer , serviceAccountCreator , fileHandler )
2022-03-22 11:03:15 -04:00
}
2022-06-29 09:26:29 -04:00
// initialize initializes a Constellation.
2022-06-21 11:59:12 -04:00
func initialize ( cmd * cobra . Command , dialer grpcDialer , serviceAccCreator serviceAccountCreator ,
fileHandler file . Handler ,
2022-03-22 11:03:15 -04:00
) error {
2022-04-13 09:01:02 -04:00
flags , err := evalFlagArgs ( cmd , fileHandler )
if err != nil {
return err
}
2022-05-23 09:01:39 -04:00
var stat state . ConstellationState
err = fileHandler . ReadJSON ( constants . StateFilename , & stat )
if errors . Is ( err , fs . ErrNotExist ) {
2022-06-09 10:10:42 -04:00
return fmt . Errorf ( "missing Constellation state file: %w. Please do 'constellation create ...' before 'constellation init'" , err )
2022-05-23 09:01:39 -04:00
} else if err != nil {
2022-06-09 10:10:42 -04:00
return fmt . Errorf ( "loading Constellation state file: %w" , err )
2022-05-23 09:01:39 -04:00
}
provider := cloudprovider . FromString ( stat . CloudProvider )
config , err := readConfig ( cmd . OutOrStdout ( ) , fileHandler , flags . configPath , provider )
2022-03-22 11:03:15 -04:00
if err != nil {
2022-06-09 10:10:42 -04:00
return fmt . Errorf ( "reading and validating config: %w" , err )
2022-03-22 11:03:15 -04:00
}
2022-05-17 04:52:37 -04:00
var sshUsers [ ] * ssh . UserKey
for _ , user := range config . SSHUsers {
sshUsers = append ( sshUsers , & ssh . UserKey {
Username : user . Username ,
PublicKey : user . PublicKey ,
} )
}
2022-05-23 09:01:39 -04:00
validators , err := cloudcmd . NewValidators ( provider , config )
2022-04-19 11:02:02 -04:00
if err != nil {
return err
2022-03-22 11:03:15 -04:00
}
2022-04-19 11:02:02 -04:00
cmd . Print ( validators . WarningsIncludeInit ( ) )
2022-03-22 11:03:15 -04:00
2022-04-05 03:11:45 -04:00
cmd . Println ( "Creating service account ..." )
2022-06-28 05:19:03 -04:00
serviceAccount , stat , err := serviceAccCreator . Create ( cmd . Context ( ) , stat , config )
2022-03-22 11:03:15 -04:00
if err != nil {
return err
}
2022-04-06 04:36:58 -04:00
if err := fileHandler . WriteJSON ( constants . StateFilename , stat , file . OptOverwrite ) ; err != nil {
2022-03-22 11:03:15 -04:00
return err
}
2022-07-08 04:59:59 -04:00
controlPlanes , workers , err := getScalingGroupsFromState ( stat , config )
2022-03-22 11:03:15 -04:00
if err != nil {
return err
}
var autoscalingNodeGroups [ ] string
2022-04-13 09:01:02 -04:00
if flags . autoscale {
2022-06-29 09:26:29 -04:00
autoscalingNodeGroups = append ( autoscalingNodeGroups , workers . GroupID )
2022-03-22 11:03:15 -04:00
}
2022-06-21 11:59:12 -04:00
req := & initproto . InitRequest {
AutoscalingNodeGroups : autoscalingNodeGroups ,
MasterSecret : flags . masterSecret ,
KmsUri : kms . ClusterKMSURI ,
StorageUri : kms . NoStoreURI ,
KeyEncryptionKeyId : "" ,
UseExistingKek : false ,
CloudServiceAccountUri : serviceAccount ,
2022-07-18 06:28:02 -04:00
KubernetesVersion : config . KubernetesVersion ,
2022-06-21 11:59:12 -04:00
SshUserKeys : ssh . ToProtoSlice ( sshUsers ) ,
2022-03-22 11:03:15 -04:00
}
2022-07-08 04:59:59 -04:00
resp , err := initCall ( cmd . Context ( ) , dialer , stat . BootstrapperHost , req )
2022-03-22 11:03:15 -04:00
if err != nil {
return err
}
2022-07-05 08:14:11 -04:00
if err := writeOutput ( resp , controlPlanes . PublicIPs ( ) [ 0 ] , cmd . OutOrStdout ( ) , fileHandler ) ; err != nil {
2022-04-12 08:20:46 -04:00
return err
}
2022-03-22 11:03:15 -04:00
return nil
}
2022-06-21 11:59:12 -04:00
func initCall ( ctx context . Context , dialer grpcDialer , ip string , req * initproto . InitRequest ) ( * initproto . InitResponse , error ) {
doer := & initDoer {
dialer : dialer ,
2022-06-29 09:26:29 -04:00
endpoint : net . JoinHostPort ( ip , strconv . Itoa ( constants . BootstrapperPort ) ) ,
2022-06-21 11:59:12 -04:00
req : req ,
2022-03-22 11:03:15 -04:00
}
2022-07-21 09:20:12 -04:00
retrier := retry . NewIntervalRetrier ( doer , 30 * time . Second , grpcRetry . ServiceIsUnavailable )
2022-06-29 08:28:37 -04:00
if err := retrier . Do ( ctx ) ; err != nil {
2022-06-21 11:59:12 -04:00
return nil , err
2022-03-22 11:03:15 -04:00
}
2022-06-21 11:59:12 -04:00
return doer . resp , nil
2022-03-22 11:03:15 -04:00
}
2022-06-21 11:59:12 -04:00
type initDoer struct {
dialer grpcDialer
endpoint string
req * initproto . InitRequest
resp * initproto . InitResponse
2022-03-22 11:03:15 -04:00
}
2022-06-21 11:59:12 -04:00
func ( d * initDoer ) Do ( ctx context . Context ) error {
conn , err := d . dialer . Dial ( ctx , d . endpoint )
if err != nil {
return fmt . Errorf ( "dialing init server: %w" , err )
}
2022-07-05 08:14:11 -04:00
defer conn . Close ( )
2022-06-21 11:59:12 -04:00
protoClient := initproto . NewAPIClient ( conn )
resp , err := protoClient . Init ( ctx , d . req )
2022-03-29 05:38:14 -04:00
if err != nil {
2022-07-05 08:14:11 -04:00
return fmt . Errorf ( "init call: %w" , err )
2022-03-29 05:38:14 -04:00
}
2022-06-21 11:59:12 -04:00
d . resp = resp
return nil
2022-03-29 05:38:14 -04:00
}
2022-07-05 08:14:11 -04:00
func writeOutput ( resp * initproto . InitResponse , ip string , wr io . Writer , fileHandler file . Handler ) error {
2022-04-27 08:21:36 -04:00
fmt . Fprint ( wr , "Your Constellation cluster was successfully initialized.\n\n" )
2022-04-05 03:13:09 -04:00
2022-07-05 08:14:11 -04:00
ownerID := base64 . StdEncoding . EncodeToString ( resp . OwnerId )
clusterID := base64 . StdEncoding . EncodeToString ( resp . ClusterId )
2022-04-05 03:13:09 -04:00
tw := tabwriter . NewWriter ( wr , 0 , 0 , 2 , ' ' , 0 )
2022-07-26 04:58:39 -04:00
// writeRow(tw, "Constellation cluster's owner identifier", ownerID)
2022-07-27 10:10:50 -04:00
writeRow ( tw , "Constellation cluster identifier" , clusterID )
2022-04-06 04:36:58 -04:00
writeRow ( tw , "Kubernetes configuration" , constants . AdminConfFilename )
2022-04-05 03:13:09 -04:00
tw . Flush ( )
fmt . Fprintln ( wr )
2022-06-21 11:59:12 -04:00
if err := fileHandler . Write ( constants . AdminConfFilename , resp . Kubeconfig , file . OptNone ) ; err != nil {
2022-04-05 03:13:09 -04:00
return fmt . Errorf ( "write kubeconfig: %w" , err )
2022-03-22 11:03:15 -04:00
}
2022-04-05 03:13:09 -04:00
2022-07-05 08:14:11 -04:00
idFile := clusterIDsFile {
ClusterID : clusterID ,
OwnerID : ownerID ,
Endpoint : net . JoinHostPort ( ip , strconv . Itoa ( constants . VerifyServiceNodePortGRPC ) ) ,
}
2022-07-05 07:52:36 -04:00
if err := fileHandler . WriteJSON ( constants . ClusterIDsFileName , idFile , file . OptNone ) ; err != nil {
2022-07-01 04:57:29 -04:00
return fmt . Errorf ( "writing Constellation id file: %w" , err )
}
2022-05-04 03:13:46 -04:00
fmt . Fprintln ( wr , "You can now connect to your cluster by executing:" )
2022-04-06 04:36:58 -04:00
fmt . Fprintf ( wr , "\texport KUBECONFIG=\"$PWD/%s\"\n" , constants . AdminConfFilename )
2022-03-22 11:03:15 -04:00
return nil
}
2022-04-05 03:13:09 -04:00
func writeRow ( wr io . Writer , col1 string , col2 string ) {
fmt . Fprint ( wr , col1 , "\t" , col2 , "\n" )
}
2022-03-22 11:03:15 -04:00
// evalFlagArgs gets the flag values and does preprocessing of these values like
// reading the content from file path flags and deriving other values from flag combinations.
2022-04-13 09:01:02 -04:00
func evalFlagArgs ( cmd * cobra . Command , fileHandler file . Handler ) ( initFlags , error ) {
2022-03-22 11:03:15 -04:00
masterSecretPath , err := cmd . Flags ( ) . GetString ( "master-secret" )
if err != nil {
2022-06-09 10:10:42 -04:00
return initFlags { } , fmt . Errorf ( "parsing master-secret path argument: %w" , err )
2022-03-22 11:03:15 -04:00
}
2022-06-09 10:10:42 -04:00
masterSecret , err := readOrGenerateMasterSecret ( cmd . OutOrStdout ( ) , fileHandler , masterSecretPath )
2022-03-22 11:03:15 -04:00
if err != nil {
2022-06-09 10:10:42 -04:00
return initFlags { } , fmt . Errorf ( "parsing or generating master mastersecret from file %s: %w" , masterSecretPath , err )
2022-03-22 11:03:15 -04:00
}
autoscale , err := cmd . Flags ( ) . GetBool ( "autoscale" )
if err != nil {
2022-06-09 10:10:42 -04:00
return initFlags { } , fmt . Errorf ( "parsing autoscale argument: %w" , err )
2022-04-13 09:01:02 -04:00
}
2022-05-13 05:56:43 -04:00
configPath , err := cmd . Flags ( ) . GetString ( "config" )
2022-04-13 09:01:02 -04:00
if err != nil {
2022-06-09 10:10:42 -04:00
return initFlags { } , fmt . Errorf ( "parsing config path argument: %w" , err )
2022-03-22 11:03:15 -04:00
}
2022-04-13 09:01:02 -04:00
return initFlags {
2022-06-21 11:59:12 -04:00
configPath : configPath ,
autoscale : autoscale ,
masterSecret : masterSecret ,
2022-03-22 11:03:15 -04:00
} , nil
}
2022-04-13 09:01:02 -04:00
// initFlags are the resulting values of flag preprocessing.
type initFlags struct {
2022-06-21 11:59:12 -04:00
configPath string
masterSecret [ ] byte
autoscale bool
2022-03-22 11:03:15 -04:00
}
2022-06-09 10:10:42 -04:00
// readOrGenerateMasterSecret reads a base64 encoded master secret from file or generates a new 32 byte secret.
func readOrGenerateMasterSecret ( writer io . Writer , fileHandler file . Handler , filename string ) ( [ ] byte , error ) {
2022-03-22 11:03:15 -04:00
if filename != "" {
// Try to read the base64 secret from file
encodedSecret , err := fileHandler . Read ( filename )
if err != nil {
return nil , err
}
decoded , err := base64 . StdEncoding . DecodeString ( string ( encodedSecret ) )
if err != nil {
return nil , err
}
2022-07-26 04:58:39 -04:00
if len ( decoded ) < crypto . MasterSecretLengthMin {
2022-03-22 11:03:15 -04:00
return nil , errors . New ( "provided master secret is smaller than the required minimum of 16 Bytes" )
}
return decoded , nil
}
// No file given, generate a new secret, and save it to disk
2022-07-26 04:58:39 -04:00
masterSecret , err := crypto . GenerateRandomBytes ( crypto . MasterSecretLengthDefault )
2022-03-22 11:03:15 -04:00
if err != nil {
return nil , err
}
2022-04-06 04:36:58 -04:00
if err := fileHandler . Write ( constants . MasterSecretFilename , [ ] byte ( base64 . StdEncoding . EncodeToString ( masterSecret ) ) , file . OptNone ) ; err != nil {
2022-03-22 11:03:15 -04:00
return nil , err
}
2022-06-09 10:10:42 -04:00
fmt . Fprintf ( writer , "Your Constellation master secret was successfully written to ./%s\n" , constants . MasterSecretFilename )
2022-03-22 11:03:15 -04:00
return masterSecret , nil
}
2022-07-08 04:59:59 -04:00
func getScalingGroupsFromState ( stat state . ConstellationState , config * config . Config ) ( controlPlanes , workers cloudtypes . ScalingGroup , err error ) {
2022-03-22 11:03:15 -04:00
switch {
2022-06-29 09:26:29 -04:00
case len ( stat . GCPControlPlanes ) != 0 :
2022-03-22 11:03:15 -04:00
return getGCPInstances ( stat , config )
2022-06-29 09:26:29 -04:00
case len ( stat . AzureControlPlane ) != 0 :
2022-03-29 07:30:50 -04:00
return getAzureInstances ( stat , config )
2022-06-29 09:26:29 -04:00
case len ( stat . QEMUControlPlane ) != 0 :
2022-05-02 04:54:54 -04:00
return getQEMUInstances ( stat , config )
2022-03-22 11:03:15 -04:00
default :
2022-06-07 11:15:23 -04:00
return cloudtypes . ScalingGroup { } , cloudtypes . ScalingGroup { } , errors . New ( "no instances to initialize" )
2022-03-22 11:03:15 -04:00
}
}
2022-06-29 09:26:29 -04:00
func getGCPInstances ( stat state . ConstellationState , config * config . Config ) ( controlPlanes , workers cloudtypes . ScalingGroup , err error ) {
if len ( stat . GCPControlPlanes ) == 0 {
return cloudtypes . ScalingGroup { } , cloudtypes . ScalingGroup { } , errors . New ( "no control-plane workers available, can't create Constellation without any instance" )
2022-03-22 11:03:15 -04:00
}
2022-06-07 11:15:23 -04:00
2022-06-29 09:26:29 -04:00
// GroupID of controlPlanes is empty, since they currently do not scale.
controlPlanes = cloudtypes . ScalingGroup {
Instances : stat . GCPControlPlanes ,
2022-04-25 11:21:58 -04:00
GroupID : "" ,
}
2022-03-22 11:03:15 -04:00
2022-06-29 09:26:29 -04:00
if len ( stat . GCPWorkers ) == 0 {
return cloudtypes . ScalingGroup { } , cloudtypes . ScalingGroup { } , errors . New ( "no worker workers available, can't create Constellation with one instance" )
2022-03-22 11:03:15 -04:00
}
// TODO: make min / max configurable and abstract autoscaling for different cloud providers
2022-06-29 09:26:29 -04:00
workers = cloudtypes . ScalingGroup {
Instances : stat . GCPWorkers ,
GroupID : gcp . AutoscalingNodeGroup ( stat . GCPProject , stat . GCPZone , stat . GCPWorkerInstanceGroup , config . AutoscalingNodeGroupMin , config . AutoscalingNodeGroupMax ) ,
2022-03-22 11:03:15 -04:00
}
return
}
2022-06-29 09:26:29 -04:00
func getAzureInstances ( stat state . ConstellationState , config * config . Config ) ( controlPlanes , workers cloudtypes . ScalingGroup , err error ) {
if len ( stat . AzureControlPlane ) == 0 {
return cloudtypes . ScalingGroup { } , cloudtypes . ScalingGroup { } , errors . New ( "no control-plane workers available, can't create Constellation cluster without any instance" )
2022-03-22 11:03:15 -04:00
}
2022-06-07 11:15:23 -04:00
2022-06-29 09:26:29 -04:00
// GroupID of controlPlanes is empty, since they currently do not scale.
controlPlanes = cloudtypes . ScalingGroup {
Instances : stat . AzureControlPlane ,
2022-04-25 11:21:58 -04:00
GroupID : "" ,
}
2022-03-22 11:03:15 -04:00
2022-06-29 09:26:29 -04:00
if len ( stat . AzureWorkers ) == 0 {
return cloudtypes . ScalingGroup { } , cloudtypes . ScalingGroup { } , errors . New ( "no worker workers available, can't create Constellation cluster with one instance" )
2022-03-22 11:03:15 -04:00
}
// TODO: make min / max configurable and abstract autoscaling for different cloud providers
2022-06-29 09:26:29 -04:00
workers = cloudtypes . ScalingGroup {
Instances : stat . AzureWorkers ,
GroupID : azure . AutoscalingNodeGroup ( stat . AzureWorkersScaleSet , config . AutoscalingNodeGroupMin , config . AutoscalingNodeGroupMax ) ,
2022-03-22 11:03:15 -04:00
}
return
}
2022-07-08 04:59:59 -04:00
func getQEMUInstances ( stat state . ConstellationState , _ * config . Config ) ( controlPlanes , workers cloudtypes . ScalingGroup , err error ) {
2022-06-29 09:26:29 -04:00
controlPlanesMap := stat . QEMUControlPlane
if len ( controlPlanesMap ) == 0 {
return cloudtypes . ScalingGroup { } , cloudtypes . ScalingGroup { } , errors . New ( "no controlPlanes available, can't create Constellation without any instance" )
2022-05-02 04:54:54 -04:00
}
2022-06-07 11:15:23 -04:00
2022-05-02 04:54:54 -04:00
// QEMU does not support autoscaling
2022-06-29 09:26:29 -04:00
controlPlanes = cloudtypes . ScalingGroup {
Instances : stat . QEMUControlPlane ,
2022-05-02 04:54:54 -04:00
GroupID : "" ,
}
2022-06-29 09:26:29 -04:00
if len ( stat . QEMUWorkers ) == 0 {
return cloudtypes . ScalingGroup { } , cloudtypes . ScalingGroup { } , errors . New ( "no workers available, can't create Constellation with one instance" )
2022-05-02 04:54:54 -04:00
}
// QEMU does not support autoscaling
2022-06-29 09:26:29 -04:00
workers = cloudtypes . ScalingGroup {
Instances : stat . QEMUWorkers ,
2022-05-02 04:54:54 -04:00
GroupID : "" ,
}
return
}
2022-03-22 11:03:15 -04:00
// initCompletion handels the completion of CLI arguments. It is frequently called
// while the user types arguments of the command to suggest completion.
func initCompletion ( cmd * cobra . Command , args [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
if len ( args ) != 0 {
return [ ] string { } , cobra . ShellCompDirectiveError
}
return [ ] string { } , cobra . ShellCompDirectiveDefault
}
2022-06-21 11:59:12 -04:00
type grpcDialer interface {
Dial ( ctx context . Context , target string ) ( * grpc . ClientConn , error )
}