github.com/jenkins-x/jx/v2@v2.1.155/pkg/vault/create/create.go (about)

     1  package create
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/jenkins-x/jx/v2/pkg/errorutil"
     8  
     9  	"github.com/banzaicloud/bank-vaults/operator/pkg/apis/vault/v1alpha1"
    10  	"github.com/banzaicloud/bank-vaults/operator/pkg/client/clientset/versioned"
    11  	"github.com/google/uuid"
    12  	"github.com/jenkins-x/jx-logging/pkg/log"
    13  	"github.com/jenkins-x/jx/v2/pkg/cloud"
    14  	"github.com/jenkins-x/jx/v2/pkg/cloud/amazon/session"
    15  	awsvault "github.com/jenkins-x/jx/v2/pkg/cloud/amazon/vault"
    16  	"github.com/jenkins-x/jx/v2/pkg/cloud/gke"
    17  	gkevault "github.com/jenkins-x/jx/v2/pkg/cloud/gke/vault"
    18  	"github.com/jenkins-x/jx/v2/pkg/kube/serviceaccount"
    19  	"github.com/jenkins-x/jx/v2/pkg/kube/services"
    20  	"github.com/jenkins-x/jx/v2/pkg/kube/vault"
    21  	"github.com/jenkins-x/jx/v2/pkg/util"
    22  	"github.com/jenkins-x/jx/v2/pkg/versionstream"
    23  	"github.com/pkg/errors"
    24  	"k8s.io/client-go/kubernetes"
    25  )
    26  
    27  const (
    28  	autoCreateTableName = "vault-data"
    29  )
    30  
    31  // VaultCreationParam encapsulates the parameters needed to create a Vault instance.
    32  type VaultCreationParam struct {
    33  	VaultName            string
    34  	ClusterName          string
    35  	Namespace            string
    36  	ServiceAccountName   string
    37  	KubeProvider         string
    38  	SecretsPathPrefix    string
    39  	CreateCloudResources bool
    40  	Boot                 bool
    41  	BatchMode            bool
    42  	VaultOperatorClient  versioned.Interface
    43  	KubeClient           kubernetes.Interface
    44  	VersionResolver      versionstream.VersionResolver
    45  	FileHandles          util.IOFileHandles
    46  	GKE                  *GKEParam
    47  	AWS                  *AWSParam
    48  	Azure                *AzureParam
    49  }
    50  
    51  // GKEParam encapsulates the parameters needed to create a Vault instance on GKE.
    52  type GKEParam struct {
    53  	ProjectID      string
    54  	Zone           string
    55  	BucketName     string
    56  	KeyringName    string
    57  	KeyName        string
    58  	RecreateBucket bool
    59  }
    60  
    61  // GKEParam encapsulates the parameters needed to create a Vault instance on AWS.
    62  type AWSParam struct {
    63  	IAMUsername     string
    64  	S3Bucket        string
    65  	S3Region        string
    66  	S3Prefix        string
    67  	TemplatesDir    string
    68  	DynamoDBTable   string
    69  	DynamoDBRegion  string
    70  	KMSKeyID        string
    71  	KMSRegion       string
    72  	AccessKeyID     string
    73  	SecretAccessKey string
    74  	AutoCreate      bool
    75  }
    76  
    77  // AzureParam encapsulates the parameters needed to create a Vault instance on AWS.
    78  type AzureParam struct {
    79  	TenantID           string
    80  	StorageAccountKey  string
    81  	StorageAccountName string
    82  	ContainerName      string
    83  	VaultName          string
    84  	KeyName            string
    85  }
    86  
    87  // VaultCreator defines the interface to create and update Vault instances
    88  type VaultCreator interface {
    89  	CreateOrUpdateVault(param VaultCreationParam) error
    90  }
    91  
    92  type defaultVaultCreator struct {
    93  }
    94  
    95  // NewVaultCreator creates an instance of the default VaultCreator.
    96  func NewVaultCreator() VaultCreator {
    97  	return &defaultVaultCreator{}
    98  }
    99  
   100  func (p *VaultCreationParam) validate() error {
   101  	var validationErrors []error
   102  	if p.VaultName == "" {
   103  		validationErrors = append(validationErrors, errors.New("the Vault name needs to be provided"))
   104  	}
   105  
   106  	if p.Namespace == "" {
   107  		validationErrors = append(validationErrors, errors.New("the namespace to create the Vault instance into needs to be provided"))
   108  	}
   109  
   110  	if p.KubeClient == nil {
   111  		validationErrors = append(validationErrors, errors.New("a kube client needs to be provided"))
   112  	}
   113  
   114  	if p.VaultOperatorClient == nil {
   115  		validationErrors = append(validationErrors, errors.New("a vault operator client needs to be provided"))
   116  	}
   117  
   118  	if p.KubeProvider == "" {
   119  		validationErrors = append(validationErrors, errors.New("a kube/cloud provider needs be provided"))
   120  	}
   121  
   122  	if p.KubeProvider == cloud.GKE {
   123  		if p.GKE == nil {
   124  			validationErrors = append(validationErrors, errors.Errorf("%s selected as kube provider, but no %s specific parameters provided", cloud.GKE, cloud.GKE))
   125  		}
   126  		err := p.GKE.validate()
   127  		if err != nil {
   128  			validationErrors = append(validationErrors, err)
   129  		}
   130  	}
   131  
   132  	if p.KubeProvider == cloud.AWS {
   133  		if p.AWS == nil {
   134  			validationErrors = append(validationErrors, errors.Errorf("%s selected as kube provider, but no %s specific parameters provided", cloud.AWS, cloud.AWS))
   135  		}
   136  		if err := p.AWS.validate(); err != nil {
   137  			validationErrors = append(validationErrors, err)
   138  		}
   139  	}
   140  
   141  	return errorutil.CombineErrors(validationErrors...)
   142  }
   143  
   144  func (p *GKEParam) validate() error {
   145  	var validationErrors []error
   146  	if p == nil {
   147  		return nil
   148  	}
   149  	if p.ProjectID == "" {
   150  		validationErrors = append(validationErrors, errors.New("the GKE project ID needs to be provided"))
   151  	}
   152  	if p.Zone == "" {
   153  		validationErrors = append(validationErrors, errors.New("the GKE zone needs to be provided"))
   154  	}
   155  	return errorutil.CombineErrors(validationErrors...)
   156  }
   157  
   158  func (p *AWSParam) validate() error {
   159  	var validationErrors []error
   160  	if p == nil {
   161  		return nil
   162  	}
   163  	if p.TemplatesDir == "" {
   164  		validationErrors = append(validationErrors, errors.New("the cloud formation template dir needs to be provided"))
   165  	}
   166  	if p.AccessKeyID == "" {
   167  		validationErrors = append(validationErrors, errors.New("the AccessKeyID needs to be provided"))
   168  	}
   169  	if p.SecretAccessKey == "" {
   170  		validationErrors = append(validationErrors, errors.New("the SecretAccessKey needs to be provided"))
   171  	}
   172  	return errorutil.CombineErrors(validationErrors...)
   173  }
   174  
   175  // CreateOrUpdateVault creates or updates a Vault instance in the specified namespace.
   176  func (v *defaultVaultCreator) CreateOrUpdateVault(param VaultCreationParam) error {
   177  	err := param.validate()
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	vaultAuthServiceAccount, err := v.createAuthServiceAccount(param.KubeClient, param.VaultName, param.ServiceAccountName, param.Namespace)
   183  	if err != nil {
   184  		return errors.Wrap(err, "creating Vault authentication service account")
   185  	}
   186  	log.Logger().Debugf("Created service account '%s' for Vault authentication", util.ColorInfo(vaultAuthServiceAccount))
   187  
   188  	images, err := v.dockerImages(param.VersionResolver)
   189  	if err != nil {
   190  		return errors.Wrap(err, "loading docker images from versions repository")
   191  	}
   192  
   193  	vaultCRD, err := vault.NewVaultCRD(param.KubeClient, param.VaultName, param.Namespace, images, vaultAuthServiceAccount, param.Namespace, param.SecretsPathPrefix)
   194  
   195  	err = v.setCloudProviderSpecificSettings(vaultCRD, param)
   196  	if err != nil {
   197  		return errors.Wrap(err, "unable to set cloud provider specific Vault configuration")
   198  	}
   199  
   200  	err = vault.CreateOrUpdateVault(vaultCRD, param.VaultOperatorClient, param.Namespace)
   201  	if err != nil {
   202  		return errors.Wrap(err, "creating vault")
   203  	}
   204  
   205  	// wait for vault service to become ready before finishing the provisioning
   206  	err = services.WaitForService(param.KubeClient, param.VaultName, param.Namespace, 1*time.Minute)
   207  	if err != nil {
   208  		return errors.Wrap(err, "waiting for vault service")
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func (v *defaultVaultCreator) dockerImages(resolver versionstream.VersionResolver) (map[string]string, error) {
   215  	images := map[string]string{
   216  		vault.BankVaultsImage: "",
   217  		vault.VaultImage:      "",
   218  	}
   219  
   220  	for image := range images {
   221  		version, err := resolver.ResolveDockerImage(image)
   222  		if err != nil {
   223  			return images, errors.Wrapf(err, "resolving docker image %q", image)
   224  		}
   225  		images[image] = version
   226  	}
   227  	return images, nil
   228  }
   229  
   230  // createAuthServiceAccount creates a Service Account for the Auth service for vault
   231  func (v *defaultVaultCreator) createAuthServiceAccount(client kubernetes.Interface, vaultName, serviceAccountName string, namespace string) (string, error) {
   232  	if serviceAccountName == "" {
   233  		serviceAccountName = v.authServiceAccountName(vaultName)
   234  	}
   235  
   236  	_, err := serviceaccount.CreateServiceAccount(client, namespace, serviceAccountName)
   237  	if err != nil {
   238  		return "", errors.Wrap(err, "creating vault auth service account")
   239  	}
   240  	return serviceAccountName, nil
   241  }
   242  
   243  // authServiceAccountName creates a service account name for a given vault and cluster name
   244  func (v *defaultVaultCreator) authServiceAccountName(vaultName string) string {
   245  	return fmt.Sprintf("%s-%s", vaultName, "auth-sa")
   246  }
   247  
   248  func (v *defaultVaultCreator) setCloudProviderSpecificSettings(vaultCRD *v1alpha1.Vault, param VaultCreationParam) error {
   249  	var cloudProviderConfig vault.CloudProviderConfig
   250  	var err error
   251  
   252  	if param.CreateCloudResources {
   253  		switch param.KubeProvider {
   254  		case cloud.GKE:
   255  			cloudProviderConfig, err = v.vaultGKEConfig(vaultCRD, param)
   256  		case cloud.AWS, cloud.EKS:
   257  			cloudProviderConfig, err = v.vaultAWSConfig(vaultCRD, param)
   258  		case cloud.AKS:
   259  			cloudProviderConfig, err = v.vaultAzureConfig(vaultCRD, param)
   260  		default:
   261  			err = errors.Errorf("vault is not supported on cloud provider %s", param.KubeProvider)
   262  		}
   263  		if err != nil {
   264  			return errors.Wrapf(err, "unable to apply cloud provider config")
   265  		}
   266  	} else {
   267  		log.Logger().Warn("Upgrading Vault CRD from within the pipeline. No changes to the cloud provider specific configuration will occur.")
   268  
   269  		existingVaultCRD, err := vault.GetVault(param.VaultOperatorClient, vaultCRD.Name, param.Namespace)
   270  		if err != nil {
   271  			return errors.Wrapf(err, "expected to find existing Vault configuration")
   272  		}
   273  
   274  		cloudProviderConfig, err = v.extractCloudProviderConfig(existingVaultCRD)
   275  		if err != nil {
   276  			return errors.Wrapf(err, "unable to extract cloud provider specific configuration from Vault CRD %s", vaultCRD.Name)
   277  		}
   278  	}
   279  
   280  	vaultCRD.Spec.Config["storage"] = cloudProviderConfig.Storage
   281  	vaultCRD.Spec.Config["seal"] = cloudProviderConfig.Seal
   282  	vaultCRD.Spec.UnsealConfig = cloudProviderConfig.UnsealConfig
   283  	vaultCRD.Spec.CredentialsConfig = cloudProviderConfig.CredentialsConfig
   284  	return nil
   285  }
   286  
   287  func (v *defaultVaultCreator) vaultGKEConfig(vaultCRD *v1alpha1.Vault, param VaultCreationParam) (vault.CloudProviderConfig, error) {
   288  	gcloud := &gke.GCloud{}
   289  
   290  	err := gcloud.Login("", true)
   291  	if err != nil {
   292  		return vault.CloudProviderConfig{}, errors.Wrap(err, "login into GCP")
   293  	}
   294  
   295  	args := []string{"config", "set", "project", param.GKE.ProjectID}
   296  	cmd := util.Command{
   297  		Name: "gcloud",
   298  		Args: args,
   299  	}
   300  	_, err = cmd.RunWithoutRetry()
   301  	if err != nil {
   302  		return vault.CloudProviderConfig{}, err
   303  	}
   304  
   305  	log.Logger().Debugf("Ensure KMS API is enabled")
   306  	err = gcloud.EnableAPIs(param.GKE.ProjectID, "cloudkms")
   307  	if err != nil {
   308  		return vault.CloudProviderConfig{}, errors.Wrap(err, "unable to enable 'cloudkms' API")
   309  	}
   310  
   311  	log.Logger().Debugf("Creating GCP service account for Vault backend")
   312  	gcpServiceAccountSecretName, err := gkevault.CreateVaultGCPServiceAccount(gcloud, param.KubeClient, vaultCRD.Name, param.Namespace, param.ClusterName, param.GKE.ProjectID)
   313  	if err != nil {
   314  		return vault.CloudProviderConfig{}, errors.Wrap(err, "creating GCP service account")
   315  	}
   316  	log.Logger().Debugf("'%s' service account created", util.ColorInfo(gcpServiceAccountSecretName))
   317  
   318  	log.Logger().Debugf("Setting up GCP KMS configuration")
   319  	kmsConfig, err := gkevault.CreateKmsConfig(gcloud, vaultCRD.Name, param.GKE.KeyringName, param.GKE.KeyName, param.GKE.ProjectID)
   320  	if err != nil {
   321  		return vault.CloudProviderConfig{}, errors.Wrap(err, "creating KMS configuration")
   322  	}
   323  	log.Logger().Debugf("KMS Key '%s' created in keying '%s'", util.ColorInfo(kmsConfig.Key), util.ColorInfo(kmsConfig.Keyring))
   324  
   325  	vaultBucket, err := gkevault.CreateBucket(gcloud, vaultCRD.Name, param.GKE.BucketName, param.GKE.ProjectID, param.GKE.Zone, param.GKE.RecreateBucket, param.BatchMode, param.FileHandles)
   326  	if err != nil {
   327  		return vault.CloudProviderConfig{}, errors.Wrap(err, "creating Vault GCS data bucket")
   328  	}
   329  	log.Logger().Infof("GCS bucket '%s' was created for Vault backend", util.ColorInfo(vaultBucket))
   330  
   331  	gcpConfig := &vault.GCPConfig{
   332  		ProjectId:   param.GKE.ProjectID,
   333  		KmsKeyring:  kmsConfig.Keyring,
   334  		KmsKey:      kmsConfig.Key,
   335  		KmsLocation: kmsConfig.Location,
   336  		GcsBucket:   vaultBucket,
   337  	}
   338  	return vault.PrepareGKEVaultCRD(gcpServiceAccountSecretName, gcpConfig)
   339  }
   340  
   341  func (v *defaultVaultCreator) vaultAWSConfig(vaultCRD *v1alpha1.Vault, param VaultCreationParam) (vault.CloudProviderConfig, error) {
   342  	_, clusterRegion, err := session.GetCurrentlyConnectedRegionAndClusterName()
   343  	if err != nil {
   344  		return vault.CloudProviderConfig{}, errors.Wrap(err, "finding default AWS region")
   345  	}
   346  
   347  	v.applyDefaultRegionIfEmpty(param.AWS, clusterRegion)
   348  
   349  	if param.AWS.AutoCreate {
   350  		domain := "jenkins-x-domain"
   351  		username := param.AWS.IAMUsername
   352  		if username == "" {
   353  			username = "vault_" + clusterRegion
   354  		}
   355  
   356  		bucketName := param.AWS.S3Bucket
   357  		if bucketName == "" {
   358  			bucketName = "vault-unseal." + param.AWS.S3Region + "." + domain
   359  		}
   360  
   361  		valueUUID, err := uuid.NewUUID()
   362  		if err != nil {
   363  			return vault.CloudProviderConfig{}, errors.Wrapf(err, "Generating UUID failed")
   364  		}
   365  
   366  		// Create suffix to apply to resources
   367  		suffixString := valueUUID.String()[:7]
   368  		var kmsID, s3Name, tableName, accessID, secretKey *string
   369  		if param.Boot {
   370  			accessID, secretKey, kmsID, s3Name, tableName, err = awsvault.CreateVaultResourcesBoot(awsvault.ResourceCreationOpts{
   371  				Region:          clusterRegion,
   372  				Domain:          domain,
   373  				Username:        username,
   374  				BucketName:      bucketName,
   375  				TableName:       autoCreateTableName,
   376  				AWSTemplatesDir: param.AWS.TemplatesDir,
   377  				AccessKeyID:     param.AWS.AccessKeyID,
   378  				SecretAccessKey: param.AWS.SecretAccessKey,
   379  				UniqueSuffix:    suffixString,
   380  			})
   381  		} else {
   382  			// left for non-boot clusters until deprecation
   383  			accessID, secretKey, kmsID, s3Name, tableName, err = awsvault.CreateVaultResources(awsvault.ResourceCreationOpts{
   384  				Region:     clusterRegion,
   385  				Domain:     domain,
   386  				Username:   username,
   387  				BucketName: bucketName,
   388  				TableName:  autoCreateTableName,
   389  			})
   390  		}
   391  
   392  		if err != nil {
   393  			return vault.CloudProviderConfig{}, errors.Wrap(err, "an error occurred while creating the vaultCRD resources")
   394  		}
   395  		if s3Name != nil {
   396  			param.AWS.S3Bucket = *s3Name
   397  		}
   398  		if kmsID != nil {
   399  			param.AWS.KMSKeyID = *kmsID
   400  		}
   401  		if tableName != nil {
   402  			param.AWS.DynamoDBTable = *tableName
   403  		}
   404  		if accessID != nil {
   405  			param.AWS.AccessKeyID = *accessID
   406  		}
   407  		if secretKey != nil {
   408  			param.AWS.SecretAccessKey = *secretKey
   409  		}
   410  
   411  	} else {
   412  		if param.AWS.S3Bucket == "" {
   413  			return vault.CloudProviderConfig{}, fmt.Errorf("missing S3 bucket flag")
   414  		}
   415  		if param.AWS.KMSKeyID == "" {
   416  			return vault.CloudProviderConfig{}, fmt.Errorf("missing AWS KMS key id flag")
   417  		}
   418  		if param.AWS.AccessKeyID == "" {
   419  			return vault.CloudProviderConfig{}, fmt.Errorf("missing AWS access key id flag")
   420  		}
   421  		if param.AWS.SecretAccessKey == "" {
   422  			return vault.CloudProviderConfig{}, fmt.Errorf("missing AWS secret access key flag")
   423  		}
   424  	}
   425  
   426  	awsServiceAccountSecretName, err := awsvault.StoreAWSCredentialsIntoSecret(param.KubeClient, param.AWS.AccessKeyID, param.AWS.SecretAccessKey, vaultCRD.Name, param.Namespace)
   427  	if err != nil {
   428  		return vault.CloudProviderConfig{}, errors.Wrap(err, "storing the service account credentials into a secret")
   429  	}
   430  
   431  	vaultConfig := vault.AWSConfig{
   432  		AutoCreate:          param.AWS.AutoCreate,
   433  		DynamoDBTable:       param.AWS.DynamoDBTable,
   434  		DynamoDBRegion:      param.AWS.DynamoDBRegion,
   435  		AccessKeyID:         param.AWS.AccessKeyID,
   436  		SecretAccessKey:     param.AWS.SecretAccessKey,
   437  		ProvidedIAMUsername: param.AWS.IAMUsername,
   438  		AWSUnsealConfig: v1alpha1.AWSUnsealConfig{
   439  			KMSKeyID:  param.AWS.KMSKeyID,
   440  			KMSRegion: param.AWS.KMSRegion,
   441  			S3Bucket:  param.AWS.S3Bucket,
   442  			S3Prefix:  param.AWS.S3Prefix,
   443  			S3Region:  param.AWS.S3Region,
   444  		},
   445  	}
   446  
   447  	return vault.PrepareAWSVaultCRD(awsServiceAccountSecretName, &vaultConfig)
   448  }
   449  
   450  func (v *defaultVaultCreator) extractCloudProviderConfig(vaultCRD *v1alpha1.Vault) (vault.CloudProviderConfig, error) {
   451  	var cloudProviderConfig = vault.CloudProviderConfig{}
   452  
   453  	storageConfig := vaultCRD.Spec.Config["storage"]
   454  	if storageConfig == nil {
   455  		return cloudProviderConfig, errors.Errorf("unable to find storage config in Vault CRD %s", vaultCRD.Name)
   456  	}
   457  	storage, ok := storageConfig.(map[string]interface{})
   458  	if !ok {
   459  		return cloudProviderConfig, errors.Errorf("unexpected storage config in Vault CRD %s: %v", vaultCRD.Name, storageConfig)
   460  	}
   461  
   462  	sealConfig := vaultCRD.Spec.Config["seal"]
   463  	if sealConfig == nil {
   464  		return cloudProviderConfig, errors.Errorf("unable to find seal config in Vault CRD %s", vaultCRD.Name)
   465  	}
   466  	seal, ok := sealConfig.(map[string]interface{})
   467  	if !ok {
   468  		return cloudProviderConfig, errors.Errorf("unexpected seal config in Vault CRD %s: %v", vaultCRD.Name, sealConfig)
   469  	}
   470  
   471  	cloudProviderConfig = vault.CloudProviderConfig{
   472  		Storage:           storage,
   473  		Seal:              seal,
   474  		UnsealConfig:      vaultCRD.Spec.UnsealConfig,
   475  		CredentialsConfig: vaultCRD.Spec.CredentialsConfig,
   476  	}
   477  
   478  	return cloudProviderConfig, nil
   479  }
   480  
   481  // applyDefaultRegionIfEmpty applies the default region to all AWS resources
   482  func (v *defaultVaultCreator) applyDefaultRegionIfEmpty(awsParam *AWSParam, defaultRegion string) {
   483  	if awsParam.DynamoDBRegion == "" {
   484  		log.Logger().Infof("DynamoDBRegion not specified, defaulting to %s", util.ColorInfo(defaultRegion))
   485  		if awsParam.DynamoDBRegion == "" {
   486  			awsParam.DynamoDBRegion = defaultRegion
   487  		}
   488  	}
   489  
   490  	if awsParam.KMSRegion == "" {
   491  		log.Logger().Infof("KMSRegion not specified, defaulting to %s", util.ColorInfo(defaultRegion))
   492  		if awsParam.KMSRegion == "" {
   493  			awsParam.KMSRegion = defaultRegion
   494  		}
   495  	}
   496  
   497  	if awsParam.S3Region == "" {
   498  		log.Logger().Infof("S3Region not specified, defaulting to %s", util.ColorInfo(defaultRegion))
   499  		if awsParam.S3Region == "" {
   500  			awsParam.S3Region = defaultRegion
   501  		}
   502  	}
   503  }
   504  
   505  func (v *defaultVaultCreator) vaultAzureConfig(_ *v1alpha1.Vault, param VaultCreationParam) (vault.CloudProviderConfig, error) {
   506  
   507  	vaultConfig := vault.AzureConfig{
   508  		AzureUnsealConfig: v1alpha1.AzureUnsealConfig{
   509  			KeyVaultName: param.Azure.VaultName,
   510  		},
   511  		StorageAccountName: param.Azure.StorageAccountName,
   512  		StorageAccountKey:  param.Azure.StorageAccountKey,
   513  		ContainerName:      param.Azure.ContainerName,
   514  		TenantID:           param.Azure.TenantID,
   515  		VaultName:          param.Azure.VaultName,
   516  		KeyName:            param.Azure.KeyName,
   517  	}
   518  
   519  	return vault.PrepareAzureVaultCRD(&vaultConfig)
   520  }