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 support for experimental hermetic execution mode to TaskRuns #3956

Merged
merged 1 commit into from
May 20, 2021
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
5 changes: 5 additions & 0 deletions cmd/entrypoint/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"syscall"

"github.com/tektoncd/pipeline/pkg/entrypoint"
"github.com/tektoncd/pipeline/pkg/pod"
)

// TODO(jasonhall): Test that original exit code is propagated and that
Expand Down Expand Up @@ -58,6 +59,10 @@ func (rr *realRunner) Run(ctx context.Context, args ...string) error {
// main process and all children
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

if os.Getenv("TEKTON_RESOURCE_NAME") == "" && os.Getenv(pod.TektonHermeticEnvVar) == "1" {
dropNetworking(cmd)
}

// Start defined command
if err := cmd.Start(); err != nil {
if ctx.Err() == context.DeadlineExceeded {
Expand Down
51 changes: 51 additions & 0 deletions docs/hermetic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!--
---
linkTitle: "Hermetic"
weight: 10
---
-->
# Hermetic Execution Mode
A Hermetic Build is a release engineering best practice for increasing the reliability and consistency of software builds.
They are self-contained, and do not depend on anything outside of the build environment.
This means they do not have network access, and cannot fetch dependencies at runtime.

When hermetic execution mode is enabled, all TaskRun steps will be run without access to a network.
_Note: hermetic execution mode does NOT apply to sidecar containers_

Hermetic execution mode is currently an alpha experimental feature.

## Enabling Hermetic Execution Mode
To enable hermetic execution mode:
1. Make sure `enable-api-fields` is set to `"alpha"` in the `feature-flags` configmap, see [`install.md`](./install.md#customizing-the-pipelines-controller-behavior) for details
1. Set the following annotation on any TaskRun you want to run hermetically:

```yaml
experimental.tekton.dev/execution-mode: hermetic
```

## Sample Hermetic TaskRun
This example TaskRun demonstrates running a container in a hermetic environment.

The Step attempts to install curl, but this step **SHOULD FAIL** if the hermetic environment is working as expected.

```yaml
kind: TaskRun
apiVersion: tekton.dev/v1beta1
metadata:
generateName: hermetic-should-fail
annotations:
experimental.tekton.dev/execution-mode: hermetic
spec:
timeout: 60s
taskSpec:
steps:
- name: hermetic
image: ubuntu
script: |
#!/usr/bin/env bash
apt-get update
apt-get install -y curl
```

## Further Details
To learn more about hermetic execution mode, check out the [TEP](https://github.com/tektoncd/community/blob/main/teps/0025-hermekton.md).
1 change: 1 addition & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ Features currently in "alpha" are:
- [Tekton Bundles](./taskruns.md#tekton-bundles)
- [Custom Tasks](./runs.md)
- [Isolated Step & Sidecar Workspaces](./workspaces.md#isolated-workspaces)
- [Hermetic Execution Mode](./hermetic.md)

## Configuring High Availability

Expand Down
1 change: 1 addition & 0 deletions docs/taskruns.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ weight: 2
- [Monitoring `Results`](#monitoring-results)
- [Cancelling a `TaskRun`](#cancelling-a-taskrun)
- [Events](events.md#taskruns)
- [Running a TaskRun Hermetically](hermetic.md)
- [Code examples](#code-examples)
- [Example `TaskRun` with a referenced `Task`](#example-taskrun-with-a-referenced-task)
- [Example `TaskRun` with an embedded `Task`](#example-taskrun-with-an-embedded-task)
Expand Down
17 changes: 17 additions & 0 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ const (

// TaskRunLabelKey is the name of the label added to the Pod to identify the TaskRun
TaskRunLabelKey = pipeline.GroupName + pipeline.TaskRunLabelKey

// TektonHermeticEnvVar is the env var we set in containers to indicate they should be run hermetically
TektonHermeticEnvVar = "TEKTON_HERMETIC"
// ExecutionModeAnnotation is an experimental optional annotation to set the execution mode on a TaskRun
ExecutionModeAnnotation = "experimental.tekton.dev/execution-mode"
// ExecutionModeHermetic indicates hermetic execution mode
ExecutionModeHermetic = "hermetic"
)

// These are effectively const, but Go doesn't have such an annotation.
Expand Down Expand Up @@ -168,6 +175,16 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
}
}

// Add env var if hermetic execution was requested & if the alpha API is enabled
alphaAPIEnabled := config.FromContextOrDefaults(ctx).FeatureFlags.EnableAPIFields == config.AlphaAPIFields
if taskRun.Annotations[ExecutionModeAnnotation] == ExecutionModeHermetic && alphaAPIEnabled {
for i, s := range stepContainers {
priyawadhwa marked this conversation as resolved.
Show resolved Hide resolved
// Add it at the end so it overrides
env := append(s.Env, corev1.EnvVar{Name: TektonHermeticEnvVar, Value: "1"})
stepContainers[i].Env = env
}
}

// Add implicit volume mounts to each step, unless the step specifies
// its own volume mount at that path.
for i, s := range stepContainers {
Expand Down
97 changes: 97 additions & 0 deletions pkg/pod/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,103 @@ script-heredoc-randomly-generated-78c5n
}},
Volumes: append(implicitVolumes, toolsVolume, downwardVolume),
},
}, {
desc: "hermetic env var",
featureFlags: map[string]string{"enable-api-fields": "alpha"},
ts: v1beta1.TaskSpec{
Steps: []v1beta1.Step{{Container: corev1.Container{
Name: "name",
Image: "image",
Command: []string{"cmd"}, // avoid entrypoint lookup.
}}},
},
trAnnotation: map[string]string{
"experimental.tekton.dev/execution-mode": "hermetic",
},
want: &corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
InitContainers: []corev1.Container{placeToolsInit},
Containers: []corev1.Container{{
Name: "step-name",
Image: "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",
"--",
},
VolumeMounts: append([]corev1.VolumeMount{toolsMount, downwardMount, {
Name: "tekton-creds-init-home-0",
MountPath: "/tekton/creds",
}}, implicitVolumeMounts...),
Resources: corev1.ResourceRequirements{Requests: allZeroQty()},
TerminationMessagePath: "/tekton/termination",
Env: []corev1.EnvVar{
{Name: "TEKTON_HERMETIC", Value: "1"},
},
}},
Volumes: append(implicitVolumes, toolsVolume, downwardVolume, corev1.Volume{
Name: "tekton-creds-init-home-0",
VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}},
}),
},
}, {
desc: "override hermetic env var",
featureFlags: map[string]string{"enable-api-fields": "alpha"},
ts: v1beta1.TaskSpec{
Steps: []v1beta1.Step{{Container: corev1.Container{
Name: "name",
Image: "image",
Command: []string{"cmd"}, // avoid entrypoint lookup.
Env: []corev1.EnvVar{{Name: "TEKTON_HERMETIC", Value: "something_else"}},
}}},
},
trAnnotation: map[string]string{
"experimental.tekton.dev/execution-mode": "hermetic",
},
want: &corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
InitContainers: []corev1.Container{placeToolsInit},
Containers: []corev1.Container{{
Name: "step-name",
Image: "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",
"--",
},
VolumeMounts: append([]corev1.VolumeMount{toolsMount, downwardMount, {
Name: "tekton-creds-init-home-0",
MountPath: "/tekton/creds",
}}, implicitVolumeMounts...),
Resources: corev1.ResourceRequirements{Requests: allZeroQty()},
TerminationMessagePath: "/tekton/termination",
Env: []corev1.EnvVar{
{Name: "TEKTON_HERMETIC", Value: "something_else"},
// this value must be second to override the first
{Name: "TEKTON_HERMETIC", Value: "1"},
},
}},
Volumes: append(implicitVolumes, toolsVolume, downwardVolume, corev1.Volume{
Name: "tekton-creds-init-home-0",
VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}},
}),
},
}} {
t.Run(c.desc, func(t *testing.T) {
names.TestingSeed()
Expand Down
2 changes: 1 addition & 1 deletion test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ go test ./...
# Integration tests (against your current kube cluster)
go test -v -count=1 -tags=e2e -timeout=20m ./test

#conformance tests (against your current kube cluster)
# Conformance tests (against your current kube cluster)
priyawadhwa marked this conversation as resolved.
Show resolved Hide resolved
go test -v -count=1 -tags=conformance -timeout=10m ./test
```

Expand Down
93 changes: 93 additions & 0 deletions test/hermetic_taskrun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// +build e2e

/*
Copyright 2021 The Tekton Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package test

import (
"context"
"testing"
"time"

"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TestHermeticTaskRun make sure that the hermetic execution mode actually drops network from a TaskRun step
// it does this by first running the TaskRun normally to make sure it passes
// Then, it enables hermetic mode and makes sure the same TaskRun fails because it no longer has access to a network.
func TestHermeticTaskRun(t *testing.T) {
ctx := context.Background()
Copy link
Member

Choose a reason for hiding this comment

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

I think we had that issue before for other test calling dropNetworking()

if this test is running on a dev laptop environment that is not Linux, the test will fail.

Since calling to dropNetworking which is only available on Linux https://github.com/tektoncd/pipeline/blob/567dce3cc/cmd/entrypoint/namespaces.go#L10 would panic()

Copy link
Member

Choose a reason for hiding this comment

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

ah nm this is the e2e tests, i didn't realize so forget my comment :)

Copy link
Member

Choose a reason for hiding this comment

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

Right @chmouel this is not gonna be a problem. One problem though might be running this test against OpenShift or a cluster that enforce some lower privileges (default to user, drop some privileges) that might make the calls in dropNetworking to fail 🤔 I am not entirely sure though.

Copy link
Member

Choose a reason for hiding this comment

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

Not just OpenShift but i guess dropNetworking would be a privileged operation in general.

ctx, cancel := context.WithCancel(ctx)
defer cancel()

c, namespace := setup(ctx, t, requireAnyGate(map[string]string{"enable-api-fields": "alpha"}))
t.Parallel()
defer tearDown(ctx, t, c, namespace)

// first, run the task run with hermetic=false to prove that it succeeds
regularTaskRunName := "not-hermetic"
regularTaskRun := taskRun(regularTaskRunName, namespace, "")
t.Logf("Creating TaskRun %s, hermetic=false", regularTaskRunName)
if _, err := c.TaskRunClient.Create(ctx, regularTaskRun, metav1.CreateOptions{}); err != nil {
t.Fatalf("Failed to create TaskRun `%s`: %s", regularTaskRunName, err)
}
if err := WaitForTaskRunState(ctx, c, regularTaskRunName, Succeed(regularTaskRunName), "TaskRunCompleted"); err != nil {
t.Fatalf("Error waiting for TaskRun %s to finish: %s", regularTaskRunName, err)
}

// now, run the task mode with hermetic mode
priyawadhwa marked this conversation as resolved.
Show resolved Hide resolved
// it should fail, since it shouldn't be able to access any network
hermeticTaskRunName := "hermetic-should-fail"
hermeticTaskRun := taskRun(hermeticTaskRunName, namespace, "hermetic")
t.Logf("Creating TaskRun %s, hermetic=true", hermeticTaskRunName)
if _, err := c.TaskRunClient.Create(ctx, hermeticTaskRun, metav1.CreateOptions{}); err != nil {
t.Fatalf("Failed to create TaskRun `%s`: %s", regularTaskRun.Name, err)
}
if err := WaitForTaskRunState(ctx, c, hermeticTaskRunName, Failed(hermeticTaskRunName), "Failed"); err != nil {
t.Fatalf("Error waiting for TaskRun %s to fail: %s", hermeticTaskRunName, err)
}
}

func taskRun(name, namespace, executionMode string) *v1beta1.TaskRun {
return &v1beta1.TaskRun{
ObjectMeta: metav1.ObjectMeta{Name: name,
Namespace: namespace,
Annotations: map[string]string{
"experimental.tekton.dev/execution-mode": executionMode,
},
},
Spec: v1beta1.TaskRunSpec{
Timeout: &metav1.Duration{Duration: time.Minute},
TaskSpec: &v1beta1.TaskSpec{
Steps: []v1beta1.Step{
This conversation was marked as resolved.
Show resolved Hide resolved
{
Container: corev1.Container{
Name: "access-network",
Image: "ubuntu",
},
Script: `#!/bin/bash
set -ex
apt-get update
apt-get install -y curl`,
},
},
},
},
}
}