Skip to content

Commit

Permalink
Implement step scripts
Browse files Browse the repository at this point in the history
This adds a `Script` field to the `Step` type which, when specified,
results in a temporary generated executable script file being invoked
containing the specified script contents.

The result is an easy-to-use scripting option for users who just want
to invoke simple scripts inside containers. Details of generated scripts
should be considered an implementation detail, and should not be relied
upon by users.
  • Loading branch information
imjasonh authored and tekton-robot committed Oct 22, 2019
1 parent f4c53fc commit 9873614
Show file tree
Hide file tree
Showing 8 changed files with 392 additions and 57 deletions.
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

0 comments on commit 9873614

Please sign in to comment.