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

Add Script Support to Sidecars #1987

Merged
Merged
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
10 changes: 10 additions & 0 deletions docs/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,16 @@ volumes:
emptyDir: {}
```

Sidecars can also run a script, like a Step:

```yaml
sidecars:
image: busybox
name: hello-sidecar
script: |
echo 'Hello from sidecar!'
```

Note: There is a known bug with Tekton's existing sidecar implementation.
Tekton uses a specific image, called "nop", to stop sidecars. The "nop" image
is configurable using a flag of the Tekton controller. If the configured "nop"
Expand Down
30 changes: 30 additions & 0 deletions examples/taskruns/sidecar-ready-script.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: tekton.dev/v1alpha1
kind: TaskRun
metadata:
generateName: sidecar-ready-
spec:
taskSpec:
sidecars:
- name: slow-sidecar
image: ubuntu
script: |
echo "hello from sidecar" > /shared/message
volumeMounts:
- name: shared
mountPath: /shared

steps:
- name: check-ready
image: ubuntu
# The step will only succeed if the sidecar has written this file, which
# it does 5s after it starts, before it reports Ready.
Copy link

Choose a reason for hiding this comment

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

I think I'm missing something here - where is the 5s delay happening? And what is meant here by the sidecar reporting Ready?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was me copy/pasting another example without deleting the comments. It isn't really waiting for anything to be ready.

script: cat /shared/message
volumeMounts:
- name: shared
mountPath: /shared

# Sidecars don't have /workspace mounted by default, so we have to define
# our own shared volume.
volumes:
- name: shared
emptyDir: {}
4 changes: 3 additions & 1 deletion pkg/apis/pipeline/v1alpha1/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type TaskSpec struct {

// Sidecars are run alongside the Task's step containers. They begin before
// the steps start and end after the steps complete.
Sidecars []corev1.Container `json:"sidecars,omitempty"`
Sidecars []Sidecar `json:"sidecars,omitempty"`

// Workspaces are the volumes that this Task requires.
Workspaces []WorkspaceDeclaration `json:"workspaces,omitempty"`
Expand All @@ -70,6 +70,8 @@ type TaskSpec struct {
// provided by Container.
type Step = v1alpha2.Step

type Sidecar = v1alpha2.Sidecar

// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/pipeline/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion pkg/apis/pipeline/v1alpha2/task_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ type TaskSpec struct {

// Sidecars are run alongside the Task's step containers. They begin before
// the steps start and end after the steps complete.
Sidecars []corev1.Container `json:"sidecars,omitempty"`
Sidecars []Sidecar `json:"sidecars,omitempty"`

// Workspaces are the volumes that this Task requires.
Workspaces []WorkspaceDeclaration
Expand All @@ -98,6 +98,9 @@ type Step struct {
Script string `json:"script,omitempty"`
}

// A sidecar has the same data structure as a Step, consisting of a Container, and optional Script.
type Sidecar = Step

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// TaskList contains a list of Task
type TaskList struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/apis/pipeline/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha

// Convert any steps with Script to command+args.
// If any are found, append an init container to initialize scripts.
scriptsInit, stepContainers := convertScripts(images.ShellImage, steps)
scriptsInit, stepContainers, sidecarContainers := convertScripts(images.ShellImage, steps, taskSpec.Sidecars)
if scriptsInit != nil {
initContainers = append(initContainers, *scriptsInit)
volumes = append(volumes, scriptsVolume)
Expand Down Expand Up @@ -171,9 +171,10 @@ func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha
return nil, err
}

// Merge sidecar containers with step containers.
mergedPodContainers := stepContainers
for _, sc := range taskSpec.Sidecars {

// Merge sidecar containers with step containers.
for _, sc := range sidecarContainers {
sc.Name = names.SimpleNameGenerator.RestrictLength(fmt.Sprintf("%v%v", sidecarPrefix, sc.Name))
mergedPodContainers = append(mergedPodContainers, sc)
}
Expand Down
82 changes: 78 additions & 4 deletions pkg/pod/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,14 @@ func TestMakePod(t *testing.T) {
Image: "primary-image",
Command: []string{"cmd"}, // avoid entrypoint lookup.
}}},
Sidecars: []corev1.Container{{
Name: "sc-name",
Image: "sidecar-image",
}},
Sidecars: []v1alpha1.Sidecar{
{
Container: corev1.Container{
Name: "sc-name",
Image: "sidecar-image",
},
},
},
},
wantAnnotations: map[string]string{},
want: &corev1.PodSpec{
Expand Down Expand Up @@ -394,6 +398,76 @@ func TestMakePod(t *testing.T) {
}},
Volumes: append(implicitVolumes, toolsVolume, downwardVolume),
},
}, {
desc: "sidecar container with script",
ts: v1alpha1.TaskSpec{
Steps: []v1alpha1.Step{{Container: corev1.Container{
Name: "primary-name",
Image: "primary-image",
Command: []string{"cmd"}, // avoid entrypoint lookup.
}}},
Sidecars: []v1alpha1.Sidecar{
{
Container: corev1.Container{
Copy link

Choose a reason for hiding this comment

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

Suggest collapsing these brackets to match format with the Step on line 404. Similarly for the init container on line 422.

Name: "sc-name",
Image: "sidecar-image",
},
Script: "#!/bin/sh\necho hello from sidecar",
},
},
},
wantAnnotations: map[string]string{},
want: &corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
InitContainers: []corev1.Container{
{
Name: "place-scripts",
Image: "busybox",
Command: []string{"sh"},
TTY: true,
VolumeMounts: []corev1.VolumeMount{scriptsVolumeMount},
Args: []string{"-c", `tmpfile="/tekton/scripts/sidecar-script-0-9l9zj"
touch ${tmpfile} && chmod +x ${tmpfile}
cat > ${tmpfile} << 'sidecar-script-heredoc-randomly-generated-mz4c7'
#!/bin/sh
echo hello from sidecar
sidecar-script-heredoc-randomly-generated-mz4c7
`},
},
placeToolsInit,
},
Containers: []corev1.Container{{
Name: "step-primary-name",
Image: "primary-image",
Command: []string{"/tekton/tools/entrypoint"},
Args: []string{
"-wait_file",
"/tekton/downward/ready",
"-wait_file_content",
"-post_file",
"/tekton/tools/0",
"-termination_path",
"/tekton/termination",
"-entrypoint",
"cmd",
"--",
},
Env: implicitEnvVars,
VolumeMounts: append([]corev1.VolumeMount{toolsMount, downwardMount}, implicitVolumeMounts...),
WorkingDir: pipeline.WorkspaceDir,
Resources: corev1.ResourceRequirements{Requests: allZeroQty()},
TerminationMessagePath: "/tekton/termination",
}, {
Name: "sidecar-sc-name",
Image: "sidecar-image",
Resources: corev1.ResourceRequirements{
Requests: nil,
},
Command: []string{"/tekton/scripts/sidecar-script-0-9l9zj"},
VolumeMounts: []corev1.VolumeMount{scriptsVolumeMount},
}},
Volumes: append(implicitVolumes, scriptsVolume, toolsVolume, downwardVolume),
},
}, {
desc: "resource request",
ts: v1alpha1.TaskSpec{
Expand Down
34 changes: 22 additions & 12 deletions pkg/pod/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ var (
}
)

// convertScripts converts any steps that specify a Script field into a normal Container.
// convertScripts converts any steps and sidecars that specify a Script field into a normal Container.
//
// It does this by prepending a container that writes specified Script bodies
// to executable files in a shared volumeMount, then produces Containers that
// simply run those executable files.
func convertScripts(shellImage string, steps []v1alpha1.Step) (*corev1.Container, []corev1.Container) {
func convertScripts(shellImage string, steps []v1alpha1.Step, sidecars []v1alpha1.Sidecar) (*corev1.Container, []corev1.Container, []corev1.Container) {
Copy link

Choose a reason for hiding this comment

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

Rather than amending the signature and adding the extra func to account for Sidecars, couldn't the caller (MakePod) simply execute this function twice? Once for Steps and once for Sidecars?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes you can definitely do the same thing that way. I think in order to do that you need to change the signature of convertScripts anyways to include context about the namePrefix. Would you prefer I changed it to that?

Copy link

Choose a reason for hiding this comment

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

That would be excellent, thank you @tomgeorge !

placeScripts := false
placeScriptsInit := corev1.Container{
Name: "place-scripts",
Expand All @@ -61,7 +61,21 @@ func convertScripts(shellImage string, steps []v1alpha1.Step) (*corev1.Container
VolumeMounts: []corev1.VolumeMount{scriptsVolumeMount},
}

var containers []corev1.Container
convertedStepContainers := convertListOfSteps(steps, &placeScriptsInit, &placeScripts, "script")
sidecarContainers := convertListOfSteps(sidecars, &placeScriptsInit, &placeScripts, "sidecar-script")

if placeScripts {
return &placeScriptsInit, convertedStepContainers, sidecarContainers
}
return nil, convertedStepContainers, sidecarContainers
}

// convertListOfSteps does the heavy lifting for convertScripts.
//
// It iterates through the list of steps (or sidecars), generates the script file name and heredoc termination string,
// adds an entry to the init container args, sets up the step container to run the script, and sets the volume mounts.
func convertListOfSteps(steps []v1alpha1.Step, initContainer *corev1.Container, placeScripts *bool, format string) []corev1.Container {
Copy link

Choose a reason for hiding this comment

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

suggest "namePrefix" instead of "format" here, since the value isn't expected to be a format string.

containers := []corev1.Container{}
for i, s := range steps {
if s.Script == "" {
// Nothing to convert.
Expand All @@ -81,20 +95,20 @@ func convertScripts(shellImage string, steps []v1alpha1.Step) (*corev1.Container

// At least one step uses a script, so we should return a
// non-nil init container.
placeScripts = true
*placeScripts = true

// Append to the place-scripts script to place the
// script file in a known location in the scripts volume.
tmpFile := filepath.Join(scriptsDir, names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("script-%d", i)))
tmpFile := filepath.Join(scriptsDir, names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("%s-%d", format, i)))
// heredoc is the "here document" placeholder string
// used to cat script contents into the file. Typically
// this is the string "EOF" but if this value were
// "EOF" it would prevent users from including the
// string "EOF" in their own scripts. Instead we
// randomly generate a string to (hopefully) prevent
// collisions.
heredoc := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("script-heredoc-randomly-generated")
placeScriptsInit.Args[1] += fmt.Sprintf(`tmpfile="%s"
heredoc := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix(fmt.Sprintf("%s-heredoc-randomly-generated", format))
initContainer.Args[1] += fmt.Sprintf(`tmpfile="%s"
touch ${tmpfile} && chmod +x ${tmpfile}
cat > ${tmpfile} << '%s'
%s
Expand All @@ -110,9 +124,5 @@ cat > ${tmpfile} << '%s'
steps[i].VolumeMounts = append(steps[i].VolumeMounts, scriptsVolumeMount)
containers = append(containers, steps[i].Container)
}

if placeScripts {
return &placeScriptsInit, containers
}
return nil, containers
return containers
}
Loading