Skip to content

Commit c126a0d

Browse files
committed
Abstract k8s container representation from debug transformers
1 parent 55f5860 commit c126a0d

16 files changed

+350
-255
lines changed

pkg/skaffold/debug/cnb.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
cnbl "github.com/buildpacks/lifecycle/launch"
2626
shell "github.com/kballard/go-shellquote"
2727
"github.com/sirupsen/logrus"
28-
v1 "k8s.io/api/core/v1"
2928

3029
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug/annotations"
3130
)
@@ -105,7 +104,7 @@ func hasCNBLauncherEntrypoint(ic imageConfiguration) bool {
105104
// the default process type. `CNB_PROCESS_TYPE` is ignored in this situation. A different process
106105
// can be used by overriding the image entrypoint. Direct and script launches are supported by
107106
// setting the entrypoint to `/cnb/lifecycle/launcher` and providing the appropriate arguments.
108-
func updateForCNBImage(container *v1.Container, ic imageConfiguration, transformer func(container *v1.Container, ic imageConfiguration) (annotations.ContainerDebugConfiguration, string, error)) (annotations.ContainerDebugConfiguration, string, error) {
107+
func updateForCNBImage(container *operableContainer, ic imageConfiguration, transformer func(container *operableContainer, ic imageConfiguration) (annotations.ContainerDebugConfiguration, string, error)) (annotations.ContainerDebugConfiguration, string, error) {
109108
// buildpacks/lifecycle 0.6.0 embeds the process definitions into a special image label.
110109
// The build metadata isn't absolutely required as the image args could be
111110
// a command line (e.g., `python xxx`) but it likely indicates the

pkg/skaffold/debug/cnb_test.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -381,23 +381,25 @@ func TestUpdateForCNBImage(t *testing.T) {
381381
// Test that when a transform modifies the command-line arguments, then
382382
// the changes are reflected to the launcher command-line
383383
testutil.Run(t, test.description+" (args changed)", func(t *testutil.T) {
384-
argsChangedTransform := func(c *v1.Container, ic imageConfiguration) (annotations.ContainerDebugConfiguration, string, error) {
384+
argsChangedTransform := func(c *operableContainer, ic imageConfiguration) (annotations.ContainerDebugConfiguration, string, error) {
385385
c.Args = ic.arguments
386386
return annotations.ContainerDebugConfiguration{}, "", nil
387387
}
388-
copy := v1.Container{}
389-
c, _, err := updateForCNBImage(&copy, test.input, argsChangedTransform)
390-
t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, copy)
388+
operable := operableContainer{}
389+
container := v1.Container{}
390+
c, _, err := updateForCNBImage(&operable, test.input, argsChangedTransform)
391+
applyFromOperable(&operable, &container)
392+
t.CheckErrorAndDeepEqual(test.shouldErr, err, test.expected, container)
391393
t.CheckErrorAndDeepEqual(test.shouldErr, err, test.config, c)
392394
})
393395

394396
// Test that when the arguments are left unchanged, that the container is unchanged
395397
testutil.Run(t, test.description+" (args unchanged)", func(t *testutil.T) {
396-
argsUnchangedTransform := func(c *v1.Container, ic imageConfiguration) (annotations.ContainerDebugConfiguration, string, error) {
398+
argsUnchangedTransform := func(c *operableContainer, ic imageConfiguration) (annotations.ContainerDebugConfiguration, string, error) {
397399
return annotations.ContainerDebugConfiguration{WorkingDir: ic.workingDir}, "", nil
398400
}
399401

400-
copy := v1.Container{}
402+
copy := operableContainer{}
401403
_, _, err := updateForCNBImage(&copy, test.input, argsUnchangedTransform)
402404
t.CheckError(test.shouldErr, err)
403405
if copy.Args != nil {

pkg/skaffold/debug/container.go

+290
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package debug
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug/annotations"
7+
shell "github.com/kballard/go-shellquote"
8+
"github.com/sirupsen/logrus"
9+
v1 "k8s.io/api/core/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
)
12+
13+
type operableContainer struct {
14+
Name string
15+
Command []string
16+
Args []string
17+
Env containerEnv
18+
Ports []containerPort
19+
}
20+
21+
// adapted from github.com/kubernetes/api/core/v1/types.go
22+
type containerPort struct {
23+
Name string
24+
HostPort int32
25+
ContainerPort int32
26+
Protocol string
27+
HostIP string
28+
}
29+
30+
type containerEnv struct {
31+
Order []string
32+
Env map[string]string
33+
}
34+
35+
// containerTransforms are the set of configured transformers
36+
var containerTransforms []containerTransformer
37+
38+
// containerTransformer transforms a container definition
39+
type containerTransformer interface {
40+
// IsApplicable determines if this container is suitable to be transformed.
41+
IsApplicable(config imageConfiguration) bool
42+
43+
// Apply configures a container definition for debugging, returning the debug configuration details
44+
// and required initContainer (an empty string if not required), or return a non-nil error if
45+
// the container could not be transformed. The initContainer image is intended to install any
46+
// required debug support tools.
47+
Apply(container *operableContainer, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (annotations.ContainerDebugConfiguration, string, error)
48+
}
49+
50+
// transformContainer rewrites the container definition to enable debugging.
51+
// Returns a debugging configuration description with associated language runtime support
52+
// container image, or an error if the rewrite was unsuccessful.
53+
func transformContainer(container *operableContainer, config imageConfiguration, portAlloc portAllocator) (annotations.ContainerDebugConfiguration, string, error) {
54+
// Update the image configuration's environment with those set in the k8s manifest.
55+
// (Environment variables in the k8s container's `env` add to the image configuration's `env` settings rather than replace.)
56+
for _, key := range container.Env.Order {
57+
// FIXME handle ValueFrom?
58+
if config.env == nil {
59+
config.env = make(map[string]string)
60+
}
61+
config.env[key] = container.Env.Env[key]
62+
}
63+
64+
if len(container.Command) > 0 {
65+
config.entrypoint = container.Command
66+
}
67+
if len(container.Args) > 0 {
68+
config.arguments = container.Args
69+
}
70+
71+
// Apply command-line unwrapping for buildpack images and images using `sh -c`-style command-lines
72+
next := func(container *operableContainer, config imageConfiguration) (annotations.ContainerDebugConfiguration, string, error) {
73+
return performContainerTransform(container, config, portAlloc)
74+
}
75+
if isCNBImage(config) {
76+
return updateForCNBImage(container, config, next)
77+
}
78+
return updateForShDashC(container, config, next)
79+
}
80+
81+
func rewriteContainers(metadata *metav1.ObjectMeta, podSpec *v1.PodSpec, retrieveImageConfiguration configurationRetriever, debugHelpersRegistry string) bool {
82+
// skip annotated podspecs — allows users to customize their own image
83+
if _, found := metadata.Annotations[annotations.DebugConfig]; found {
84+
return false
85+
}
86+
87+
portAlloc := func(desiredPort int32) int32 {
88+
return allocatePort(podSpec, desiredPort)
89+
}
90+
// map of containers -> debugging configuration maps; k8s ensures that a pod's containers are uniquely named
91+
configurations := make(map[string]annotations.ContainerDebugConfiguration)
92+
// the container images that require debugging support files
93+
var containersRequiringSupport []*v1.Container
94+
// the set of image IDs required to provide debugging support files
95+
requiredSupportImages := make(map[string]bool)
96+
for i := range podSpec.Containers {
97+
container := podSpec.Containers[i] // make a copy and only apply changes on successful transform
98+
99+
// the usual retriever returns an error for non-build artifacts
100+
imageConfig, err := retrieveImageConfiguration(container.Image)
101+
if err != nil {
102+
continue
103+
}
104+
operable := operableContainerFromK8sContainer(&container)
105+
// requiredImage, if not empty, is the image ID providing the debugging support files
106+
// `err != nil` means that the container did not or could not be transformed
107+
if configuration, requiredImage, err := transformContainer(operable, imageConfig, portAlloc); err == nil {
108+
configuration.Artifact = imageConfig.artifact
109+
if configuration.WorkingDir == "" {
110+
configuration.WorkingDir = imageConfig.workingDir
111+
}
112+
configurations[container.Name] = configuration
113+
applyFromOperable(operable, &container)
114+
podSpec.Containers[i] = container // apply any configuration changes
115+
if len(requiredImage) > 0 {
116+
logrus.Infof("%q requires debugging support image %q", container.Name, requiredImage)
117+
containersRequiringSupport = append(containersRequiringSupport, &podSpec.Containers[i])
118+
requiredSupportImages[requiredImage] = true
119+
}
120+
} else {
121+
logrus.Warnf("Image %q not configured for debugging: %v", container.Name, err)
122+
}
123+
}
124+
125+
// check if we have any images requiring additional debugging support files
126+
if len(containersRequiringSupport) > 0 {
127+
logrus.Infof("Configuring installation of debugging support files")
128+
// we create the volume that will hold the debugging support files
129+
supportVolume := v1.Volume{Name: debuggingSupportFilesVolume, VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}}
130+
podSpec.Volumes = append(podSpec.Volumes, supportVolume)
131+
132+
// this volume is mounted in the containers at `/dbg`
133+
supportVolumeMount := v1.VolumeMount{Name: debuggingSupportFilesVolume, MountPath: "/dbg"}
134+
// the initContainers are responsible for populating the contents of `/dbg`
135+
for imageID := range requiredSupportImages {
136+
supportFilesInitContainer := v1.Container{
137+
Name: fmt.Sprintf("install-%s-debug-support", imageID),
138+
Image: fmt.Sprintf("%s/%s", debugHelpersRegistry, imageID),
139+
VolumeMounts: []v1.VolumeMount{supportVolumeMount},
140+
}
141+
podSpec.InitContainers = append(podSpec.InitContainers, supportFilesInitContainer)
142+
}
143+
// the populated volume is then mounted in the containers at `/dbg` too
144+
for _, container := range containersRequiringSupport {
145+
container.VolumeMounts = append(container.VolumeMounts, supportVolumeMount)
146+
}
147+
}
148+
149+
if len(configurations) > 0 {
150+
if metadata.Annotations == nil {
151+
metadata.Annotations = make(map[string]string)
152+
}
153+
metadata.Annotations[annotations.DebugConfig] = encodeConfigurations(configurations)
154+
return true
155+
}
156+
return false
157+
}
158+
159+
func updateForShDashC(container *operableContainer, ic imageConfiguration, transformer func(*operableContainer, imageConfiguration) (annotations.ContainerDebugConfiguration, string, error)) (annotations.ContainerDebugConfiguration, string, error) {
160+
var rewriter func([]string)
161+
copy := ic
162+
switch {
163+
// Case 1: entrypoint = ["/bin/sh", "-c"], arguments = ["<cmd-line>", args ...]
164+
case len(ic.entrypoint) == 2 && len(ic.arguments) > 0 && isShDashC(ic.entrypoint[0], ic.entrypoint[1]):
165+
if split, err := shell.Split(ic.arguments[0]); err == nil {
166+
copy.entrypoint = split
167+
copy.arguments = nil
168+
rewriter = func(rewrite []string) {
169+
container.Command = nil // inherit from container
170+
container.Args = append([]string{shJoin(rewrite)}, ic.arguments[1:]...)
171+
}
172+
}
173+
174+
// Case 2: entrypoint = ["/bin/sh", "-c", "<cmd-line>", args...], arguments = [args ...]
175+
case len(ic.entrypoint) > 2 && isShDashC(ic.entrypoint[0], ic.entrypoint[1]):
176+
if split, err := shell.Split(ic.entrypoint[2]); err == nil {
177+
copy.entrypoint = split
178+
copy.arguments = nil
179+
rewriter = func(rewrite []string) {
180+
container.Command = append([]string{ic.entrypoint[0], ic.entrypoint[1], shJoin(rewrite)}, ic.entrypoint[3:]...)
181+
}
182+
}
183+
184+
// Case 3: entrypoint = [] or an entrypoint launcher (and so ignored), arguments = ["/bin/sh", "-c", "<cmd-line>", args...]
185+
case (len(ic.entrypoint) == 0 || isEntrypointLauncher(ic.entrypoint)) && len(ic.arguments) > 2 && isShDashC(ic.arguments[0], ic.arguments[1]):
186+
if split, err := shell.Split(ic.arguments[2]); err == nil {
187+
copy.entrypoint = split
188+
copy.arguments = nil
189+
rewriter = func(rewrite []string) {
190+
container.Command = nil
191+
container.Args = append([]string{ic.arguments[0], ic.arguments[1], shJoin(rewrite)}, ic.arguments[3:]...)
192+
}
193+
}
194+
}
195+
196+
c, image, err := transformer(container, copy)
197+
if err == nil && rewriter != nil && container.Command != nil {
198+
rewriter(container.Command)
199+
}
200+
return c, image, err
201+
}
202+
203+
func isShDashC(cmd, arg string) bool {
204+
return (cmd == "/bin/sh" || cmd == "/bin/bash") && arg == "-c"
205+
}
206+
207+
func performContainerTransform(container *operableContainer, config imageConfiguration, portAlloc portAllocator) (annotations.ContainerDebugConfiguration, string, error) {
208+
logrus.Tracef("Examining container %q with config %v", container.Name, config)
209+
for _, transform := range containerTransforms {
210+
if transform.IsApplicable(config) {
211+
return transform.Apply(container, config, portAlloc, Protocols)
212+
}
213+
}
214+
return annotations.ContainerDebugConfiguration{}, "", fmt.Errorf("unable to determine runtime for %q", container.Name)
215+
}
216+
217+
// operableContainerFromK8sContainer creates an instance of an operableContainer
218+
// from a v1.Container reference. This object will be passed around to accept
219+
// transforms, and will eventually overwrite fields from the creating v1.Container
220+
// in the manifest-under-transformation's pod spec.
221+
func operableContainerFromK8sContainer(c *v1.Container) *operableContainer {
222+
return &operableContainer{
223+
Command: c.Command,
224+
Args: c.Args,
225+
Env: k8sEnvToContainerEnv(c.Env),
226+
Ports: k8sPortsToContainerPorts(c.Ports),
227+
}
228+
}
229+
230+
func k8sEnvToContainerEnv(k8sEnv []v1.EnvVar) containerEnv {
231+
// TODO(nkubala): ValueFrom is ignored. Do we care?
232+
env := make(map[string]string, len(k8sEnv))
233+
var order []string
234+
for _, entry := range k8sEnv {
235+
order = append(order, entry.Name)
236+
env[entry.Name] = entry.Value
237+
}
238+
return containerEnv{
239+
Order: order,
240+
Env: env,
241+
}
242+
}
243+
244+
func containerEnvToK8sEnv(env containerEnv) []v1.EnvVar {
245+
var k8sEnv []v1.EnvVar
246+
for _, k := range env.Order {
247+
k8sEnv = append(k8sEnv, v1.EnvVar{
248+
Name: k,
249+
Value: env.Env[k],
250+
})
251+
}
252+
return k8sEnv
253+
}
254+
255+
func k8sPortsToContainerPorts(k8sPorts []v1.ContainerPort) []containerPort {
256+
var containerPorts []containerPort
257+
for _, port := range k8sPorts {
258+
containerPorts = append(containerPorts, containerPort{
259+
Name: port.Name,
260+
HostPort: port.HostPort,
261+
ContainerPort: port.ContainerPort,
262+
Protocol: string(port.Protocol),
263+
HostIP: port.HostIP,
264+
})
265+
}
266+
return containerPorts
267+
}
268+
269+
func containerPortsToK8sPorts(containerPorts []containerPort) []v1.ContainerPort {
270+
var k8sPorts []v1.ContainerPort
271+
for _, port := range containerPorts {
272+
k8sPorts = append(k8sPorts, v1.ContainerPort{
273+
Name: port.Name,
274+
HostPort: port.HostPort,
275+
ContainerPort: port.ContainerPort,
276+
Protocol: v1.Protocol(port.Protocol),
277+
HostIP: port.HostIP,
278+
})
279+
}
280+
return k8sPorts
281+
}
282+
283+
// applyFromOperable takes the relevant fields from the operable container
284+
// and applies them to the referenced v1.Container from the manifest's pod spec
285+
func applyFromOperable(o *operableContainer, c *v1.Container) {
286+
c.Args = o.Args
287+
c.Command = o.Command
288+
c.Env = containerEnvToK8sEnv(o.Env)
289+
c.Ports = containerPortsToK8sPorts(o.Ports)
290+
}

pkg/skaffold/debug/debug_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,12 @@ func (t testTransformer) IsApplicable(config imageConfiguration) bool {
113113
return true
114114
}
115115

116-
func (t testTransformer) Apply(container *v1.Container, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (annotations.ContainerDebugConfiguration, string, error) {
116+
func (t testTransformer) Apply(container *operableContainer, config imageConfiguration, portAlloc portAllocator, overrideProtocols []string) (annotations.ContainerDebugConfiguration, string, error) {
117117
port := portAlloc(9999)
118-
container.Ports = append(container.Ports, v1.ContainerPort{Name: "test", ContainerPort: port})
118+
container.Ports = append(container.Ports, containerPort{Name: "test", ContainerPort: port})
119119

120-
testEnv := v1.EnvVar{Name: "KEY", Value: "value"}
121-
container.Env = append(container.Env, testEnv)
120+
testEnv := containerEnv{Order: []string{"KEY"}, Env: map[string]string{"KEY": "value"}}
121+
container.Env = testEnv
122122

123123
return annotations.ContainerDebugConfiguration{Runtime: "test"}, "", nil
124124
}

0 commit comments

Comments
 (0)