Skip to content

Commit

Permalink
Add experimental hermetic execution mode to TaskRun
Browse files Browse the repository at this point in the history
This PR adds supoprt for an experimental hermetic execution mode. If users specify this on their TaskRun, then all user containers are run without network access.
Any containers created or injected by tekton (init containers or sidecar containers) are not affected, and user sidecar containers are also not affected.

Some notes around this PR:
1. Adds documentation around hermetic execution mode and points to it from taskrun.md
2. Removes the API change & instead specify execution mode as an annotation on a TaskRun
3. Also puts hermetic execution mode behind the `alpha` feature flag
4. Adds a unit test to make sure that the TEKTON_HERMETIC env var is set such that it can't be overridden

Relevant TEP: https://github.com/tektoncd/community/blob/main/teps/0025-hermekton.md
  • Loading branch information
Priya Wadhwa authored and tekton-robot committed May 20, 2021
1 parent 55ae856 commit b7fa888
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 1 deletion.
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 {
// 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)
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()
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
// 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{
{
Container: corev1.Container{
Name: "access-network",
Image: "ubuntu",
},
Script: `#!/bin/bash
set -ex
apt-get update
apt-get install -y curl`,
},
},
},
},
}
}

0 comments on commit b7fa888

Please sign in to comment.