Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement step scripts #1432

Merged
merged 1 commit into from
Oct 22, 2019
Merged
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
63 changes: 62 additions & 1 deletion docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ The `steps` field is required. You define one or more `steps` fields to define
the body of a `Task`.

If multiple `steps` are defined, they will be executed in the same order as they
are defined, if the `Task` is invoked by a [TaskRun](taskruns.md).
are defined, if the `Task` is invoked by a [`TaskRun`](taskruns.md).

Each `steps` in a `Task` must specify a container image that adheres to the
[container contract](./container-contract.md). For each of the `steps` fields,
Expand All @@ -146,6 +146,67 @@ or container images that you define:
image in the Task, rather than requesting the sum of all of the container
image's resource requests.

#### Step Script

To simplify executing scripts inside a container, a step can specify a `script`.
If this field is present, the step cannot specify `command` or `args`.

When specified, a `script` gets invoked as if it were the contents of a file in
the container. Scripts should start with a
[shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) line to declare what
tool should be used to interpret the script. That tool must then also be
available within the step's container.

This allows you to execute a Bash script, if the image includes `bash`:

```yaml
steps:
- image: ubuntu # contains bash
script: |
#!/usr/bin/env bash
echo "Hello from Bash!"
```

...or to execute a Python script, if the image includes `python`:

```yaml
steps:
- image: python # contains python
script: |
#!/usr/bin/env python3
print("Hello from Python!")
```

...or to execute a Node script, if the image includes `node`:

```yaml
steps:
- image: node # contains node
script: |
#!/usr/bin/env node
console.log("Hello from Node!")
```

This also simplifies executing script files in the workspace:

```yaml
steps:
- image: ubuntu
script: |
#!/usr/bin/env bash
/workspace/my-script.sh # provided by an input resource
```

...or in the container image:

```yaml
steps:
- image: my-image # contains /bin/my-binary
script: |
#!/usr/bin/env bash
/bin/my-binary
```

### Inputs

A `Task` can declare the inputs it needs, which can be either or both of:
Expand Down
51 changes: 51 additions & 0 deletions examples/taskruns/step-script.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: step-script-
spec:
taskSpec:
steps:
- name: bash
image: ubuntu
env:
- name: FOO
value: foooooooo
script: |
#!/usr/bin/env bash
set -euxo pipefail
echo "Hello from Bash!"
echo FOO is ${FOO}
echo substring is ${FOO:2:4}
for i in {1..10}; do
echo line $i
done

- name: place-file
image: ubuntu
script: |
#!/usr/bin/env bash
echo "echo Hello from script file" > /workspace/hello
chmod +x /workspace/hello
- name: run-file
image: ubuntu
script: |
#!/usr/bin/env bash
/workspace/hello

- name: node
image: node
script: |
#!/usr/bin/env node
console.log("Hello from Node!")

- name: python
image: python
script: |
#!/usr/bin/env python3
print("Hello from Python!")

- name: perl
image: perl
script: |
#!/usr/bin/perl
print "Hello from Perl!"
3 changes: 2 additions & 1 deletion pkg/apis/pipeline/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ limitations under the License.

package pipeline

// Images holds the images reference for a number of container images used accross tektoncd pipelines
// Images holds the images reference for a number of container images used
// across tektoncd pipelines.
type Images struct {
// EntryPointImage is container image containing our entrypoint binary.
EntryPointImage string
Expand Down
5 changes: 5 additions & 0 deletions pkg/apis/pipeline/v1alpha1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ type TaskSpec struct {
// provided by Container.
type Step struct {
corev1.Container

// Script is the contents of an executable file to execute.
//
// If Script is not empty, the Step cannot have an Command or Args.
Script string `json:"script,omitempty"`
}

// Check that Task may be validated and defaulted.
Expand Down
15 changes: 15 additions & 0 deletions pkg/apis/pipeline/v1alpha1/task_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,21 @@ func validateSteps(steps []Step) *apis.FieldError {
return apis.ErrMissingField("Image")
}

if s.Script != "" {
if len(s.Args) > 0 || len(s.Command) > 0 {
return &apis.FieldError{
Message: "script cannot be used with args or command",
Paths: []string{"script"},
}
}
if !strings.HasPrefix(strings.TrimSpace(s.Script), "#!") {
return &apis.FieldError{
Message: "script must start with a shebang (#!)",
Paths: []string{"script"},
}
}
}

if s.Name == "" {
continue
}
Expand Down
139 changes: 94 additions & 45 deletions pkg/apis/pipeline/v1alpha1/task_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ func TestTaskSpecValidate(t *testing.T) {
Image: "some-image",
},
},
}, {
name: "valid step with script",
fields: fields{
Steps: []v1alpha1.Step{{
Container: corev1.Container{
Image: "my-image",
},
Script: `
#!/usr/bin/env bash
hello world`,
}},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -263,19 +275,17 @@ func TestTaskSpecValidateError(t *testing.T) {
fields: fields{
Inputs: &v1alpha1.Inputs{
Resources: []v1alpha1.TaskResource{validResource},
Params: []v1alpha1.ParamSpec{
{
Name: "validparam",
Type: v1alpha1.ParamTypeString,
Description: "parameter",
Default: builder.ArrayOrString("default"),
}, {
Name: "param-with-invalid-type",
Type: "invalidtype",
Description: "invalidtypedesc",
Default: builder.ArrayOrString("default"),
},
},
Params: []v1alpha1.ParamSpec{{
Name: "validparam",
Type: v1alpha1.ParamTypeString,
Description: "parameter",
Default: builder.ArrayOrString("default"),
}, {
Name: "param-with-invalid-type",
Type: "invalidtype",
Description: "invalidtypedesc",
Default: builder.ArrayOrString("default"),
}},
},
Steps: validSteps,
},
Expand All @@ -288,14 +298,12 @@ func TestTaskSpecValidateError(t *testing.T) {
fields: fields{
Inputs: &v1alpha1.Inputs{
Resources: []v1alpha1.TaskResource{validResource},
Params: []v1alpha1.ParamSpec{
{
Name: "task",
Type: v1alpha1.ParamTypeArray,
Description: "param",
Default: builder.ArrayOrString("default"),
},
},
Params: []v1alpha1.ParamSpec{{
Name: "task",
Type: v1alpha1.ParamTypeArray,
Description: "param",
Default: builder.ArrayOrString("default"),
}},
},
Steps: validSteps,
},
Expand All @@ -308,14 +316,12 @@ func TestTaskSpecValidateError(t *testing.T) {
fields: fields{
Inputs: &v1alpha1.Inputs{
Resources: []v1alpha1.TaskResource{validResource},
Params: []v1alpha1.ParamSpec{
{
Name: "task",
Type: v1alpha1.ParamTypeString,
Description: "param",
Default: builder.ArrayOrString("default", "array"),
},
},
Params: []v1alpha1.ParamSpec{{
Name: "task",
Type: v1alpha1.ParamTypeString,
Description: "param",
Default: builder.ArrayOrString("default", "array"),
}},
},
Steps: validSteps,
},
Expand Down Expand Up @@ -598,22 +604,65 @@ func TestTaskSpecValidateError(t *testing.T) {
Message: `non-existent variable in "$(inputs.params.foo) && $(inputs.params.inexistent)" for step arg[0]`,
Paths: []string{"taskspec.steps.arg[0]"},
},
},
{
name: "Multiple volumes with same name",
fields: fields{
Steps: validSteps,
Volumes: []corev1.Volume{{
Name: "workspace",
}, {
Name: "workspace",
}},
},
expectedError: apis.FieldError{
Message: `multiple volumes with same name "workspace"`,
Paths: []string{"volumes.name"},
},
}}
}, {
name: "Multiple volumes with same name",
fields: fields{
Steps: validSteps,
Volumes: []corev1.Volume{{
Name: "workspace",
}, {
Name: "workspace",
}},
},
expectedError: apis.FieldError{
Message: `multiple volumes with same name "workspace"`,
Paths: []string{"volumes.name"},
},
}, {
name: "step with script and args",
fields: fields{
Steps: []v1alpha1.Step{{
Container: corev1.Container{
Image: "myimage",
Args: []string{"arg"},
},
Script: "script",
}},
},
expectedError: apis.FieldError{
Message: "script cannot be used with args or command",
Paths: []string{"steps.script"},
},
}, {
name: "step with script without shebang",
fields: fields{
Steps: []v1alpha1.Step{{
Container: corev1.Container{
Image: "my-image",
},
Script: "does not begin with shebang",
}},
},
expectedError: apis.FieldError{
Message: "script must start with a shebang (#!)",
Paths: []string{"steps.script"},
},
}, {
name: "step with script and command",
fields: fields{
Steps: []v1alpha1.Step{{
Container: corev1.Container{
Image: "myimage",
Command: []string{"command"},
},
Script: "script",
}},
},
expectedError: apis.FieldError{
Message: "script cannot be used with args or command",
Paths: []string{"steps.script"},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ts := &v1alpha1.TaskSpec{
Expand Down
Loading