diff --git a/docs/cmd/tkn_pipeline_start.md b/docs/cmd/tkn_pipeline_start.md index 7c3acf1739..2f8e2bfc40 100644 --- a/docs/cmd/tkn_pipeline_start.md +++ b/docs/cmd/tkn_pipeline_start.md @@ -74,6 +74,7 @@ my-csi-template and my-volume-claim-template) --pipeline-timeout string timeout for PipelineRun --pod-template string local or remote file containing a PodTemplate definition --prefix-name string specify a prefix for the PipelineRun name (must be lowercase alphanumeric characters) + --resolvertype string resolver type for remote pipelines (hub, git, http, cluster, bundle, remote) -s, --serviceaccount string pass the serviceaccount name --showlog show logs right after starting the Pipeline --skip-optional-workspace skips the prompt for optional workspaces diff --git a/docs/man/man1/tkn-pipeline-start.1 b/docs/man/man1/tkn-pipeline-start.1 index eac1634b6a..3ca854fb61 100644 --- a/docs/man/man1/tkn-pipeline-start.1 +++ b/docs/man/man1/tkn-pipeline-start.1 @@ -75,6 +75,10 @@ Parameters, at least those that have no default value \fB\-\-prefix\-name\fP="" specify a prefix for the PipelineRun name (must be lowercase alphanumeric characters) +.PP +\fB\-\-resolvertype\fP="" + resolver type for remote pipelines (hub, git, http, cluster, bundle, remote) + .PP \fB\-s\fP, \fB\-\-serviceaccount\fP="" pass the serviceaccount name diff --git a/pkg/cmd/pipeline/start.go b/pkg/cmd/pipeline/start.go index 53a49326ff..90707eda29 100644 --- a/pkg/cmd/pipeline/start.go +++ b/pkg/cmd/pipeline/start.go @@ -81,6 +81,7 @@ type startOptions struct { TektonOptions flags.TektonOptions PodTemplate string SkipOptionalWorkspace bool + ResolverType string } func startCommand(p cli.Params) *cobra.Command { @@ -106,6 +107,18 @@ func startCommand(p cli.Params) *cobra.Command { tkn pipeline start foo -s ServiceAccountName -n bar + Re-run the last PipelineRun for a specific pipeline + + tkn pipeline start foo --last -n bar + + Re-run the last PipelineRun that used any remote resolver + + tkn pipeline start --last --resolvertype=remote -n bar + + Re-run the last PipelineRun that used git resolver + + tkn pipeline start --last --resolvertype=git -n bar + For params value, if you want to provide multiple values, provide them comma separated like cat,foo,bar @@ -134,7 +147,7 @@ For passing the workspaces via flags: SilenceUsage: true, ValidArgsFunction: formatted.ParentCompletion, - Args: func(cmd *cobra.Command, _ []string) error { + Args: func(cmd *cobra.Command, args []string) error { if err := flags.InitParams(p, cmd); err != nil { return err } @@ -147,6 +160,34 @@ For passing the workspaces via flags: if opt.UseParamDefaults && (opt.Last || opt.UsePipelineRun != "") { return errors.New("cannot use --last or --use-pipelinerun options with --use-param-defaults option") } + + // Validate resolvertype values + if opt.ResolverType != "" { + validResolvers := []string{"hub", "git", "http", "cluster", "bundle", "remote"} + isValid := false + for _, valid := range validResolvers { + if opt.ResolverType == valid { + isValid = true + break + } + } + if !isValid { + return fmt.Errorf("invalid resolvertype '%s'. Valid values are: %s", opt.ResolverType, strings.Join(validResolvers, ", ")) + } + } + + // Validate flag combinations according to requirements + if opt.ResolverType != "" && !opt.Last { + // Case: --resolvertype only + // Special case: remote resolver doesn't require pipeline name (it finds latest with any resolver) + if opt.ResolverType != "remote" && len(args) == 0 && opt.Filename == "" { + return errors.New("pipeline name is required when using --resolvertype flag") + } + } + + // Case: --resolvertype and --last (pipeline name is optional) + // Case: --last only (pipeline name is optional) - already handled by existing logic + format := strings.ToLower(opt.Output) if format != "" && format != "json" && format != "yaml" && format != "name" { return fmt.Errorf("output format specified is %s but must be yaml or json", opt.Output) @@ -163,6 +204,11 @@ For passing the workspaces via flags: Err: cmd.OutOrStderr(), } + // Handle different scenarios based on flags + if opt.ResolverType != "" { + return opt.runWithResolver(args) + } + pipeline, err := NameArg(args, p, opt.Filename) if err != nil { return err @@ -211,6 +257,14 @@ For passing the workspaces via flags: return formatted.BaseCompletion("serviceaccount", args) }, ) + + c.Flags().StringVar(&opt.ResolverType, "resolvertype", "", "resolver type for remote pipelines (hub, git, http, cluster, bundle, remote)") + _ = c.RegisterFlagCompletionFunc("resolvertype", + func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{"hub", "git", "http", "cluster", "bundle", "remote"}, cobra.ShellCompDirectiveNoFileComp + }, + ) + return c } @@ -222,76 +276,304 @@ func (opt *startOptions) run(pipeline *v1beta1.Pipeline) error { return opt.startPipeline(pipeline) } -func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { +func (opt *startOptions) runWithResolver(args []string) error { cs, err := opt.cliparams.Clients() if err != nil { return err } - objMeta := metav1.ObjectMeta{ - Namespace: opt.cliparams.Namespace(), + var pipelineName string + if len(args) > 0 { + pipelineName = args[0] } - var pr *v1beta1.PipelineRun - if opt.Filename == "" { - pr = &v1beta1.PipelineRun{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "tekton.dev/v1beta1", - Kind: "PipelineRun", - }, - ObjectMeta: objMeta, - Spec: v1beta1.PipelineRunSpec{ - PipelineRef: &v1beta1.PipelineRef{Name: pipelineStart.ObjectMeta.Name}, + + // Special case: if resolvertype is "remote", find and rerun the latest PipelineRun with any resolver + if opt.ResolverType == "remote" { + return opt.runWithRemoteResolver(cs, pipelineName) + } + + if opt.Last && opt.ResolverType != "" { + // Case: --resolvertype and --last + return opt.runWithResolverAndLast(cs, pipelineName) + } + // Case: --resolvertype only + if pipelineName == "" { + return errors.New("pipeline name is required when using --resolvertype flag") + } + return opt.runWithResolverOnly(cs, pipelineName) +} + +// validatePipelineExists checks if a pipeline exists in the namespace +func (opt *startOptions) validatePipelineExists(cs *cli.Clients, pipelineName string) error { + _, err := getPipelineV1beta1(pipelineGroupResource, cs, pipelineName, opt.cliparams.Namespace()) + if err != nil { + return fmt.Errorf(errInvalidPipeline, pipelineName, opt.cliparams.Namespace()) + } + return nil +} + +func (opt *startOptions) runWithResolverOnly(cs *cli.Clients, pipelineName string) error { + // Validate that the pipeline exists locally before proceeding + if err := opt.validatePipelineExists(cs, pipelineName); err != nil { + return err + } + + // Create ObjectMeta using helper function + objMeta := opt.createObjectMeta(nil, pipelineName) + + pr := &v1beta1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "PipelineRun", + }, + ObjectMeta: objMeta, + Spec: v1beta1.PipelineRunSpec{ + PipelineRef: &v1beta1.PipelineRef{ + ResolverRef: v1beta1.ResolverRef{ + Resolver: v1beta1.ResolverName(opt.ResolverType), + Params: []v1beta1.Param{ + { + Name: "name", + Value: v1beta1.ParamValue{StringVal: pipelineName}, + }, + }, + }, }, + }, + } + + return opt.createAndRunPipelineRun(pr, pipelineName) +} + +func (opt *startOptions) runWithResolverAndLast(cs *cli.Clients, pipelineName string) error { + var lastPipelineRun *v1beta1.PipelineRun + var err error + + if pipelineName != "" { + // Validate that the pipeline exists locally before proceeding + if err := opt.validatePipelineExists(cs, pipelineName); err != nil { + return err + } + + // Get last run for specific pipeline + name, err := pipelinepkg.LastRunName(cs, pipelineName, opt.cliparams.Namespace()) + if err != nil { + return err + } + lastPipelineRun, err = getPipelineRunV1beta1(pipelineRunGroupResource, cs, name, opt.cliparams.Namespace()) + if err != nil { + return err } } else { - pr = &v1beta1.PipelineRun{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "tekton.dev/v1beta1", - Kind: "PipelineRun", - }, - ObjectMeta: objMeta, - Spec: v1beta1.PipelineRunSpec{ - PipelineSpec: &pipelineStart.Spec, - }, + // Get last run from any pipeline with resolver + lastPipelineRun, err = opt.getLastPipelineRunWithResolver(cs) + if err != nil { + return err } } - if opt.Last || opt.UsePipelineRun != "" { - var usepr *v1beta1.PipelineRun - if opt.Last { - name, err := pipelinepkg.LastRunName(cs, pipelineStart.ObjectMeta.Name, opt.cliparams.Namespace()) - if err != nil { - return err - } - usepr, err = getPipelineRunV1beta1(pipelineRunGroupResource, cs, name, opt.cliparams.Namespace()) - if err != nil { - return err + // Check if the last run used a resolver + if lastPipelineRun.Spec.PipelineRef == nil || lastPipelineRun.Spec.PipelineRef.ResolverRef.Resolver == "" { + return errors.New("last PipelineRun did not use a resolver") + } + + // Create ObjectMeta using helper function + objMeta := opt.createObjectMeta(lastPipelineRun, pipelineName) + + pr := &v1beta1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "PipelineRun", + }, + ObjectMeta: objMeta, + Spec: lastPipelineRun.Spec, + } + + // Reapply blank status in case PipelineRun used was cancelled + pr.Spec.Status = "" + + // Extract pipeline name for logging + logPipelineName := pipelineName + if logPipelineName == "" && lastPipelineRun.Spec.PipelineRef != nil && lastPipelineRun.Spec.PipelineRef.Name != "" { + logPipelineName = lastPipelineRun.Spec.PipelineRef.Name + } + + return opt.createAndRunPipelineRun(pr, logPipelineName) +} + +func (opt *startOptions) getLastPipelineRunWithResolver(cs *cli.Clients) (*v1beta1.PipelineRun, error) { + options := metav1.ListOptions{} + + var runs *v1.PipelineRunList + err := actions.ListV1(pipelineRunGroupResource, cs, options, opt.cliparams.Namespace(), &runs) + if err != nil { + return nil, err + } + + if len(runs.Items) == 0 { + return nil, fmt.Errorf("no pipelineruns found in namespace %s", opt.cliparams.Namespace()) + } + + // Filter runs that use resolvers and find the latest + var filteredRuns []v1.PipelineRun + for _, run := range runs.Items { + if run.Spec.PipelineRef != nil && run.Spec.PipelineRef.ResolverRef.Resolver != "" { + // If resolvertype is specified, filter by that resolver type + if opt.ResolverType == "" || string(run.Spec.PipelineRef.ResolverRef.Resolver) == opt.ResolverType { + filteredRuns = append(filteredRuns, run) } - } else { - usepr, err = getPipelineRunV1beta1(pipelineRunGroupResource, cs, opt.UsePipelineRun, opt.cliparams.Namespace()) - if err != nil { - return err + } + } + + if len(filteredRuns) == 0 { + if opt.ResolverType != "" { + return nil, fmt.Errorf("no pipelineruns with resolver type '%s' found in namespace %s", opt.ResolverType, opt.cliparams.Namespace()) + } + return nil, fmt.Errorf("no pipelineruns with resolvers found in namespace %s", opt.cliparams.Namespace()) + } + + latest := filteredRuns[0] + for _, run := range filteredRuns { + if run.CreationTimestamp.Time.After(latest.CreationTimestamp.Time) { + latest = run + } + } + + // Convert v1 to v1beta1 + var pipelinerunBeta v1beta1.PipelineRun + err = pipelinerunBeta.ConvertFrom(context.Background(), &latest) + if err != nil { + return nil, err + } + + return &pipelinerunBeta, nil +} + +func (opt *startOptions) runWithRemoteResolver(cs *cli.Clients, pipelineName string) error { + // Find the latest PipelineRun with any resolver type + lastPipelineRun, err := opt.getLastPipelineRunWithAnyResolver(cs, pipelineName) + if err != nil { + return err + } + + // Create ObjectMeta using helper function + objMeta := opt.createObjectMeta(lastPipelineRun, pipelineName) + + pr := &v1beta1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "PipelineRun", + }, + ObjectMeta: objMeta, + Spec: lastPipelineRun.Spec, + } + + // Reapply blank status in case PipelineRun used was cancelled + pr.Spec.Status = "" + + // Extract pipeline name for logging + logPipelineName := pipelineName + if logPipelineName == "" && lastPipelineRun.Spec.PipelineRef != nil && lastPipelineRun.Spec.PipelineRef.Name != "" { + logPipelineName = lastPipelineRun.Spec.PipelineRef.Name + } else if logPipelineName == "" { + logPipelineName = "remote-pipeline" // fallback for remote pipelines + } + + return opt.createAndRunPipelineRun(pr, logPipelineName) +} + +// Get Latest Pipelinerun with any resolver type +// If pipeline name find then first it filters +// pipelinrun by give pipeline name and will return latest pipelinerun +func (opt *startOptions) getLastPipelineRunWithAnyResolver(cs *cli.Clients, pipelineName string) (*v1beta1.PipelineRun, error) { + options := metav1.ListOptions{} + + // If pipeline name is provided, filter by that pipeline + if pipelineName != "" { + options = metav1.ListOptions{ + LabelSelector: fmt.Sprintf("tekton.dev/pipeline=%s", pipelineName), + } + } + + var runs *v1.PipelineRunList + err := actions.ListV1(pipelineRunGroupResource, cs, options, opt.cliparams.Namespace(), &runs) + if err != nil { + return nil, err + } + + if len(runs.Items) == 0 { + if pipelineName != "" { + return nil, fmt.Errorf("no pipelineruns found for pipeline %s in namespace %s", pipelineName, opt.cliparams.Namespace()) + } + return nil, fmt.Errorf("no pipelineruns found in namespace %s", opt.cliparams.Namespace()) + } + + // Filter runs that use any resolver (hub, git, http, cluster, bundles) + validResolvers := []string{"hub", "git", "http", "cluster", "bundles"} + var filteredRuns []v1.PipelineRun + for _, run := range runs.Items { + if run.Spec.PipelineRef != nil && run.Spec.PipelineRef.ResolverRef.Resolver != "" { + resolverType := string(run.Spec.PipelineRef.ResolverRef.Resolver) + for _, validResolver := range validResolvers { + if resolverType == validResolver { + filteredRuns = append(filteredRuns, run) + break + } } } + } + + if len(filteredRuns) == 0 { + if pipelineName != "" { + return nil, fmt.Errorf("no pipelineruns with resolvers found for pipeline %s in namespace %s", pipelineName, opt.cliparams.Namespace()) + } + return nil, fmt.Errorf("no pipelineruns with resolvers found in namespace %s", opt.cliparams.Namespace()) + } - if len(usepr.ObjectMeta.GenerateName) > 0 && opt.PrefixName == "" { - pr.ObjectMeta.GenerateName = usepr.ObjectMeta.GenerateName - } else if opt.PrefixName == "" { - pr.ObjectMeta.GenerateName = usepr.ObjectMeta.Name + "-" + // Find the latest one + latest := filteredRuns[0] + for _, run := range filteredRuns { + if run.CreationTimestamp.Time.After(latest.CreationTimestamp.Time) { + latest = run } + } - // Copy over spec from last or previous PipelineRun to use same values for this PipelineRun - pr.Spec = usepr.Spec - // Reapply blank status in case PipelineRun used was cancelled - pr.Spec.Status = "" + // Convert v1 to v1beta1 + var pipelinerunBeta v1beta1.PipelineRun + err = pipelinerunBeta.ConvertFrom(context.Background(), &latest) + if err != nil { + return nil, err } - if opt.PrefixName == "" && !opt.Last && opt.UsePipelineRun == "" { - pr.ObjectMeta.GenerateName = pipelineStart.ObjectMeta.Name + "-run-" - } else if opt.PrefixName != "" { - pr.ObjectMeta.GenerateName = opt.PrefixName + "-" + return &pipelinerunBeta, nil +} + +// createObjectMeta creates ObjectMeta for PipelineRun with appropriate GenerateName +func (opt *startOptions) createObjectMeta(lastPipelineRun *v1beta1.PipelineRun, pipelineName string) metav1.ObjectMeta { + objMeta := metav1.ObjectMeta{ + Namespace: opt.cliparams.Namespace(), } + // Handle GenerateName based on different scenarios + switch { + case opt.PrefixName != "": + objMeta.GenerateName = opt.PrefixName + "-" + case lastPipelineRun != nil && len(lastPipelineRun.ObjectMeta.GenerateName) > 0: + objMeta.GenerateName = lastPipelineRun.ObjectMeta.GenerateName + case lastPipelineRun != nil: + objMeta.GenerateName = lastPipelineRun.ObjectMeta.Name + "-" + case pipelineName != "": + objMeta.GenerateName = pipelineName + "-run-" + default: + objMeta.GenerateName = "pipeline-run-" + } + + return objMeta +} + +// configurePipelineRun applies common configurations to a PipelineRun +func (opt *startOptions) configurePipelineRun(pr *v1beta1.PipelineRun, cs *cli.Clients) error { + // Apply timeouts if opt.TimeOut != "" { timeoutDuration, err := time.ParseDuration(opt.TimeOut) if err != nil { @@ -308,24 +590,28 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { } } + // Apply labels labels, err := labels.MergeLabels(pr.ObjectMeta.Labels, opt.Labels) if err != nil { return err } pr.ObjectMeta.Labels = labels + // Apply params param, err := params.MergeParam(pr.Spec.Params, opt.Params) if err != nil { return err } pr.Spec.Params = param + // Apply workspaces workspaces, err := workspaces.Merge(pr.Spec.Workspaces, opt.Workspaces, cs.HTTPClient) if err != nil { return err } pr.Spec.Workspaces = workspaces + // Apply service accounts if err := mergeSvc(pr, opt.ServiceAccounts); err != nil { return err } @@ -334,6 +620,7 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { pr.Spec.ServiceAccountName = opt.ServiceAccountName } + // Apply pod template podTemplateLocation := opt.PodTemplate if podTemplateLocation != "" { podTemplate, err := pods.ParsePodTemplate(cs.HTTPClient, podTemplateLocation, file.IsYamlFile(), fmt.Errorf("invalid file format for %s: .yaml or .yml file extension and format required", podTemplateLocation)) @@ -343,6 +630,12 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { pr.Spec.PodTemplate = &podTemplate } + return nil +} + +// executePipelineRun handles dry-run, creation, and logging for a PipelineRun +func (opt *startOptions) executePipelineRun(pr *v1beta1.PipelineRun, cs *cli.Clients, pipelineName string) error { + // Handle dry run if opt.DryRun { format := strings.ToLower(opt.Output) if format == "name" { @@ -366,11 +659,13 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { return printPipelineRun(opt.Output, opt.stream, pr) } + // Create the PipelineRun prCreated, err := pipelinerun.Create(cs, pr, metav1.CreateOptions{}, opt.cliparams.Namespace()) if err != nil { return err } + // Handle output formatting if opt.Output != "" { format := strings.ToLower(opt.Output) if format == "name" { @@ -394,6 +689,7 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { return printPipelineRun(opt.Output, opt.stream, prCreated) } + // Show success message and logs if requested fmt.Fprintf(opt.stream.Out, "PipelineRun started: %s\n", prCreated.Name) if !opt.ShowLog { inOrderString := "\nIn order to track the PipelineRun progress run:\ntkn pipelinerun " @@ -408,7 +704,7 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { fmt.Fprintf(opt.stream.Out, "Waiting for logs to be available...\n") runLogOpts := &options.LogOptions{ - PipelineName: pipelineStart.ObjectMeta.Name, + PipelineName: pipelineName, PipelineRunName: prCreated.Name, Stream: opt.stream, Follow: true, @@ -420,6 +716,97 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { return prcmd.Run(runLogOpts) } +// createAndRunPipelineRun is a streamlined function that uses helper functions +func (opt *startOptions) createAndRunPipelineRun(pr *v1beta1.PipelineRun, pipelineName string) error { + cs, err := opt.cliparams.Clients() + if err != nil { + return err + } + + // Configure the PipelineRun with common settings + if err := opt.configurePipelineRun(pr, cs); err != nil { + return err + } + + // Execute the PipelineRun (dry-run, create, and log) + return opt.executePipelineRun(pr, cs, pipelineName) +} + +func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error { + cs, err := opt.cliparams.Clients() + if err != nil { + return err + } + + // Initialize PipelineRun with basic structure + var pr *v1beta1.PipelineRun + var lastPipelineRun *v1beta1.PipelineRun + + // Handle --last or --use-pipelinerun options + if opt.Last || opt.UsePipelineRun != "" { + var usepr *v1beta1.PipelineRun + if opt.Last { + name, err := pipelinepkg.LastRunName(cs, pipelineStart.ObjectMeta.Name, opt.cliparams.Namespace()) + if err != nil { + return err + } + usepr, err = getPipelineRunV1beta1(pipelineRunGroupResource, cs, name, opt.cliparams.Namespace()) + if err != nil { + return err + } + } else { + usepr, err = getPipelineRunV1beta1(pipelineRunGroupResource, cs, opt.UsePipelineRun, opt.cliparams.Namespace()) + if err != nil { + return err + } + } + lastPipelineRun = usepr + } + + // Create ObjectMeta using helper function + objMeta := opt.createObjectMeta(lastPipelineRun, pipelineStart.ObjectMeta.Name) + + // Create PipelineRun based on filename or pipeline reference + if opt.Filename == "" { + pr = &v1beta1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "PipelineRun", + }, + ObjectMeta: objMeta, + Spec: v1beta1.PipelineRunSpec{ + PipelineRef: &v1beta1.PipelineRef{Name: pipelineStart.ObjectMeta.Name}, + }, + } + } else { + pr = &v1beta1.PipelineRun{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "tekton.dev/v1beta1", + Kind: "PipelineRun", + }, + ObjectMeta: objMeta, + Spec: v1beta1.PipelineRunSpec{ + PipelineSpec: &pipelineStart.Spec, + }, + } + } + + // Copy spec from last/previous PipelineRun if using --last or --use-pipelinerun + if lastPipelineRun != nil { + pr.Spec = lastPipelineRun.Spec + // Reapply blank status in case PipelineRun used was cancelled + pr.Spec.Status = "" + } + + // Configure the PipelineRun with common settings + if err := opt.configurePipelineRun(pr, cs); err != nil { + return err + } + + // Execute the PipelineRun (dry-run, create, and log) + return opt.executePipelineRun(pr, cs, pipelineStart.ObjectMeta.Name) +} + func (opt *startOptions) getInput(pipeline *v1beta1.Pipeline) error { params.FilterParamsByType(pipeline.Spec.Params) if !opt.Last && opt.UsePipelineRun == "" { diff --git a/pkg/cmd/pipeline/start_test.go b/pkg/cmd/pipeline/start_test.go index 64a5ec69e7..b69ce715b8 100644 --- a/pkg/cmd/pipeline/start_test.go +++ b/pkg/cmd/pipeline/start_test.go @@ -2912,3 +2912,513 @@ func Test_start_pipeline_with_skip_optional_workspace_flag_v1beta1(t *testing.T) expected := "PipelineRun started: random\n\nIn order to track the PipelineRun progress run:\ntkn pipelinerun logs random -f -n ns\n" test.AssertOutput(t, expected, got) } + +func TestPipelineStart_WithGitResolver(t *testing.T) { + pipelineName := "test-pipeline" + + // Create test pipeline data + pipelines := []*v1beta1.Pipeline{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: pipelineName, + Namespace: "ns", + }, + Spec: v1beta1.PipelineSpec{ + Tasks: []v1beta1.PipelineTask{ + { + Name: "unit-test-1", + TaskRef: &v1beta1.TaskRef{ + Name: "unit-test-task", + }, + }, + }, + }, + }, + } + + // Create namespace data + ns := []*corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "ns", + }, + }, + } + + cs, _ := test.SeedV1beta1TestData(t, test.Data{Pipelines: pipelines, Namespaces: ns}) + cs.Pipeline.Resources = cb.APIResourceList("v1beta1", []string{"pipeline", "pipelinerun"}) + cs.Pipeline.PrependReactor("create", "pipelineruns", func(action k8stest.Action) (bool, runtime.Object, error) { + create := action.(k8stest.CreateAction) + pr := create.GetObject().(*v1beta1.PipelineRun) + + // Verify that the PipelineRun has the correct resolver configuration + if pr.Spec.PipelineRef == nil { + t.Errorf("Expected PipelineRef to be set") + } + if pr.Spec.PipelineRef.ResolverRef.Resolver != "git" { + t.Errorf("Expected resolver to be 'git', got %s", pr.Spec.PipelineRef.ResolverRef.Resolver) + } + + // Check that the name parameter is set correctly + found := false + for _, param := range pr.Spec.PipelineRef.ResolverRef.Params { + if param.Name == "name" && param.Value.StringVal == pipelineName { + found = true + break + } + } + if !found { + t.Errorf("Expected name parameter to be set to %s", pipelineName) + } + + pr.Name = "random" + pr.Namespace = "ns" + return true, pr, nil + }) + + objs := []runtime.Object{pipelines[0]} + _, tdc := newV1beta1PipelineClient(objs...) + dc, err := tdc.Client( + cb.UnstructuredV1beta1P(pipelines[0], "v1beta1"), + ) + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + + pipeline := Command(p) + got, _ := test.ExecuteCommand(pipeline, "start", pipelineName, + "--resolvertype=git", + "-n", "ns", + ) + + expected := "PipelineRun started: random\n\nIn order to track the PipelineRun progress run:\ntkn pipelinerun logs random -f -n ns\n" + test.AssertOutput(t, expected, got) +} + +func TestPipelineStart_WithRemoteResolver(t *testing.T) { + pipelineName := "test-pipeline" + + // The remote resolver now looks for existing PipelineRuns with resolvers + // So we expect it to fail when no such PipelineRuns exist + seedData, _ := test.SeedV1beta1TestData(t, test.Data{}) + cs := pipelinetest.Clients{ + Pipeline: seedData.Pipeline, + Kube: seedData.Kube, + } + cs.Pipeline.Resources = cb.APIResourceList("v1beta1", []string{"pipeline", "pipelinerun"}) + + objs := []runtime.Object{} + _, tdc := newV1beta1PipelineClient(objs...) + dc, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + + pipeline := Command(p) + _, err = test.ExecuteCommand(pipeline, "start", pipelineName, + "--resolvertype=remote", + "-n", "ns", + ) + + // Should fail because no PipelineRuns with resolvers exist for the specified pipeline + if err == nil { + t.Errorf("Expected error but got none") + } + expectedErr := "no pipelineruns found for pipeline test-pipeline in namespace ns" + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Expected error to contain '%s', got '%s'", expectedErr, err.Error()) + } +} + +func TestPipelineStart_ComprehensiveResolverValidation(t *testing.T) { + tests := []struct { + name string + args []string + expectedErr string + description string + }{ + // Invalid resolver type validation + { + name: "invalid resolver type", + args: []string{"start", "--resolvertype=invalid"}, + expectedErr: "invalid resolvertype 'invalid'. Valid values are: hub, git, http, cluster, bundle, remote", + description: "Should reject invalid resolver types", + }, + + // No flags scenarios + { + name: "no flags without pipeline name", + args: []string{"start"}, + expectedErr: "missing Pipeline name", + description: "Should require pipeline name when no flags are provided", + }, + { + name: "no flags with pipeline name should work", + args: []string{"start", "test-pipeline"}, + expectedErr: "Pipeline name test-pipeline does not exist in namespace", + description: "Should work with pipeline name and no flags (normal behavior)", + }, + + // --resolvertype only scenarios (without --last) + { + name: "hub resolver without pipeline name", + args: []string{"start", "--resolvertype=hub"}, + expectedErr: "pipeline name is required when using --resolvertype flag", + description: "Hub resolver should require pipeline name", + }, + { + name: "git resolver without pipeline name", + args: []string{"start", "--resolvertype=git"}, + expectedErr: "pipeline name is required when using --resolvertype flag", + description: "Git resolver should require pipeline name", + }, + { + name: "http resolver without pipeline name", + args: []string{"start", "--resolvertype=http"}, + expectedErr: "pipeline name is required when using --resolvertype flag", + description: "HTTP resolver should require pipeline name", + }, + { + name: "cluster resolver without pipeline name", + args: []string{"start", "--resolvertype=cluster"}, + expectedErr: "pipeline name is required when using --resolvertype flag", + description: "Cluster resolver should require pipeline name", + }, + { + name: "bundle resolver without pipeline name", + args: []string{"start", "--resolvertype=bundle"}, + expectedErr: "pipeline name is required when using --resolvertype flag", + description: "bundle resolver should require pipeline name", + }, + { + name: "remote resolver without pipeline name should work", + args: []string{"start", "--resolvertype=remote"}, + expectedErr: "no pipelineruns found in namespace", + description: "Remote resolver should NOT require pipeline name (special case)", + }, + + // --resolvertype with pipeline name (without --last) + { + name: "hub resolver with pipeline name", + args: []string{"start", "test-pipeline", "--resolvertype=hub"}, + expectedErr: "Pipeline name test-pipeline does not exist in namespace", // Should fail early due to pipeline validation + description: "Hub resolver with pipeline name should validate pipeline existence", + }, + { + name: "git resolver with pipeline name", + args: []string{"start", "test-pipeline", "--resolvertype=git"}, + expectedErr: "Pipeline name test-pipeline does not exist in namespace", // Should fail early due to pipeline validation + description: "Git resolver with pipeline name should validate pipeline existence", + }, + { + name: "remote resolver with pipeline name", + args: []string{"start", "test-pipeline", "--resolvertype=remote"}, + expectedErr: "no pipelineruns found for pipeline test-pipeline", + description: "Remote resolver with pipeline name should look for existing runs", + }, + { + name: "http resolver with non-existent pipeline name", + args: []string{"start", "non-existent-pipeline", "--resolvertype=http"}, + expectedErr: "Pipeline name non-existent-pipeline does not exist in namespace", + description: "HTTP resolver should fail early when pipeline doesn't exist", + }, + + // --last only scenarios (without --resolvertype) + { + name: "last flag without pipeline name", + args: []string{"start", "--last"}, + expectedErr: "missing Pipeline name", + description: "Last flag without pipeline name requires pipeline name in current implementation", + }, + { + name: "last flag with pipeline name", + args: []string{"start", "test-pipeline", "--last"}, + expectedErr: "Pipeline name test-pipeline does not exist in namespace", + description: "Last flag with pipeline name should work", + }, + + // --resolvertype and --last together + { + name: "hub resolver with last flag without pipeline name", + args: []string{"start", "--resolvertype=hub", "--last"}, + expectedErr: "no pipelineruns found in namespace", + description: "Resolver + last without pipeline name should work (optional pipeline name)", + }, + { + name: "git resolver with last flag and pipeline name", + args: []string{"start", "test-pipeline", "--resolvertype=git", "--last"}, + expectedErr: "Pipeline name test-pipeline does not exist in namespace", + description: "Resolver + last with pipeline name should validate pipeline existence first", + }, + { + name: "remote resolver with last flag without pipeline name", + args: []string{"start", "--resolvertype=remote", "--last"}, + expectedErr: "no pipelineruns found in namespace", + description: "Remote resolver + last without pipeline name should work", + }, + { + name: "remote resolver with last flag and pipeline name", + args: []string{"start", "test-pipeline", "--resolvertype=remote", "--last"}, + expectedErr: "no pipelineruns found for pipeline test-pipeline", + description: "Remote resolver + last with pipeline name should work", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Logf("Testing: %s", tt.description) + + seedData, _ := test.SeedV1beta1TestData(t, test.Data{}) + cs := pipelinetest.Clients{ + Pipeline: seedData.Pipeline, + Kube: seedData.Kube, + } + cs.Pipeline.Resources = cb.APIResourceList("v1beta1", []string{"pipeline", "pipelinerun"}) + + // Add reactor for successful PipelineRun creation cases + cs.Pipeline.PrependReactor("create", "pipelineruns", func(action k8stest.Action) (bool, runtime.Object, error) { + create := action.(k8stest.CreateAction) + pr := create.GetObject().(*v1beta1.PipelineRun) + pr.Name = "random" + return true, pr, nil + }) + + objs := []runtime.Object{} + _, tdc := newV1beta1PipelineClient(objs...) + dc, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + + pipeline := Command(p) + got, err := test.ExecuteCommand(pipeline, tt.args...) + + // Check if this is a success case (expectedErr contains "PipelineRun started:") + if strings.Contains(tt.expectedErr, "PipelineRun started:") { + // This should succeed + if err != nil { + t.Errorf("Expected success but got error: %s", err.Error()) + } + if !strings.Contains(got, tt.expectedErr) { + t.Errorf("Expected output to contain '%s', got '%s'", tt.expectedErr, got) + } + } else { + // This should fail with specific error + if err == nil { + t.Errorf("Expected error but got none. Output: %s", got) + } else if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("Expected error to contain '%s', got '%s'", tt.expectedErr, err.Error()) + } + } + }) + } +} + +func TestPipelineStart_RemoteResolverFindsLatestAcrossAllPipelines(t *testing.T) { + // Test that remote resolver attempts to find PipelineRuns with resolvers + // This test verifies the error case when no PipelineRuns with resolvers exist + cs, _ := test.SeedV1beta1TestData(t, test.Data{}) + cs.Pipeline.Resources = cb.APIResourceList("v1beta1", []string{"pipeline", "pipelinerun"}) + + objs := []runtime.Object{} + _, tdc := newV1beta1PipelineClient(objs...) + dc, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + + pipeline := Command(p) + _, err = test.ExecuteCommand(pipeline, "start", + "--resolvertype=remote", + "-n", "ns", + ) + + // Should fail because no PipelineRuns exist + if err == nil { + t.Errorf("Expected error but got none") + } + expectedErr := "no pipelineruns found in namespace ns" + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Expected error to contain '%s', got '%s'", expectedErr, err.Error()) + } +} + +func TestPipelineStart_RemoteResolverIgnoresNonResolverRuns(t *testing.T) { + // Test that remote resolver ignores PipelineRuns that don't use resolvers + prs := []*v1beta1.PipelineRun{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "direct-pipeline-run", + Namespace: "ns", + Labels: map[string]string{"tekton.dev/pipeline": "direct-pipeline"}, + CreationTimestamp: metav1.Time{Time: time.Now().Add(-1 * time.Hour)}, + }, + Spec: v1beta1.PipelineRunSpec{ + PipelineRef: &v1beta1.PipelineRef{ + Name: "direct-pipeline", // No resolver + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "inline-pipeline-run", + Namespace: "ns", + CreationTimestamp: metav1.Time{Time: time.Now().Add(-30 * time.Minute)}, + }, + Spec: v1beta1.PipelineRunSpec{ + PipelineSpec: &v1beta1.PipelineSpec{}, // Inline spec, no resolver + }, + }, + } + + cs, _ := test.SeedV1beta1TestData(t, test.Data{ + PipelineRuns: prs, + }) + cs.Pipeline.Resources = cb.APIResourceList("v1beta1", []string{"pipeline", "pipelinerun"}) + + objs := []runtime.Object{} + _, tdc := newV1beta1PipelineClient(objs...) + dc, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + + pipeline := Command(p) + _, err = test.ExecuteCommand(pipeline, "start", + "--resolvertype=remote", + "-n", "ns", + ) + + // Should fail because no PipelineRuns with resolvers exist + if err == nil { + t.Errorf("Expected error but got none") + } + expectedErr := "no pipelineruns found in namespace ns" + if !strings.Contains(err.Error(), expectedErr) { + t.Errorf("Expected error to contain '%s', got '%s'", expectedErr, err.Error()) + } +} +func TestPipelineStart_RemoteResolverValidationLogic(t *testing.T) { + // Test that remote resolver validation logic works correctly + tests := []struct { + name string + args []string + expectError bool + errorMsg string + }{ + { + name: "remote resolver without pipeline name should not error in validation", + args: []string{"start", "--resolvertype=remote"}, + expectError: true, // Will error later when no PipelineRuns found, not in validation + errorMsg: "no pipelineruns found", + }, + { + name: "remote resolver with pipeline name should work", + args: []string{"start", "test-pipeline", "--resolvertype=remote"}, + expectError: true, // Will error when no PipelineRuns found for pipeline + errorMsg: "no pipelineruns found for pipeline test-pipeline", + }, + { + name: "remote resolver with last flag should work", + args: []string{"start", "--resolvertype=remote", "--last"}, + expectError: true, // Will error when no PipelineRuns found + errorMsg: "no pipelineruns found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs, _ := test.SeedV1beta1TestData(t, test.Data{}) + cs.Pipeline.Resources = cb.APIResourceList("v1beta1", []string{"pipeline", "pipelinerun"}) + objs := []runtime.Object{} + _, tdc := newV1beta1PipelineClient(objs...) + dc, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + + pipeline := Command(p) + _, err = test.ExecuteCommand(pipeline, tt.args...) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error to contain '%s', got '%s'", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %s", err.Error()) + } + } + }) + } +} + +func TestPipelineStart_RemoteResolverVsOtherResolvers(t *testing.T) { + // Test that remote resolver behaves differently from other resolvers + tests := []struct { + name string + resolverType string + args []string + expectError bool + errorMsg string + }{ + { + name: "git resolver requires pipeline name", + resolverType: "git", + args: []string{"start", "--resolvertype=git"}, + expectError: true, + errorMsg: "pipeline name is required when using --resolvertype flag", + }, + { + name: "hub resolver requires pipeline name", + resolverType: "hub", + args: []string{"start", "--resolvertype=hub"}, + expectError: true, + errorMsg: "pipeline name is required when using --resolvertype flag", + }, + { + name: "remote resolver does not require pipeline name", + resolverType: "remote", + args: []string{"start", "--resolvertype=remote"}, + expectError: true, // Different error - no PipelineRuns found + errorMsg: "no pipelineruns found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs, _ := test.SeedV1beta1TestData(t, test.Data{}) + cs.Pipeline.Resources = cb.APIResourceList("v1beta1", []string{"pipeline", "pipelinerun"}) + objs := []runtime.Object{} + _, tdc := newV1beta1PipelineClient(objs...) + dc, err := tdc.Client() + if err != nil { + t.Errorf("unable to create dynamic client: %v", err) + } + p := &test.Params{Tekton: cs.Pipeline, Kube: cs.Kube, Dynamic: dc} + + pipeline := Command(p) + _, err = test.ExecuteCommand(pipeline, tt.args...) + + if tt.expectError { + if err == nil { + t.Errorf("Expected error but got none") + } else if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Expected error to contain '%s', got '%s'", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("Expected no error but got: %s", err.Error()) + } + } + }) + } +}