diff --git a/cmd/main.go b/cmd/main.go index b4977f20..5294b53a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,8 @@ import ( "flag" "log" + "github.com/spf13/pflag" + "github.com/statnett/image-scanner-operator/internal/config" "github.com/statnett/image-scanner-operator/internal/operator" ) @@ -13,7 +15,14 @@ func main() { cfg.Zap.Development = true opr := operator.Operator{} - if err := opr.BindConfig(&cfg, flag.CommandLine); err != nil { + if err := opr.BindFlags(&cfg, flag.CommandLine); err != nil { + log.Fatal(err) + } + + pflag.CommandLine.AddGoFlagSet(flag.CommandLine) + pflag.Parse() + + if err := opr.UnmarshalConfig(&cfg); err != nil { log.Fatal(err) } diff --git a/go.mod b/go.mod index 6eb81a8f..c402df2b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/distribution/distribution v2.8.1+incompatible github.com/go-logr/logr v1.2.3 + github.com/mitchellh/mapstructure v1.5.0 github.com/onsi/ginkgo/v2 v2.7.1 github.com/onsi/gomega v1.26.0 github.com/opencontainers/go-digest v1.0.0 @@ -57,7 +58,6 @@ require ( github.com/mattn/go-isatty v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index 49fa2fce..7eb62043 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,7 @@ package config import ( + "regexp" "time" "github.com/statnett/controller-runtime-viper/pkg/zap" @@ -9,14 +10,15 @@ import ( ) type Config struct { - MetricsLabels []string `mapstructure:"cis-metrics-labels"` - ScanInterval time.Duration `mapstructure:"scan-interval"` - ScanJobNamespace string `mapstructure:"scan-job-namespace"` - ScanJobServiceAccount string `mapstructure:"scan-job-service-account"` - ScanNamespaces []string `mapstructure:"namespaces"` - ScanWorkloadResources []string `mapstructure:"scan-workload-resources"` - TrivyImage string `mapstructure:"trivy-image"` - Zap zap.Options `mapstructure:"-"` + MetricsLabels []string `mapstructure:"cis-metrics-labels"` + ScanInterval time.Duration `mapstructure:"scan-interval"` + ScanJobNamespace string `mapstructure:"scan-job-namespace"` + ScanJobServiceAccount string `mapstructure:"scan-job-service-account"` + ScanNamespaces []string `mapstructure:"namespaces"` + ScanNamespaceExcludeRegexp *regexp.Regexp `mapstructure:"scan-namespace-exclude-regexp"` + ScanWorkloadResources []string `mapstructure:"scan-workload-resources"` + TrivyImage string `mapstructure:"trivy-image"` + Zap zap.Options `mapstructure:"-"` } func (c Config) TimeUntilNextScan(cis *stasv1alpha1.ContainerImageScan) time.Duration { diff --git a/internal/controller/stas/containerimagescan_controller.go b/internal/controller/stas/containerimagescan_controller.go index 10f86f11..ae3815fa 100644 --- a/internal/controller/stas/containerimagescan_controller.go +++ b/internal/controller/stas/containerimagescan_controller.go @@ -62,12 +62,18 @@ func (r *ContainerImageScanReconciler) Reconcile(ctx context.Context, req ctrl.R // SetupWithManager sets up the controller with the Manager. func (r *ContainerImageScanReconciler) SetupWithManager(mgr ctrl.Manager) error { + var predicates []predicate.Predicate + if r.ScanNamespaceExcludeRegexp != nil { + predicates = append(predicates, predicate.Not(namespaceMatchRegexp(r.ScanNamespaceExcludeRegexp))) + } + return ctrl.NewControllerManagedBy(mgr). For(&stasv1alpha1.ContainerImageScan{}, builder.WithPredicates( predicate.GenerationChangedPredicate{}, ignoreDeletionPredicate(), )). + WithEventFilter(predicate.And(predicates...)). Complete(r) } diff --git a/internal/controller/stas/predicates.go b/internal/controller/stas/predicates.go index b9b3bbcb..d70d3412 100644 --- a/internal/controller/stas/predicates.go +++ b/internal/controller/stas/predicates.go @@ -14,11 +14,11 @@ import ( stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1" ) -var systemNamespaceRegex = regexp.MustCompile("^(kube-|openshift-).*") - -var systemNamespace = predicate.NewPredicateFuncs(func(object client.Object) bool { - return systemNamespaceRegex.MatchString(object.GetNamespace()) -}) +func namespaceMatchRegexp(re *regexp.Regexp) predicate.Predicate { + return predicate.NewPredicateFuncs(func(object client.Object) bool { + return re.MatchString(object.GetNamespace()) + }) +} func podContainerStatusImagesChanged() predicate.Predicate { return predicate.Funcs{ diff --git a/internal/controller/stas/workload_controller.go b/internal/controller/stas/workload_controller.go index ec5a49d3..82249f1c 100644 --- a/internal/controller/stas/workload_controller.go +++ b/internal/controller/stas/workload_controller.go @@ -62,6 +62,13 @@ func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { groupKinds[i] = k.GroupKind() } + predicates := []predicate.Predicate{ + predicate.Not(managedByImageScanner), + } + if r.ScanNamespaceExcludeRegexp != nil { + predicates = append(predicates, predicate.Not(namespaceMatchRegexp(r.ScanNamespaceExcludeRegexp))) + } + bldr := ctrl.NewControllerManagedBy(mgr). For(&corev1.Pod{}, builder.WithPredicates( @@ -69,10 +76,7 @@ func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { predicate.Or(controllerInKinds(groupKinds...), noController), ignoreDeletionPredicate(), )). - WithEventFilter(predicate.And( - predicate.Not(systemNamespace), - predicate.Not(managedByImageScanner), - )). + WithEventFilter(predicate.And(predicates...)). Watches(&source.Kind{Type: &stasv1alpha1.ContainerImageScan{}}, &handler.EnqueueRequestForOwner{OwnerType: &corev1.Pod{}}, builder.WithPredicates( diff --git a/internal/operator/decode_hooks.go b/internal/operator/decode_hooks.go new file mode 100644 index 00000000..ab39ad1d --- /dev/null +++ b/internal/operator/decode_hooks.go @@ -0,0 +1,26 @@ +package operator + +import ( + "reflect" + "regexp" + + "github.com/mitchellh/mapstructure" +) + +// stringToRegexpHookFunc returns a DecodeHookFunc that converts strings to regexp.Regexp. +func stringToRegexpHookFunc() mapstructure.DecodeHookFunc { + return func( + f reflect.Type, + t reflect.Type, + data interface{}) (interface{}, error) { + if f.Kind() != reflect.String { + return data, nil + } + + if t != reflect.TypeOf(®exp.Regexp{}) { + return data, nil + } + + return regexp.Compile(data.(string)) + } +} diff --git a/internal/operator/operator.go b/internal/operator/operator.go index f78d1528..411dc4b1 100644 --- a/internal/operator/operator.go +++ b/internal/operator/operator.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/mitchellh/mapstructure" "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/statnett/controller-runtime-viper/pkg/zap" @@ -46,25 +47,24 @@ func init() { type Operator struct{} -func (o Operator) BindConfig(cfg *config.Config, fs *flag.FlagSet) error { - flag.String("metrics-bind-address", ":8080", "The address the metric endpoint binds to.") - flag.String("health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.Bool("leader-elect", false, +func (o Operator) BindFlags(cfg *config.Config, fs *flag.FlagSet) error { + fs.String("metrics-bind-address", ":8080", "The address the metric endpoint binds to.") + fs.String("health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + fs.Bool("leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.Bool("enable-profiling", false, "Enable profiling (pprof); available on metrics endpoint.") - flag.String("namespaces", "", "comma-separated list of namespaces to watch") - flag.String("cis-metrics-labels", "", "comma-separated list of labels in CIS resources to create metrics labels for") - flag.Duration("scan-interval", 12*time.Hour, "The minimum time between fetch scan reports from image scanner") - flag.String("scan-job-namespace", "", "The namespace to schedule scan jobs.") - flag.String("scan-job-service-account", "default", "The service account used to run scan jobs.") - flag.String("scan-workload-resources", "", "comma-separated list of workload resources to scan") - flag.String("trivy-image", "", "The image used for obtaining the trivy binary.") - flag.Bool("help", false, "print out usage and a summary of options") + fs.Bool("enable-profiling", false, "Enable profiling (pprof); available on metrics endpoint.") + fs.String("namespaces", "", "comma-separated list of namespaces to watch") + fs.String("cis-metrics-labels", "", "comma-separated list of labels in CIS resources to create metrics labels for") + fs.Duration("scan-interval", 12*time.Hour, "The minimum time between fetch scan reports from image scanner") + fs.String("scan-job-namespace", "", "The namespace to schedule scan jobs.") + fs.String("scan-job-service-account", "default", "The service account used to run scan jobs.") + fs.String("scan-workload-resources", "", "comma-separated list of workload resources to scan") + fs.String("scan-namespace-exclude-regexp", "^(kube-|openshift-).*", "regexp for namespace to exclude from scanning") + fs.String("trivy-image", "", "The image used for obtaining the trivy binary.") + fs.Bool("help", false, "print out usage and a summary of options") cfg.Zap.BindFlags(fs) - pflag.CommandLine.AddGoFlagSet(fs) - pflag.Parse() pfs := &pflag.FlagSet{} pfs.AddGoFlagSet(fs) @@ -76,13 +76,22 @@ func (o Operator) BindConfig(cfg *config.Config, fs *flag.FlagSet) error { viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + return nil +} + +func (o Operator) UnmarshalConfig(cfg *config.Config) error { helpRequested := viper.GetBool("help") if helpRequested { pflag.Usage() os.Exit(0) } - if err := viper.Unmarshal(cfg); err != nil { + hook := mapstructure.ComposeDecodeHookFunc( + mapstructure.StringToTimeDurationHookFunc(), + mapstructure.StringToSliceHookFunc(","), + stringToRegexpHookFunc(), + ) + if err := viper.Unmarshal(cfg, viper.DecodeHook(hook)); err != nil { return fmt.Errorf("unable to decode config into struct: %w", err) } diff --git a/internal/operator/operator_test.go b/internal/operator/operator_test.go new file mode 100644 index 00000000..c6234d93 --- /dev/null +++ b/internal/operator/operator_test.go @@ -0,0 +1,48 @@ +package operator + +import ( + "flag" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/statnett/image-scanner-operator/internal/config" +) + +var _ = Describe("Operator config from flags", func() { + var ( + opr Operator = Operator{} + fs *flag.FlagSet + cfg *config.Config + ) + + BeforeEach(func() { + fs = flag.NewFlagSet("test", flag.ExitOnError) + cfg = &config.Config{} + Expect(opr.BindFlags(cfg, fs)).To(Succeed()) + }) + + Context("Using scan-namespace-exclude-regexp flag", func() { + It("Should have correct default", func() { + Expect(fs.Parse(nil)).To(Succeed()) + Expect(opr.UnmarshalConfig(cfg)).To(Succeed()) + Expect(cfg.ScanNamespaceExcludeRegexp).NotTo(BeNil()) + Expect(cfg.ScanNamespaceExcludeRegexp.String()).To(Equal("^(kube-|openshift-).*")) + }) + + It("Should be configurable", func() { + args := []string{"--scan-namespace-exclude-regexp=^$"} + Expect(fs.Parse(args)).To(Succeed()) + Expect(opr.UnmarshalConfig(cfg)).To(Succeed()) + Expect(cfg.ScanNamespaceExcludeRegexp).NotTo(BeNil()) + Expect(cfg.ScanNamespaceExcludeRegexp.String()).To(Equal("^$")) + }) + + It("Should error on invalid regexp", func() { + args := []string{"--scan-namespace-exclude-regexp=["} + Expect(fs.Parse(args)).To(Succeed()) + Expect(opr.UnmarshalConfig(cfg)).To(MatchError(ContainSubstring("error parsing regexp"))) + }) + }) + +}) diff --git a/internal/operator/suite_test.go b/internal/operator/suite_test.go new file mode 100644 index 00000000..51ea852a --- /dev/null +++ b/internal/operator/suite_test.go @@ -0,0 +1,13 @@ +package operator + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestOperator(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Operator Suite") +}