Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/cmd/tkn_pipeline_start.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ my-csi-template and my-volume-claim-template)
-f, --filename string local or remote file name containing a Pipeline definition to start a PipelineRun
--finally-timeout string timeout for Finally TaskRuns
-h, --help help for start
--json string PipelineRun spec as JSON (inline, '-' for stdin, or '@path' for a file)
-l, --labels strings pass labels as label=value.
-L, --last re-run the Pipeline using last PipelineRun values
-o, --output string format of PipelineRun (yaml, json or name)
Expand Down
1 change: 1 addition & 0 deletions docs/cmd/tkn_task_start.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ For passing the workspaces via flags:
-f, --filename string local or remote file name containing a Task definition to start a TaskRun
-h, --help help for start
-i, --image string use an oci bundle
--json string TaskRun spec as JSON (inline, '-' for stdin, or '@path' for a file)
-l, --labels strings pass labels as label=value.
-L, --last re-run the Task using last TaskRun values
--output string format of TaskRun (yaml or json)
Expand Down
4 changes: 4 additions & 0 deletions docs/man/man1/tkn-pipeline-start.1
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ Parameters, at least those that have no default value
\fB\-h\fP, \fB\-\-help\fP[=false]
help for start

.PP
\fB\-\-json\fP=""
PipelineRun spec as JSON (inline, '\-' for stdin, or '@path' for a file)

.PP
\fB\-l\fP, \fB\-\-labels\fP=[]
pass labels as label=value.
Expand Down
4 changes: 4 additions & 0 deletions docs/man/man1/tkn-task-start.1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Start Tasks
\fB\-i\fP, \fB\-\-image\fP=""
use an oci bundle

.PP
\fB\-\-json\fP=""
TaskRun spec as JSON (inline, '\-' for stdin, or '@path' for a file)

.PP
\fB\-l\fP, \fB\-\-labels\fP=[]
pass labels as label=value.
Expand Down
70 changes: 70 additions & 0 deletions pkg/cmd/pipeline/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
Expand Down Expand Up @@ -82,6 +83,32 @@ type startOptions struct {
PodTemplate string
SkipOptionalWorkspace bool
ResolverType string
JSONSpec string
}

// readJSONSpec resolves a --json flag value to raw bytes.
// Supported forms:
// - "-" read from stdin
// - "@path" read from the file at path
// - anything else is treated as a literal JSON string
func readJSONSpec(raw string, stdin io.Reader) ([]byte, error) {
switch {
case raw == "-":
b, err := io.ReadAll(stdin)
if err != nil {
return nil, fmt.Errorf("reading JSON spec from stdin: %w", err)
}
return b, nil
case strings.HasPrefix(raw, "@"):
path := raw[1:]
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading JSON spec from file %q: %w", path, err)
}
return b, nil
default:
return []byte(raw), nil
}
}

func startCommand(p cli.Params) *cobra.Command {
Expand Down Expand Up @@ -265,6 +292,8 @@ For passing the workspaces via flags:
},
)

c.Flags().StringVar(&opt.JSONSpec, "json", "", "PipelineRun spec as JSON (inline, '-' for stdin, or '@path' for a file)")

return c
}

Expand Down Expand Up @@ -572,7 +601,43 @@ func (opt *startOptions) createObjectMeta(lastPipelineRun *v1beta1.PipelineRun,
}

// configurePipelineRun applies common configurations to a PipelineRun
func (opt *startOptions) applyJSONSpec(pr *v1beta1.PipelineRun, stdin io.Reader) error {
if opt.JSONSpec == "" {
return nil
}
raw, err := readJSONSpec(opt.JSONSpec, stdin)
if err != nil {
return err
}
var spec v1beta1.PipelineRunSpec
if err := json.Unmarshal(raw, &spec); err != nil {
return fmt.Errorf("invalid JSON spec: %w", err)
}

if spec.Params != nil {
pr.Spec.Params = append(spec.Params, pr.Spec.Params...)
}
if spec.Workspaces != nil {
pr.Spec.Workspaces = append(spec.Workspaces, pr.Spec.Workspaces...)
}
if spec.ServiceAccountName != "" && pr.Spec.ServiceAccountName == "" {
pr.Spec.ServiceAccountName = spec.ServiceAccountName
}
if spec.Timeouts != nil && pr.Spec.Timeouts == nil {
pr.Spec.Timeouts = spec.Timeouts
}
if spec.PodTemplate != nil && pr.Spec.PodTemplate == nil {
pr.Spec.PodTemplate = spec.PodTemplate
}
return nil
}

func (opt *startOptions) configurePipelineRun(pr *v1beta1.PipelineRun, cs *cli.Clients) error {

if err := opt.applyJSONSpec(pr, os.Stdin); err != nil {
return err
}

// Apply timeouts
if opt.TimeOut != "" {
timeoutDuration, err := time.ParseDuration(opt.TimeOut)
Expand Down Expand Up @@ -809,6 +874,11 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error {

func (opt *startOptions) getInput(pipeline *v1beta1.Pipeline) error {
params.FilterParamsByType(pipeline.Spec.Params)

if opt.JSONSpec != "" {
return nil
}

if !opt.Last && opt.UsePipelineRun == "" {
skipParams, err := params.ParseParams(opt.Params)
if err != nil {
Expand Down
123 changes: 115 additions & 8 deletions pkg/cmd/pipeline/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package pipeline

import (
"fmt"
"os"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -296,14 +297,15 @@ func TestPipelineStart_ExecuteCommand_v1beta1(t *testing.T) {
c7 := &test.Params{Tekton: cs7.Pipeline, Kube: cs7.Kube, Dynamic: dc7, Clock: clock}

testParams := []struct {
name string
command []string
namespace string
input *test.Params
wantError bool
hasPrefix bool
want string
goldenFile bool
name string
command []string
namespace string
input *test.Params
wantError bool
hasPrefix bool
want string
wantContains string
goldenFile bool
}{
{
name: "Invalid namespace",
Expand Down Expand Up @@ -848,6 +850,57 @@ func TestPipelineStart_ExecuteCommand_v1beta1(t *testing.T) {
wantError: false,
goldenFile: true,
},
{
name: "--json inline params appear in dry-run output",
command: []string{
"start", "test-pipeline",
"-n", "ns",
"--json", `{"params":[{"name":"pipeline-param","value":"from-json"}]}`,
"--dry-run",
"--output", "yaml",
},
input: c2,
wantContains: "from-json",
},
{
name: "--json with --param flag both apply",
command: []string{
"start", "test-pipeline",
"-n", "ns",
"--json", `{"params":[{"name":"pipeline-param","value":"from-json"}]}`,
"-p=rev-param=from-flag",
"--dry-run",
"--output", "yaml",
},
input: c2,
wantContains: "from-flag",
},
{
name: "--json with malformed JSON returns error",
command: []string{
"start", "test-pipeline",
"-n", "ns",
"--json", `{broken`,
"--dry-run",
},
input: c2,
wantError: true,
hasPrefix: true,
want: "invalid JSON spec",
},
{
name: "--json with missing file returns error",
command: []string{
"start", "test-pipeline",
"-n", "ns",
"--json", "@/no/such/file.json",
"--dry-run",
},
input: c2,
wantError: true,
hasPrefix: true,
want: "reading JSON spec from file",
},
}

for _, tp := range testParams {
Expand All @@ -874,6 +927,10 @@ func TestPipelineStart_ExecuteCommand_v1beta1(t *testing.T) {
}
if tp.goldenFile {
golden.Assert(t, got, strings.ReplaceAll(fmt.Sprintf("%s.golden", t.Name()), "/", "-"))
} else if tp.wantContains != "" {
if !strings.Contains(got, tp.wantContains) {
t.Errorf("expected output to contain %q, got:\n%s", tp.wantContains, got)
}
} else {
test.AssertOutput(t, tp.want, got)
}
Expand Down Expand Up @@ -3422,3 +3479,53 @@ func TestPipelineStart_RemoteResolverVsOtherResolvers(t *testing.T) {
})
}
}

func TestReadJSONSpec(t *testing.T) {
const payload = `{"params":[{"name":"repo","value":"x"}]}`

t.Run("inline JSON", func(t *testing.T) {
b, err := readJSONSpec(payload, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(b) != payload {
t.Fatalf("got %q, want %q", b, payload)
}
})

t.Run("stdin (-)", func(t *testing.T) {
r := strings.NewReader(payload)
b, err := readJSONSpec("-", r)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(b) != payload {
t.Fatalf("got %q, want %q", b, payload)
}
})

t.Run("file (@path)", func(t *testing.T) {
f, err := os.CreateTemp(t.TempDir(), "spec-*.json")
if err != nil {
t.Fatal(err)
}
if _, err := f.WriteString(payload); err != nil {
t.Fatal(err)
}
f.Close()
b, err := readJSONSpec("@"+f.Name(), nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if string(b) != payload {
t.Fatalf("got %q, want %q", b, payload)
}
})

t.Run("missing file returns error", func(t *testing.T) {
_, err := readJSONSpec("@/nonexistent/path.json", nil)
if err == nil {
t.Fatal("expected error for missing file")
}
})
}
65 changes: 65 additions & 0 deletions pkg/cmd/task/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"encoding/json"
"errors"
"io"
"os"
"strings"
"time"
Expand Down Expand Up @@ -79,6 +80,59 @@ type startOptions struct {
PodTemplate string
SkipOptionalWorkspace bool
remoteOptions bundle.RemoteOptions
JSONSpec string
}

// readJSONSpec resolves a --json flag value to raw bytes.
// Supported forms:
// - "-" read from stdin
// - "@path" read from the file at path
// - anything else is treated as a literal JSON string
func readJSONSpec(raw string, stdin io.Reader) ([]byte, error) {
switch {
case raw == "-":
b, err := io.ReadAll(stdin)
if err != nil {
return nil, fmt.Errorf("reading JSON spec from stdin: %w", err)
}
return b, nil
case strings.HasPrefix(raw, "@"):
path := raw[1:]
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading JSON spec from file %q: %w", path, err)
}
return b, nil
default:
return []byte(raw), nil
}
}

func applyTaskRunJSONSpec(tr *v1beta1.TaskRun, raw string, stdin io.Reader) error {
b, err := readJSONSpec(raw, stdin)
if err != nil {
return err
}
var spec v1beta1.TaskRunSpec
if err := json.Unmarshal(b, &spec); err != nil {
return fmt.Errorf("invalid JSON spec: %w", err)
}
if spec.Params != nil {
tr.Spec.Params = append(spec.Params, tr.Spec.Params...)
}
if spec.Workspaces != nil {
tr.Spec.Workspaces = append(spec.Workspaces, tr.Spec.Workspaces...)
}
if spec.ServiceAccountName != "" && tr.Spec.ServiceAccountName == "" {
tr.Spec.ServiceAccountName = spec.ServiceAccountName
}
if spec.Timeout != nil && tr.Spec.Timeout == nil {
tr.Spec.Timeout = spec.Timeout
}
if spec.PodTemplate != nil && tr.Spec.PodTemplate == nil {
tr.Spec.PodTemplate = spec.PodTemplate
}
return nil
}

// NameArg validates that the first argument is a valid task name
Expand Down Expand Up @@ -243,6 +297,7 @@ For passing the workspaces via flags:
c.Flags().BoolVarP(&opt.UseParamDefaults, "use-param-defaults", "", false, "use default parameter values without prompting for input")
c.Flags().StringVar(&opt.PodTemplate, "pod-template", "", "local or remote file containing a PodTemplate definition")
c.Flags().BoolVarP(&opt.SkipOptionalWorkspace, "skip-optional-workspace", "", false, "skips the prompt for optional workspaces")
c.Flags().StringVar(&opt.JSONSpec, "json", "", "TaskRun spec as JSON (inline, '-' for stdin, or '@path' for a file)")
bundle.AddRemoteFlags(c.Flags(), &opt.remoteOptions)

return c
Expand Down Expand Up @@ -354,6 +409,12 @@ func startTask(opt startOptions, args []string) error {
}
}

if opt.JSONSpec != "" {
if err := applyTaskRunJSONSpec(tr, opt.JSONSpec, os.Stdin); err != nil {
return err
}
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated


if err := opt.getInputs(); err != nil {
return err
}
Expand Down Expand Up @@ -503,6 +564,10 @@ func printTaskRun(output string, s *cli.Stream, tr interface{}) error {
}

func (opt *startOptions) getInputs() error {
if opt.JSONSpec != "" {
return nil
}

if opt.Last && opt.UseTaskRun != "" {
fmt.Fprintf(opt.stream.Err, "option --last and option --use-taskrun are not compatible \n")
return nil
Expand Down
Loading
Loading