Skip to content

Commit

Permalink
Added entrypoint log grabber to taskrun controller
Browse files Browse the repository at this point in the history
What is the problem being solved?
This PR addresses issue #143.  This issue is that it when Build's complete, logs from the steps the Build ran are garbage collected by kubernetes and no longer available to the user.

Why is this the best approach?
1) No special kubernetes configuration (eg. changing garbage collector values)

What other approaches did you consider?
1) Changing kubernetes garbage collection for these containers so that they are not immediately deleted and log capture was possible

What side effects will this approach have?
1) With this approach, users will have to specify the "Command" value for the containers they which to run as the Entrypoint is not retrievable.  This means that containers/flows setup to use only the Entrypoint will no longer be supported.

What future work remains to be done?
1) It is possible to have the PVCs changed to EmptyDir volumes once a log uploader is created.  This will help with the issue that currently PVCs are being created and not cleaned up.

Co-authored-by: Christie Wilson <christiewilson@google.com>
  • Loading branch information
2 people authored and knative-prow-robot committed Oct 24, 2018
1 parent 1a65b67 commit 6f6b071
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 116 deletions.
9 changes: 6 additions & 3 deletions Concepts.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Pipeline CRDs
Pipeline CRDs is an open source implementation to configure and run CI/CD style pipelines for your kubernetes application.

Pipeline CRDs creates [Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) as building blocks to declare pipelines.
Pipeline CRDs creates [Custom Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) as building blocks to declare pipelines.

A custom resource is an extension of Kubernetes API which can create a custom [Kubernetest Object](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#understanding-kubernetes-objects).
Once a custom resource is installed, users can create and access its objects with kubectl, just as they do for built-in resources like pods, deployments etc.
Expand All @@ -20,7 +20,9 @@ A task will run inside a container on your cluster. A Task declares,
1. Outputs the task will produce.
1. Sequence of steps to execute.

Each step defines an container image. This image is of type [Builder Image](https://github.com/knative/docs/blob/master/build/builder-contract.md). A Builder Image is an image whose entrypoint is a tool that performs some action and exits with a zero status on success. These entrypoints are often command-line tools, for example, git, docker, mvn, and so on.
Each step defines an container image. This image is of type [Builder Image](https://github.com/knative/docs/blob/master/build/builder-contract.md). A Builder Image is an image whose `command` performs some action and exits with a zero status on success.

NOTE: Currently to get the logs out of a Builder Image, entrypoint overrides are used. This means that each step in `steps:` must have a container with a `command:` specified.

Here is an example simple Task definition which echoes "hello world". The `hello-world` task does not define any inputs or outputs.

Expand All @@ -37,8 +39,9 @@ spec:
steps:
- name: echo
image: busybox
args:
command:
- echo
args:
- "hello world!"
```
Examples of `Task` definitions with inputs and outputs are [here](./examples)
Expand Down
4 changes: 2 additions & 2 deletions config/200-clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ metadata:
name: knative-build-pipeline-admin
rules:
- apiGroups: [""]
resources: ["pods", "namespaces", "secrets", "events", "serviceaccounts", "configmaps"]
resources: ["pods", "namespaces", "secrets", "events", "serviceaccounts", "configmaps", "persistentvolumeclaims"]
verbs: ["get", "list", "create", "update", "delete", "patch", "watch"]
- apiGroups: ["extensions"]
resources: ["deployments"]
Expand All @@ -20,4 +20,4 @@ rules:
verbs: ["get", "list", "create", "update", "delete", "patch", "watch"]
- apiGroups: ["build.knative.dev"]
resources: ["builds", "buildtemplates", "clusterbuildtemplates"]
verbs: ["get", "list", "create", "update", "delete", "patch", "watch"]
verbs: ["get", "list", "create", "update", "delete", "patch", "watch"]
8 changes: 4 additions & 4 deletions pkg/reconciler/v1alpha1/pipelinerun/pipelinerun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,23 +93,23 @@ func TestReconcile(t *testing.T) {
Tasks: ts,
PipelineParams: pp,
}
c, _, client := test.GetPipelineRunController(d)
c, _, clients := test.GetPipelineRunController(d)
err := c.Reconciler.Reconcile(context.Background(), "foo/test-pipeline-run-success")
if err != nil {
t.Errorf("Did not expect to see error when reconciling valid Pipeline but saw %s", err)
}
if len(client.Actions()) == 0 {
if len(clients.Pipeline.Actions()) == 0 {
t.Fatalf("Expected client to have been used to create a TaskRun but it wasn't")
}

// Check that the PipelineRun was reconciled correctly
reconciledRun, err := client.Pipeline().PipelineRuns("foo").Get("test-pipeline-run-success", metav1.GetOptions{})
reconciledRun, err := clients.Pipeline.Pipeline().PipelineRuns("foo").Get("test-pipeline-run-success", metav1.GetOptions{})
if err != nil {
t.Fatalf("Somehow had error getting reconciled run out of fake client: %s", err)
}

// Check that the expected TaskRun was created
actual := client.Actions()[0].(ktesting.CreateAction).GetObject()
actual := clients.Pipeline.Actions()[0].(ktesting.CreateAction).GetObject()
trueB := true
expectedTaskRun := &v1alpha1.TaskRun{
ObjectMeta: metav1.ObjectMeta{
Expand Down
103 changes: 103 additions & 0 deletions pkg/reconciler/v1alpha1/taskrun/resources/entrypoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
Copyright 2018 The Knative 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 resources

import (
"encoding/json"
"fmt"

corev1 "k8s.io/api/core/v1"
)

const (
// MountName is the name of the pvc being mounted (which
// will contain the entrypoint binary and eventually the logs)
MountName = "tools"

mountPoint = "/tools"
entrypointBin = mountPoint + "/entrypoint"
entrypointJSONConfigEnvVar = "ENTRYPOINT_OPTIONS"
EntrypointImage = "gcr.io/k8s-prow/entrypoint@sha256:7c7cd8906ce4982ffee326218e9fc75da2d4896d53cabc9833b9cc8d2d6b2b8f"
)

var toolsMount = corev1.VolumeMount{
Name: MountName,
MountPath: mountPoint,
}

// GetCopyStep will return a Build Step (Container) that will
// copy the entrypoint binary from the entrypoint image into the
// volume mounted at mountPoint, so that it can be mounted by
// subsequent steps and used to capture logs.
func GetCopyStep() corev1.Container {
return corev1.Container{
Name: "place-tools",
Image: EntrypointImage,
Command: []string{"/bin/cp"},
Args: []string{"/entrypoint", entrypointBin},
VolumeMounts: []corev1.VolumeMount{toolsMount},
}
}

type entrypointArgs struct {
Args []string `json:"args"`
ProcessLog string `json:"process_log"`
MarkerFile string `json:"marker_file"`
}

func getEnvVar(cmd, args []string) (string, error) {
entrypointArgs := entrypointArgs{
Args: append(cmd, args...),
ProcessLog: "/tools/process-log.txt",
MarkerFile: "/tools/marker-file.txt",
}
j, err := json.Marshal(entrypointArgs)
if err != nil {
return "", fmt.Errorf("couldn't marshal arguments %q for entrypoint env var: %s", entrypointArgs, err)
}
return string(j), nil
}

// TODO: add more test cases after all, e.g. with existing env
// var and volume mounts

// AddEntrypoint will modify each of the steps/containers such that
// the binary being run is no longer the one specified by the Command
// and the Args, but is instead the entrypoint binary, which will
// itself invoke the Command and Args, but also capture logs.
// TODO: This will not work when a step uses an image that has its
// own entrypoint, i.e. `Command` is a required field. In later iterations
// we can update the controller to inspect the image's `Entrypoint`
// and use that if required.
func AddEntrypoint(steps []corev1.Container) error {
for i := range steps {
step := &steps[i]
e, err := getEnvVar(step.Command, step.Args)
if err != nil {
return fmt.Errorf("couldn't get env var for entrypoint: %s", err)
}
step.Command = []string{entrypointBin}
step.Args = []string{}

step.Env = append(step.Env, corev1.EnvVar{
Name: entrypointJSONConfigEnvVar,
Value: e,
})
step.VolumeMounts = append(step.VolumeMounts, toolsMount)
}
return nil
}
84 changes: 73 additions & 11 deletions pkg/reconciler/v1alpha1/taskrun/taskrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/cache"
Expand All @@ -55,6 +56,8 @@ const (
// taskRunControllerName defines name for TaskRun Controller
taskRunControllerName = "TaskRun"
taskRunNameLabelKey = "taskrun.knative.dev/taskName"

pvcSizeBytes = 5 * 1024 * 1024 * 1024 // 5 GBs
)

var (
Expand Down Expand Up @@ -102,13 +105,6 @@ func NewController(
UpdateFunc: controller.PassNew(impl.Enqueue),
})

// TODO(aaron-prindle) what to do if a task is deleted?
// taskInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
// AddFunc: impl.Enqueue,
// UpdateFunc: controller.PassNew(impl.Enqueue),
// DeleteFunc: impl.Enqueue,
// })

c.tracker = tracker.New(impl.EnqueueKey, opt.GetTrackerLease())
buildInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.tracker.OnChanged,
Expand Down Expand Up @@ -166,8 +162,20 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1alpha1.TaskRun) error
// get build the same as the taskrun, this is the value we use for 1:1 mapping and retrieval
build, err := c.BuildClientSet.BuildV1alpha1().Builds(tr.Namespace).Get(tr.Name, metav1.GetOptions{})
if errors.IsNotFound(err) {
pvc, err := c.KubeClientSet.CoreV1().PersistentVolumeClaims(tr.Namespace).Get(tr.Name, metav1.GetOptions{})
if errors.IsNotFound(err) {
// Create a persistent volume claim to hold Build logs
pvc, err = c.createPVC(tr)
if err != nil {
return fmt.Errorf("Failed to create persistent volume claim %s for task %q: %v", tr.Name, err, tr.Name)
}
} else if err != nil {
c.Logger.Errorf("Failed to reconcile taskrun: %q, failed to get pvc %q; %v", tr.Name, tr.Name, err)
return err
}

// Build is not present, create build
build, err = c.createBuild(tr)
build, err = c.createBuild(tr, pvc.Name)
if err != nil {
// This Run has failed, so we need to mark it as failed and stop reconciling it
tr.Status.SetCondition(&duckv1alpha1.Condition{
Expand Down Expand Up @@ -224,8 +232,40 @@ func (c *Reconciler) updateStatus(taskrun *v1alpha1.TaskRun) (*v1alpha1.TaskRun,
return newtaskrun, nil
}

// createBuild creates a build from the task, using the task's buildspec.
func (c *Reconciler) createBuild(tr *v1alpha1.TaskRun) (*buildv1alpha1.Build, error) {
// createVolume will create a persistent volume mount for tr which
// will be used to gather logs using the entrypoint wrapper
func (c *Reconciler) createPVC(tr *v1alpha1.TaskRun) (*corev1.PersistentVolumeClaim, error) {
v, err := c.KubeClientSet.CoreV1().PersistentVolumeClaims(tr.Namespace).Create(
&corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Namespace: tr.Namespace,
// This pvc is specific to this TaskRun, so we'll use the same name
Name: tr.Name,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(tr, groupVersionKind),
},
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.ReadWriteOnce,
},
Resources: corev1.ResourceRequirements{
Requests: map[corev1.ResourceName]resource.Quantity{
corev1.ResourceStorage: *resource.NewQuantity(pvcSizeBytes, resource.BinarySI),
},
},
},
},
)
if err != nil {
return nil, fmt.Errorf("failed to claim Persistent Volume %q due to error: %s", tr.Name, err)
}
return v, nil
}

// createBuild creates a build from the task, using the task's buildspec
// with pvcName as a volumeMount
func (c *Reconciler) createBuild(tr *v1alpha1.TaskRun, pvcName string) (*buildv1alpha1.Build, error) {
// Get related task for taskrun
t, err := c.taskLister.Tasks(tr.Namespace).Get(tr.Spec.TaskRef.Name)
if err != nil {
Expand All @@ -237,6 +277,28 @@ func (c *Reconciler) createBuild(tr *v1alpha1.TaskRun) (*buildv1alpha1.Build, er
return nil, fmt.Errorf("task %s has nil BuildSpec", t.Name)
}

bSpec := t.Spec.BuildSpec.DeepCopy()
bSpec.Volumes = append(bSpec.Volumes, corev1.Volume{
Name: resources.MountName,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: pvcName,
},
},
})

// Override the entrypoint so that we can use our custom
// entrypoint which copies logs
err = resources.AddEntrypoint(bSpec.Steps)
if err != nil {
return nil, fmt.Errorf("Failed to add entrypoint to steps of Build: %s", err)
}

// Add the step which will copy the entrypoint into the volume
// we are going to be using, so that all of the steps will have
// access to it.
bSpec.Steps = append([]corev1.Container{resources.GetCopyStep()}, bSpec.Steps...)

b := &buildv1alpha1.Build{
ObjectMeta: metav1.ObjectMeta{
Name: tr.Name,
Expand All @@ -247,7 +309,7 @@ func (c *Reconciler) createBuild(tr *v1alpha1.TaskRun) (*buildv1alpha1.Build, er
// Attach new label and pass taskrun labels to build
Labels: makeLabels(tr),
},
Spec: *t.Spec.BuildSpec,
Spec: *bSpec,
}
// Pass service account name from taskrun to build
// if task specifies service account name override with taskrun SA
Expand Down
Loading

0 comments on commit 6f6b071

Please sign in to comment.