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

Move Debugger from Runner into Deployer #6021

Merged
merged 3 commits into from
Jun 17, 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
4 changes: 2 additions & 2 deletions integration/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"time"

"github.com/GoogleContainerTools/skaffold/integration/skaffold"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug"
debugannotations "github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug/annotations"
"github.com/GoogleContainerTools/skaffold/proto/v1"
"github.com/GoogleContainerTools/skaffold/testutil"
)
Expand Down Expand Up @@ -77,7 +77,7 @@ func TestDebug(t *testing.T) {
skaffold.Debug(test.args...).InDir(test.dir).InNs(ns.Name).RunBackground(t)

verifyDebugAnnotations := func(annotations map[string]string) {
var configs map[string]debug.ContainerDebugConfiguration
var configs map[string]debugannotations.ContainerDebugConfiguration
if anno, found := annotations["debug.cloud.google.com/config"]; !found {
t.Errorf("deployment missing debug annotation: %v", annotations)
} else if err := json.Unmarshal([]byte(anno), &configs); err != nil {
Expand Down
40 changes: 40 additions & 0 deletions pkg/skaffold/debug/annotations/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright 2021 The Skaffold 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 annotations

// ContainerDebugConfiguration captures debugging information for a specific container.
// This structure is serialized out and included in the pod metadata.
type ContainerDebugConfiguration struct {
// Artifact is the corresponding artifact's image name used in the skaffold.yaml
Artifact string `json:"artifact,omitempty"`
// Runtime represents the underlying language runtime (`go`, `jvm`, `nodejs`, `python`, `netcore`)
Runtime string `json:"runtime,omitempty"`
// WorkingDir is the working directory in the image configuration; may be empty
WorkingDir string `json:"workingDir,omitempty"`
// Ports is the list of debugging ports, keyed by protocol type
Ports map[string]uint32 `json:"ports,omitempty"`
}

const (
// DebugConfig is the name of the podspec annotation that records debugging configuration information.
// The annotation should be a JSON-encoded map of container-name to a `ContainerDebugConfiguration` object.
DebugConfig = "debug.cloud.google.com/config"

// DebugProbesAnnotation is the name of the podspec annotation that disables rewriting of probe timeouts.
// The annotation value should be `skip`.
DebugProbeTimeouts = "debug.cloud.google.com/probe/timeouts"
)
161 changes: 161 additions & 0 deletions pkg/skaffold/debug/apply_transforms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
Copyright 2019 The Skaffold 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 debug

import (
"bufio"
"bytes"
"context"
"fmt"
"strings"

"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/client-go/kubernetes/scheme"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/manifest"
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/runner/runcontext"
)

var (
decodeFromYaml = scheme.Codecs.UniversalDeserializer().Decode
encodeAsYaml = func(o runtime.Object) ([]byte, error) {
s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
var b bytes.Buffer
w := bufio.NewWriter(&b)
if err := s.Encode(o, w); err != nil {
return nil, err
}
w.Flush()
return b.Bytes(), nil
}
)

// ApplyDebuggingTransforms applies language-platform-specific transforms to a list of manifests.
func ApplyDebuggingTransforms(l manifest.ManifestList, builds []graph.Artifact, registries manifest.Registries) (manifest.ManifestList, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

retriever := func(image string) (imageConfiguration, error) {
if artifact := findArtifact(image, builds); artifact != nil {
return retrieveImageConfiguration(ctx, artifact, registries.InsecureRegistries)
}
return imageConfiguration{}, fmt.Errorf("no build artifact for %q", image)
}
return applyDebuggingTransforms(l, retriever, registries.DebugHelpersRegistry)
}

func applyDebuggingTransforms(l manifest.ManifestList, retriever configurationRetriever, debugHelpersRegistry string) (manifest.ManifestList, error) {
var updated manifest.ManifestList
for _, manifest := range l {
obj, _, err := decodeFromYaml(manifest, nil, nil)
if err != nil {
logrus.Debugf("Unable to interpret manifest for debugging: %v\n", err)
} else if transformManifest(obj, retriever, debugHelpersRegistry) {
manifest, err = encodeAsYaml(obj)
if err != nil {
return nil, fmt.Errorf("marshalling yaml: %w", err)
}
if logrus.IsLevelEnabled(logrus.DebugLevel) {
logrus.Debugln("Applied debugging transform:\n", string(manifest))
}
}
updated = append(updated, manifest)
}

return updated, nil
}

// findArtifact finds the corresponding artifact for the given image.
// If `builds` is empty, then treat all `image` images as a build artifact.
func findArtifact(image string, builds []graph.Artifact) *graph.Artifact {
if len(builds) == 0 {
logrus.Debugf("No build artifacts specified: using image as-is %q", image)
return &graph.Artifact{ImageName: image, Tag: image}
}
for _, artifact := range builds {
if image == artifact.ImageName || image == artifact.Tag {
Copy link
Contributor

Choose a reason for hiding this comment

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

if image == artifact.ImageName || image == artifact.Tag 

are we intentionally matching against both? If we're expecting to always match by only artifactName or only tag maybe we could be specific, or else call it out in the function name/description?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't write this, just moved it :)

I assume it's intentional, maybe this can happen either before or after we compute the tag? @briandealwis would know more. I think we probably shouldn't touch this in this PR though

logrus.Debugf("Found artifact for image %q", image)
return &artifact
}
}
return nil
}

// retrieveImageConfiguration retrieves the image container configuration for
// the given build artifact
func retrieveImageConfiguration(ctx context.Context, artifact *graph.Artifact, insecureRegistries map[string]bool) (imageConfiguration, error) {
// TODO: use the proper RunContext
apiClient, err := docker.NewAPIClient(&runcontext.RunContext{
InsecureRegistries: insecureRegistries,
})
if err != nil {
return imageConfiguration{}, fmt.Errorf("could not connect to local docker daemon: %w", err)
}

// the apiClient will go to the remote registry if local docker daemon is not available
manifest, err := apiClient.ConfigFile(ctx, artifact.Tag)
if err != nil {
logrus.Debugf("Error retrieving image manifest for %v: %v", artifact.Tag, err)
return imageConfiguration{}, fmt.Errorf("retrieving image config for %q: %w", artifact.Tag, err)
}

config := manifest.Config
logrus.Debugf("Retrieved local image configuration for %v: %v", artifact.Tag, config)
// need to duplicate slices as apiClient caches requests
return imageConfiguration{
artifact: artifact.ImageName,
env: envAsMap(config.Env),
entrypoint: dupArray(config.Entrypoint),
arguments: dupArray(config.Cmd),
labels: dupMap(config.Labels),
workingDir: config.WorkingDir,
}, nil
}

// envAsMap turns an array of environment "NAME=value" strings into a map
func envAsMap(env []string) map[string]string {
result := make(map[string]string)
for _, pair := range env {
s := strings.SplitN(pair, "=", 2)
result[s[0]] = s[1]
}
return result
}

func dupArray(s []string) []string {
if len(s) == 0 {
return nil
}
dup := make([]string, len(s))
copy(dup, s)
return dup
}

func dupMap(s map[string]string) map[string]string {
if len(s) == 0 {
return nil
}
dup := make(map[string]string, len(s))
for k, v := range s {
dup[k] = v
}
return dup
}
12 changes: 7 additions & 5 deletions pkg/skaffold/debug/cnb.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
shell "github.com/kballard/go-shellquote"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"

"github.com/GoogleContainerTools/skaffold/pkg/skaffold/debug/annotations"
)

const (
Expand Down Expand Up @@ -102,22 +104,22 @@ func hasCNBLauncherEntrypoint(ic imageConfiguration) bool {
// found in `/cnb/process/`, and the image entrypoint is set to the corresponding executable for
// the default process type. `CNB_PROCESS_TYPE` is ignored in this situation. A different process
// can be used by overriding the image entrypoint. Direct and script launches are supported by
// setting the entrypoint to `/cnb/lifecycle/launcher` and providing the appropriate argments.
func updateForCNBImage(container *v1.Container, ic imageConfiguration, transformer func(container *v1.Container, ic imageConfiguration) (ContainerDebugConfiguration, string, error)) (ContainerDebugConfiguration, string, error) {
// setting the entrypoint to `/cnb/lifecycle/launcher` and providing the appropriate arguments.
func updateForCNBImage(container *v1.Container, ic imageConfiguration, transformer func(container *v1.Container, ic imageConfiguration) (annotations.ContainerDebugConfiguration, string, error)) (annotations.ContainerDebugConfiguration, string, error) {
// buildpacks/lifecycle 0.6.0 embeds the process definitions into a special image label.
// The build metadata isn't absolutely required as the image args could be
// a command line (e.g., `python xxx`) but it likely indicates the
// image was built with an older lifecycle.
metadataJSON, found := ic.labels["io.buildpacks.build.metadata"]
if !found {
return ContainerDebugConfiguration{}, "", fmt.Errorf("image is missing buildpacks metadata; perhaps built with older lifecycle?")
return annotations.ContainerDebugConfiguration{}, "", fmt.Errorf("image is missing buildpacks metadata; perhaps built with older lifecycle?")
}
m := cnb.BuildMetadata{}
if err := json.Unmarshal([]byte(metadataJSON), &m); err != nil {
return ContainerDebugConfiguration{}, "", fmt.Errorf("unable to parse image buildpacks metadata")
return annotations.ContainerDebugConfiguration{}, "", fmt.Errorf("unable to parse image buildpacks metadata")
}
if len(m.Processes) == 0 {
return ContainerDebugConfiguration{}, "", fmt.Errorf("buildpacks metadata has no processes")
return annotations.ContainerDebugConfiguration{}, "", fmt.Errorf("buildpacks metadata has no processes")
}

needsCnbLauncher := ic.entrypoint[0] != cnbLauncher
Expand Down
Loading