Skip to content

Commit

Permalink
Implement --skip-prefixes parameter
Browse files Browse the repository at this point in the history
* Allows specific image prefixes to be skipped from digest resolution
* Implemented for webhook and kpt
* kpt support also includes environment variable SKIP_PREFIXES bound to the same parameter
* refactors kubeconfig parsing of multi value strings into util.StringArray for reuse
* common-issues.md updated describing the use case
  • Loading branch information
iamasmith authored and halvards committed Nov 22, 2023
1 parent 64c4f4b commit 400181d
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 21 deletions.
25 changes: 25 additions & 0 deletions build/examples/pod-diff-registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2021 Google LLC
#
# 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.

apiVersion: v1
kind: Pod
metadata:
name: hello-pod
spec:
containers:
- name: busybox
image: busybox:latest
initContainers:
- name: init
image: gcr.io/google-samples/hello-app:1.0
14 changes: 8 additions & 6 deletions cmd/function/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/go-logr/logr"
"github.com/spf13/cobra"
Expand All @@ -33,6 +32,7 @@ import (

"github.com/google/k8s-digester/pkg/logging"
"github.com/google/k8s-digester/pkg/resolve"
"github.com/google/k8s-digester/pkg/util"
)

// Cmd creates the KRM function command. This is the root command.
Expand All @@ -50,24 +50,23 @@ func createResourceFn(ctx context.Context, log logr.Logger) framework.ResourceLi
return func(resourceList *framework.ResourceList) error {
log.V(2).Info("kubeconfig", "kubeconfig", viper.GetString("kubeconfig"))
log.V(2).Info("offline", "offline", viper.GetBool("offline"))
log.V(2).Info("skip-prefixes", "skip-prefixes", util.StringArray(viper.GetString("skip-prefixes")))
var config *rest.Config
if !viper.GetBool("offline") {
var kubeconfig string
var err error
kubeconfigs := strings.FieldsFunc(viper.GetString("kubeconfig"), func(r rune) bool {
return r == ':' || r == ';'
})
kubeconfigs := util.StringArray(viper.GetString("kubeconfig"))
if len(kubeconfigs) > 0 {
kubeconfig = kubeconfigs[0]
}

config, err = createConfig(log, kubeconfig)
if err != nil {
return fmt.Errorf("could not create k8s client config: %w", err)
}
}
for _, r := range resourceList.Items {
if err := resolve.ImageTags(ctx, log, config, r); err != nil {
if err := resolve.ImageTags(ctx, log, config, r, util.StringArray(viper.GetString("skip-prefixes"))); err != nil {
return err
}
}
Expand All @@ -92,6 +91,9 @@ func customizeCmd(cmd *cobra.Command) {
"do not connect to Kubernetes API server to retrieve imagePullSecrets")
viper.BindPFlag("offline", cmd.Flags().Lookup("offline"))
viper.BindEnv("offline")
cmd.Flags().String("skip-prefixes", "", "(optional) image prefixes that should not be resolved to digests, colon separated")
viper.BindPFlag("skip-prefixes", cmd.Flags().Lookup("skip-prefixes"))
viper.BindEnv("skip-prefixes", "SKIP_PREFIXES")
}

// getKubeconfigDefault determines the default value of the --kubeconfig flag.
Expand Down
7 changes: 5 additions & 2 deletions cmd/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ var (
offline bool
port int
ignoreErrors bool
skipPrefixes string
)

func init() {
Expand All @@ -90,6 +91,7 @@ func init() {
Cmd.Flags().BoolVar(&offline, "offline", false, "do not connect to API server to retrieve imagePullSecrets")
Cmd.Flags().IntVar(&port, "port", defaultPort, "webhook server port")
Cmd.Flags().BoolVar(&ignoreErrors, "ignore-errors", false, "do not fail on webhook admission errors, just log them")
Cmd.Flags().StringVar(&skipPrefixes, "skip-prefixes", "", "(optional) image prefixes that should not be resolved to digests, colon separated")
}

func run(ctx context.Context) error {
Expand Down Expand Up @@ -146,7 +148,7 @@ func run(ctx context.Context) error {
close(certSetupFinished)
}

go setupControllers(mgr, log, dryRun, ignoreErrors, certSetupFinished)
go setupControllers(mgr, log, dryRun, ignoreErrors, certSetupFinished, util.StringArray(skipPrefixes))

log.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
Expand All @@ -155,7 +157,7 @@ func run(ctx context.Context) error {
return nil
}

func setupControllers(mgr manager.Manager, log logr.Logger, dryRun bool, ignoreErrors bool, certSetupFinished chan struct{}) {
func setupControllers(mgr manager.Manager, log logr.Logger, dryRun bool, ignoreErrors bool, certSetupFinished chan struct{}, skipPrefixes []string) {
log.Info("waiting for cert rotation setup")
<-certSetupFinished
log.Info("done waiting for cert rotation setup")
Expand All @@ -168,6 +170,7 @@ func setupControllers(mgr manager.Manager, log logr.Logger, dryRun bool, ignoreE
DryRun: dryRun,
IgnoreErrors: ignoreErrors,
Config: config,
SkipPrefixes: skipPrefixes,
}
mwh := &admission.Webhook{Handler: whh}
log.Info("starting webhook server", "path", webhookPath)
Expand Down
37 changes: 37 additions & 0 deletions docs/common-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,40 @@ The steps below use the `HTTPS_PROXY` environment variable.
Note that this will not work for proxies that require NTLM authentication.

Ref: https://knative.dev/docs/serving/tag-resolution/#corporate-proxy

## Interaction with systems expecting tags, particularly cloud managed services

If digester updates an image tag that is being actively managed by a cloud controller then
it may cause the cloud controller to behave unexpectedly.

One example of this is the Anthos Service Mesh Managed Dataplane Controller which looks
for specific tagged versions of the istio-proxy sidecar injected by the mutating webhook.

Replacement of the tagged names with digest values can, under these circumstances, create
an edge case for the cloud managed services handling unepected values in unforseen ways such
as updating pods and terminating them once they have already been updated (since the image
does not match the value set by the controller with only the tag).

In these circumstances and if you are using digester to provide a tag feature when using
Binary Authorization it is worth noting that there is a capability to whitelist certain
image registries and repo locations within Binary Authorization. ASM images are by default
whitelisted by the policy.

To avoid digester replacing the tagged version expected by mdp-controller in these instances
one can utilise the --skip-prefixes option to the webhook which takes a set of prefixes
separated by a colon (if multiple prefixes are needed).

The parameter can be added to the webhook args in the deployment, the following is an
example
```
args:
- webhook
- --cert-dir=/certs # kpt-set: --cert-dir=${cert-dir}
- --disable-cert-rotation=false # kpt-set: --disable-cert-rotation=${disable-cert-rotation}
- --dry-run=false # kpt-set: --dry-run=${dry-run}
- --health-addr=:9090 # kpt-set: --health-addr=:${health-port}
- --metrics-addr=:8888 # kpt-set: --metrics-addr=:${metrics-port}
- --offline=false # kpt-set: --offline=${offline}
- --port=8443 # kpt-set: --port=${port}
- --skip-prefixes=gcr.io/gke-release/asm/mdp:gcr.io/gke-release/asm/proxyv2
```
3 changes: 2 additions & 1 deletion pkg/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Handler struct {
DryRun bool
IgnoreErrors bool
Config *rest.Config
SkipPrefixes []string
}

var resolveImageTags = resolve.ImageTags // override for testing
Expand Down Expand Up @@ -74,7 +75,7 @@ func (h *Handler) Handle(ctx context.Context, req admission.Request) admission.R
return h.admissionError(err)
}

if err = resolveImageTags(ctx, h.Log, h.Config, r); err != nil {
if err = resolveImageTags(ctx, h.Log, h.Config, r, h.SkipPrefixes); err != nil {
return h.admissionError(err)
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func Test_Handle_NotPatchedWhenNoChange(t *testing.T) {
},
},
}
resolveImageTags = func(_ context.Context, _ logr.Logger, _ *rest.Config, _ *yaml.RNode) error {
resolveImageTags = func(_ context.Context, _ logr.Logger, _ *rest.Config, _ *yaml.RNode, _ []string) error {
return nil
}
h := &Handler{Log: log}
Expand All @@ -146,7 +146,7 @@ func Test_Handle_Patch(t *testing.T) {
},
}
imageWithDigest := "registry.example.com/repository/image:tag@sha256:digest"
resolveImageTags = func(_ context.Context, _ logr.Logger, _ *rest.Config, n *yaml.RNode) error {
resolveImageTags = func(_ context.Context, _ logr.Logger, _ *rest.Config, n *yaml.RNode, _ []string) error {
return n.PipeE(yaml.Lookup("spec", "containers", "0", "image"), yaml.FieldSetter{StringValue: imageWithDigest})
}
h := &Handler{Log: log}
Expand Down
18 changes: 13 additions & 5 deletions pkg/resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,15 @@ var resolveTagFn = resolveTag // override for unit testing
// - `spec.jobTemplate.spec.template.spec.initContainers`
// The `config` input parameter can be null. In this case, the function
// will not attempt to retrieve imagePullSecrets from the cluster.
func ImageTags(ctx context.Context, log logr.Logger, config *rest.Config, n *yaml.RNode) error {
func ImageTags(ctx context.Context, log logr.Logger, config *rest.Config, n *yaml.RNode, skipPrefixes []string) error {
kc, err := keychain.Create(ctx, log, config, n)
if err != nil {
return fmt.Errorf("could not create keychain: %w", err)
}
imageTagFilter := &ImageTagFilter{
Log: log,
Keychain: kc,
Log: log,
Keychain: kc,
SkipPrefixes: &skipPrefixes,
}
// if input is a CronJob, we need to look up the image tags in the
// `spec.jobTemplate.spec.template.spec` path as well
Expand All @@ -76,8 +77,9 @@ func ImageTags(ctx context.Context, log logr.Logger, config *rest.Config, n *yam

// ImageTagFilter resolves image tags to digests
type ImageTagFilter struct {
Log logr.Logger
Keychain authn.Keychain
Log logr.Logger
Keychain authn.Keychain
SkipPrefixes *[]string
}

var _ yaml.Filter = &ImageTagFilter{}
Expand All @@ -97,6 +99,12 @@ func (f *ImageTagFilter) filterImage(n *yaml.RNode) error {
return fmt.Errorf("could not lookup image in node %v: %w", s, err)
}
image := yaml.GetValue(imageNode)
for _, prefix := range *f.SkipPrefixes {
if strings.HasPrefix(image, prefix) {
// Image should be excluded from digest resolution
return nil
}
}
if strings.Contains(image, "@") {
return nil // already has digest, skip
}
Expand Down
28 changes: 23 additions & 5 deletions pkg/resolve/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ var (
ctx = context.Background()
log = logging.CreateDiscardLogger()
filter = &ImageTagFilter{
Log: log,
Keychain: &anonymousKeychain{},
Log: log,
Keychain: &anonymousKeychain{},
SkipPrefixes: &[]string{},
}
)

Expand Down Expand Up @@ -100,7 +101,7 @@ func Test_ImageTags_Pod(t *testing.T) {
t.Fatalf("could not create pod node: %v", err)
}

if err := ImageTags(ctx, log, nil, node); err != nil {
if err := ImageTags(ctx, log, nil, node, []string{}); err != nil {
t.Fatalf("problem resolving image tags: %v", err)
}
t.Log(node.MustString())
Expand All @@ -111,13 +112,30 @@ func Test_ImageTags_Pod(t *testing.T) {
assertContainer(t, node, "image3@sha256:b0542da3f90bad69318e16ec7fcb6b13b089971886999e08bec91cea34891f0f", "spec", "initContainers", "[name=initcontainer1]")
}

func Test_ImageTags_Pod_Skip_Prefixes(t *testing.T) {
node, err := createPodNode([]string{"image0", "skip1.local/image1"}, []string{"image2", "skip2.local/image3"})
if err != nil {
t.Fatalf("could not create pod node: %v", err)
}

if err := ImageTags(ctx, log, nil, node, []string{"skip1.local", "skip2.local"}); err != nil {
t.Fatalf("problem resolving image tags: %v", err)
}
t.Log(node.MustString())

assertContainer(t, node, "image0@sha256:07d7d43fe9dd151e40f0a8d54c5211a8601b04e4a8fa7ad57ea5e73e4ffa7e4a", "spec", "containers", "[name=container0]")
assertContainer(t, node, "skip1.local/image1", "spec", "containers", "[name=container1]")
assertContainer(t, node, "image2@sha256:5bb21ac469b5e7df4e17899d4aae0adfb430f0f0b336a2242ef1a22d25bd2e53", "spec", "initContainers", "[name=initcontainer0]")
assertContainer(t, node, "skip2.local/image3", "spec", "initContainers", "[name=initcontainer1]")
}

func Test_ImageTags_CronJob(t *testing.T) {
node, err := createCronJobNode([]string{"image0", "image1"}, []string{"image2", "image3"})
if err != nil {
t.Fatalf("could not create pod node: %v", err)
}

if err := ImageTags(ctx, log, nil, node); err != nil {
if err := ImageTags(ctx, log, nil, node, []string{}); err != nil {
t.Fatalf("problem resolving image tags: %v", err)
}
t.Log(node.MustString())
Expand All @@ -134,7 +152,7 @@ func Test_ImageTags_Deployment(t *testing.T) {
t.Fatalf("could not create deployment node: %v", err)
}

if err := ImageTags(ctx, log, nil, node); err != nil {
if err := ImageTags(ctx, log, nil, node, []string{}); err != nil {
t.Fatalf("problem resolving image tags: %v", err)
}
t.Log(node.MustString())
Expand Down
11 changes: 11 additions & 0 deletions pkg/util/stringarray.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package util

import (
"strings"
)

func StringArray(str string) []string {
return strings.FieldsFunc(str, func(r rune) bool {
return r == ':' || r == ';'
})
}

0 comments on commit 400181d

Please sign in to comment.