github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/kube/env.go (about) 1 package kube 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "os/user" 8 "path/filepath" 9 "regexp" 10 "sort" 11 "strings" 12 13 "github.com/olli-ai/jx/v2/pkg/jenkinsfile" 14 15 "github.com/ghodss/yaml" 16 "github.com/pkg/errors" 17 18 v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1" 19 "github.com/jenkins-x/jx-api/pkg/client/clientset/versioned" 20 "github.com/jenkins-x/jx-logging/pkg/log" 21 "github.com/olli-ai/jx/v2/pkg/auth" 22 "github.com/olli-ai/jx/v2/pkg/config" 23 "github.com/olli-ai/jx/v2/pkg/gits" 24 "github.com/olli-ai/jx/v2/pkg/util" 25 survey "gopkg.in/AlecAivazis/survey.v1" 26 corev1 "k8s.io/api/core/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/client-go/kubernetes" 29 ) 30 31 var useForkForEnvGitRepo = false 32 33 // ResolveChartMuseumURLFn used to resolve the chart repository URL if using remote environments 34 type ResolveChartMuseumURLFn func() (string, error) 35 36 // CreateEnvironmentSurvey creates a Survey on the given environment using the default options 37 // from the CLI 38 func CreateEnvironmentSurvey(batchMode bool, authConfigSvc auth.ConfigService, devEnv *v1.Environment, data *v1.Environment, 39 config *v1.Environment, update bool, forkEnvGitURL string, ns string, jxClient versioned.Interface, kubeClient kubernetes.Interface, envDir string, 40 gitRepoOptions *gits.GitRepositoryOptions, helmValues config.HelmValuesConfig, prefix string, git gits.Gitter, chartMusemFn ResolveChartMuseumURLFn, handles util.IOFileHandles) (gits.GitProvider, error) { 41 surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err) 42 name := data.Name 43 createMode := name == "" 44 if createMode { 45 if config.Name != "" { 46 err := ValidNameOption(OptionName, config.Name) 47 if err != nil { 48 return nil, err 49 } 50 if !update { 51 err = ValidateEnvironmentDoesNotExist(jxClient, ns, config.Name) 52 if err != nil { 53 return nil, err 54 } 55 } 56 data.Name = config.Name 57 } else { 58 if batchMode { 59 return nil, fmt.Errorf("environment name cannot be empty. Use --name option.") 60 } 61 62 validator := func(val interface{}) error { 63 err := ValidateName(val) 64 if err != nil { 65 return err 66 } 67 str, ok := val.(string) 68 if !ok { 69 return fmt.Errorf("Expected string value") 70 } 71 v := ValidateEnvironmentDoesNotExist(jxClient, ns, str) 72 return v 73 } 74 75 q := &survey.Input{ 76 Message: "Name:", 77 Help: "The Environment name must be unique, lower case and a valid DNS name", 78 } 79 err := survey.AskOne(q, &data.Name, validator, surveyOpts) 80 if err != nil { 81 return nil, err 82 } 83 } 84 } 85 if string(config.Spec.Kind) != "" { 86 data.Spec.Kind = config.Spec.Kind 87 } else { 88 if string(data.Spec.Kind) == "" { 89 data.Spec.Kind = v1.EnvironmentKindTypePermanent 90 } 91 } 92 if config.Spec.Label != "" { 93 data.Spec.Label = config.Spec.Label 94 } else { 95 defaultValue := data.Spec.Label 96 if defaultValue == "" { 97 defaultValue = strings.Title(data.Name) 98 } 99 q := &survey.Input{ 100 Message: "Label:", 101 Default: defaultValue, 102 Help: "The Environment label is a person friendly descriptive text like 'Staging' or 'Production'", 103 } 104 err := survey.AskOne(q, &data.Spec.Label, survey.Required, surveyOpts) 105 if err != nil { 106 return nil, err 107 } 108 } 109 if config.Spec.Namespace != "" { 110 err := ValidNameOption(OptionNamespace, config.Spec.Namespace) 111 if err != nil { 112 return nil, err 113 } 114 data.Spec.Namespace = config.Spec.Namespace 115 } else { 116 defaultValue := data.Spec.Namespace 117 if defaultValue == "" { 118 // lets use the namespace as a team name 119 defaultValue = data.Namespace 120 if defaultValue == "" { 121 defaultValue = ns 122 } 123 if data.Name != "" { 124 if defaultValue == "" { 125 defaultValue = data.Name 126 } else { 127 defaultValue += "-" + data.Name 128 } 129 } 130 } 131 if batchMode { 132 data.Spec.Namespace = defaultValue 133 } else { 134 q := &survey.Input{ 135 Message: "Namespace:", 136 Default: defaultValue, 137 Help: "The Kubernetes namespace name to use for this Environment", 138 } 139 err := survey.AskOne(q, &data.Spec.Namespace, ValidateName, surveyOpts) 140 if err != nil { 141 return nil, err 142 } 143 } 144 } 145 146 if helmValues.ExposeController.Config.Domain == "" { 147 ic, err := GetIngressConfig(kubeClient, ns) 148 if err != nil { 149 return nil, err 150 } 151 152 if batchMode { 153 log.Logger().Infof("Running in batch mode and no domain flag used so defaulting to team domain %s", ic.Domain) 154 helmValues.ExposeController.Config.Domain = ic.Domain 155 } else { 156 q := &survey.Input{ 157 Message: "Domain:", 158 Default: ic.Domain, 159 Help: "Domain to expose ingress endpoints. Example: jenkinsx.io, leave blank if no appplications are to be exposed via ingress rules", 160 } 161 err := survey.AskOne(q, &helmValues.ExposeController.Config.Domain, nil, surveyOpts) 162 if err != nil { 163 return nil, err 164 } 165 } 166 } 167 168 data.Spec.RemoteCluster = config.Spec.RemoteCluster 169 if !batchMode { 170 var err error 171 data.Spec.RemoteCluster, err = util.Confirm("Environment in separate cluster to Dev Environment:", 172 data.Spec.RemoteCluster, " Is this Environment going to be in a different cluster to the Development environment. For help on Multi Cluster support see: https://jenkins-x.io/getting-started/multi-cluster/", handles) 173 if err != nil { 174 return nil, err 175 } 176 } 177 if config.Spec.Cluster != "" { 178 data.Spec.Cluster = config.Spec.Cluster 179 } else { 180 if data.Spec.RemoteCluster { 181 // lets not show the UI for this if users specify the namespace via arguments 182 if !createMode || config.Spec.Namespace == "" { 183 defaultValue := data.Spec.Cluster 184 if batchMode { 185 data.Spec.Cluster = defaultValue 186 } else { 187 q := &survey.Input{ 188 Message: "Cluster URL:", 189 Default: defaultValue, 190 Help: "The Kubernetes cluster URL to use to host this Environment. You can leave this blank for now.", 191 } 192 // TODO validate/transform to match valid kubnernetes cluster syntax 193 err := survey.AskOne(q, &data.Spec.Cluster, nil, surveyOpts) 194 if err != nil { 195 return nil, err 196 } 197 } 198 } 199 } 200 } 201 if string(config.Spec.PromotionStrategy) != "" { 202 data.Spec.PromotionStrategy = config.Spec.PromotionStrategy 203 } else { 204 promoteValues := []string{ 205 string(v1.PromotionStrategyTypeAutomatic), 206 string(v1.PromotionStrategyTypeManual), 207 string(v1.PromotionStrategyTypeNever), 208 } 209 defaultValue := string(data.Spec.PromotionStrategy) 210 if defaultValue == "" { 211 defaultValue = string(v1.PromotionStrategyTypeAutomatic) 212 } 213 q := &survey.Select{ 214 Message: "Promotion Strategy:", 215 Options: promoteValues, 216 Default: defaultValue, 217 Help: "Whether we promote to this Environment automatically, manually or never", 218 } 219 textValue := "" 220 err := survey.AskOne(q, &textValue, survey.Required, surveyOpts) 221 if err != nil { 222 return nil, err 223 } 224 if textValue != "" { 225 data.Spec.PromotionStrategy = v1.PromotionStrategyType(textValue) 226 } 227 } 228 if string(data.Spec.PromotionStrategy) == "" { 229 data.Spec.PromotionStrategy = v1.PromotionStrategyTypeAutomatic 230 } 231 if config.Spec.Order != 0 { 232 data.Spec.Order = config.Spec.Order 233 } else { 234 order := data.Spec.Order 235 if order == 0 { 236 // TODO should we generate an order to default to last one? 237 order = 100 238 } 239 defaultValue := util.Int32ToA(order) 240 q := &survey.Input{ 241 Message: "Order:", 242 Default: defaultValue, 243 Help: "This number is used to sort Environments in sequential order, lowest first", 244 } 245 textValue := "" 246 err := survey.AskOne(q, &textValue, survey.Required, surveyOpts) 247 if err != nil { 248 return nil, err 249 } 250 if textValue != "" { 251 i, err := util.AtoInt32(textValue) 252 if err != nil { 253 return nil, fmt.Errorf("Failed to convert input '%s' to number: %s", textValue, err) 254 } 255 data.Spec.Order = i 256 } 257 } 258 if batchMode && gitRepoOptions.Owner == "" { 259 devEnvGitOwner, err := GetDevEnvGitOwner(jxClient) 260 if err != nil { 261 return nil, fmt.Errorf("Failed to get default Git owner for repos: %s", err) 262 } 263 if devEnvGitOwner != "" { 264 gitRepoOptions.Owner = devEnvGitOwner 265 } else { 266 gitRepoOptions.Owner = gitRepoOptions.Username 267 } 268 log.Logger().Infof("Using %s environment git owner in batch mode.", util.ColorInfo(gitRepoOptions.Owner)) 269 } 270 _, gitProvider, err := CreateEnvGitRepository(batchMode, authConfigSvc, devEnv, data, config, forkEnvGitURL, envDir, gitRepoOptions, helmValues, prefix, git, chartMusemFn, handles) 271 return gitProvider, err 272 } 273 274 // CreateEnvGitRepository creates the git repository for the given Environment 275 func CreateEnvGitRepository(batchMode bool, authConfigSvc auth.ConfigService, devEnv *v1.Environment, data *v1.Environment, config *v1.Environment, forkEnvGitURL string, envDir string, gitRepoOptions *gits.GitRepositoryOptions, helmValues config.HelmValuesConfig, prefix string, git gits.Gitter, chartMusemFn ResolveChartMuseumURLFn, handles util.IOFileHandles) (*gits.GitRepository, gits.GitProvider, error) { 276 var gitProvider gits.GitProvider 277 var repo *gits.GitRepository 278 surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err) 279 createRepo := false 280 if config.Spec.Source.URL != "" { 281 data.Spec.Source.URL = config.Spec.Source.URL 282 } else { 283 showURLEdit := devEnv.Spec.TeamSettings.UseGitOps 284 if data.Spec.Source.URL == "" && !showURLEdit { 285 if devEnv.Spec.TeamSettings.AskOnCreate { 286 confirm := &survey.Confirm{ 287 Message: "Would you like to use GitOps to manage this environment? :", 288 Default: false, 289 } 290 err := survey.AskOne(confirm, &showURLEdit, nil, surveyOpts) 291 if err != nil { 292 return repo, nil, errors.Wrap(err, "asking enable GitOps question") 293 } 294 } else { 295 showURLEdit = true 296 } 297 } 298 if showURLEdit { 299 if data.Spec.Source.URL == "" { 300 if batchMode { 301 createRepo = true 302 } else { 303 confirm := &survey.Confirm{ 304 Message: fmt.Sprintf("We will now create a Git repository to store your %s environment, ok? :", data.Name), 305 Default: true, 306 } 307 err := survey.AskOne(confirm, &createRepo, nil, surveyOpts) 308 if err != nil { 309 return repo, nil, errors.Wrapf(err, "asking to create the git repository %q", data.Name) 310 } 311 } 312 313 if createRepo { 314 showURLEdit = false 315 var err error 316 repo, gitProvider, err = DoCreateEnvironmentGitRepo(batchMode, authConfigSvc, data, forkEnvGitURL, envDir, gitRepoOptions, helmValues, prefix, git, chartMusemFn, handles) 317 if err != nil { 318 return repo, gitProvider, errors.Wrap(err, "creating environment git repository") 319 } 320 data.Spec.Source.URL = repo.CloneURL 321 } 322 } else { 323 showURLEdit = true 324 } 325 if showURLEdit && !batchMode { 326 q := &survey.Input{ 327 Message: "Git URL for the Environment source code:", 328 Default: data.Spec.Source.URL, 329 Help: "The git clone URL for the Environment's Helm charts source code and custom configuration", 330 } 331 err := survey.AskOne(q, &data.Spec.Source.URL, survey.Required, surveyOpts) 332 if err != nil { 333 return repo, nil, errors.Wrap(err, "asking for environment git clone URL") 334 } 335 } 336 } 337 } 338 if config.Spec.Source.Ref != "" { 339 data.Spec.Source.Ref = config.Spec.Source.Ref 340 } else { 341 if data.Spec.Source.URL != "" || data.Spec.Source.Ref != "" { 342 if batchMode { 343 createRepo = true 344 } else { 345 defaultBranch := data.Spec.Source.Ref 346 if defaultBranch == "" { 347 defaultBranch = "master" 348 } 349 q := &survey.Input{ 350 Message: "Git branch for the Environment source code:", 351 Default: defaultBranch, 352 Help: "The Git release branch in the Environments Git repository used to store Helm charts source code and custom configuration", 353 } 354 err := survey.AskOne(q, &data.Spec.Source.Ref, nil, surveyOpts) 355 if err != nil { 356 return repo, nil, errors.Wrap(err, "asking git branch for environment source") 357 } 358 } 359 } 360 } 361 return repo, gitProvider, nil 362 } 363 364 // DoCreateEnvironmentGitRepo actually creates the git repository for the environment 365 func DoCreateEnvironmentGitRepo(batchMode bool, authConfigSvc auth.ConfigService, env *v1.Environment, forkEnvGitURL string, 366 environmentsDir string, gitRepoOptions *gits.GitRepositoryOptions, helmValues config.HelmValuesConfig, prefix string, 367 git gits.Gitter, chartMuseumFn ResolveChartMuseumURLFn, handles util.IOFileHandles) (*gits.GitRepository, gits.GitProvider, error) { 368 defaultRepoName := fmt.Sprintf("environment-%s-%s", prefix, env.Name) 369 details, err := gits.PickNewGitRepository(batchMode, authConfigSvc, defaultRepoName, gitRepoOptions, nil, nil, git, handles) 370 if err != nil { 371 return nil, nil, errors.Wrap(err, "picking new git repository for environment") 372 } 373 org := details.Organisation 374 375 repoName := details.RepoName 376 owner := org 377 if owner == "" { 378 owner = details.User.Username 379 } 380 envDir := filepath.Join(environmentsDir, owner) 381 provider := details.GitProvider 382 383 repo, err := provider.GetRepository(owner, repoName) 384 if err == nil { 385 log.Logger().Infof("Git repository %s/%s already exists", util.ColorInfo(owner), util.ColorInfo(repoName)) 386 387 if env.Spec.RemoteCluster { 388 log.Logger().Infof("git repository %s is remote so not modifying it", util.ColorInfo(repo.HTMLURL)) 389 return repo, provider, nil 390 } 391 392 // if the repo already exists then lets just modify it if required 393 dir, err := util.CreateUniqueDirectory(envDir, details.RepoName, util.MaximumNewDirectoryAttempts) 394 if err != nil { 395 return nil, nil, errors.Wrap(err, "creating unique directory for environment repo") 396 } 397 pushGitURL, err := git.CreateAuthenticatedURL(repo.CloneURL, details.User) 398 if err != nil { 399 return nil, nil, errors.Wrap(err, "creating push URL for environment repo") 400 } 401 err = git.Clone(pushGitURL, dir) 402 if err != nil { 403 return nil, nil, errors.Wrapf(err, "cloning environment from %q into %q", pushGitURL, dir) 404 } 405 err = ModifyNamespace(handles.Out, dir, env, git, chartMuseumFn) 406 if err != nil { 407 return nil, nil, errors.Wrap(err, "modifying environment namespace") 408 } 409 err = addValues(handles.Out, dir, helmValues, git) 410 if err != nil { 411 return nil, nil, errors.Wrap(err, "adding helm values to the environment") 412 } 413 err = git.PushMaster(dir) 414 if err != nil { 415 return nil, nil, errors.Wrap(err, "pushing environment master branch") 416 } 417 log.Logger().Infof("Pushed Git repository to %s\n\n", util.ColorInfo(repo.HTMLURL)) 418 } else { 419 log.Logger().Infof("Creating Git repository %s/%s\n", util.ColorInfo(owner), util.ColorInfo(repoName)) 420 421 if forkEnvGitURL != "" { 422 gitInfo, err := gits.ParseGitURL(forkEnvGitURL) 423 if err != nil { 424 return nil, nil, errors.Wrapf(err, "parsing forked environment git URL %q", forkEnvGitURL) 425 } 426 originalOrg := gitInfo.Organisation 427 originalRepo := gitInfo.Name 428 if useForkForEnvGitRepo && gitInfo.IsGitHub() && provider.IsGitHub() && originalOrg != "" && originalRepo != "" { 429 // lets try fork the repository and rename it 430 repo, err := provider.ForkRepository(originalOrg, originalRepo, org) 431 if err != nil { 432 return nil, nil, fmt.Errorf("failed to fork GitHub repo %s/%s to organisation %s due to %s", 433 originalOrg, originalRepo, org, err) 434 } 435 if repoName != originalRepo { 436 repo, err = provider.RenameRepository(owner, originalRepo, repoName) 437 if err != nil { 438 return nil, nil, fmt.Errorf("failed to rename GitHub repo %s/%s to organisation %s due to %s", 439 originalOrg, originalRepo, repoName, err) 440 } 441 } 442 log.Logger().Infof("Forked Git repository to %s\n\n", util.ColorInfo(repo.HTMLURL)) 443 444 dir, err := util.CreateUniqueDirectory(envDir, repoName, util.MaximumNewDirectoryAttempts) 445 if err != nil { 446 return nil, nil, errors.Wrapf(err, "creating unique dir to fork environment repository %q", envDir) 447 } 448 err = git.Clone(repo.CloneURL, dir) 449 if err != nil { 450 return nil, nil, errors.Wrapf(err, "cloning the environment %q", repo.CloneURL) 451 } 452 err = git.SetRemoteURL(dir, "upstream", forkEnvGitURL) 453 if err != nil { 454 return nil, nil, errors.Wrapf(err, "setting remote upstream %q in forked environment repo", forkEnvGitURL) 455 } 456 err = git.PullUpstream(dir) 457 if err != nil { 458 return nil, nil, errors.Wrap(err, "pulling upstream of forked environment repository") 459 } 460 err = ModifyNamespace(handles.Out, dir, env, git, chartMuseumFn) 461 if err != nil { 462 return nil, nil, errors.Wrap(err, "modifying namespace of forked environment") 463 } 464 err = addValues(handles.Out, dir, helmValues, git) 465 if err != nil { 466 return nil, nil, errors.Wrap(err, "adding helm values to the forked environment repo") 467 } 468 err = git.Push(dir, "origin", false, "HEAD") 469 if err != nil { 470 return nil, nil, errors.Wrapf(err, "pushing forked environment dir %q", dir) 471 } 472 return repo, provider, nil 473 } 474 } 475 476 // default to forking the URL if possible... 477 repo, err = details.CreateRepository() 478 if err != nil { 479 return nil, nil, errors.Wrap(err, "creating the repository") 480 } 481 482 if forkEnvGitURL != "" { 483 // now lets clone the fork and push it... 484 dir, err := util.CreateUniqueDirectory(envDir, details.RepoName, util.MaximumNewDirectoryAttempts) 485 if err != nil { 486 return nil, nil, errors.Wrap(err, "create unique directory for environment fork clone") 487 } 488 err = git.Clone(forkEnvGitURL, dir) 489 if err != nil { 490 return nil, nil, errors.Wrapf(err, "cloning the forked environment %q into %q", forkEnvGitURL, dir) 491 } 492 pushGitURL, err := git.CreateAuthenticatedURL(repo.CloneURL, details.User) 493 if err != nil { 494 return nil, nil, errors.Wrapf(err, "creating the push URL for %q", repo.CloneURL) 495 } 496 err = git.AddRemote(dir, "upstream", forkEnvGitURL) 497 if err != nil { 498 return nil, nil, errors.Wrapf(err, "adding remote %q to forked env clone", forkEnvGitURL) 499 } 500 err = git.UpdateRemote(dir, pushGitURL) 501 if err != nil { 502 return nil, nil, errors.Wrapf(err, "updating remote %q", pushGitURL) 503 } 504 err = ModifyNamespace(handles.Out, dir, env, git, chartMuseumFn) 505 if err != nil { 506 return nil, nil, errors.Wrapf(err, "modifying dev environment namespace") 507 } 508 err = addValues(handles.Out, dir, helmValues, git) 509 if err != nil { 510 return nil, nil, errors.Wrap(err, "adding helm values into environment git repository") 511 } 512 err = git.PushMaster(dir) 513 if err != nil { 514 return nil, nil, errors.Wrap(err, "push forked environment git repository") 515 } 516 log.Logger().Infof("Pushed Git repository to %s\n\n", util.ColorInfo(repo.HTMLURL)) 517 } 518 } 519 return repo, provider, nil 520 } 521 522 // GetDevEnvTeamSettings gets the team settings from the specified namespace. 523 func GetDevEnvTeamSettings(jxClient versioned.Interface, ns string) (*v1.TeamSettings, error) { 524 devEnv, err := GetDevEnvironment(jxClient, ns) 525 if err != nil { 526 log.Logger().Errorf("Error loading team settings. %v", err) 527 return nil, err 528 } 529 if devEnv != nil { 530 return &devEnv.Spec.TeamSettings, nil 531 } 532 return nil, fmt.Errorf("unable to find development environment in %s to get team settings", ns) 533 } 534 535 // GetDevEnvGitOwner gets the default GitHub owner/organisation to use for Environment repos. This takes the setting 536 // from the 'jx' Dev Env to get the one that was selected at installation time. 537 func GetDevEnvGitOwner(jxClient versioned.Interface) (string, error) { 538 adminDevEnv, err := GetDevEnvironment(jxClient, "jx") 539 if err != nil { 540 log.Logger().Errorf("Error loading team settings. %v", err) 541 return "", err 542 } 543 if adminDevEnv != nil { 544 return adminDevEnv.Spec.TeamSettings.EnvOrganisation, nil 545 } 546 return "", errors.New("Unable to find development environment in 'jx' to take git owner from") 547 } 548 549 // ModifyNamespace modifies the namespace 550 func ModifyNamespace(out io.Writer, dir string, env *v1.Environment, git gits.Gitter, chartMusemFn ResolveChartMuseumURLFn) error { 551 ns := env.Spec.Namespace 552 if ns == "" { 553 return fmt.Errorf("No Namespace is defined for Environment %s", env.Name) 554 } 555 556 // makefile changes 557 file := filepath.Join(dir, "Makefile") 558 exists, err := util.FileExists(file) 559 if err != nil { 560 return err 561 } 562 if !exists { 563 log.Logger().Warnf("WARNING: Could not find a Makefile in %s", dir) 564 return nil 565 } 566 input, err := ioutil.ReadFile(file) 567 if err != nil { 568 return err 569 } 570 lines := strings.Split(string(input), "\n") 571 err = ReplaceMakeVariable(lines, "NAMESPACE", "\""+ns+"\"") 572 if err != nil { 573 return err 574 } 575 output := strings.Join(lines, "\n") 576 err = ioutil.WriteFile(file, []byte(output), 0600) 577 if err != nil { 578 return err 579 } 580 581 // Jenkinsfile changes 582 file = filepath.Join(dir, "Jenkinsfile") 583 exists, err = util.FileExists(file) 584 if err != nil { 585 return err 586 } 587 if exists { 588 input, err := ioutil.ReadFile(file) 589 if err != nil { 590 return err 591 } 592 lines := strings.Split(string(input), "\n") 593 err = replaceEnvVar(lines, "DEPLOY_NAMESPACE", ns) 594 if err != nil { 595 return err 596 } 597 output := strings.Join(lines, "\n") 598 err = ioutil.WriteFile(file, []byte(output), 0600) 599 if err != nil { 600 return err 601 } 602 } 603 604 // lets ensure the namespace is set in a jenkins-x.yml file for tekton 605 projectConfig, projectConfigFile, err := config.LoadProjectConfig(dir) 606 if err != nil { 607 return err 608 } 609 foundEnv := false 610 for i := range projectConfig.Env { 611 if projectConfig.Env[i].Name == "DEPLOY_NAMESPACE" { 612 projectConfig.Env[i].Value = ns 613 foundEnv = true 614 break 615 } 616 } 617 if !foundEnv { 618 projectConfig.Env = append(projectConfig.Env, corev1.EnvVar{ 619 Name: "DEPLOY_NAMESPACE", 620 Value: ns, 621 }) 622 } 623 foundEnv = false 624 pipelineConfig := projectConfig.PipelineConfig 625 if pipelineConfig == nil { 626 projectConfig.PipelineConfig = &jenkinsfile.PipelineConfig{} 627 pipelineConfig = projectConfig.PipelineConfig 628 } 629 for i := range pipelineConfig.Env { 630 if pipelineConfig.Env[i].Name == "DEPLOY_NAMESPACE" { 631 pipelineConfig.Env[i].Value = ns 632 foundEnv = true 633 break 634 } 635 } 636 if !foundEnv { 637 pipelineConfig.Env = append(pipelineConfig.Env, corev1.EnvVar{ 638 Name: "DEPLOY_NAMESPACE", 639 Value: ns, 640 }) 641 } 642 643 if env.Spec.RemoteCluster && chartMusemFn != nil { 644 // lets ensure we have a chart museum env var 645 u, err := chartMusemFn() 646 if err != nil { 647 return errors.Wrapf(err, "failed to resolve Chart Museum URL for remote Environment %s", env.Name) 648 } 649 if u != "" { 650 pipelineConfig.Env = SetEnvVar(pipelineConfig.Env, "CHART_REPOSITORY", u) 651 } 652 } 653 654 err = projectConfig.SaveConfig(projectConfigFile) 655 if err != nil { 656 return err 657 } 658 659 err = git.Add(dir, "*") 660 if err != nil { 661 return err 662 } 663 changes, err := git.HasChanges(dir) 664 if err != nil { 665 return err 666 } 667 if changes { 668 return git.CommitDir(dir, "Use correct namespace for environment") 669 } 670 return nil 671 } 672 673 func addValues(out io.Writer, dir string, values config.HelmValuesConfig, git gits.Gitter) error { 674 file := filepath.Join(dir, "env", "values.yaml") 675 exists, err := util.FileExists(file) 676 if err != nil { 677 return err 678 } 679 if !exists { 680 return fmt.Errorf("could not find a values.yaml in %s", dir) 681 } 682 683 oldText, err := ioutil.ReadFile(file) 684 if err != nil { 685 return err 686 } 687 688 text, err := values.String() 689 if err != nil { 690 return err 691 } 692 693 sourceMap := map[string]interface{}{} 694 overrideMap := map[string]interface{}{} 695 err = yaml.Unmarshal(oldText, &sourceMap) 696 if err != nil { 697 return errors.Wrapf(err, "failed to parse YAML for file %s", file) 698 } 699 err = yaml.Unmarshal([]byte(text), &overrideMap) 700 if err != nil { 701 return errors.Wrapf(err, "failed to parse YAML for file %s", file) 702 } 703 704 if sourceMap != nil { 705 // now lets merge together the 2 blobs of YAML 706 util.CombineMapTrees(sourceMap, overrideMap) 707 } else { 708 sourceMap = overrideMap 709 } 710 711 output, err := yaml.Marshal(sourceMap) 712 if err != nil { 713 return errors.Wrap(err, "Failed to marshal the combined values YAML files back to YAML") 714 } 715 err = ioutil.WriteFile(file, output, util.DefaultWritePermissions) 716 if err != nil { 717 return errors.Wrapf(err, "Failed to save YAML file %s", file) 718 } 719 720 err = git.Add(dir, "*") 721 if err != nil { 722 return err 723 } 724 changes, err := git.HasChanges(dir) 725 if err != nil { 726 return err 727 } 728 if changes { 729 return git.CommitDir(dir, "Add environment configuration") 730 } 731 return nil 732 } 733 734 // ReplaceMakeVariable needs a description 735 func ReplaceMakeVariable(lines []string, name string, value string) error { 736 re, err := regexp.Compile(name + "\\s*:?=\\s*(.*)") 737 if err != nil { 738 return err 739 } 740 replaceValue := name + " := " + value 741 for i, line := range lines { 742 lines[i] = re.ReplaceAllString(line, replaceValue) 743 } 744 return nil 745 } 746 747 func replaceEnvVar(lines []string, name string, value string) error { 748 for i, line := range lines { 749 trimmed := strings.TrimSpace(line) 750 if strings.HasPrefix(trimmed, name) { 751 remain := strings.TrimSpace(strings.TrimPrefix(trimmed, name)) 752 if strings.HasPrefix(remain, "=") { 753 // lets preserve whitespace 754 idx := strings.Index(line, name) 755 lines[i] = line[0:idx] + name + ` = "` + value + `"` 756 } 757 } 758 } 759 return nil 760 } 761 762 // GetEnvironmentNames returns the sorted list of environment names 763 func GetEnvironmentNames(jxClient versioned.Interface, ns string) ([]string, error) { 764 envNames := []string{} 765 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 766 if err != nil { 767 return envNames, err 768 } 769 SortEnvironments(envs.Items) 770 for _, env := range envs.Items { 771 n := env.Name 772 if n != "" { 773 envNames = append(envNames, n) 774 } 775 } 776 sort.Strings(envNames) 777 return envNames, nil 778 } 779 780 func IsPreviewEnvironment(env *v1.Environment) bool { 781 return env != nil && env.Spec.Kind == v1.EnvironmentKindTypePreview 782 } 783 784 // GetFilteredEnvironmentNames returns the sorted list of environment names 785 func GetFilteredEnvironmentNames(jxClient versioned.Interface, ns string, fn func(environment *v1.Environment) bool) ([]string, error) { 786 envNames := []string{} 787 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 788 if err != nil { 789 return envNames, err 790 } 791 SortEnvironments(envs.Items) 792 for _, e := range envs.Items { 793 env := e 794 n := env.Name 795 if n != "" && fn(&env) { 796 envNames = append(envNames, n) 797 } 798 } 799 sort.Strings(envNames) 800 return envNames, nil 801 } 802 803 // GetOrderedEnvironments returns a map of the environments along with the correctly ordered names 804 func GetOrderedEnvironments(jxClient versioned.Interface, ns string) (map[string]*v1.Environment, []string, error) { 805 m := map[string]*v1.Environment{} 806 807 envNames := []string{} 808 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 809 if err != nil { 810 return m, envNames, err 811 } 812 SortEnvironments(envs.Items) 813 for _, env := range envs.Items { 814 n := env.Name 815 copy := env 816 m[n] = © 817 if n != "" { 818 envNames = append(envNames, n) 819 } 820 } 821 return m, envNames, nil 822 } 823 824 // GetEnvironments returns a map of the environments along with a sorted list of names 825 func GetEnvironments(jxClient versioned.Interface, ns string) (map[string]*v1.Environment, []string, error) { 826 m := map[string]*v1.Environment{} 827 828 envNames := []string{} 829 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 830 if err != nil { 831 return m, envNames, err 832 } 833 for _, env := range envs.Items { 834 n := env.Name 835 copy := env 836 m[n] = © 837 if n != "" { 838 envNames = append(envNames, n) 839 } 840 } 841 sort.Strings(envNames) 842 return m, envNames, nil 843 } 844 845 // GetEnvironment find an environment by name 846 func GetEnvironment(jxClient versioned.Interface, ns string, name string) (*v1.Environment, error) { 847 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 848 if err != nil { 849 return nil, err 850 } 851 for _, env := range envs.Items { 852 if env.GetName() == name { 853 return &env, nil 854 } 855 } 856 return nil, fmt.Errorf("no environment with name '%s' found", name) 857 } 858 859 // GetEnvironmentsByPrURL find an environment by a pull request URL 860 func GetEnvironmentsByPrURL(jxClient versioned.Interface, ns string, prURL string) (*v1.Environment, error) { 861 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 862 if err != nil { 863 return nil, err 864 } 865 for _, env := range envs.Items { 866 if env.Spec.PullRequestURL == prURL { 867 return &env, nil 868 } 869 } 870 return nil, fmt.Errorf("no environment found for PR '%s'", prURL) 871 } 872 873 // GetEnvironments returns the namespace name for a given environment 874 func GetEnvironmentNamespace(jxClient versioned.Interface, ns, environment string) (string, error) { 875 env, err := jxClient.JenkinsV1().Environments(ns).Get(environment, metav1.GetOptions{}) 876 if err != nil { 877 return "", err 878 } 879 if env == nil { 880 return "", fmt.Errorf("no environment found called %s, try running `jx get env`", environment) 881 } 882 return env.Spec.Namespace, nil 883 } 884 885 // GetEditEnvironmentNamespace returns the namespace of the current users edit environment 886 func GetEditEnvironmentNamespace(jxClient versioned.Interface, ns string) (string, error) { 887 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 888 if err != nil { 889 return "", err 890 } 891 u, err := user.Current() 892 if err != nil { 893 return "", err 894 } 895 for _, env := range envs.Items { 896 if env.Spec.Kind == v1.EnvironmentKindTypeEdit && env.Spec.PreviewGitSpec.User.Username == u.Username { 897 return env.Spec.Namespace, nil 898 } 899 } 900 return "", fmt.Errorf("The user %s does not have an Edit environment in home namespace %s", u.Username, ns) 901 } 902 903 // GetDevNamespace returns the developer environment namespace 904 // which is the namespace that contains the Environments and the developer tools like Jenkins 905 func GetDevNamespace(kubeClient kubernetes.Interface, ns string) (string, string, error) { 906 env := "" 907 namespace, err := kubeClient.CoreV1().Namespaces().Get(ns, metav1.GetOptions{}) 908 if err != nil { 909 return ns, env, err 910 } 911 if namespace == nil { 912 return ns, env, fmt.Errorf("No namespace found for %s", ns) 913 } 914 if namespace.Labels != nil { 915 answer := namespace.Labels[LabelTeam] 916 if answer != "" { 917 ns = answer 918 } 919 env = namespace.Labels[LabelEnvironment] 920 } 921 return ns, env, nil 922 } 923 924 // GetTeams returns the Teams the user is a member of 925 func GetTeams(kubeClient kubernetes.Interface) ([]*corev1.Namespace, []string, error) { 926 names := []string{} 927 answer := []*corev1.Namespace{} 928 namespaceList, err := kubeClient.CoreV1().Namespaces().List(metav1.ListOptions{}) 929 if err != nil { 930 return answer, names, err 931 } 932 for idx, namespace := range namespaceList.Items { 933 if namespace.Labels[LabelEnvironment] == LabelValueDevEnvironment { 934 answer = append(answer, &namespaceList.Items[idx]) 935 names = append(names, namespace.Name) 936 } 937 } 938 sort.Strings(names) 939 return answer, names, nil 940 } 941 942 func PickEnvironment(envNames []string, defaultEnv string, handles util.IOFileHandles) (string, error) { 943 surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err) 944 name := "" 945 if len(envNames) == 0 { 946 return "", nil 947 } else if len(envNames) == 1 { 948 name = envNames[0] 949 } else { 950 prompt := &survey.Select{ 951 Message: "Pick environment:", 952 Options: envNames, 953 Default: defaultEnv, 954 } 955 err := survey.AskOne(prompt, &name, nil, surveyOpts) 956 if err != nil { 957 return "", err 958 } 959 } 960 return name, nil 961 } 962 963 type ByOrder []v1.Environment 964 965 func (a ByOrder) Len() int { return len(a) } 966 func (a ByOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 967 func (a ByOrder) Less(i, j int) bool { 968 env1 := a[i] 969 env2 := a[j] 970 o1 := env1.Spec.Order 971 o2 := env2.Spec.Order 972 if o1 == o2 { 973 return env1.Name < env2.Name 974 } 975 return o1 < o2 976 } 977 978 func SortEnvironments(environments []v1.Environment) { 979 sort.Sort(ByOrder(environments)) 980 } 981 982 // ByTimestamp is used to fileter a list of PipelineActivities by their given timestamp 983 type ByTimestamp []v1.PipelineActivity 984 985 func (a ByTimestamp) Len() int { return len(a) } 986 func (a ByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 987 func (a ByTimestamp) Less(i, j int) bool { 988 act1 := a[i] 989 act2 := a[j] 990 t1 := act1.Spec.StartedTimestamp 991 if t1 == nil { 992 return false 993 } 994 t2 := act2.Spec.StartedTimestamp 995 if t2 == nil { 996 return true 997 } 998 999 return t1.Before(t2) 1000 } 1001 1002 // SortActivities sorts a list of PipelineActivities 1003 func SortActivities(activities []v1.PipelineActivity) { 1004 sort.Sort(ByTimestamp(activities)) 1005 } 1006 1007 // NewPermanentEnvironment creates a new permanent environment for testing 1008 func NewPermanentEnvironment(name string) *v1.Environment { 1009 return &v1.Environment{ 1010 ObjectMeta: metav1.ObjectMeta{ 1011 Name: name, 1012 Namespace: "jx", 1013 }, 1014 Spec: v1.EnvironmentSpec{ 1015 Label: strings.Title(name), 1016 Namespace: "jx-" + name, 1017 PromotionStrategy: v1.PromotionStrategyTypeAutomatic, 1018 Kind: v1.EnvironmentKindTypePermanent, 1019 }, 1020 } 1021 } 1022 1023 // NewPermanentEnvironment creates a new permanent environment for testing 1024 func NewPermanentEnvironmentWithGit(name string, gitUrl string) *v1.Environment { 1025 env := NewPermanentEnvironment(name) 1026 env.Spec.Source.URL = gitUrl 1027 env.Spec.Source.Ref = "master" 1028 return env 1029 } 1030 1031 // NewPreviewEnvironment creates a new preview environment for testing 1032 func NewPreviewEnvironment(name string) *v1.Environment { 1033 return &v1.Environment{ 1034 ObjectMeta: metav1.ObjectMeta{ 1035 Name: name, 1036 Namespace: "jx", 1037 }, 1038 Spec: v1.EnvironmentSpec{ 1039 Label: strings.Title(name), 1040 Namespace: "jx-preview-" + name, 1041 PromotionStrategy: v1.PromotionStrategyTypeAutomatic, 1042 Kind: v1.EnvironmentKindTypePreview, 1043 }, 1044 } 1045 } 1046 1047 // GetDevEnvironment returns the current development environment using the jxClient for the given ns. 1048 // If the Dev Environment cannot be found, returns nil Environment (rather than an error). A non-nil error is only 1049 // returned if there is an error fetching the Dev Environment. 1050 func GetDevEnvironment(jxClient versioned.Interface, ns string) (*v1.Environment, error) { 1051 //Find the settings for the team 1052 environmentInterface := jxClient.JenkinsV1().Environments(ns) 1053 name := LabelValueDevEnvironment 1054 answer, err := environmentInterface.Get(name, metav1.GetOptions{}) 1055 if err == nil { 1056 return answer, nil 1057 } 1058 selector := "env=dev" 1059 envList, err := environmentInterface.List(metav1.ListOptions{ 1060 LabelSelector: selector, 1061 }) 1062 if err != nil { 1063 return nil, err 1064 } 1065 if len(envList.Items) == 1 { 1066 return &envList.Items[0], nil 1067 } 1068 if len(envList.Items) == 0 { 1069 return nil, nil 1070 } 1071 return nil, fmt.Errorf("Error fetching dev environment resource definition in namespace %s, No Environment called: %s or with selector: %s found %d entries: %v", 1072 ns, name, selector, len(envList.Items), envList.Items) 1073 } 1074 1075 // GetPreviewEnvironmentReleaseName returns the (helm) release name for the given (preview) environment 1076 // or the empty string is the environment is not a preview environment, or has no release name associated with it 1077 func GetPreviewEnvironmentReleaseName(env *v1.Environment) string { 1078 if !IsPreviewEnvironment(env) { 1079 return "" 1080 } 1081 return env.Annotations[AnnotationReleaseName] 1082 } 1083 1084 // IsPermanentEnvironment indicates if an environment is permanent 1085 func IsPermanentEnvironment(env *v1.Environment) bool { 1086 return env.Spec.Kind == v1.EnvironmentKindTypePermanent 1087 } 1088 1089 // GetPermanentEnvironments returns a list with the current permanent environments 1090 func GetPermanentEnvironments(jxClient versioned.Interface, ns string) ([]*v1.Environment, error) { 1091 result := []*v1.Environment{} 1092 envs, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{}) 1093 if err != nil { 1094 return result, errors.Wrapf(err, "listing the environments in namespace %q", ns) 1095 } 1096 for i := range envs.Items { 1097 env := &envs.Items[i] 1098 if IsPermanentEnvironment(env) { 1099 result = append(result, env) 1100 } 1101 } 1102 return result, nil 1103 }