Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
73 changes: 73 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,6 +601,37 @@ 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 {
// Apply timeouts
if opt.TimeOut != "" {
Expand Down Expand Up @@ -723,6 +783,10 @@ func (opt *startOptions) createAndRunPipelineRun(pr *v1beta1.PipelineRun, pipeli
return err
}

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

// Configure the PipelineRun with common settings
if err := opt.configurePipelineRun(pr, cs); err != nil {
return err
Expand Down Expand Up @@ -798,6 +862,10 @@ func (opt *startOptions) startPipeline(pipelineStart *v1beta1.Pipeline) error {
pr.Spec.Status = ""
}

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

// Configure the PipelineRun with common settings
if err := opt.configurePipelineRun(pr, cs); err != nil {
return err
Expand All @@ -809,6 +877,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")
}
})
}
Loading