github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/step/syntax/step_syntax_effective.go (about) 1 package syntax 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/jenkins-x/jx/v2/pkg/cmd/opts/step" 12 "github.com/jenkins-x/jx/v2/pkg/tekton" 13 "k8s.io/client-go/kubernetes" 14 15 "github.com/jenkins-x/jx/v2/pkg/versionstream" 16 17 "github.com/jenkins-x/jx-logging/pkg/log" 18 "github.com/jenkins-x/jx/v2/pkg/cmd/helper" 19 "github.com/jenkins-x/jx/v2/pkg/cmd/opts" 20 "github.com/jenkins-x/jx/v2/pkg/cmd/templates" 21 "github.com/jenkins-x/jx/v2/pkg/config" 22 "github.com/jenkins-x/jx/v2/pkg/gits" 23 "github.com/jenkins-x/jx/v2/pkg/jenkinsfile" 24 "github.com/jenkins-x/jx/v2/pkg/jenkinsfile/gitresolver" 25 "github.com/jenkins-x/jx/v2/pkg/kube" 26 "github.com/jenkins-x/jx/v2/pkg/tekton/syntax" 27 "github.com/jenkins-x/jx/v2/pkg/util" 28 "github.com/pkg/errors" 29 "github.com/spf13/cobra" 30 corev1 "k8s.io/api/core/v1" 31 "sigs.k8s.io/yaml" 32 ) 33 34 // StepSyntaxEffectiveOptions contains the command line flags 35 type StepSyntaxEffectiveOptions struct { 36 step.StepOptions 37 38 Pack string 39 BuildPackURL string 40 BuildPackRef string 41 Context string 42 CustomImage string 43 DefaultImage string 44 UseKaniko bool 45 KanikoImage string 46 ProjectID string 47 DockerRegistry string 48 DockerRegistryOrg string 49 SourceName string 50 CustomEnvs []string 51 OutputFile string 52 ShortView bool 53 54 ValidateInCluster bool 55 56 PodTemplates map[string]*corev1.Pod 57 58 GitInfo *gits.GitRepository 59 VersionResolver *versionstream.VersionResolver 60 } 61 62 var ( 63 stepSyntaxEffectiveLong = templates.LongDesc(` 64 Reads the appropriate jenkins-x.yml, depending on context, from the current directory, if one exists, and outputs an effective representation of the pipelines 65 `) 66 67 stepSyntaxEffectiveExample = templates.Examples(` 68 # view the effective pipeline 69 jx step syntax effective 70 71 # view the short version of the effective pipeline 72 jx step syntax effective -s 73 74 `) 75 ) 76 77 // NewCmdStepSyntaxEffective Creates a new Command object 78 func NewCmdStepSyntaxEffective(commonOpts *opts.CommonOptions) *cobra.Command { 79 options := &StepSyntaxEffectiveOptions{ 80 StepOptions: step.StepOptions{ 81 CommonOptions: commonOpts, 82 }, 83 } 84 85 cmd := &cobra.Command{ 86 Use: "effective", 87 Short: "Outputs an effective representation of the pipeline to be executed", 88 Long: stepSyntaxEffectiveLong, 89 Example: stepSyntaxEffectiveExample, 90 Run: func(cmd *cobra.Command, args []string) { 91 options.Cmd = cmd 92 options.Args = args 93 err := options.Run() 94 helper.CheckErr(err) 95 }, 96 } 97 98 cmd.Flags().StringArrayVarP(&options.CustomEnvs, "env", "e", nil, "List of custom environment variables to be applied to resources that are created") 99 100 options.addFlags(cmd) 101 return cmd 102 } 103 104 func (o *StepSyntaxEffectiveOptions) addFlags(cmd *cobra.Command) { 105 cmd.Flags().StringVarP(&o.OutDir, "output-dir", "", "", "The directory to write the output to as YAML. Defaults to STDOUT if neither --output-dir nor --output-file is specified.") 106 cmd.Flags().StringVarP(&o.OutputFile, "output-file", "", "", "The file to write the output to as YAML. If unspecified and --output-dir is specified, the filename defaults to 'jenkins-x[-context]-effective.yml'") 107 cmd.Flags().StringVarP(&o.Pack, "pack", "p", "", "The build pack name. If none is specified its discovered from the source code") 108 cmd.Flags().StringVarP(&o.BuildPackURL, "url", "u", "", "The URL for the build pack Git repository") 109 cmd.Flags().StringVarP(&o.BuildPackRef, "ref", "r", "", "The Git reference (branch,tag,sha) in the Git repository to use") 110 cmd.Flags().StringVarP(&o.Context, "context", "c", "", "The pipeline context if there are multiple separate pipelines for a given branch") 111 cmd.Flags().StringVarP(&o.ServiceAccount, "service-account", "", tekton.DefaultPipelineSA, "The Kubernetes ServiceAccount to use to run the pipeline") 112 cmd.Flags().StringVarP(&o.SourceName, "source", "", "source", "The name of the source repository") 113 cmd.Flags().StringVarP(&o.CustomImage, "image", "", "", "Specify a custom image to use for the steps which overrides the image in the PodTemplates") 114 cmd.Flags().StringVarP(&o.DefaultImage, "default-image", "", syntax.DefaultContainerImage, "Specify the docker image to use if there is no image specified for a step and there's no Pod Template") 115 cmd.Flags().BoolVarP(&o.UseKaniko, "use-kaniko", "", true, "Enables using kaniko directly for building docker images") 116 cmd.Flags().BoolVarP(&o.ShortView, "short", "s", false, "Use short concise output") 117 cmd.Flags().StringVarP(&o.KanikoImage, "kaniko-image", "", syntax.KanikoDockerImage, "The docker image for Kaniko") 118 cmd.Flags().StringVarP(&o.ProjectID, "project-id", "", "", "The cloud project ID. If not specified we default to the install project") 119 cmd.Flags().StringVarP(&o.DockerRegistry, "docker-registry", "", "", "The Docker Registry host name to use which is added as a prefix to docker images") 120 cmd.Flags().StringVarP(&o.DockerRegistryOrg, "docker-registry-org", "", "", "The Docker registry organisation. If blank the git repository owner is used") 121 cmd.Flags().BoolVarP(&o.ValidateInCluster, "validate-in-cluster", "", false, "Validate that resources referenced in the effective pipeline, such as volumes, exist in the current context cluster") 122 } 123 124 // Run implements this command 125 func (o *StepSyntaxEffectiveOptions) Run() error { 126 settings, err := o.TeamSettings() 127 if err != nil { 128 return err 129 } 130 131 kubeClient, ns, err := o.KubeClientAndDevNamespace() 132 if err != nil { 133 return errors.Wrap(err, "unable to create Kube client") 134 } 135 136 if o.ProjectID == "" { 137 if !o.RemoteCluster { 138 data, err := kube.ReadInstallValues(kubeClient, ns) 139 if err != nil { 140 return errors.Wrapf(err, "failed to read install values from namespace %s", ns) 141 } 142 o.ProjectID = data["projectID"] 143 } 144 if o.ProjectID == "" { 145 o.ProjectID = "todo" 146 } 147 } 148 if o.DefaultImage == "" { 149 o.DefaultImage = syntax.DefaultContainerImage 150 } 151 if o.VersionResolver == nil { 152 o.VersionResolver, err = o.GetVersionResolver() 153 if err != nil { 154 return err 155 } 156 } 157 if o.KanikoImage == "" { 158 o.KanikoImage = syntax.KanikoDockerImage 159 } 160 o.KanikoImage, err = o.VersionResolver.ResolveDockerImage(o.KanikoImage) 161 if err != nil { 162 return err 163 } 164 if o.Verbose { 165 log.Logger().Info("setting up docker registry\n") 166 } 167 168 if o.DockerRegistry == "" { 169 data, err := kube.GetConfigMapData(kubeClient, kube.ConfigMapJenkinsDockerRegistry, ns) 170 if err != nil { 171 return fmt.Errorf("could not find ConfigMap %s in namespace %s: %s", kube.ConfigMapJenkinsDockerRegistry, ns, err) 172 } 173 o.DockerRegistry = data["docker.registry"] 174 if o.DockerRegistry == "" { 175 return util.MissingOption("docker-registry") 176 } 177 } 178 179 workingDir, err := os.Getwd() 180 if err != nil { 181 return err 182 } 183 o.GitInfo, err = o.FindGitInfo(workingDir) 184 if err != nil { 185 return errors.Wrapf(err, "failed to find git information from dir %s", workingDir) 186 } 187 projectConfig, projectConfigFile, err := o.LoadProjectConfig(workingDir) 188 if err != nil { 189 return errors.Wrapf(err, "failed to load project config in dir %s", workingDir) 190 } 191 if o.BuildPackURL == "" || o.BuildPackRef == "" { 192 if projectConfig.BuildPackGitURL != "" { 193 o.BuildPackURL = projectConfig.BuildPackGitURL 194 } else if o.BuildPackURL == "" { 195 o.BuildPackURL = settings.BuildPackURL 196 } 197 if projectConfig.BuildPackGitURef != "" { 198 o.BuildPackRef = projectConfig.BuildPackGitURef 199 } else if o.BuildPackRef == "" { 200 o.BuildPackRef = settings.BuildPackRef 201 } 202 } 203 if o.BuildPackURL == "" { 204 return util.MissingOption("url") 205 } 206 if o.BuildPackRef == "" { 207 return util.MissingOption("ref") 208 } 209 210 if o.Pack == "" { 211 o.Pack = projectConfig.BuildPack 212 } 213 if o.Pack == "" { 214 o.Pack, err = o.DiscoverBuildPack(workingDir, projectConfig, o.Pack) 215 if err != nil { 216 return errors.Wrapf(err, "failed to discover the build pack") 217 } 218 } 219 220 if o.Pack == "" { 221 return util.MissingOption("pack") 222 } 223 224 o.PodTemplates, err = kube.LoadPodTemplates(kubeClient, ns) 225 if err != nil { 226 return err 227 } 228 229 packsDir, err := gitresolver.InitBuildPack(o.Git(), o.BuildPackURL, o.BuildPackRef) 230 if err != nil { 231 return err 232 } 233 234 resolver, err := gitresolver.CreateResolver(packsDir, o.Git()) 235 if err != nil { 236 return err 237 } 238 239 effectiveConfig, err := o.CreateEffectivePipeline(packsDir, projectConfig, projectConfigFile, resolver) 240 if err != nil { 241 return err 242 } 243 244 if o.ShortView { 245 effectiveConfig = o.makeConcisePipeline(effectiveConfig) 246 } 247 248 effectiveYaml, err := yaml.Marshal(effectiveConfig) 249 if err != nil { 250 return errors.Wrap(err, "failed to marshal effective pipeline") 251 } 252 if o.OutDir == "" && o.OutputFile == "" { 253 if o.ShortView { 254 for _, line := range strings.Split(string(effectiveYaml), "\n") { 255 prefix := "command: " 256 idx := strings.Index(line, prefix) 257 if idx >= 0 { 258 line = line[0:idx] + prefix + util.ColorInfo(line[idx+len(prefix):]) 259 } 260 fmt.Printf("%s\n", line) 261 } 262 } else { 263 fmt.Printf("%s\n", effectiveYaml) 264 } 265 } else { 266 outputDir := o.OutDir 267 if outputDir == "" { 268 outputDir, err = os.Getwd() 269 if err != nil { 270 return errors.Wrap(err, "failed to get current directory") 271 } 272 } 273 outputFilename := o.OutputFile 274 if outputFilename == "" { 275 outputFilename = "jenkins-x" 276 if o.Context != "" { 277 outputFilename += "-" + o.Context 278 } 279 outputFilename += "-effective.yml" 280 } 281 outputFile := filepath.Join(outputDir, outputFilename) 282 err = ioutil.WriteFile(outputFile, effectiveYaml, util.DefaultWritePermissions) 283 if err != nil { 284 return errors.Wrapf(err, "failed to write effective pipeline to %s", outputFile) 285 } 286 log.Logger().Infof("Effective pipeline written to %s", outputFile) 287 } 288 return nil 289 } 290 291 // CreateEffectivePipeline takes a project config and generates the effective version of the pipeline for it, including 292 // build packs, inheritance, overrides, defaults, etc. 293 func (o *StepSyntaxEffectiveOptions) CreateEffectivePipeline(packsDir string, projectConfig *config.ProjectConfig, projectConfigFile string, resolver jenkinsfile.ImportFileResolver) (*config.ProjectConfig, error) { 294 name := o.Pack 295 packDir := filepath.Join(packsDir, name) 296 297 pipelineConfig := projectConfig.PipelineConfig 298 if name != "none" { 299 pipelineFile := filepath.Join(packDir, jenkinsfile.PipelineConfigFileName) 300 exists, err := util.FileExists(pipelineFile) 301 if err != nil { 302 return nil, errors.Wrapf(err, "failed to find build pack pipeline YAML: %s", pipelineFile) 303 } 304 if !exists { 305 return nil, fmt.Errorf("no build pack for %s exists at directory %s", name, packDir) 306 } 307 pipelineConfig, err = jenkinsfile.LoadPipelineConfig(pipelineFile, resolver, true, false) 308 if err != nil { 309 return nil, errors.Wrapf(err, "failed to load build pack pipeline YAML: %s", pipelineFile) 310 } 311 312 localPipelineConfig := projectConfig.PipelineConfig 313 if localPipelineConfig != nil { 314 err = localPipelineConfig.ExtendPipeline(pipelineConfig, false) 315 if err != nil { 316 return nil, errors.Wrapf(err, "failed to override PipelineConfig using configuration in file %s", projectConfigFile) 317 } 318 pipelineConfig = localPipelineConfig 319 } 320 } else { 321 pipelineConfig.PopulatePipelinesFromDefault() 322 } 323 324 if pipelineConfig == nil { 325 return nil, fmt.Errorf("failed to find PipelineConfig in file %s", projectConfigFile) 326 } 327 328 err := o.combineEnvVars(pipelineConfig) 329 if err != nil { 330 return nil, errors.Wrapf(err, "failed to combine env vars") 331 } 332 333 pipelines := pipelineConfig.Pipelines 334 // First, handle release. 335 if pipelines.Release != nil { 336 releaseLifecycles := pipelines.Release 337 338 // lets add a pre-step to setup the credentials 339 if releaseLifecycles.Setup == nil { 340 releaseLifecycles.Setup = &jenkinsfile.PipelineLifecycle{} 341 } 342 steps := []*syntax.Step{ 343 { 344 Command: "jx step git credentials", 345 Name: "jx-git-credentials", 346 }, 347 } 348 releaseLifecycles.Setup.Steps = append(steps, releaseLifecycles.Setup.Steps...) 349 parsed, err := o.createPipelineForKind(jenkinsfile.PipelineKindRelease, releaseLifecycles, pipelines, projectConfig, pipelineConfig) 350 if err != nil { 351 return nil, errors.Wrapf(err, "failed to create effective pipeline for release") 352 } 353 pipelines.Release = &jenkinsfile.PipelineLifecycles{ 354 Pipeline: parsed, 355 SetVersion: releaseLifecycles.SetVersion, 356 } 357 } 358 if pipelines.PullRequest != nil { 359 prLifecycles := pipelines.PullRequest 360 parsed, err := o.createPipelineForKind(jenkinsfile.PipelineKindPullRequest, prLifecycles, pipelines, projectConfig, pipelineConfig) 361 if err != nil { 362 return nil, errors.Wrapf(err, "failed to create effective pipeline for pull request") 363 } 364 pipelines.PullRequest = &jenkinsfile.PipelineLifecycles{ 365 Pipeline: parsed, 366 SetVersion: prLifecycles.SetVersion, 367 } 368 } 369 if pipelines.Feature != nil { 370 featureLifecycles := pipelines.Feature 371 parsed, err := o.createPipelineForKind(jenkinsfile.PipelineKindFeature, featureLifecycles, pipelines, projectConfig, pipelineConfig) 372 if err != nil { 373 return nil, errors.Wrapf(err, "failed to create effective pipeline for pull request") 374 } 375 pipelines.Feature = &jenkinsfile.PipelineLifecycles{ 376 Pipeline: parsed, 377 SetVersion: featureLifecycles.SetVersion, 378 } 379 } 380 381 pipelineConfig.Pipelines = pipelines 382 projectConfig.PipelineConfig = pipelineConfig 383 384 return projectConfig, nil 385 } 386 387 func (o *StepSyntaxEffectiveOptions) createPipelineForKind(kind string, lifecycles *jenkinsfile.PipelineLifecycles, pipelines jenkinsfile.Pipelines, projectConfig *config.ProjectConfig, pipelineConfig *jenkinsfile.PipelineConfig) (*syntax.ParsedPipeline, error) { 388 var parsed *syntax.ParsedPipeline 389 var err error 390 391 if lifecycles != nil && lifecycles.Pipeline != nil { 392 parsed = lifecycles.Pipeline 393 if projectConfig.BuildPack == "" || projectConfig.BuildPack == "none" { 394 for _, override := range pipelines.Overrides { 395 if override.MatchesPipeline(kind) { 396 // If no step/steps, other overrides, or stage is specified, just remove the whole pipeline. 397 // TODO: This is probably pointless functionality. 398 if override.Step == nil && len(override.Steps) == 0 && !override.HasNonStepOverrides() && override.Stage == "" { 399 return nil, nil 400 } 401 parsed = syntax.ApplyStepOverridesToPipeline(parsed, override) 402 } 403 } 404 } 405 } else { 406 args := jenkinsfile.CreatePipelineArguments{ 407 Lifecycles: lifecycles, 408 PodTemplates: o.PodTemplates, 409 CustomImage: o.CustomImage, 410 DefaultImage: o.DefaultImage, 411 WorkspaceDir: o.getWorkspaceDir(), 412 GitHost: o.GitInfo.Host, 413 GitName: o.GitInfo.Name, 414 GitOrg: o.GitInfo.Organisation, 415 ProjectID: o.ProjectID, 416 DockerRegistry: o.getDockerRegistry(projectConfig), 417 DockerRegistryOrg: o.GetDockerRegistryOrg(projectConfig, o.GitInfo), 418 KanikoImage: o.KanikoImage, 419 UseKaniko: o.UseKaniko, 420 // Make sure we don't inject the setversion steps 421 NoReleasePrepare: false, 422 StepCounter: 0, 423 } 424 parsed, _, err = pipelineConfig.CreatePipelineForBuildPack(args) 425 if err != nil { 426 return nil, errors.Wrapf(err, "Failed to generate pipeline from build pack") 427 } 428 } 429 430 // Replace placeholders in directories. 431 replacePlaceholderArgs := syntax.StepPlaceholderReplacementArgs{ 432 WorkspaceDir: o.getWorkspaceDir(), 433 GitName: o.GitInfo.Name, 434 GitOrg: o.GitInfo.Organisation, 435 GitHost: o.GitInfo.Host, 436 ProjectID: o.ProjectID, 437 DockerRegistry: o.getDockerRegistry(projectConfig), 438 DockerRegistryOrg: o.GetDockerRegistryOrg(projectConfig, o.GitInfo), 439 KanikoImage: o.KanikoImage, 440 UseKaniko: o.UseKaniko, 441 } 442 parsed.ReplacePlaceholdersInStepAndStageDirs(replacePlaceholderArgs) 443 parsed.AddContainerEnvVarsToPipeline(pipelineConfig.Env) 444 445 if pipelineConfig.ContainerOptions != nil { 446 if parsed.Options == nil { 447 parsed.Options = &syntax.RootOptions{} 448 } 449 mergedContainer, err := syntax.MergeContainers(pipelineConfig.ContainerOptions, parsed.Options.ContainerOptions) 450 if err != nil { 451 return nil, errors.Wrapf(err, "Could not merge containerOptions from parent") 452 } 453 parsed.Options.ContainerOptions = mergedContainer 454 } 455 456 for _, override := range pipelines.Overrides { 457 if override.MatchesPipeline(kind) { 458 parsed = syntax.ApplyNonStepOverridesToPipeline(parsed, override) 459 } 460 } 461 462 var kubeClient kubernetes.Interface 463 var ns string 464 465 // If we're validating in the cluster, get the kubeClient. Otherwise it'll be nil and ignored. 466 if o.ValidateInCluster { 467 kubeClient, ns, err = o.KubeClientAndDevNamespace() 468 if err != nil { 469 return nil, errors.Wrap(err, "unable to create Kube client") 470 } 471 } 472 473 // TODO: Seeing weird behavior seemingly related to https://golang.org/doc/faq#nil_error 474 // if err is reused, maybe we need to switch return types (perhaps upstream in build-pipeline)? 475 ctx := context.Background() 476 if validateErr := parsed.ValidateInCluster(ctx, kubeClient, ns); validateErr != nil { 477 return nil, errors.Wrapf(validateErr, "validation failed for Pipeline") 478 } 479 480 // lets override any container options env vars from any custom injected env vars from the metapipeline client 481 if parsed != nil && parsed.Options != nil && parsed.Options.ContainerOptions != nil { 482 parsed.Options.ContainerOptions.Env = syntax.CombineEnv(pipelineConfig.Env, parsed.Options.ContainerOptions.Env) 483 } 484 return parsed, nil 485 } 486 487 func (o *StepSyntaxEffectiveOptions) combineEnvVars(projectConfig *jenkinsfile.PipelineConfig) error { 488 // add any custom env vars 489 envMap := make(map[string]corev1.EnvVar) 490 for _, e := range projectConfig.Env { 491 envMap[e.Name] = e 492 } 493 for _, customEnvVar := range o.CustomEnvs { 494 parts := strings.Split(customEnvVar, "=") 495 if len(parts) != 2 { 496 return errors.Errorf("expected 2 parts to env var but got %v", len(parts)) 497 } 498 e := corev1.EnvVar{ 499 Name: parts[0], 500 Value: parts[1], 501 } 502 envMap[e.Name] = e 503 } 504 projectConfig.Env = syntax.EnvMapToSlice(envMap) 505 return nil 506 } 507 508 func (o *StepSyntaxEffectiveOptions) getWorkspaceDir() string { 509 return filepath.Join("/workspace", o.SourceName) 510 } 511 512 func (o *StepSyntaxEffectiveOptions) getDockerRegistry(projectConfig *config.ProjectConfig) string { 513 dockerRegistry := o.DockerRegistry 514 if dockerRegistry == "" { 515 dockerRegistry = o.GetDockerRegistry(projectConfig) 516 } 517 return dockerRegistry 518 } 519 520 // LoadProjectConfig loads the pipeline config from the given workingDir 521 func (o *StepSyntaxEffectiveOptions) LoadProjectConfig(workingDir string) (*config.ProjectConfig, string, error) { 522 if o.Context != "" { 523 fileName := filepath.Join(workingDir, fmt.Sprintf("jenkins-x-%s.yml", o.Context)) 524 exists, err := util.FileExists(fileName) 525 if err != nil { 526 return nil, fileName, errors.Wrapf(err, "failed to check if file exists %s", fileName) 527 } 528 if exists { 529 config, err := config.LoadProjectConfigFile(fileName) 530 return config, fileName, err 531 } 532 } 533 return config.LoadProjectConfig(workingDir) 534 } 535 536 func (o *StepSyntaxEffectiveOptions) makeConcisePipeline(projectConfig *config.ProjectConfig) *config.ProjectConfig { 537 for _, pipelines := range projectConfig.PipelineConfig.Pipelines.All() { 538 if pipelines != nil { 539 if pipelines.Pipeline != nil { 540 o.makeConciseStages(pipelines.Pipeline.Stages) 541 } 542 } 543 } 544 return projectConfig 545 } 546 547 func (o *StepSyntaxEffectiveOptions) makeConciseStages(stages []syntax.Stage) { 548 for i := range stages { 549 stage := &stages[i] 550 for j := range stage.Steps { 551 o.makeConciseStep(&stage.Steps[j]) 552 } 553 } 554 } 555 556 func (o *StepSyntaxEffectiveOptions) makeConciseStep(step *syntax.Step) { 557 for _, child := range step.Steps { 558 o.makeConciseStep(child) 559 } 560 c := step.Command 561 if c == "" { 562 return 563 } 564 args := step.Arguments 565 if len(args) > 0 { 566 c = c + " " + strings.Join(args, " ") 567 step.Arguments = nil 568 } 569 step.Command = c 570 }