Skip to content

Commit e228f11

Browse files
imjasonhtekton-robot
authored andcommitted
Split out entrypoint resolution and injection, and script conversion
This is the next and largest step of the effort to simplify and separate MakePod out into digestible chunks (#1605) Behavioral changes: - Script->Command conversion now happens before entrypoint rewriting, rather than converting the rewritten entrypoint args. - Image name->digest lookups are cached locally while resolving a single TaskRun's steps (with test!) - Entrypoint lookups also update the step's digest. This was a race before: if an image was pushed between resolution and pod start, the resolved command might be out-of-date. Some redundant test cases have been removed from taskrun_test.go -- this file should test only behavior of taskrun.go, which is now smaller. Unit tests for individual transformation behavior has been moved into individual test files in pkg/pod, with some integration tests in pod_test.go -- some of these could likely be removed as well, if we feel like it.
1 parent 40a827d commit e228f11

27 files changed

+1369
-1913
lines changed

cmd/controller/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ var (
5454
func main() {
5555
flag.Parse()
5656
images := pipeline.Images{
57-
EntryPointImage: *entrypointImage,
57+
EntrypointImage: *entrypointImage,
5858
NopImage: *nopImage,
5959
GitImage: *gitImage,
6060
CredsImage: *credsImage,

examples/taskruns/steps-run-in-order.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ spec:
66
taskSpec:
77
steps:
88
- image: busybox
9-
command: ['/bin/sh']
9+
# NB: command is not set, so it must be looked up from the registry.
1010
args: ['-c', 'sleep 3 && touch foo']
1111
- image: busybox
12-
command: ['/bin/sh']
1312
args: ['-c', 'ls', 'foo']

pkg/apis/pipeline/images.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ package pipeline
1919
// Images holds the images reference for a number of container images used
2020
// across tektoncd pipelines.
2121
type Images struct {
22-
// EntryPointImage is container image containing our entrypoint binary.
23-
EntryPointImage string
22+
// EntrypointImage is container image containing our entrypoint binary.
23+
EntrypointImage string
2424
// NopImage is the container image used to kill sidecars.
2525
NopImage string
2626
// GitImage is the container image with Git that we use to implement the Git source step.

pkg/apis/pipeline/v1alpha1/build_gcs_resource_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import (
2828
)
2929

3030
var images = pipeline.Images{
31-
EntryPointImage: "override-with-entrypoint:latest",
31+
EntrypointImage: "override-with-entrypoint:latest",
3232
NopImage: "tianon/true",
3333
GitImage: "override-with-git:latest",
3434
CredsImage: "override-with-creds:latest",

pkg/artifacts/artifact_storage_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434

3535
var (
3636
images = pipeline.Images{
37-
EntryPointImage: "override-with-entrypoint:latest",
37+
EntrypointImage: "override-with-entrypoint:latest",
3838
NopImage: "tianon/true",
3939
GitImage: "override-with-git:latest",
4040
CredsImage: "override-with-creds:latest",

pkg/pod/entrypoint.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package pod
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"path/filepath"
7+
"strings"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/client-go/kubernetes"
12+
)
13+
14+
const (
15+
toolsVolumeName = "tools"
16+
mountPoint = "/builder/tools"
17+
entrypointBinary = mountPoint + "/entrypoint"
18+
19+
downwardVolumeName = "downward"
20+
downwardMountPoint = "/builder/downward"
21+
downwardMountReadyFile = "ready"
22+
ReadyAnnotation = "tekton.dev/ready"
23+
ReadyAnnotationValue = "READY"
24+
25+
StepPrefix = "step-"
26+
SidecarPrefix = "sidecar-"
27+
)
28+
29+
var (
30+
// TODO(#1605): Generate volumeMount names, to avoid collisions.
31+
// TODO(#1605): Unexport these vars when Pod conversion is entirely within
32+
// this package.
33+
ToolsMount = corev1.VolumeMount{
34+
Name: toolsVolumeName,
35+
MountPath: mountPoint,
36+
}
37+
ToolsVolume = corev1.Volume{
38+
Name: toolsVolumeName,
39+
VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
40+
}
41+
42+
// TODO(#1605): Signal sidecar readiness by injecting entrypoint,
43+
// remove dependency on Downward API.
44+
DownwardVolume = corev1.Volume{
45+
Name: downwardVolumeName,
46+
VolumeSource: corev1.VolumeSource{
47+
DownwardAPI: &corev1.DownwardAPIVolumeSource{
48+
Items: []corev1.DownwardAPIVolumeFile{{
49+
Path: downwardMountReadyFile,
50+
FieldRef: &corev1.ObjectFieldSelector{
51+
FieldPath: fmt.Sprintf("metadata.annotations['%s']", ReadyAnnotation),
52+
},
53+
}},
54+
},
55+
},
56+
}
57+
DownwardMount = corev1.VolumeMount{
58+
Name: downwardVolumeName,
59+
MountPath: downwardMountPoint,
60+
}
61+
)
62+
63+
// OrderContainers returns the specified steps, modified so that they are
64+
// executed in order by overriding the entrypoint binary. It also returns the
65+
// init container that places the entrypoint binary pulled from the
66+
// entrypointImage.
67+
//
68+
// Containers must have Command specified; if the user didn't specify a
69+
// command, we must have fetched the image's ENTRYPOINT before calling this
70+
// method, using entrypoint_lookup.go.
71+
//
72+
// TODO(#1605): Also use entrypoint injection to order sidecar start/stop.
73+
func OrderContainers(entrypointImage string, steps []corev1.Container) (corev1.Container, []corev1.Container, error) {
74+
toolsInit := corev1.Container{
75+
Name: "place-tools",
76+
Image: entrypointImage,
77+
Command: []string{"cp", "/ko-app/entrypoint", entrypointBinary},
78+
VolumeMounts: []corev1.VolumeMount{ToolsMount},
79+
}
80+
81+
if len(steps) == 0 {
82+
return corev1.Container{}, nil, errors.New("No steps specified")
83+
}
84+
85+
for i, s := range steps {
86+
var argsForEntrypoint []string
87+
switch i {
88+
case 0:
89+
argsForEntrypoint = []string{
90+
// First step waits for the Downward volume file.
91+
"-wait_file", filepath.Join(downwardMountPoint, downwardMountReadyFile),
92+
"-wait_file_content", // Wait for file contents, not just an empty file.
93+
// Start next step.
94+
"-post_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i)),
95+
}
96+
default:
97+
// All other steps wait for previous file, write next file.
98+
argsForEntrypoint = []string{
99+
"-wait_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i-1)),
100+
"-post_file", filepath.Join(mountPoint, fmt.Sprintf("%d", i)),
101+
}
102+
}
103+
104+
cmd, args := s.Command, s.Args
105+
if len(cmd) == 0 {
106+
return corev1.Container{}, nil, fmt.Errorf("Step %d did not specify command", i)
107+
}
108+
if len(cmd) > 1 {
109+
args = append(cmd[1:], args...)
110+
cmd = []string{cmd[0]}
111+
}
112+
argsForEntrypoint = append(argsForEntrypoint, "-entrypoint", cmd[0], "--")
113+
argsForEntrypoint = append(argsForEntrypoint, args...)
114+
115+
steps[i].Command = []string{entrypointBinary}
116+
steps[i].Args = argsForEntrypoint
117+
steps[i].VolumeMounts = append(steps[i].VolumeMounts, ToolsMount)
118+
}
119+
// Mount the Downward volume into the first step container.
120+
steps[0].VolumeMounts = append(steps[0].VolumeMounts, DownwardMount)
121+
122+
return toolsInit, steps, nil
123+
}
124+
125+
// UpdateReady updates the Pod's annotations to signal the first step to start
126+
// by projecting the ready annotation via the Downward API.
127+
func UpdateReady(kubeclient kubernetes.Interface, pod corev1.Pod) error {
128+
newPod, err := kubeclient.CoreV1().Pods(pod.Namespace).Get(pod.Name, metav1.GetOptions{})
129+
if err != nil {
130+
return fmt.Errorf("Error getting Pod %q when updating ready annotation: %w", pod.Name, err)
131+
}
132+
133+
// Update the Pod's "READY" annotation to signal the first step to
134+
// start.
135+
if newPod.ObjectMeta.Annotations == nil {
136+
newPod.ObjectMeta.Annotations = map[string]string{}
137+
}
138+
if newPod.ObjectMeta.Annotations[ReadyAnnotation] != ReadyAnnotationValue {
139+
newPod.ObjectMeta.Annotations[ReadyAnnotation] = ReadyAnnotationValue
140+
if _, err := kubeclient.CoreV1().Pods(newPod.Namespace).Update(newPod); err != nil {
141+
return fmt.Errorf("Error adding ready annotation to Pod %q: %w", pod.Name, err)
142+
}
143+
}
144+
return nil
145+
}
146+
147+
// StopSidecars updates sidecar containers in the Pod to a nop image, which
148+
// exits successfully immediately.
149+
func StopSidecars(nopImage string, kubeclient kubernetes.Interface, pod corev1.Pod) error {
150+
newPod, err := kubeclient.CoreV1().Pods(pod.Namespace).Get(pod.Name, metav1.GetOptions{})
151+
if err != nil {
152+
return fmt.Errorf("Error getting Pod %q when stopping sidecars: %w", pod.Name, err)
153+
}
154+
155+
updated := false
156+
if newPod.Status.Phase == corev1.PodRunning {
157+
for _, s := range newPod.Status.ContainerStatuses {
158+
if IsContainerSidecar(s.Name) && s.State.Running != nil {
159+
for j, c := range newPod.Spec.Containers {
160+
if c.Name == s.Name && c.Image != nopImage {
161+
updated = true
162+
newPod.Spec.Containers[j].Image = nopImage
163+
}
164+
}
165+
}
166+
}
167+
}
168+
if updated {
169+
if _, err := kubeclient.CoreV1().Pods(newPod.Namespace).Update(newPod); err != nil {
170+
return fmt.Errorf("Error adding ready annotation to Pod %q: %w", pod.Name, err)
171+
}
172+
}
173+
return nil
174+
}
175+
176+
func IsContainerStep(name string) bool { return strings.HasPrefix(name, StepPrefix) }
177+
func IsContainerSidecar(name string) bool { return strings.HasPrefix(name, SidecarPrefix) }
178+
179+
func TrimStepPrefix(name string) string { return strings.TrimPrefix(name, StepPrefix) }
180+
func TrimSidecarPrefix(name string) string { return strings.TrimPrefix(name, SidecarPrefix) }

pkg/pod/entrypoint_lookup.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package pod
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/google/go-containerregistry/pkg/authn"
7+
"github.com/google/go-containerregistry/pkg/authn/k8schain"
8+
"github.com/google/go-containerregistry/pkg/name"
9+
"github.com/google/go-containerregistry/pkg/v1/remote"
10+
lru "github.com/hashicorp/golang-lru"
11+
corev1 "k8s.io/api/core/v1"
12+
"k8s.io/client-go/kubernetes"
13+
)
14+
15+
// ResolveEntrypoints looks up container image ENTRYPOINTs for all steps that
16+
// don't specify a Command.
17+
//
18+
// Images that are not specified by digest will be specified by digest after
19+
// lookup in the resulting list of containers.
20+
func ResolveEntrypoints(cache EntrypointCache, namespace, serviceAccountName string, steps []corev1.Container) ([]corev1.Container, error) {
21+
// Keep a local cache of image->digest lookups, just for the scope of
22+
// resolving this set of steps. If the image is pushed to, we need to
23+
// resolve its digest and entrypoint again, but we can skip lookups
24+
// while resolving the same TaskRun.
25+
type result struct {
26+
digest name.Digest
27+
ep []string
28+
}
29+
localCache := map[string]result{}
30+
for i, s := range steps {
31+
if len(s.Command) != 0 {
32+
// Nothing to resolve.
33+
continue
34+
}
35+
36+
var digest name.Digest
37+
var ep []string
38+
var err error
39+
if r, found := localCache[s.Image]; found {
40+
digest = r.digest
41+
ep = r.ep
42+
} else {
43+
// Look it up in the cache, which will also resolve the digest.
44+
ep, digest, err = cache.Get(s.Image, namespace, serviceAccountName)
45+
if err != nil {
46+
return nil, err
47+
}
48+
localCache[s.Image] = result{digest, ep} // Cache it locally in case another step specifies the same image.
49+
}
50+
51+
steps[i].Image = digest.String() // Specify image by digest, since we know it now.
52+
steps[i].Command = ep // Specify the command explicitly.
53+
}
54+
return steps, nil
55+
}
56+
57+
const cacheSize = 1024
58+
59+
type EntrypointCache interface {
60+
Get(imageName, namespace, serviceAccountName string) (cmd []string, d name.Digest, err error)
61+
}
62+
63+
type entrypointCache struct {
64+
kubeclient kubernetes.Interface
65+
lru *lru.Cache // cache of digest string -> image entrypoint []string
66+
}
67+
68+
func NewEntrypointCache(kubeclient kubernetes.Interface) (EntrypointCache, error) {
69+
lru, err := lru.New(cacheSize)
70+
if err != nil {
71+
return nil, err
72+
}
73+
return &entrypointCache{
74+
kubeclient: kubeclient,
75+
lru: lru,
76+
}, nil
77+
}
78+
79+
func (e *entrypointCache) Get(imageName, namespace, serviceAccountName string) (cmd []string, d name.Digest, err error) {
80+
ref, err := name.ParseReference(imageName, name.WeakValidation)
81+
if err != nil {
82+
return nil, name.Digest{}, fmt.Errorf("Error parsing reference %q: %v", imageName, err)
83+
}
84+
85+
// If image is specified by digest, check the local cache.
86+
if digest, ok := ref.(name.Digest); ok {
87+
if ep, ok := e.lru.Get(digest.String()); ok {
88+
return ep.([]string), digest, nil
89+
}
90+
}
91+
92+
// If the image wasn't specified by digest, or if the entrypoint
93+
// wasn't found, we have to consult the remote registry, using
94+
// imagePullSecrets.
95+
kc, err := k8schain.New(e.kubeclient, k8schain.Options{
96+
Namespace: namespace,
97+
ServiceAccountName: serviceAccountName,
98+
})
99+
if err != nil {
100+
return nil, name.Digest{}, fmt.Errorf("Error creating k8schain: %v", err)
101+
}
102+
mkc := authn.NewMultiKeychain(kc)
103+
img, err := remote.Image(ref, remote.WithAuthFromKeychain(mkc))
104+
if err != nil {
105+
return nil, name.Digest{}, fmt.Errorf("Error getting image manifest: %v", err)
106+
}
107+
digest, err := img.Digest()
108+
if err != nil {
109+
return nil, name.Digest{}, fmt.Errorf("Error getting image digest: %v", err)
110+
}
111+
cfg, err := img.ConfigFile()
112+
if err != nil {
113+
return nil, name.Digest{}, fmt.Errorf("Error getting image config: %v", err)
114+
}
115+
ep := cfg.Config.Entrypoint
116+
if len(ep) == 0 {
117+
ep = cfg.Config.Cmd
118+
}
119+
e.lru.Add(digest.String(), ep) // Populate the cache.
120+
121+
d, err = name.NewDigest(imageName+"@"+digest.String(), name.WeakValidation)
122+
if err != nil {
123+
return nil, name.Digest{}, fmt.Errorf("Error constructing resulting digest: %v", err)
124+
}
125+
return ep, d, nil
126+
}

0 commit comments

Comments
 (0)