Repository: fluxcd/flux
Pull request #2850 (2020-02-13 17:30:05)

Let people explicitly include images to scan
Currently there's an option to exclude specific images, which has a
handy default of excluding the Kubernetes components' images. But if
you only ever care about your specific images, it's probably going to
be easier to supply a list of images to _include_.

This commit adds the flag `--registry-include-image`, and a tiny
abstraction so that the exclude/include logic can be applied to image
scanning.


Patch:
diff --git a/cmd/fluxd/main.go b/cmd/fluxd/main.go
index 00d6317b..c2616c98 100644
--- a/cmd/fluxd/main.go
+++ b/cmd/fluxd/main.go
@@ -159,6 +159,7 @@ func main() {
 		registryTrace           = fs.Bool("registry-trace", false, "output trace of image registry requests to log")
 		registryInsecure        = fs.StringSlice("registry-insecure-host", []string{}, "let these registry hosts skip TLS host verification and fall back to using HTTP instead of HTTPS; this allows man-in-the-middle attacks, so use with extreme caution")
 		registryExcludeImage    = fs.StringSlice("registry-exclude-image", []string{"k8s.gcr.io/*"}, "do not scan images that match these glob expressions; the default is to exclude the 'k8s.gcr.io/*' images")
+		registryIncludeImage    = fs.StringSlice("registry-include-image", nil, "if a value or values is given, scan _only_ images matching the glob pattern(s) (less any explicitly excluded)")
 		registryUseLabels       = fs.StringSlice("registry-use-labels", []string{"index.docker.io/weaveworks/*", "index.docker.io/fluxcd/*"}, "use the timestamp (RFC3339) from labels for (canonical) image refs that match these glob expression")
 
 		// AWS authentication
@@ -504,7 +505,9 @@ func main() {
 		for _, n := range append(*k8sNamespaceWhitelist, *k8sAllowNamespace...) {
 			allowedNamespaces[n] = struct{}{}
 		}
-		k8sInst := kubernetes.NewCluster(client, kubectlApplier, sshKeyRing, logger, allowedNamespaces, *registryExcludeImage, *k8sExcludeResource)
+
+		imageIncluder := cluster.ExcludeIncludeGlob{Exclude: *registryExcludeImage, Include: *registryIncludeImage}
+		k8sInst := kubernetes.NewCluster(client, kubectlApplier, sshKeyRing, logger, allowedNamespaces, imageIncluder, *k8sExcludeResource)
 		k8sInst.GC = *syncGC
 		k8sInst.DryGC = *dryGC
 
diff --git a/pkg/cluster/includelist.go b/pkg/cluster/includelist.go
new file mode 100644
index 00000000..be40a06d
--- /dev/null
+++ b/pkg/cluster/includelist.go
@@ -0,0 +1,50 @@
+package cluster
+
+import (
+	"github.com/ryanuber/go-glob"
+)
+
+// This is to represent "include-exclude" predicate, which is used
+// for deciding images to scan.
+
+type Includer interface {
+	IsIncluded(string) bool
+}
+
+type IncluderFunc func(string) bool
+
+func (f IncluderFunc) IsIncluded(s string) bool {
+	return f(s)
+}
+
+var AlwaysInclude = IncluderFunc(func(string) bool { return true })
+
+// ExcludeIncludeGlob is an Includer that uses glob patterns to decide
+// what to include or exclude. Note that Include and Exclude are
+// treated differently -- see the method IsIncluded.
+type ExcludeIncludeGlob struct {
+	Include []string
+	Exclude []string
+}
+
+// IsIncluded implements Includer using the logic:
+//  - if the string matches any exclude pattern, don't include it
+//  - otherwise, if there are no include patterns, include it
+//  - otherwise, if it matches an include pattern, include it
+//  = otherwise don't include it.
+func (ei ExcludeIncludeGlob) IsIncluded(s string) bool {
+	for _, ex := range ei.Exclude {
+		if glob.Glob(ex, s) {
+			return false
+		}
+	}
+	if len(ei.Include) == 0 {
+		return true
+	}
+	for _, in := range ei.Include {
+		if glob.Glob(in, s) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/cluster/kubernetes/images.go b/pkg/cluster/kubernetes/images.go
index 60eb4229..4dc2f3c4 100644
--- a/pkg/cluster/kubernetes/images.go
+++ b/pkg/cluster/kubernetes/images.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/go-kit/kit/log"
 	"github.com/pkg/errors"
-	"github.com/ryanuber/go-glob"
 	apiv1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -147,7 +146,7 @@ func (c *Cluster) ImagesToFetch() registry.ImageCreds {
 			imageCreds := make(registry.ImageCreds)
 			for _, workload := range workloads {
 				logger := log.With(c.logger, "resource", resource.MakeID(workload.GetNamespace(), kind, workload.GetName()))
-				mergeCredentials(logger.Log, c.includeImage, c.client, workload.GetNamespace(), workload.podTemplate, imageCreds, imagePullSecretCache)
+				mergeCredentials(logger.Log, c.imageIncluder.IsIncluded, c.client, workload.GetNamespace(), workload.podTemplate, imageCreds, imagePullSecretCache)
 			}
 
 			// Merge creds
@@ -167,12 +166,3 @@ func (c *Cluster) ImagesToFetch() registry.ImageCreds {
 
 	return allImageCreds
 }
-
-func (c *Cluster) includeImage(imageName string) bool {
-	for _, exp := range c.imageExcludeList {
-		if glob.Glob(exp, imageName) {
-			return false
-		}
-	}
-	return true
-}
diff --git a/pkg/cluster/kubernetes/kubernetes.go b/pkg/cluster/kubernetes/kubernetes.go
index d97cdf02..76bfe007 100644
--- a/pkg/cluster/kubernetes/kubernetes.go
+++ b/pkg/cluster/kubernetes/kubernetes.go
@@ -109,13 +109,17 @@ type Cluster struct {
 	allowedNamespaces map[string]struct{}
 	loggedAllowedNS   map[string]bool // to keep track of whether we've logged a problem with seeing an allowed namespace
 
-	imageExcludeList    []string
+	imageIncluder       cluster.Includer
 	resourceExcludeList []string
 	mu                  sync.Mutex
 }
 
 // NewCluster returns a usable cluster.
-func NewCluster(client ExtendedClient, applier Applier, sshKeyRing ssh.KeyRing, logger log.Logger, allowedNamespaces map[string]struct{}, imageExcludeList []string, resourceExcludeList []string) *Cluster {
+func NewCluster(client ExtendedClient, applier Applier, sshKeyRing ssh.KeyRing, logger log.Logger, allowedNamespaces map[string]struct{}, imageIncluder cluster.Includer, resourceExcludeList []string) *Cluster {
+	if imageIncluder == nil {
+		imageIncluder = cluster.AlwaysInclude
+	}
+
 	c := &Cluster{
 		client:              client,
 		applier:             applier,
@@ -123,7 +127,7 @@ func NewCluster(client ExtendedClient, applier Applier, sshKeyRing ssh.KeyRing,
 		sshKeyRing:          sshKeyRing,
 		allowedNamespaces:   allowedNamespaces,
 		loggedAllowedNS:     map[string]bool{},
-		imageExcludeList:    imageExcludeList,
+		imageIncluder:       imageIncluder,
 		resourceExcludeList: resourceExcludeList,
 	}
 


Test patch:
diff --git a/pkg/cluster/includelist_test.go b/pkg/cluster/includelist_test.go
new file mode 100644
index 00000000..678489d0
--- /dev/null
+++ b/pkg/cluster/includelist_test.go
@@ -0,0 +1,70 @@
+package cluster
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIncluderFunc(t *testing.T) {
+	in := IncluderFunc(func(s string) bool {
+		return s == "included"
+	})
+	assert.True(t, in.IsIncluded("included"))
+	assert.False(t, in.IsIncluded("excluded"))
+}
+
+func TestExcludeInclude(t *testing.T) {
+	test := func(ei Includer, s string, expected bool) {
+		if expected {
+			t.Run("includes "+s, func(t *testing.T) {
+				assert.True(t, ei.IsIncluded(s))
+			})
+		} else {
+			t.Run("excludes "+s, func(t *testing.T) {
+				assert.False(t, ei.IsIncluded(s))
+			})
+		}
+	}
+
+	// Only exclude stuff
+	ei1 := ExcludeIncludeGlob{
+		Exclude: []string{"foo/*"},
+	}
+
+	for _, t := range []string{
+		"",
+		"completely unrelated",
+		"foo",
+		"starts/foo/bar",
+	} {
+		test(ei1, t, true)
+	}
+
+	for _, t := range []string{
+		"foo/bar",
+	} {
+		test(ei1, t, false)
+	}
+
+	// Explicitly include stuff
+	ei2 := ExcludeIncludeGlob{
+		Exclude: []string{"foo/bar/*"},
+		Include: []string{"foo/*", "boo/*"},
+	}
+
+	for _, t := range []string{
+		"boo/whatever",
+		"foo/something/else",
+	} {
+		test(ei2, t, true)
+	}
+
+	for _, t := range []string{
+		"baz/anything",
+		"foo/bar/something",
+		"anything not explicitly included",
+	} {
+		test(ei2, t, false)
+	}
+}
diff --git a/pkg/cluster/kubernetes/kubernetes_test.go b/pkg/cluster/kubernetes/kubernetes_test.go
index 7be112b2..9c18229a 100644
--- a/pkg/cluster/kubernetes/kubernetes_test.go
+++ b/pkg/cluster/kubernetes/kubernetes_test.go
@@ -32,7 +32,7 @@ func testGetAllowedNamespaces(t *testing.T, namespace []string, expected []strin
 	for _, n := range namespace {
 		allowedNamespaces[n] = struct{}{}
 	}
-	c := NewCluster(client, nil, nil, log.NewNopLogger(), allowedNamespaces, []string{}, []string{})
+	c := NewCluster(client, nil, nil, log.NewNopLogger(), allowedNamespaces, nil, []string{})
 
 	namespaces, err := c.getAllowedAndExistingNamespaces(context.Background())
 	if err != nil {