github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/step/boot/step_boot_vault.go (about) 1 package boot 2 3 import ( 4 "bytes" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strings" 10 "text/template" 11 12 "github.com/jenkins-x/jx-logging/pkg/log" 13 "github.com/jenkins-x/jx/v2/pkg/cloud" 14 gkevault "github.com/jenkins-x/jx/v2/pkg/cloud/gke/vault" 15 "github.com/jenkins-x/jx/v2/pkg/cmd/helper" 16 "github.com/jenkins-x/jx/v2/pkg/cmd/opts" 17 "github.com/jenkins-x/jx/v2/pkg/cmd/templates" 18 "github.com/jenkins-x/jx/v2/pkg/config" 19 "github.com/jenkins-x/jx/v2/pkg/helm" 20 "github.com/jenkins-x/jx/v2/pkg/io/secrets" 21 "github.com/jenkins-x/jx/v2/pkg/kube" 22 kubevault "github.com/jenkins-x/jx/v2/pkg/kube/vault" 23 "github.com/jenkins-x/jx/v2/pkg/util" 24 "github.com/jenkins-x/jx/v2/pkg/vault" 25 pkgvault "github.com/jenkins-x/jx/v2/pkg/vault" 26 "github.com/jenkins-x/jx/v2/pkg/vault/create" 27 "github.com/pkg/errors" 28 "github.com/spf13/cobra" 29 corev1 "k8s.io/api/core/v1" 30 "k8s.io/client-go/kubernetes" 31 "k8s.io/helm/pkg/chartutil" 32 ) 33 34 type vaultSelector struct { 35 vaultURL string 36 serviceAccountName string 37 namespace string 38 } 39 40 func NewVaultSelector(vault vault.Vault) kubevault.Selector { 41 selector := &vaultSelector{ 42 vaultURL: vault.URL, 43 serviceAccountName: vault.ServiceAccountName, 44 namespace: vault.Namespace, 45 } 46 return selector 47 } 48 49 // GetVault retrieve the given vault by name 50 func (v *vaultSelector) GetVault(name string, namespace string, useIngressURL bool) (*vault.Vault, error) { 51 vault := vault.Vault{ 52 Name: name, 53 Namespace: namespace, 54 URL: v.vaultURL, 55 ServiceAccountName: v.serviceAccountName, 56 } 57 58 return &vault, nil 59 } 60 61 // StepBootVaultOptions contains the command line flags 62 type StepBootVaultOptions struct { 63 *opts.CommonOptions 64 Dir string 65 ProviderValuesDir string 66 Namespace string 67 } 68 69 var ( 70 stepBootVaultLong = templates.LongDesc(` 71 This step boots up Vault in the current cluster if its enabled in the 'jx-requirements.yml' file and is not already installed. 72 73 This step is intended to be used in the Jenkins X Boot Pipeline: https://jenkins-x.io/docs/getting-started/setup/boot/ 74 `) 75 76 stepBootVaultExample = templates.Examples(` 77 # boots up Vault if its required 78 jx step boot vault 79 `) 80 ) 81 82 // NewCmdStepBootVault creates the command 83 func NewCmdStepBootVault(commonOpts *opts.CommonOptions) *cobra.Command { 84 o := StepBootVaultOptions{ 85 CommonOptions: commonOpts, 86 } 87 cmd := &cobra.Command{ 88 Use: "vault", 89 Short: "This step boots up Vault in the current cluster if its enabled in the 'jx-requirements.yml' file and is not already installed", 90 Long: stepBootVaultLong, 91 Example: stepBootVaultExample, 92 Run: func(cmd *cobra.Command, args []string) { 93 o.Cmd = cmd 94 o.Args = args 95 err := o.Run() 96 helper.CheckErr(err) 97 }, 98 } 99 cmd.Flags().StringVarP(&o.Dir, "dir", "d", ".", fmt.Sprintf("the directory to look for the requirements file: %s", config.RequirementsConfigFileName)) 100 cmd.Flags().StringVarP(&o.ProviderValuesDir, "provider-values-dir", "", "", "The optional directory of kubernetes provider specific files") 101 cmd.Flags().StringVarP(&o.Namespace, "namespace", "", "", "the namespace that Jenkins X will be booted into. If not specified it defaults to $DEPLOY_NAMESPACE") 102 103 return cmd 104 } 105 106 // Run runs the command 107 func (o *StepBootVaultOptions) Run() error { 108 ns, err := o.GetDeployNamespace(o.Namespace) 109 if err != nil { 110 return err 111 } 112 113 requirements, fileName, err := config.LoadRequirementsConfig(o.Dir, config.DefaultFailOnValidationError) 114 if err != nil { 115 return err 116 } 117 118 info := util.ColorInfo 119 if requirements.SecretStorage != config.SecretStorageTypeVault { 120 log.Logger().Infof("Not attempting to boot Vault as using secret storage: %s\n", info(string(requirements.SecretStorage))) 121 return nil 122 } 123 124 kubeClient, err := o.KubeClient() 125 if err != nil { 126 return errors.Wrapf(err, "failed to create Kubernetes client") 127 } 128 129 internal := requirements.Vault.URL == "" 130 // if we are not in batch mode and the key values in jx-requirements.yml are not set, interactively query the user 131 if !o.BatchMode && requirements.Vault.URL == "" && requirements.Vault.Name == "" { 132 internal, err = o.interactiveVaultConfiguration(internal, requirements, fileName) 133 if err != nil { 134 return errors.Wrapf(err, "failed to interactively configure Vault") 135 } 136 } 137 138 if internal { 139 return o.setupInClusterVault(requirements, ns, kubeClient) 140 } 141 return o.setupExternalVault(requirements, ns, kubeClient) 142 } 143 144 func (o *StepBootVaultOptions) interactiveVaultConfiguration(internal bool, requirements *config.RequirementsConfig, fileName string) (bool, error) { 145 help := "Jenkins X uses Vault to store secrets. You can provide your own Vault instance or let Jenkins X create one for you." 146 message := "Do you want Jenkins X to create and manage Vault?" 147 internal, err := util.Confirm(message, true, help, o.GetIOFileHandles()) 148 if err != nil { 149 return false, errors.Wrapf(err, "unable to process user input") 150 } 151 152 if internal { 153 return true, nil 154 } 155 156 err = o.askExternalVaultParameters(requirements, o.GetIOFileHandles()) 157 if err != nil { 158 return false, errors.Wrapf(err, "unable to ask user for Vault configuration") 159 } 160 err = requirements.SaveConfig(fileName) 161 if err != nil { 162 return false, errors.Wrap(err, "unable to write updated requirements file") 163 } 164 return false, nil 165 } 166 167 func (o *StepBootVaultOptions) askExternalVaultParameters(requirements *config.RequirementsConfig, fileHandles util.IOFileHandles) error { 168 url, err := util.PickValue("URL to Vault instance: ", "", true, "Please specify the URL to the Vault instance for storing your Jenkins X secrets", fileHandles) 169 if err != nil { 170 return errors.Wrap(err, "unable to get Vault URL from user") 171 } 172 requirements.Vault.URL = url 173 174 sa, err := util.PickValue("Authenticating service account: ", fmt.Sprintf("%s-vt", requirements.Cluster.ClusterName), true, "Please specify the service account used to authenticate against Vault", fileHandles) 175 if err != nil { 176 return errors.Wrap(err, "unable to get service account from user") 177 } 178 requirements.Vault.ServiceAccount = sa 179 180 ns, err := util.PickValue("Namespace of authenticating service account: ", requirements.Cluster.Namespace, true, "Please specify the namespace of the authenticating service account", fileHandles) 181 if err != nil { 182 return errors.Wrap(err, "unable to get namespace from user") 183 } 184 requirements.Vault.Namespace = ns 185 186 authPath, err := util.PickValue("Path under which to enable Vault's Kubernetes auth plugin: ", vault.DefaultKubernetesAuthPath, true, "Please specify the path for Vault's Kubernetes auth plugin. See https://www.vaultproject.io/docs/auth/kubernetes.", fileHandles) 187 if err != nil { 188 return errors.Wrap(err, "unable to get Kubernetes auth path from user") 189 } 190 requirements.Vault.KubernetesAuthPath = authPath 191 192 mountPoint, err := util.PickValue("Mount point for Vault's KV secret engine: ", vault.DefaultKVEngineMountPoint, true, "Please specify the mount point for Vault's KV secrets engine. See https://www.vaultproject.io/docs/secrets/kv", fileHandles) 193 if err != nil { 194 return errors.Wrap(err, "unable to get Vault URL from user") 195 } 196 requirements.Vault.SecretEngineMountPoint = mountPoint 197 198 return nil 199 } 200 201 func (o *StepBootVaultOptions) setupExternalVault(requirements *config.RequirementsConfig, ns string, kubeClient kubernetes.Interface) error { 202 namespace := requirements.Vault.Namespace 203 if namespace == "" { 204 namespace = ns 205 } 206 vault, err := vault.NewExternalVault(requirements.Vault.URL, requirements.Vault.ServiceAccount, namespace, requirements.Vault.SecretEngineMountPoint, requirements.Vault.KubernetesAuthPath) 207 if err != nil { 208 return errors.Wrapf(err, "invalid configuration for external Vault setup") 209 } 210 211 selector := NewVaultSelector(vault) 212 vaultFactory, err := kubevault.NewVaultClientFactoryWithSelector(kubeClient, selector, ns) 213 if err != nil { 214 return errors.Wrap(err, "unable to create Vault factory for external Vault instance") 215 } 216 217 _, err = vaultFactory.NewVaultClientForURL(vault, false) 218 if err != nil { 219 return errors.Wrap(err, "unable to create Vault client for external Vault instance") 220 } 221 222 err = o.storeExternalVaultConfig(kubeClient, ns, vault) 223 if err != nil { 224 return errors.Wrapf(err, "unable to store Vault configuration in ConfigMap '%s'", kube.ConfigMapNameJXInstallConfig) 225 } 226 227 log.Logger().Infof("Using external Vault instance %s - %s", vault.URL, util.ColorInfo("OK")) 228 return nil 229 } 230 231 func (o *StepBootVaultOptions) setupInClusterVault(requirements *config.RequirementsConfig, ns string, kubeClient kubernetes.Interface) error { 232 if requirements.Vault.Name == "" { 233 requirements.Vault.Name = kubevault.SystemVaultNameForCluster(requirements.Cluster.ClusterName) 234 } 235 log.Logger().Debugf("Using vault name '%s'", requirements.Vault.Name) 236 237 vault, err := vault.NewInternalVault(requirements.Vault.Name, requirements.Vault.ServiceAccount, ns) 238 if err != nil { 239 return errors.Wrapf(err, "invalid configuration for external Vault setup") 240 } 241 242 err = o.installOperator(requirements, ns) 243 if err != nil { 244 return errors.Wrapf(err, "unable to install Vault operator") 245 } 246 247 _, err = o.verifyVaultIngress(requirements, kubeClient, ns, requirements.Vault.Name) 248 if err != nil { 249 return err 250 } 251 252 err = o.storeInternalVaultConfig(kubeClient, vault, ns) 253 if err != nil { 254 return err 255 } 256 257 vaultOperatorClient, err := o.VaultOperatorClient() 258 if err != nil { 259 return errors.Wrap(err, "creating vault operator client") 260 } 261 262 resolver, err := o.CreateVersionResolver(requirements.VersionStream.URL, requirements.VersionStream.Ref) 263 if err != nil { 264 return errors.Wrap(err, "unable to create version stream resolver") 265 } 266 267 provider := requirements.Cluster.Provider 268 269 // only allow to make changes to cloud resources when running locally via `jx boot` 270 // when run in pipeline, the pipeline SA does not have permissions to create buckets, etc 271 // the assumption is that when the code runs in the pipeline all cloud resources already exist 272 // (`jx boot` has been executed once at least) 273 createCloudResources := o.IsJXBoot() 274 275 vaultCreateParam := create.VaultCreationParam{ 276 VaultName: requirements.Vault.Name, 277 Namespace: ns, 278 ClusterName: requirements.Cluster.ClusterName, 279 ServiceAccountName: requirements.Vault.ServiceAccount, 280 SecretsPathPrefix: pkgvault.DefaultSecretsPathPrefix, 281 KubeProvider: provider, 282 KubeClient: kubeClient, 283 VaultOperatorClient: vaultOperatorClient, 284 VersionResolver: *resolver, 285 FileHandles: o.GetIOFileHandles(), 286 CreateCloudResources: createCloudResources, 287 Boot: true, 288 BatchMode: true, 289 } 290 291 if provider == cloud.GKE { 292 gkeParam := &create.GKEParam{ 293 ProjectID: gkevault.GetGoogleProjectID(kubeClient, ns), 294 Zone: gkevault.GetGoogleZone(kubeClient, ns), 295 BucketName: requirements.Vault.Bucket, 296 KeyringName: requirements.Vault.Keyring, 297 KeyName: requirements.Vault.Key, 298 RecreateBucket: requirements.Vault.RecreateBucket, 299 } 300 vaultCreateParam.GKE = gkeParam 301 } else if provider == cloud.EKS { 302 awsParam, err := o.createAWSParam(requirements) 303 if err != nil { 304 return errors.Wrap(err, "unable to create Vault creation parameter from requirements") 305 } 306 vaultCreateParam.AWS = &awsParam 307 } else if provider == cloud.AKS { 308 azureParam, err := o.createAzureParam(requirements) 309 if err != nil { 310 return errors.Wrap(err, "unable to create Vault creation parameter from requirements") 311 } 312 vaultCreateParam.Azure = &azureParam 313 } 314 315 vaultCreator := create.NewVaultCreator() 316 err = vaultCreator.CreateOrUpdateVault(vaultCreateParam) 317 if err != nil { 318 return errors.Wrap(err, "unable to create/update Vault") 319 } 320 return nil 321 } 322 323 func (o *StepBootVaultOptions) createAWSParam(requirements *config.RequirementsConfig) (create.AWSParam, error) { 324 if requirements.Vault.AWSConfig == nil { 325 return create.AWSParam{}, errors.New("missing AWS configuration for Vault in requirements") 326 } 327 328 awsConfig := requirements.Vault.AWSConfig 329 secretAccessKey := os.Getenv("VAULT_AWS_SECRET_ACCESS_KEY") 330 accessKeyID := os.Getenv("VAULT_AWS_ACCESS_KEY_ID") 331 if !awsConfig.AutoCreate && (checkRequiredResource("dynamoDBTable", awsConfig.DynamoDBTable) || 332 checkRequiredResource("secretAccessKey", secretAccessKey) || 333 checkRequiredResource("accessKeyID", accessKeyID) || 334 checkRequiredResource("kmsKeyId", awsConfig.KMSKeyID) || 335 checkRequiredResource("s3Bucket", awsConfig.S3Bucket)) { 336 log.Logger().Info("Some of the required provided values are empty - We will create all resources") 337 awsConfig.AutoCreate = true 338 } 339 340 templatesDir := filepath.Join(o.Dir, o.ProviderValuesDir, cloud.EKS, "templates") 341 342 defaultRegion := requirements.Cluster.Region 343 if defaultRegion == "" { 344 return create.AWSParam{}, errors.New("unable to find cluster region in requirements") 345 } 346 347 dynamoDBRegion := awsConfig.DynamoDBRegion 348 if dynamoDBRegion == "" { 349 dynamoDBRegion = defaultRegion 350 log.Logger().Infof("Region not specified for DynamoDB, defaulting to %s", util.ColorInfo(defaultRegion)) 351 } 352 353 kmsRegion := awsConfig.KMSRegion 354 if kmsRegion == "" { 355 kmsRegion = defaultRegion 356 log.Logger().Infof("Region not specified for KMS, defaulting to %s", util.ColorInfo(defaultRegion)) 357 358 } 359 360 s3Region := awsConfig.S3Region 361 if s3Region == "" { 362 s3Region = defaultRegion 363 log.Logger().Infof("Region not specified for S3, defaulting to %s", util.ColorInfo(defaultRegion)) 364 } 365 366 awsParam := create.AWSParam{ 367 IAMUsername: awsConfig.ProvidedIAMUsername, 368 S3Bucket: awsConfig.S3Bucket, 369 S3Region: s3Region, 370 S3Prefix: awsConfig.S3Prefix, 371 TemplatesDir: templatesDir, 372 DynamoDBTable: awsConfig.DynamoDBTable, 373 DynamoDBRegion: dynamoDBRegion, 374 KMSKeyID: awsConfig.KMSKeyID, 375 KMSRegion: kmsRegion, 376 AccessKeyID: accessKeyID, 377 SecretAccessKey: secretAccessKey, 378 AutoCreate: awsConfig.AutoCreate, 379 } 380 381 return awsParam, nil 382 } 383 384 func (o *StepBootVaultOptions) createAzureParam(requirements *config.RequirementsConfig) (create.AzureParam, error) { 385 if requirements.Vault.AzureConfig == nil { 386 return create.AzureParam{}, errors.New("missing Azure configuration for Vault in requirements") 387 } 388 389 azureConfig := requirements.Vault.AzureConfig 390 storageAccessKey := os.Getenv("VAULT_AZURE_STORAGE_ACCESS_KEY") 391 392 azureParam := create.AzureParam{ 393 TenantID: azureConfig.TenantID, 394 StorageAccountKey: storageAccessKey, 395 StorageAccountName: azureConfig.StorageAccountName, 396 ContainerName: azureConfig.ContainerName, 397 KeyName: azureConfig.KeyName, 398 VaultName: azureConfig.VaultName, 399 } 400 401 return azureParam, nil 402 } 403 404 func (o *StepBootVaultOptions) storeInternalVaultConfig(kubeClient kubernetes.Interface, vaultConfig vault.Vault, ns string) error { 405 _, err := kube.DefaultModifyConfigMap(kubeClient, ns, kube.ConfigMapNameJXInstallConfig, 406 func(configMap *corev1.ConfigMap) error { 407 configMap.Data[secrets.SecretsLocationKey] = string(secrets.VaultLocationKind) 408 409 vaultConfig := vaultConfig.ToMap() 410 configMap.Data = util.MergeMaps(configMap.Data, vaultConfig) 411 412 return nil 413 }, nil) 414 if err != nil { 415 return errors.Wrapf(err, "error saving system vault name in ConfigMap %s in namespace %s", kube.ConfigMapNameJXInstallConfig, ns) 416 } 417 return nil 418 } 419 420 func (o *StepBootVaultOptions) storeExternalVaultConfig(kubeClient kubernetes.Interface, ns string, vaultConfig vault.Vault) error { 421 _, err := kube.DefaultModifyConfigMap(kubeClient, ns, kube.ConfigMapNameJXInstallConfig, 422 func(configMap *corev1.ConfigMap) error { 423 configMap.Data[secrets.SecretsLocationKey] = string(secrets.VaultLocationKind) 424 425 vaultConfig := vaultConfig.ToMap() 426 configMap.Data = util.MergeMaps(configMap.Data, vaultConfig) 427 428 return nil 429 }, nil) 430 if err != nil { 431 return errors.Wrapf(err, "error saving external Vault configuration in ConfigMap %s in namespace %s", kube.ConfigMapNameJXInstallConfig, ns) 432 } 433 return nil 434 } 435 436 func (o *StepBootVaultOptions) installOperator(requirements *config.RequirementsConfig, ns string) error { 437 tag, err := o.vaultOperatorImageTag(&requirements.VersionStream) 438 if err != nil { 439 return errors.Wrap(err, "unable to determine Vault operator version") 440 } 441 442 releaseName := o.ReleaseName 443 if releaseName == "" { 444 releaseName = kube.DefaultVaultOperatorReleaseName 445 } 446 447 values := []string{ 448 "image.repository=" + kubevault.VaultOperatorImage, 449 "image.tag=" + tag, 450 } 451 log.Logger().Infof("Installing %s operator with helm values: %v", util.ColorInfo(releaseName), util.ColorInfo(values)) 452 453 helmOptions := helm.InstallChartOptions{ 454 Chart: kube.ChartVaultOperator, 455 ReleaseName: releaseName, 456 Version: o.Version, 457 Ns: ns, 458 SetValues: values, 459 } 460 err = o.InstallChartWithOptions(helmOptions) 461 if err != nil { 462 return errors.Wrap(err, "unable to install vault operator") 463 } 464 465 log.Logger().Infof("Vault operator installed in namespace %s", ns) 466 return nil 467 } 468 469 // verifyVaultIngress verifies there is a Vault ingress and if not create one if there is a file at 470 func (o *StepBootVaultOptions) verifyVaultIngress(requirements *config.RequirementsConfig, kubeClient kubernetes.Interface, ns string, systemVaultName string) (bool, error) { 471 fileName := filepath.Join(o.Dir, "vault-ing.tmpl.yaml") 472 exists, err := util.FileExists(fileName) 473 if err != nil { 474 return false, errors.Wrapf(err, "failed to check if file exists %s", fileName) 475 } 476 if !exists { 477 log.Logger().Warnf("failed to find file %s\n", fileName) 478 return false, nil 479 } 480 data, err := readYamlTemplate(fileName, requirements) 481 if err != nil { 482 return true, errors.Wrapf(err, "failed to load vault ingress template file %s", fileName) 483 } 484 485 log.Logger().Infof("Applying vault ingress in namespace %s for vault name %s\n", util.ColorInfo(ns), util.ColorInfo(systemVaultName)) 486 487 tmpFile, err := ioutil.TempFile("", "vault-ingress-") 488 if err != nil { 489 return true, errors.Wrapf(err, "failed to create temporary file for vault YAML") 490 } 491 492 tmpFileName := tmpFile.Name() 493 err = ioutil.WriteFile(tmpFileName, data, util.DefaultWritePermissions) 494 if err != nil { 495 return true, errors.Wrapf(err, "failed to save vault ingress YAML file %s", tmpFileName) 496 } 497 498 args := []string{"apply", "--force", "-f", tmpFileName, "-n", ns} 499 err = o.RunCommand("kubectl", args...) 500 if err != nil { 501 return true, errors.Wrapf(err, "failed to apply vault ingress YAML") 502 } 503 return true, nil 504 } 505 506 // vaultOperatorImageTag lookups the vault operator image tag in the version stream 507 func (o *StepBootVaultOptions) vaultOperatorImageTag(versionStream *config.VersionStreamConfig) (string, error) { 508 resolver, err := o.CreateVersionResolver(versionStream.URL, versionStream.Ref) 509 if err != nil { 510 return "", errors.Wrap(err, "creating the vault-operator docker image version resolver") 511 } 512 fullImage, err := resolver.ResolveDockerImage(kubevault.VaultOperatorImage) 513 if err != nil { 514 return "", errors.Wrapf(err, "looking up the vault-operator %q image into the version stream", 515 kubevault.VaultOperatorImage) 516 } 517 parts := strings.Split(fullImage, ":") 518 if len(parts) != 2 { 519 return "", fmt.Errorf("no tag found for image %q in version stream", kubevault.VaultOperatorImage) 520 } 521 return parts[1], nil 522 } 523 524 // readYamlTemplate evaluates the given go template file and returns the output data 525 func readYamlTemplate(templateFile string, requirements *config.RequirementsConfig) ([]byte, error) { 526 _, name := filepath.Split(templateFile) 527 funcMap := helm.NewFunctionMap() 528 tmpl, err := template.New(name).Option("missingkey=error").Funcs(funcMap).ParseFiles(templateFile) 529 if err != nil { 530 return nil, errors.Wrapf(err, "failed to parse Ingress template: %s", templateFile) 531 } 532 533 requirementsMap, err := requirements.ToMap() 534 if err != nil { 535 return nil, errors.Wrapf(err, "failed turn requirements into a map: %v", requirements) 536 } 537 538 templateData := map[string]interface{}{ 539 "Requirements": chartutil.Values(requirementsMap), 540 "Environments": chartutil.Values(requirements.EnvironmentMap()), 541 } 542 var buf bytes.Buffer 543 err = tmpl.Execute(&buf, templateData) 544 if err != nil { 545 return nil, errors.Wrapf(err, "failed to execute Ingress template: %s", templateFile) 546 } 547 data := buf.Bytes() 548 return data, nil 549 } 550 551 func checkRequiredResource(resourceName string, value string) bool { 552 if value == "" { 553 log.Logger().Warnf("Vault.AutoCreate is false but required property %s is missing", resourceName) 554 return true 555 } 556 return false 557 }