github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/verify/step_verify_preinstall.go (about) 1 package verify 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path" 8 "path/filepath" 9 "strings" 10 11 "github.com/blang/semver" 12 13 "github.com/olli-ai/jx/v2/pkg/cloud/amazon/session" 14 "github.com/olli-ai/jx/v2/pkg/prow" 15 "sigs.k8s.io/yaml" 16 17 "github.com/jenkins-x/jx-logging/pkg/log" 18 "github.com/olli-ai/jx/v2/pkg/boot" 19 "github.com/olli-ai/jx/v2/pkg/cloud" 20 "github.com/olli-ai/jx/v2/pkg/cloud/amazon" 21 "github.com/olli-ai/jx/v2/pkg/cloud/buckets" 22 "github.com/olli-ai/jx/v2/pkg/cloud/factory" 23 "github.com/olli-ai/jx/v2/pkg/cloud/gke" 24 "github.com/olli-ai/jx/v2/pkg/cmd/create" 25 "github.com/olli-ai/jx/v2/pkg/cmd/helper" 26 "github.com/olli-ai/jx/v2/pkg/cmd/namespace" 27 "github.com/olli-ai/jx/v2/pkg/cmd/opts" 28 "github.com/olli-ai/jx/v2/pkg/cmd/opts/step" 29 "github.com/olli-ai/jx/v2/pkg/config" 30 "github.com/olli-ai/jx/v2/pkg/gits" 31 "github.com/olli-ai/jx/v2/pkg/io/secrets" 32 "github.com/olli-ai/jx/v2/pkg/kube" 33 "github.com/olli-ai/jx/v2/pkg/kube/cluster" 34 "github.com/olli-ai/jx/v2/pkg/kube/naming" 35 "github.com/olli-ai/jx/v2/pkg/packages" 36 "github.com/olli-ai/jx/v2/pkg/util" 37 "github.com/pkg/errors" 38 "github.com/spf13/cobra" 39 corev1 "k8s.io/api/core/v1" 40 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 41 "k8s.io/client-go/kubernetes" 42 ) 43 44 // StepVerifyPreInstallOptions contains the command line flags 45 type StepVerifyPreInstallOptions struct { 46 StepVerifyOptions 47 Debug bool 48 Dir string 49 LazyCreate bool 50 DisableVerifyHelm bool 51 DisableVerifyPackages bool 52 LazyCreateFlag string 53 Namespace string 54 ProviderValuesDir string 55 TestKanikoSecretData string 56 TestVeleroSecretData string 57 WorkloadIdentity bool 58 } 59 60 // NewCmdStepVerifyPreInstall creates the `jx step verify pod` command 61 func NewCmdStepVerifyPreInstall(commonOpts *opts.CommonOptions) *cobra.Command { 62 63 options := &StepVerifyPreInstallOptions{ 64 StepVerifyOptions: StepVerifyOptions{ 65 StepOptions: step.StepOptions{ 66 CommonOptions: commonOpts, 67 }, 68 }, 69 } 70 71 cmd := &cobra.Command{ 72 Use: "preinstall", 73 Aliases: []string{"pre-install", "pre"}, 74 Short: "Verifies all of the cloud infrastructure is setup before we try to boot up a cluster via 'jx boot'", 75 Run: func(cmd *cobra.Command, args []string) { 76 options.Cmd = cmd 77 options.Args = args 78 err := options.Run() 79 helper.CheckErr(err) 80 }, 81 } 82 cmd.Flags().BoolVarP(&options.Debug, "debug", "", false, "Output logs of any failed pod") 83 cmd.Flags().StringVarP(&options.Dir, "dir", "d", ".", "the directory to look for the install requirements file") 84 cmd.Flags().StringVarP(&options.LazyCreateFlag, "lazy-create", "", "", fmt.Sprintf("Specify true/false as to whether to lazily create missing resources. If not specified it is enabled if Terraform is not specified in the %s file", config.RequirementsConfigFileName)) 85 cmd.Flags().StringVarP(&options.Namespace, "namespace", "", "", "the namespace that Jenkins X will be booted into. If not specified it defaults to $DEPLOY_NAMESPACE") 86 cmd.Flags().StringVarP(&options.ProviderValuesDir, "provider-values-dir", "", "", "The optional directory of kubernetes provider specific files") 87 cmd.Flags().BoolVarP(&options.WorkloadIdentity, "workload-identity", "", false, "Enable this if using GKE Workload Identity to avoid reconnecting to the Cluster.") 88 cmd.Flags().BoolVarP(&options.DisableVerifyPackages, "disable-verify-packages", "", false, "Disable packages verification, helpful when testing different package versions.") 89 cmd.Flags().BoolVarP(&options.DisableVerifyHelm, "disable-verify-helm", "", false, "Disable Helm verification, helpful when testing different Helm versions.") 90 91 return cmd 92 } 93 94 // Run implements this command 95 func (o *StepVerifyPreInstallOptions) Run() error { 96 info := util.ColorInfo 97 requirements, requirementsFileName, err := config.LoadRequirementsConfig(o.Dir, config.DefaultFailOnValidationError) 98 if err != nil { 99 return err 100 } 101 102 err = o.ConfigureCommonOptions(requirements) 103 if err != nil { 104 return err 105 } 106 107 requirements, err = o.gatherRequirements(requirements, requirementsFileName) 108 if err != nil { 109 return err 110 } 111 112 err = o.ValidateRequirements(requirements, requirementsFileName) 113 if err != nil { 114 return err 115 } 116 117 o.LazyCreate, err = requirements.IsLazyCreateSecrets(o.LazyCreateFlag) 118 if err != nil { 119 return err 120 } 121 122 // lets find the namespace to use 123 ns, err := o.GetDeployNamespace(o.Namespace) 124 if err != nil { 125 return err 126 } 127 kubeClient, err := o.KubeClient() 128 if err != nil { 129 return err 130 } 131 132 err = o.verifyTLS(requirements) 133 if err != nil { 134 return errors.WithStack(err) 135 } 136 137 o.SetDevNamespace(ns) 138 139 log.Logger().Infof("Verifying the kubernetes cluster before we try to boot Jenkins X in namespace: %s", info(ns)) 140 if o.LazyCreate { 141 log.Logger().Infof("Trying to lazily create any missing resources to get the current cluster ready to boot Jenkins X") 142 } else { 143 log.Logger().Warn("Lazy create of cloud resources is disabled") 144 } 145 146 err = o.verifyDevNamespace(kubeClient, ns) 147 if err != nil { 148 if o.LazyCreate { 149 log.Logger().Infof("Attempting to lazily create the deploy namespace %s", info(ns)) 150 151 err = kube.EnsureDevNamespaceCreatedWithoutEnvironment(kubeClient, ns) 152 if err != nil { 153 return errors.Wrapf(err, "failed to lazily create the namespace %s", ns) 154 } 155 // lets rerun the verify step to ensure its all sorted now 156 err = o.verifyDevNamespace(kubeClient, ns) 157 if err != nil { 158 return errors.Wrapf(err, "failed to verify the namespace %s", ns) 159 } 160 } 161 } 162 163 err = o.verifyIngress(requirements, requirementsFileName) 164 if err != nil { 165 return err 166 } 167 no := &namespace.NamespaceOptions{} 168 no.CommonOptions = o.CommonOptions 169 no.Args = []string{ns} 170 err = no.Run() 171 if err != nil { 172 return err 173 } 174 log.Logger().Info("\n") 175 176 err = o.installMissingDependencies() 177 if err != nil { 178 return err 179 } 180 181 po := &StepVerifyPackagesOptions{} 182 po.CommonOptions = o.CommonOptions 183 po.Packages = []string{"kubectl", "git", "helm"} 184 po.Dir = o.Dir 185 if !o.DisableVerifyPackages { 186 err = po.Run() 187 if err != nil { 188 return err 189 } 190 log.Logger().Info("\n") 191 } 192 193 err = o.VerifyInstallConfig(kubeClient, ns, requirements, requirementsFileName) 194 if err != nil { 195 return err 196 } 197 err = o.verifyStorage(requirements, requirementsFileName) 198 if err != nil { 199 return err 200 } 201 log.Logger().Info("\n") 202 if !o.DisableVerifyHelm && !requirements.Helmfile { 203 err = o.verifyHelm(ns) 204 if err != nil { 205 return err 206 } 207 } 208 if requirements.Kaniko { 209 if requirements.Cluster.Provider == cloud.GKE { 210 log.Logger().Infof("Validating Kaniko secret in namespace %s", info(ns)) 211 212 err = o.validateKaniko(ns) 213 if err != nil { 214 if o.LazyCreate { 215 log.Logger().Infof("attempting to lazily create the deploy namespace %s", info(ns)) 216 217 err = o.lazyCreateKanikoSecret(requirements, ns) 218 if err != nil { 219 return errors.Wrapf(err, "failed to lazily create the kaniko secret in: %s", ns) 220 } 221 // lets rerun the verify step to ensure its all sorted now 222 err = o.validateKaniko(ns) 223 } 224 } 225 if err != nil { 226 return err 227 } 228 log.Logger().Info("\n") 229 } 230 } 231 232 if vns := requirements.Velero.Namespace; vns != "" { 233 if requirements.Cluster.Provider == cloud.GKE { 234 log.Logger().Infof("Validating the velero secret in namespace %s", info(vns)) 235 236 err = o.validateVelero(vns) 237 if err != nil { 238 if o.LazyCreate { 239 log.Logger().Infof("Attempting to lazily create the deploy namespace %s", info(vns)) 240 241 err = o.lazyCreateVeleroSecret(requirements, vns) 242 if err != nil { 243 return errors.Wrapf(err, "failed to lazily create the velero secret in: %s", vns) 244 } 245 // lets rerun the verify step to ensure its all sorted now 246 err = o.validateVelero(vns) 247 } 248 } 249 if err != nil { 250 return err 251 } 252 log.Logger().Info("\n") 253 } 254 } 255 256 if requirements.Webhook == config.WebhookTypeLighthouse { 257 // we don't need the ConfigMaps for prow yet 258 err = o.verifyProwConfigMaps(kubeClient, ns) 259 if err != nil { 260 return err 261 } 262 } 263 264 if requirements.Cluster.Provider == cloud.EKS && o.LazyCreate { 265 if !cluster.IsInCluster() || os.Getenv("OVERRIDE_IRSA_IN_CLUSTER") == "true" { 266 log.Logger().Info("Attempting to lazily create the IAM Role for Service Accounts permissions") 267 err = amazon.EnableIRSASupportInCluster(requirements) 268 if err != nil { 269 return errors.Wrap(err, "error enabling IRSA in cluster") 270 } 271 err = amazon.CreateIRSAManagedServiceAccounts(requirements, o.ProviderValuesDir) 272 if err != nil { 273 return errors.Wrap(err, "error creating the IRSA managed Service Accounts") 274 } 275 } else { 276 log.Logger().Info("Running in cluster, not recreating permissions") 277 } 278 } 279 280 // Lets update the TeamSettings with the VersionStream data from the jx-requirements.yml file so we make sure 281 // we are upgrading with the latest versions 282 log.Logger().Infof("Cluster looks good, you are ready to '%s' now!", info("jx boot")) 283 fmt.Println() 284 return nil 285 } 286 287 // installMissingDependencies installs missing deps like helm and kubectl 288 func (o *StepVerifyPreInstallOptions) installMissingDependencies() error { 289 var deps []string 290 deps = packages.AddRequiredBinary("kubectl", deps) 291 deps = packages.AddRequiredBinary("helm", deps) 292 return o.DoInstallMissingDependencies(deps) 293 } 294 295 // EnsureHelm ensures helm is installed 296 func (o *StepVerifyPreInstallOptions) verifyHelm(ns string) error { 297 log.Logger().Debug("Verifying Helm...") 298 // lets make sure we don't try use tiller 299 o.EnableRemoteKubeCluster() 300 v, err := o.Helm().Version(false) 301 if err != nil { 302 err = o.InstallHelm() 303 if err != nil { 304 return errors.Wrap(err, "failed to install Helm") 305 } 306 v, err = o.Helm().Version(false) 307 if err != nil { 308 return errors.Wrap(err, "failed to get Helm version after install") 309 } 310 } 311 currVersion, err := semver.Make(v) 312 if err != nil { 313 return errors.Wrapf(err, "unable to parse semantic version %s", v) 314 } 315 noInitRequiredVersion := semver.MustParse("3.0.0") 316 if currVersion.LT(noInitRequiredVersion) { 317 cfg := opts.InitHelmConfig{ 318 Namespace: ns, 319 OnlyHelmClient: true, 320 Helm3: false, 321 SkipTiller: true, 322 GlobalTiller: false, 323 TillerNamespace: "", 324 TillerRole: "", 325 } 326 err = o.InitHelm(cfg) 327 if err != nil { 328 return errors.Wrapf(err, "initializing helm with config: %v", cfg) 329 } 330 } 331 332 o.EnableRemoteKubeCluster() 333 334 _, err = o.AddHelmBinaryRepoIfMissing(kube.DefaultChartMuseumURL, kube.DefaultChartMuseumJxRepoName, "", "") 335 if err != nil { 336 return errors.Wrapf(err, "adding '%s' helm charts repository", kube.DefaultChartMuseumURL) 337 } 338 log.Logger().Infof("Ensuring Helm chart repository %s is configured\n", kube.DefaultChartMuseumURL) 339 340 return nil 341 } 342 343 func (o *StepVerifyPreInstallOptions) verifyDevNamespace(kubeClient kubernetes.Interface, ns string) error { 344 log.Logger().Debug("Verifying Dev Namespace...") 345 ns, envName, err := kube.GetDevNamespace(kubeClient, ns) 346 if err != nil { 347 return err 348 } 349 if ns == "" { 350 return fmt.Errorf("no dev namespace name found") 351 } 352 if envName == "" { 353 return fmt.Errorf("namespace %s has no team label", ns) 354 } 355 return nil 356 } 357 358 func (o *StepVerifyPreInstallOptions) lazyCreateKanikoSecret(requirements *config.RequirementsConfig, ns string) error { 359 log.Logger().Debugf("Lazily creating the kaniko secret") 360 io := &create.InstallOptions{} 361 io.CommonOptions = o.CommonOptions 362 io.Flags.Kaniko = true 363 io.Flags.Namespace = ns 364 io.Flags.Provider = requirements.Cluster.Provider 365 io.SetInstallValues(map[string]string{ 366 kube.ClusterName: requirements.Cluster.ClusterName, 367 kube.ProjectID: requirements.Cluster.ProjectID, 368 }) 369 if o.TestKanikoSecretData != "" { 370 io.AdminSecretsService.Flags.KanikoSecret = o.TestKanikoSecretData 371 } else { 372 err := io.ConfigureKaniko() 373 if err != nil { 374 return err 375 } 376 } 377 data := io.AdminSecretsService.Flags.KanikoSecret 378 if data == "" { 379 return fmt.Errorf("failed to create the kaniko secret data") 380 } 381 return o.createSecret(ns, kube.SecretKaniko, kube.SecretKaniko, data) 382 } 383 384 func (o *StepVerifyPreInstallOptions) lazyCreateVeleroSecret(requirements *config.RequirementsConfig, ns string) error { 385 log.Logger().Debugf("Lazily creating the velero secret") 386 var data string 387 var err error 388 if o.TestVeleroSecretData != "" { 389 data = o.TestVeleroSecretData 390 } else { 391 data, err = o.configureVelero(requirements) 392 if err != nil { 393 return errors.Wrap(err, "failed to create the velero secret data") 394 } 395 } 396 if data == "" { 397 return nil 398 } 399 return o.createSecret(ns, kube.SecretVelero, "cloud", data) 400 } 401 402 // ConfigureVelero configures the velero SA and secret 403 func (o *StepVerifyPreInstallOptions) configureVelero(requirements *config.RequirementsConfig) (string, error) { 404 if requirements.Cluster.Provider != cloud.GKE { 405 log.Logger().Infof("we are assuming your IAM roles are setup so that Velero has cluster-admin\n") 406 return "", nil 407 } 408 409 serviceAccountDir, err := ioutil.TempDir("", "gke") 410 if err != nil { 411 return "", errors.Wrap(err, "creating a temporary folder where the service account will be stored") 412 } 413 defer os.RemoveAll(serviceAccountDir) 414 415 clusterName := requirements.Cluster.ClusterName 416 projectID := requirements.Cluster.ProjectID 417 if projectID == "" || clusterName == "" { 418 if kubeClient, ns, err := o.KubeClientAndDevNamespace(); err == nil { 419 if data, err := kube.ReadInstallValues(kubeClient, ns); err == nil && data != nil { 420 if projectID == "" { 421 projectID = data[kube.ProjectID] 422 } 423 if clusterName == "" { 424 clusterName = data[kube.ClusterName] 425 } 426 } 427 } 428 } 429 if projectID == "" { 430 projectID, err = o.GetGoogleProjectID("") 431 if err != nil { 432 return "", errors.Wrap(err, "getting the GCP project ID") 433 } 434 requirements.Cluster.ProjectID = projectID 435 } 436 if clusterName == "" { 437 clusterName, err = o.GetGKEClusterNameFromContext() 438 if err != nil { 439 return "", errors.Wrap(err, "gettting the GKE cluster name from current context") 440 } 441 requirements.Cluster.ClusterName = clusterName 442 } 443 444 serviceAccountName := requirements.Velero.ServiceAccount 445 if serviceAccountName == "" { 446 serviceAccountName = naming.ToValidNameTruncated(fmt.Sprintf("%s-vo", clusterName), 30) 447 requirements.Velero.ServiceAccount = serviceAccountName 448 } 449 log.Logger().Infof("Configuring Velero service account %s for project %s", util.ColorInfo(serviceAccountName), util.ColorInfo(projectID)) 450 serviceAccountPath, err := o.GCloud().GetOrCreateServiceAccount(serviceAccountName, projectID, serviceAccountDir, gke.VeleroServiceAccountRoles) 451 if err != nil { 452 return "", errors.Wrap(err, "creating the service account") 453 } 454 455 bucket := requirements.Storage.Backup.URL 456 if bucket == "" { 457 return "", fmt.Errorf("missing requirements.storage.backup.url") 458 } 459 err = o.GCloud().ConfigureBucketRoles(projectID, serviceAccountName, bucket, gke.VeleroServiceAccountRoles) 460 if err != nil { 461 return "", errors.Wrap(err, "associate the IAM roles to the bucket") 462 } 463 464 serviceAccount, err := ioutil.ReadFile(serviceAccountPath) 465 if err != nil { 466 return "", errors.Wrapf(err, "reading the service account from file '%s'", serviceAccountPath) 467 } 468 return string(serviceAccount), nil 469 } 470 471 // VerifyInstallConfig lets ensure we modify the install ConfigMap with the requirements 472 func (o *StepVerifyPreInstallOptions) VerifyInstallConfig(kubeClient kubernetes.Interface, ns string, requirements *config.RequirementsConfig, requirementsFileName string) error { 473 log.Logger().Debug("Verifying Install Config...") 474 _, err := kube.DefaultModifyConfigMap(kubeClient, ns, kube.ConfigMapNameJXInstallConfig, 475 func(configMap *corev1.ConfigMap) error { 476 secretsLocation := string(secrets.FileSystemLocationKind) 477 if requirements.SecretStorage == config.SecretStorageTypeVault { 478 secretsLocation = string(secrets.VaultLocationKind) 479 } 480 modifyMapIfNotBlank(configMap.Data, kube.KubeProvider, requirements.Cluster.Provider) 481 modifyMapIfNotBlank(configMap.Data, kube.ProjectID, requirements.Cluster.ProjectID) 482 modifyMapIfNotBlank(configMap.Data, kube.ClusterName, requirements.Cluster.ClusterName) 483 modifyMapIfNotBlank(configMap.Data, secrets.SecretsLocationKey, secretsLocation) 484 modifyMapIfNotBlank(configMap.Data, kube.Region, requirements.Cluster.Region) 485 modifyMapIfNotBlank(configMap.Data, kube.Zone, requirements.Cluster.Zone) 486 return nil 487 }, nil) 488 if err != nil { 489 return errors.Wrapf(err, "saving secrets location in ConfigMap %s in namespace %s", kube.ConfigMapNameJXInstallConfig, ns) 490 } 491 return nil 492 } 493 494 // gatherRequirements gathers cluster requirements and connects to the cluster if required 495 func (o *StepVerifyPreInstallOptions) gatherRequirements(requirements *config.RequirementsConfig, requirementsFileName string) (*config.RequirementsConfig, error) { 496 log.Logger().Debug("Gathering Requirements...") 497 if o.BatchMode { 498 msg := "please specify '%s' in jx-requirements when running in batch mode" 499 if requirements.Cluster.Provider == "" { 500 return nil, errors.Errorf(msg, "provider") 501 } 502 if requirements.Cluster.Provider == cloud.EKS || requirements.Cluster.Provider == cloud.AWS { 503 if requirements.Cluster.Region == "" { 504 return nil, errors.Errorf(msg, "region") 505 } 506 } 507 if requirements.Cluster.Provider == cloud.GKE { 508 if requirements.Cluster.ProjectID == "" { 509 return nil, errors.Errorf(msg, "project") 510 } 511 if requirements.Cluster.Zone == "" { 512 return nil, errors.Errorf(msg, "zone") 513 } 514 } 515 if requirements.Cluster.EnvironmentGitOwner == "" { 516 return nil, errors.Errorf(msg, "environmentGitOwner") 517 } 518 if requirements.Cluster.ClusterName == "" { 519 return nil, errors.Errorf(msg, "clusterName") 520 } 521 } 522 var err error 523 if requirements.Cluster.Provider == "" { 524 requirements.Cluster.Provider, err = util.PickName(cloud.KubernetesProviders, "Select Kubernetes provider", "the type of Kubernetes installation", o.GetIOFileHandles()) 525 if err != nil { 526 return nil, errors.Wrap(err, "selecting Kubernetes provider") 527 } 528 } 529 530 if requirements.Cluster.Provider != cloud.GKE && requirements.Cluster.Provider != cloud.EKS { 531 // lets check we want to try installation as we've only tested on GKE at the moment 532 if answer, err := o.showProvideFeedbackMessage(); err != nil { 533 return requirements, err 534 } else if !answer { 535 return requirements, errors.New("finishing execution") 536 } 537 } 538 539 if requirements.Cluster.Provider == cloud.GKE { 540 var currentProject, currentZone, currentClusterName string 541 autoAcceptDefaults := false 542 if requirements.Cluster.ProjectID == "" || requirements.Cluster.Zone == "" || requirements.Cluster.ClusterName == "" { 543 kubeConfig, _, err := o.Kube().LoadConfig() 544 if err != nil { 545 return nil, errors.Wrapf(err, "loading kubeconfig") 546 } 547 context := kube.Cluster(kubeConfig) 548 currentProject, currentZone, currentClusterName, err = gke.ParseContext(context) 549 if err != nil { 550 return nil, errors.Wrapf(err, "") 551 } 552 if currentClusterName != "" && currentProject != "" && currentZone != "" { 553 log.Logger().Infof("Currently connected cluster is %s in %s in project %s", util.ColorInfo(currentClusterName), util.ColorInfo(currentZone), util.ColorInfo(currentProject)) 554 autoAcceptDefaults, err = util.Confirm(fmt.Sprintf("Do you want to jx boot the %s cluster?", util.ColorInfo(currentClusterName)), true, "Enter Y to use the currently connected cluster or enter N to specify a different cluster", o.GetIOFileHandles()) 555 if err != nil { 556 return nil, err 557 } 558 } else { 559 log.Logger().Infof("Enter the cluster you want to jx boot") 560 } 561 } 562 563 if requirements.Cluster.ProjectID == "" { 564 if autoAcceptDefaults && currentProject != "" { 565 requirements.Cluster.ProjectID = currentProject 566 } else { 567 requirements.Cluster.ProjectID, err = o.GetGoogleProjectID(currentProject) 568 if err != nil { 569 return nil, errors.Wrap(err, "getting project ID") 570 } 571 } 572 } 573 if requirements.Cluster.Zone == "" { 574 if autoAcceptDefaults && currentZone != "" { 575 requirements.Cluster.Zone = currentZone 576 } else { 577 requirements.Cluster.Zone, err = o.GetGoogleZone(requirements.Cluster.ProjectID, currentZone) 578 if err != nil { 579 return nil, errors.Wrap(err, "getting GKE Zone") 580 } 581 } 582 } 583 if requirements.Cluster.ClusterName == "" { 584 if autoAcceptDefaults && currentClusterName != "" { 585 requirements.Cluster.ClusterName = currentClusterName 586 } else { 587 requirements.Cluster.ClusterName, err = util.PickValue("Cluster name", currentClusterName, true, 588 "The name for your cluster", o.GetIOFileHandles()) 589 if err != nil { 590 return nil, errors.Wrap(err, "getting cluster name") 591 } 592 if requirements.Cluster.ClusterName == "" { 593 return nil, errors.Errorf("no cluster name provided") 594 } 595 } 596 } 597 if !autoAcceptDefaults { 598 if !o.WorkloadIdentity && !o.InCluster() { 599 // connect to the specified cluster if different from the currently connected one 600 log.Logger().Infof("Connecting to cluster %s", util.ColorInfo(requirements.Cluster.ClusterName)) 601 err = o.GCloud().ConnectToCluster(requirements.Cluster.ProjectID, requirements.Cluster.Zone, requirements.Cluster.ClusterName) 602 if err != nil { 603 return nil, err 604 } 605 } else { 606 log.Logger().Info("no need to reconnect to cluster") 607 } 608 } 609 } else if requirements.Cluster.Provider == cloud.EKS || requirements.Cluster.Provider == cloud.AWS { 610 var currentRegion, currentClusterName string 611 var autoAcceptDefaults bool 612 if requirements.Cluster.Region == "" || requirements.Cluster.ClusterName == "" { 613 currentClusterName, currentRegion, err = session.GetCurrentlyConnectedRegionAndClusterName() 614 if err != nil { 615 return requirements, errors.Wrap(err, "there was a problem obtaining the current cluster name and region") 616 } 617 if currentClusterName != "" && currentRegion != "" { 618 log.Logger().Infof("") 619 log.Logger().Infof("Currently connected cluster is %s in region %s", util.ColorInfo(currentClusterName), util.ColorInfo(currentRegion)) 620 autoAcceptDefaults, err = util.Confirm(fmt.Sprintf("Do you want to jx boot the %s cluster?", util.ColorInfo(currentClusterName)), true, "Enter Y to use the currently connected cluster or enter N to specify a different cluster", o.GetIOFileHandles()) 621 if err != nil { 622 return nil, err 623 } 624 } else { 625 log.Logger().Infof("Enter the cluster you want to jx boot") 626 } 627 } 628 629 if requirements.Cluster.Region == "" { 630 if autoAcceptDefaults && currentRegion != "" { 631 requirements.Cluster.Region = currentRegion 632 } 633 } 634 if requirements.Cluster.ClusterName == "" { 635 if autoAcceptDefaults && currentClusterName != "" { 636 requirements.Cluster.ClusterName = currentClusterName 637 } else { 638 requirements.Cluster.ClusterName, err = util.PickValue("Cluster name", currentClusterName, true, 639 "The name for your cluster", o.GetIOFileHandles()) 640 if err != nil { 641 return nil, errors.Wrap(err, "getting cluster name") 642 } 643 } 644 } 645 } 646 647 if requirements.Cluster.ClusterName == "" && !o.BatchMode { 648 requirements.Cluster.ClusterName, err = util.PickValue("Cluster name", "", true, 649 "The name for your cluster", o.GetIOFileHandles()) 650 if err != nil { 651 return nil, errors.Wrap(err, "getting cluster name") 652 } 653 if requirements.Cluster.ClusterName == "" { 654 return nil, errors.Errorf("no cluster name provided") 655 } 656 } 657 658 requirements.Cluster.Provider = strings.TrimSpace(strings.ToLower(requirements.Cluster.Provider)) 659 requirements.Cluster.ProjectID = strings.TrimSpace(requirements.Cluster.ProjectID) 660 requirements.Cluster.Zone = strings.TrimSpace(strings.ToLower(requirements.Cluster.Zone)) 661 requirements.Cluster.Region = strings.TrimSpace(strings.ToLower(requirements.Cluster.Region)) 662 requirements.Cluster.ClusterName = strings.TrimSpace(strings.ToLower(requirements.Cluster.ClusterName)) 663 664 err = o.gatherGitRequirements(requirements) 665 if err != nil { 666 return nil, errors.Wrap(err, "error gathering git requirements") 667 } 668 669 // Lock the version stream to a tag 670 if requirements.VersionStream.Ref == "" { 671 requirements.VersionStream.Ref = os.Getenv(boot.VersionsRepoBaseRefEnvVarName) 672 } 673 if requirements.VersionStream.URL == "" { 674 requirements.VersionStream.URL = os.Getenv(boot.VersionsRepoURLEnvVarName) 675 } 676 677 // attempt to resolve the version stream ref to a tag 678 _, ref, err := o.CloneJXVersionsRepo(requirements.VersionStream.URL, requirements.VersionStream.Ref) 679 if err != nil { 680 return nil, errors.Wrapf(err, "resolving version stream ref") 681 } 682 if ref != "" && ref != requirements.VersionStream.Ref { 683 log.Logger().Infof("Locking version stream %s to release %s. Jenkins X will use this release rather than %s to resolve all versions from now on.", util.ColorInfo(requirements.VersionStream.URL), util.ColorInfo(ref), requirements.VersionStream.Ref) 684 requirements.VersionStream.Ref = ref 685 } 686 687 err = o.SaveConfig(requirements, requirementsFileName) 688 if err != nil { 689 return nil, errors.Wrap(err, "error saving requirements file") 690 } 691 692 err = o.writeOwnersFile(requirements) 693 if err != nil { 694 return nil, errors.Wrapf(err, "writing approvers to OWNERS file in %s", o.Dir) 695 } 696 697 return requirements, nil 698 } 699 700 func (o *StepVerifyPreInstallOptions) writeOwnersFile(requirements *config.RequirementsConfig) error { 701 if len(requirements.Cluster.DevEnvApprovers) > 0 { 702 path := filepath.Join(o.Dir, "OWNERS") 703 filename, err := filepath.Abs(path) 704 if err != nil { 705 return errors.Wrapf(err, "failed to resolve path %s", path) 706 } 707 data := prow.Owners{} 708 for _, approver := range requirements.Cluster.DevEnvApprovers { 709 data.Approvers = append(data.Approvers, approver) 710 data.Reviewers = append(data.Reviewers, approver) 711 } 712 ownersYaml, err := yaml.Marshal(data) 713 if err != nil { 714 return err 715 } 716 err = ioutil.WriteFile(filename, ownersYaml, 0600) 717 if err != nil { 718 return err 719 } 720 log.Logger().Infof("writing the following to the OWNERS file for the development environment repository:\n%s", string(ownersYaml)) 721 } 722 return nil 723 } 724 725 func (o *StepVerifyPreInstallOptions) gatherGitRequirements(requirements *config.RequirementsConfig) error { 726 requirements.Cluster.EnvironmentGitOwner = strings.TrimSpace(requirements.Cluster.EnvironmentGitOwner) 727 728 // lets fix up any missing or incorrect git kinds for public git servers 729 if gits.IsGitHubServerURL(requirements.Cluster.GitServer) { 730 requirements.Cluster.GitKind = "github" 731 } else if gits.IsGitLabServerURL(requirements.Cluster.GitServer) { 732 requirements.Cluster.GitKind = "gitlab" 733 } 734 735 var err error 736 if requirements.Cluster.EnvironmentGitOwner == "" { 737 requirements.Cluster.EnvironmentGitOwner, err = util.PickValue( 738 "Git Owner name for environment repositories", 739 "", 740 true, 741 "Jenkins X leverages GitOps to track and control what gets deployed into environments. "+ 742 "This requires a Git repository per environment. "+ 743 "This question is asking for the Git Owner where these repositories will live.", 744 o.GetIOFileHandles()) 745 if err != nil { 746 return errors.Wrap(err, "error configuring git owner for env repositories") 747 } 748 749 if requirements.Cluster.EnvironmentGitPublic { 750 log.Logger().Infof("Environment repos will be %s, if you want to create %s environment repos, please set %s to %s jx-requirements.yml", util.ColorInfo("public"), util.ColorInfo("private"), util.ColorInfo("environmentGitPublic"), util.ColorInfo("false")) 751 } else { 752 log.Logger().Infof("Environment repos will be %s, if you want to create %s environment repos, please set %s to %s in jx-requirements.yml", util.ColorInfo("private"), util.ColorInfo("public"), util.ColorInfo("environmentGitPublic"), util.ColorInfo("true")) 753 } 754 } 755 if len(requirements.Cluster.DevEnvApprovers) == 0 && !o.BatchMode { 756 approversString, err := util.PickValue( 757 "Comma-separated git provider usernames of approvers for development environment repository", 758 "", 759 true, 760 "Pull requests to the development environment repository require approval by one or more "+ 761 "users, specified in the 'OWNERS' file in the repository. Please specify a comma-separated "+ 762 "list of usernames for your Git provider to be used as approvers.", 763 o.GetIOFileHandles()) 764 if err != nil { 765 return errors.Wrap(err, "configuring approvers for development environment repository") 766 } 767 for _, a := range strings.Split(approversString, ",") { 768 requirements.Cluster.DevEnvApprovers = append(requirements.Cluster.DevEnvApprovers, strings.TrimSpace(a)) 769 } 770 } 771 return nil 772 } 773 774 // verifyStorage verifies the associated buckets exist or if enabled lazily create them 775 func (o *StepVerifyPreInstallOptions) verifyStorage(requirements *config.RequirementsConfig, requirementsFileName string) error { 776 log.Logger().Info("Verifying Storage...") 777 storage := &requirements.Storage 778 err := o.verifyStorageEntry(requirements, requirementsFileName, &storage.Logs, "logs", "Long term log storage") 779 if err != nil { 780 return err 781 } 782 err = o.verifyStorageEntry(requirements, requirementsFileName, &storage.Reports, "reports", "Long term report storage") 783 if err != nil { 784 return err 785 } 786 err = o.verifyStorageEntry(requirements, requirementsFileName, &storage.Repository, "repository", "Chart repository") 787 if err != nil { 788 return err 789 } 790 err = o.verifyStorageEntry(requirements, requirementsFileName, &storage.Backup, "backup", "backup storage") 791 if err != nil { 792 return err 793 } 794 log.Logger().Infof("Storage configuration looks good\n") 795 return nil 796 } 797 798 func (o *StepVerifyPreInstallOptions) verifyTLS(requirements *config.RequirementsConfig) error { 799 if !requirements.Ingress.TLS.Enabled { 800 confirm := false 801 // an existing Vault URL indicated an external Vault instance in which case the following warning is not necessary 802 if requirements.SecretStorage == config.SecretStorageTypeVault && requirements.Vault.URL == "" { 803 log.Logger().Warnf("Vault is enabled and TLS is not. This means your secrets will be sent to and from your cluster in the clear. See %s for more information", config.TLSDocURL) 804 confirm = true 805 } 806 if requirements.Webhook != config.WebhookTypeNone { 807 log.Logger().Warnf("TLS is not enabled so your webhooks will be called using HTTP. This means your webhook secret will be sent to your cluster in the clear. See %s for more information", config.TLSDocURL) 808 confirm = true 809 } 810 if os.Getenv(boot.OverrideTLSWarningEnvVarName) == "true" { 811 confirm = false 812 } 813 if confirm && !o.BatchMode { 814 message := fmt.Sprintf("Do you wish to continue?") 815 help := fmt.Sprintf("Jenkins X needs TLS enabled to send secrets securely. We strongly recommend enabling TLS.") 816 if answer, err := util.Confirm(message, false, help, o.GetIOFileHandles()); err != nil { 817 return err 818 } else if !answer { 819 return errors.Errorf("cannot continue because TLS is not enabled.") 820 } 821 } 822 } 823 return nil 824 } 825 826 func (o *StepVerifyPreInstallOptions) verifyStorageEntry(requirements *config.RequirementsConfig, requirementsFileName string, storageEntryConfig *config.StorageEntryConfig, name string, text string) error { 827 kubeProvider := requirements.Cluster.Provider 828 if !storageEntryConfig.Enabled { 829 if requirements.IsCloudProvider() { 830 log.Logger().Warnf("Your requirements have not enabled cloud storage for %s - we recommend enabling this for kubernetes provider %s", name, kubeProvider) 831 } 832 return nil 833 } 834 835 provider := factory.NewBucketProvider(requirements) 836 837 if storageEntryConfig.URL == "" { 838 // lets allow the storage bucket to be entered or created 839 if o.BatchMode { 840 log.Logger().Warnf("No URL provided for storage: %s", name) 841 return nil 842 } 843 scheme := buckets.KubeProviderToBucketScheme(kubeProvider) 844 if scheme == "" { 845 scheme = "s3" 846 } 847 message := fmt.Sprintf("%s bucket URL. Press enter to create and use a new bucket", text) 848 help := fmt.Sprintf("please enter the URL of the bucket to use for storage using the format %s://<bucket-name>", scheme) 849 value, err := util.PickValue(message, "", false, help, o.GetIOFileHandles()) 850 if err != nil { 851 return errors.Wrapf(err, "failed to pick storage bucket for %s", name) 852 } 853 854 if value == "" { 855 if provider == nil { 856 log.Logger().Warnf("the kubernetes provider %s has no BucketProvider in jx yet so we cannot lazily create buckets", kubeProvider) 857 log.Logger().Warnf("long term storage for %s will be disabled until you provide an existing bucket URL", name) 858 return nil 859 } 860 safeClusterName := naming.ToValidName(requirements.Cluster.ClusterName) 861 safeName := naming.ToValidName(name) 862 value, err = provider.CreateNewBucketForCluster(safeClusterName, safeName) 863 if err != nil { 864 return errors.Wrapf(err, "failed to create a dynamic bucket for cluster %s and name %s", safeClusterName, safeName) 865 } 866 } 867 if value != "" { 868 storageEntryConfig.URL = value 869 870 err = o.SaveConfig(requirements, requirementsFileName) 871 if err != nil { 872 return errors.Wrapf(err, "failed to save changes to file: %s", requirementsFileName) 873 } 874 } 875 } 876 877 if storageEntryConfig.URL != "" { 878 if provider == nil { 879 log.Logger().Warnf("the kubernetes provider %s has no BucketProvider in jx yet - so you have to manually setup and verify your bucket URLs exist", kubeProvider) 880 log.Logger().Infof("please verify this bucket exists: %s", util.ColorInfo(storageEntryConfig.URL)) 881 return nil 882 } 883 884 err := provider.EnsureBucketIsCreated(storageEntryConfig.URL) 885 if err != nil { 886 return errors.Wrapf(err, "failed to ensure the bucket URL %s is created", storageEntryConfig.URL) 887 } 888 } 889 return nil 890 } 891 892 func (o *StepVerifyPreInstallOptions) verifyProwConfigMaps(kubeClient kubernetes.Interface, ns string) error { 893 err := o.verifyConfigMapExists(kubeClient, ns, "config", "config.yaml", "pod_namespace: jx") 894 if err != nil { 895 return err 896 } 897 return o.verifyConfigMapExists(kubeClient, ns, "plugins", "plugins.yaml", "cat: {}") 898 } 899 900 func (o *StepVerifyPreInstallOptions) verifyConfigMapExists(kubeClient kubernetes.Interface, ns string, name string, key string, defaultValue string) error { 901 info := util.ColorInfo 902 configMapInterface := kubeClient.CoreV1().ConfigMaps(ns) 903 cm, err := configMapInterface.Get(name, metav1.GetOptions{}) 904 if err != nil { 905 // lets try create it 906 cm = &corev1.ConfigMap{ 907 ObjectMeta: metav1.ObjectMeta{ 908 Name: name, 909 }, 910 Data: map[string]string{ 911 key: defaultValue, 912 }, 913 } 914 cm, err = configMapInterface.Create(cm) 915 if err != nil { 916 // maybe someone else just created it - lets try one more time 917 cm2, err2 := configMapInterface.Get(name, metav1.GetOptions{}) 918 if err == nil { 919 log.Logger().Infof("created ConfigMap %s in namespace %s", info(name), info(ns)) 920 } 921 if err2 != nil { 922 return fmt.Errorf("failed to create the ConfigMap %s in namespace %s due to: %s - we cannot get it either: %s", name, ns, err.Error(), err2.Error()) 923 } 924 cm = cm2 925 err = nil 926 } 927 } 928 if err != nil { 929 return err 930 } 931 932 // lets verify that there is an entry 933 if cm.Data == nil { 934 cm.Data = map[string]string{} 935 } 936 _, ok := cm.Data[key] 937 if !ok { 938 cm.Data[key] = defaultValue 939 cm.Name = name 940 941 _, err = configMapInterface.Update(cm) 942 if err != nil { 943 return fmt.Errorf("failed to update the ConfigMap %s in namespace %s to add key %s due to: %s", name, ns, key, err.Error()) 944 } 945 } 946 log.Logger().Infof("verified there is a ConfigMap %s in namespace %s", info(name), info(ns)) 947 return nil 948 } 949 950 func (o *StepVerifyPreInstallOptions) verifyIngress(requirements *config.RequirementsConfig, requirementsFileName string) error { 951 log.Logger().Info("Verifying Ingress...") 952 domain := requirements.Ingress.Domain 953 if requirements.Ingress.IsAutoDNSDomain() && !requirements.Ingress.IgnoreLoadBalancer { 954 log.Logger().Infof("Clearing the domain %s as when using auto-DNS domains we need to regenerate to ensure its always accurate in case the cluster or ingress service is recreated", util.ColorInfo(domain)) 955 requirements.Ingress.Domain = "" 956 err := o.SaveConfig(requirements, requirementsFileName) 957 if err != nil { 958 return errors.Wrapf(err, "failed to save changes to file: %s", requirementsFileName) 959 } 960 } 961 log.Logger().Info("\n") 962 return nil 963 } 964 965 // ValidateRequirements validate the requirements; e.g. the webhook and git provider 966 func (o *StepVerifyPreInstallOptions) ValidateRequirements(requirements *config.RequirementsConfig, fileName string) error { 967 if requirements.Webhook == config.WebhookTypeProw { 968 kind := requirements.Cluster.GitKind 969 server := requirements.Cluster.GitServer 970 if (kind != "" && kind != "github") || (server != "" && !gits.IsGitHubServerURL(server)) { 971 return fmt.Errorf("invalid requirements in file %s cannot use prow as a webhook for git kind: %s server: %s. Please try using lighthouse instead", fileName, kind, server) 972 } 973 } 974 if requirements.Repository == config.RepositoryTypeBucketRepo && requirements.Cluster.ChartRepository == "" { 975 requirements.Cluster.ChartRepository = "http://bucketrepo/bucketrepo/charts/" 976 err := o.SaveConfig(requirements, fileName) 977 if err != nil { 978 return errors.Wrapf(err, "failed to save changes to file: %s", fileName) 979 } 980 } 981 982 // lets verify that we have a repository name defined for every environment 983 modified := false 984 for i, env := range requirements.Environments { 985 if env.Repository == "" { 986 clusterName := requirements.Cluster.ClusterName 987 if clusterName != "" { 988 clusterName = clusterName + "-" 989 } 990 repoName := "environment-" + clusterName + env.Key 991 requirements.Environments[i].Repository = naming.ToValidName(repoName) 992 modified = true 993 } 994 } 995 if modified { 996 err := o.SaveConfig(requirements, fileName) 997 if err != nil { 998 return errors.Wrapf(err, "failed to save changes to file: %s", fileName) 999 } 1000 } 1001 o.showPermissionsModeMessage(requirements) 1002 return nil 1003 } 1004 1005 // SaveConfig saves the configuration file to the given project directory 1006 func (o *StepVerifyPreInstallOptions) SaveConfig(c *config.RequirementsConfig, fileName string) error { 1007 data, err := yaml.Marshal(c) 1008 if err != nil { 1009 return err 1010 } 1011 err = ioutil.WriteFile(fileName, data, util.DefaultWritePermissions) 1012 if err != nil { 1013 return errors.Wrapf(err, "failed to save file %s", fileName) 1014 } 1015 1016 if c.Helmfile { 1017 y := config.RequirementsValues{ 1018 RequirementsConfig: c, 1019 } 1020 data, err = yaml.Marshal(y) 1021 if err != nil { 1022 return err 1023 } 1024 1025 err = ioutil.WriteFile(path.Join(path.Dir(fileName), config.RequirementsValuesFileName), data, util.DefaultWritePermissions) 1026 if err != nil { 1027 return errors.Wrapf(err, "failed to save file %s", config.RequirementsValuesFileName) 1028 } 1029 } 1030 return nil 1031 } 1032 1033 func modifyMapIfNotBlank(m map[string]string, key string, value string) { 1034 if m != nil { 1035 if value != "" { 1036 m[key] = value 1037 } else { 1038 log.Logger().Debugf("Cannot update key %s, value is nil", key) 1039 } 1040 } else { 1041 log.Logger().Debugf("Cannot update key %s, map is nil", key) 1042 } 1043 } 1044 1045 func (o *StepVerifyPreInstallOptions) showProvideFeedbackMessage() (bool, error) { 1046 log.Logger().Info("jx boot has only been validated on GKE and EKS, we'd love feedback and contributions for other Kubernetes providers") 1047 if !o.BatchMode { 1048 return util.Confirm("Continue execution anyway?", 1049 true, "", o.GetIOFileHandles()) 1050 } 1051 log.Logger().Info("Running in Batch Mode, execution will continue") 1052 return true, nil 1053 } 1054 1055 func (o *StepVerifyPreInstallOptions) showPermissionsModeMessage(requirementsConfig *config.RequirementsConfig) { 1056 if requirementsConfig.Cluster.StrictPermissions && requirementsConfig.Cluster.Provider != cloud.OPENSHIFT { 1057 log.Logger().Info(`The provided requirements file has 'strictPermissions' enabled but 'provider' is not Openshift. 1058 This feature is only supported on Openshift`) 1059 } 1060 }