From 8c8883d93a660d82ae7c890945dd329b59bfd3e5 Mon Sep 17 00:00:00 2001 From: Daniel Pacak Date: Fri, 23 Oct 2020 14:08:27 +0200 Subject: [PATCH] feat(operator): Allow configuring scanners Resolves: #212 Signed-off-by: Daniel Pacak --- cmd/starboard-operator/main.go | 38 ++++++---- deploy/helm/templates/rbac.yaml | 7 ++ .../03-starboard-operator.clusterrole.yaml | 7 ++ pkg/cmd/cleanup.go | 14 ++-- pkg/cmd/config.go | 2 +- pkg/cmd/find_vulnerabilities.go | 2 +- pkg/cmd/init.go | 20 ++--- pkg/cmd/kube_bench.go | 2 +- pkg/ext/id_generator.go | 3 +- pkg/find/vulnerabilities/scanner.go | 1 + pkg/find/vulnerabilities/trivy/scanner.go | 3 +- pkg/kube/cr_manager.go | 51 ++++--------- pkg/kube/runnable_job.go | 3 +- pkg/kubebench/converter_test.go | 3 +- pkg/kubehunter/model.go | 3 +- pkg/operator/etc/config.go | 7 +- pkg/operator/trivy/scanner.go | 68 ++++++++++++++--- pkg/starboard/config.go | 58 ++++++++++++--- pkg/starboard/config_test.go | 74 ++++++++++++++++++- 19 files changed, 267 insertions(+), 99 deletions(-) diff --git a/cmd/starboard-operator/main.go b/cmd/starboard-operator/main.go index c1ab064cd..d277a179f 100644 --- a/cmd/starboard-operator/main.go +++ b/cmd/starboard-operator/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" @@ -74,22 +75,22 @@ func main() { func run() error { setupLog.Info("Starting operator", "version", versionInfo) - config, err := etc.GetOperatorConfig() + operatorConfig, err := etc.GetOperatorConfig() if err != nil { return fmt.Errorf("getting operator config: %w", err) } - log.SetLogger(zap.New(zap.UseDevMode(config.Operator.LogDevMode))) + log.SetLogger(zap.New(zap.UseDevMode(operatorConfig.Operator.LogDevMode))) // Validate configured namespaces to resolve install mode. - operatorNamespace, err := config.Operator.GetOperatorNamespace() + operatorNamespace, err := operatorConfig.Operator.GetOperatorNamespace() if err != nil { return fmt.Errorf("getting operator namespace: %w", err) } - targetNamespaces := config.Operator.GetTargetNamespaces() + targetNamespaces := operatorConfig.Operator.GetTargetNamespaces() - installMode, err := config.Operator.GetInstallMode() + installMode, err := operatorConfig.Operator.GetInstallMode() if err != nil { return fmt.Errorf("getting install mode: %w", err) } @@ -100,8 +101,8 @@ func run() error { // Set the default manager options. options := manager.Options{ Scheme: scheme, - MetricsBindAddress: config.Operator.MetricsBindAddress, - HealthProbeBindAddress: config.Operator.HealthProbeBindAddress, + MetricsBindAddress: operatorConfig.Operator.MetricsBindAddress, + HealthProbeBindAddress: operatorConfig.Operator.HealthProbeBindAddress, } switch installMode { @@ -158,16 +159,27 @@ func run() error { return err } + configManager := starboard.NewConfigManager(kubernetesClientset, operatorNamespace) + err = configManager.EnsureDefault(context.Background()) + if err != nil { + return err + } + + starboardConfig, err := configManager.Read(context.Background()) + if err != nil { + return err + } + store := reports.NewStore(mgr.GetClient(), scheme) idGenerator := ext.NewGoogleUUIDGenerator() - scanner, err := getEnabledScanner(idGenerator, config) + scanner, err := getEnabledScanner(idGenerator, operatorConfig, starboardConfig) if err != nil { return err } if err = (&pod.PodController{ - Config: config.Operator, + Config: operatorConfig.Operator, Client: mgr.GetClient(), IDGenerator: idGenerator, Store: store, @@ -178,7 +190,7 @@ func run() error { } if err = (&job.JobController{ - Config: config.Operator, + Config: operatorConfig.Operator, LogsReader: logs.NewReader(kubernetesClientset), Client: mgr.GetClient(), Store: store, @@ -196,7 +208,7 @@ func run() error { return nil } -func getEnabledScanner(idGenerator ext.IDGenerator, config etc.Config) (scanner.VulnerabilityScanner, error) { +func getEnabledScanner(idGenerator ext.IDGenerator, config etc.Config, starboardConfig starboard.ConfigData) (scanner.VulnerabilityScanner, error) { if config.ScannerTrivy.Enabled && config.ScannerAquaCSP.Enabled { return nil, fmt.Errorf("invalid configuration: multiple vulnerability scanners enabled") } @@ -204,8 +216,8 @@ func getEnabledScanner(idGenerator ext.IDGenerator, config etc.Config) (scanner. return nil, fmt.Errorf("invalid configuration: none vulnerability scanner enabled") } if config.ScannerTrivy.Enabled { - setupLog.Info("Using Trivy as vulnerability scanner", "image", config.ScannerTrivy.ImageRef) - return trivy.NewScanner(idGenerator, config.ScannerTrivy), nil + setupLog.Info("Using Trivy as vulnerability scanner", "image", starboardConfig.GetTrivyImageRef()) + return trivy.NewScanner(idGenerator, starboardConfig), nil } if config.ScannerAquaCSP.Enabled { setupLog.Info("Using Aqua CSP as vulnerability scanner", "image", config.ScannerAquaCSP.ImageRef) diff --git a/deploy/helm/templates/rbac.yaml b/deploy/helm/templates/rbac.yaml index 984d7273a..85fe4c0de 100644 --- a/deploy/helm/templates/rbac.yaml +++ b/deploy/helm/templates/rbac.yaml @@ -36,6 +36,13 @@ rules: - get - list - watch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - create - apiGroups: - apps resources: # resources that own pods are inspected diff --git a/deploy/kubectl/03-starboard-operator.clusterrole.yaml b/deploy/kubectl/03-starboard-operator.clusterrole.yaml index 23c50b8d7..f9a6d8d8f 100644 --- a/deploy/kubectl/03-starboard-operator.clusterrole.yaml +++ b/deploy/kubectl/03-starboard-operator.clusterrole.yaml @@ -14,6 +14,13 @@ rules: - get - list - watch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - create - apiGroups: - apps resources: diff --git a/pkg/cmd/cleanup.go b/pkg/cmd/cleanup.go index 429a0e54a..d49cdb47d 100644 --- a/pkg/cmd/cleanup.go +++ b/pkg/cmd/cleanup.go @@ -3,6 +3,8 @@ package cmd import ( "context" + "github.com/aquasecurity/starboard/pkg/starboard" + "github.com/aquasecurity/starboard/pkg/kube" "github.com/spf13/cobra" extapi "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" @@ -14,22 +16,22 @@ func NewCleanupCmd(cf *genericclioptions.ConfigFlags) *cobra.Command { cmd := &cobra.Command{ Use: "cleanup", Short: "Delete custom resource definitions created by starboard", - RunE: func(cmd *cobra.Command, args []string) (err error) { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() config, err := cf.ToRESTConfig() if err != nil { - return + return err } clientset, err := kubernetes.NewForConfig(config) if err != nil { - return + return err } clientsetext, err := extapi.NewForConfig(config) if err != nil { - return + return err } - err = kube.NewCRManager(clientset, clientsetext).Cleanup(ctx) - return + return kube.NewCRManager(starboard.NewConfigManager(clientset, starboard.NamespaceName), clientset, clientsetext). + Cleanup(ctx) }, } return cmd diff --git a/pkg/cmd/config.go b/pkg/cmd/config.go index bdf974d96..778960de6 100644 --- a/pkg/cmd/config.go +++ b/pkg/cmd/config.go @@ -33,7 +33,7 @@ func NewConfigCmd(cf *genericclioptions.ConfigFlags, outWriter io.Writer) *cobra if err != nil { return } - config, err := starboard.NewConfigReader(clientset).Read(ctx) + config, err := starboard.NewConfigManager(clientset, starboard.NamespaceName).Read(ctx) if err != nil { return } diff --git a/pkg/cmd/find_vulnerabilities.go b/pkg/cmd/find_vulnerabilities.go index cb1367057..3c5f7df1c 100644 --- a/pkg/cmd/find_vulnerabilities.go +++ b/pkg/cmd/find_vulnerabilities.go @@ -72,7 +72,7 @@ NAME is the name of a particular Kubernetes workload. if err != nil { return err } - config, err := starboard.NewConfigReader(kubernetesClientset).Read(ctx) + config, err := starboard.NewConfigManager(kubernetesClientset, starboard.NamespaceName).Read(ctx) if err != nil { return } diff --git a/pkg/cmd/init.go b/pkg/cmd/init.go index 1c3aeed59..8c0239bca 100644 --- a/pkg/cmd/init.go +++ b/pkg/cmd/init.go @@ -3,6 +3,8 @@ package cmd import ( "context" + "github.com/aquasecurity/starboard/pkg/starboard" + "github.com/aquasecurity/starboard/pkg/kube" "github.com/spf13/cobra" extensionsapi "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" @@ -14,8 +16,7 @@ func NewInitCmd(cf *genericclioptions.ConfigFlags) *cobra.Command { cmd := &cobra.Command{ Use: "init", Short: "Create custom resource definitions used by starboard", - Long: ` -Create all the resources used by starboard. It will create the following + Long: `Create all the resources used by starboard. It will create the following in the cluster: - custom resource definitions @@ -27,24 +28,23 @@ in the cluster: The config map contains the default configuration parameters. However this can be modified to change the behaviour of the scanner. -These resources can be removed from the cluster using the "cleanup" command. -`, - RunE: func(cmd *cobra.Command, args []string) (err error) { +These resources can be removed from the cluster using the "cleanup" command.`, + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() config, err := cf.ToRESTConfig() if err != nil { - return + return err } clientset, err := kubernetes.NewForConfig(config) if err != nil { - return + return err } clientsetext, err := extensionsapi.NewForConfig(config) if err != nil { - return + return err } - err = kube.NewCRManager(clientset, clientsetext).Init(ctx) - return + return kube.NewCRManager(starboard.NewConfigManager(clientset, starboard.NamespaceName), clientset, clientsetext). + Init(ctx) }, } return cmd diff --git a/pkg/cmd/kube_bench.go b/pkg/cmd/kube_bench.go index f6c05c20e..a304533b4 100644 --- a/pkg/cmd/kube_bench.go +++ b/pkg/cmd/kube_bench.go @@ -46,7 +46,7 @@ func NewKubeBenchCmd(cf *genericclioptions.ConfigFlags) *cobra.Command { err = fmt.Errorf("listing nodes: %w", err) return } - config, err := starboard.NewConfigReader(kubernetesClientset).Read(ctx) + config, err := starboard.NewConfigManager(kubernetesClientset, starboard.NamespaceName).Read(ctx) if err != nil { return err } diff --git a/pkg/ext/id_generator.go b/pkg/ext/id_generator.go index 566623709..e45555fa3 100644 --- a/pkg/ext/id_generator.go +++ b/pkg/ext/id_generator.go @@ -2,8 +2,9 @@ package ext import ( "fmt" - "github.com/google/uuid" "sync/atomic" + + "github.com/google/uuid" ) // IDGenerator defines contract for generating universally unique identifiers. diff --git a/pkg/find/vulnerabilities/scanner.go b/pkg/find/vulnerabilities/scanner.go index 844dcccdd..e9f6114d9 100644 --- a/pkg/find/vulnerabilities/scanner.go +++ b/pkg/find/vulnerabilities/scanner.go @@ -2,6 +2,7 @@ package vulnerabilities import ( "context" + "github.com/aquasecurity/starboard/pkg/docker" starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" diff --git a/pkg/find/vulnerabilities/trivy/scanner.go b/pkg/find/vulnerabilities/trivy/scanner.go index 823adabb8..47e14bfea 100644 --- a/pkg/find/vulnerabilities/trivy/scanner.go +++ b/pkg/find/vulnerabilities/trivy/scanner.go @@ -3,9 +3,10 @@ package trivy import ( "context" "fmt" - "github.com/aquasecurity/starboard/pkg/starboard" "io" + "github.com/aquasecurity/starboard/pkg/starboard" + "github.com/aquasecurity/starboard/pkg/docker" "github.com/aquasecurity/starboard/pkg/kube/secrets" "github.com/aquasecurity/starboard/pkg/scanners" diff --git a/pkg/kube/cr_manager.go b/pkg/kube/cr_manager.go index 8a66f788e..842456a90 100644 --- a/pkg/kube/cr_manager.go +++ b/pkg/kube/cr_manager.go @@ -3,9 +3,10 @@ package kube import ( "context" "fmt" - "github.com/aquasecurity/starboard/pkg/starboard" "time" + "github.com/aquasecurity/starboard/pkg/starboard" + "k8s.io/utils/pointer" "k8s.io/apimachinery/pkg/labels" @@ -44,15 +45,6 @@ var ( }, AutomountServiceAccountToken: pointer.BoolPtr(false), } - configMap = &core.ConfigMap{ - ObjectMeta: meta.ObjectMeta{ - Name: starboard.ConfigMapName, - Labels: labels.Set{ - "app.kubernetes.io/managed-by": "starboard", - }, - }, - Data: starboard.GetDefaultConfig(), - } clusterRole = &rbac.ClusterRole{ ObjectMeta: meta.ObjectMeta{ Name: clusterRoleStarboard, @@ -136,15 +128,20 @@ type CRManager interface { } type crManager struct { - clientset kubernetes.Interface - clientsetext extapi.ApiextensionsV1beta1Interface + configManager starboard.ConfigManager + clientset kubernetes.Interface + clientsetext extapi.ApiextensionsV1beta1Interface } -// NewCRManager constructs a CRManager with the given Kubernetes interface. -func NewCRManager(clientset kubernetes.Interface, clientsetext extapi.ApiextensionsV1beta1Interface) CRManager { +// NewCRManager constructs a CRManager with the given ConfigWriter and Kubernetes interfaces. +func NewCRManager( + configManager starboard.ConfigManager, + clientset kubernetes.Interface, + clientsetext extapi.ApiextensionsV1beta1Interface) CRManager { return &crManager{ - clientset: clientset, - clientsetext: clientsetext, + configManager: configManager, + clientset: clientset, + clientsetext: clientsetext, } } @@ -175,7 +172,7 @@ func (m *crManager) Init(ctx context.Context) (err error) { return } - err = m.createConfigMapIfNotFound(ctx, configMap) + err = m.configManager.EnsureDefault(ctx) if err != nil { return } @@ -275,21 +272,6 @@ func (m *crManager) createServiceAccountIfNotFound(ctx context.Context, sa *core return } -func (m *crManager) createConfigMapIfNotFound(ctx context.Context, cm *core.ConfigMap) (err error) { - name := cm.Name - _, err = m.clientset.CoreV1().ConfigMaps(starboard.NamespaceName).Get(ctx, name, meta.GetOptions{}) - switch { - case err == nil: - klog.V(3).Infof("ConfigMap %q already exists", starboard.NamespaceName+"/"+name) - return - case errors.IsNotFound(err): - klog.V(3).Infof("Creating ConfigMap %q", starboard.NamespaceName+"/"+name) - _, err = m.clientset.CoreV1().ConfigMaps(starboard.NamespaceName).Create(ctx, cm, meta.CreateOptions{}) - return - } - return -} - func (m *crManager) createOrUpdateClusterRole(ctx context.Context, cr *rbac.ClusterRole) (err error) { existingRole, err := m.clientset.RbacV1().ClusterRoles().Get(ctx, cr.GetName(), meta.GetOptions{}) switch { @@ -374,9 +356,8 @@ func (m *crManager) Cleanup(ctx context.Context) (err error) { return } - klog.V(3).Infof("Deleting ConfigMap %q", starboard.NamespaceName+"/"+starboard.ConfigMapName) - err = m.clientset.CoreV1().ConfigMaps(starboard.NamespaceName).Delete(ctx, starboard.ConfigMapName, meta.DeleteOptions{}) - if err != nil && !errors.IsNotFound(err) { + err = m.configManager.Delete(ctx) + if err != nil { return } diff --git a/pkg/kube/runnable_job.go b/pkg/kube/runnable_job.go index e93251239..1a8ae25a6 100644 --- a/pkg/kube/runnable_job.go +++ b/pkg/kube/runnable_job.go @@ -3,9 +3,10 @@ package kube import ( "context" "fmt" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" "time" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog" "k8s.io/apimachinery/pkg/util/wait" diff --git a/pkg/kubebench/converter_test.go b/pkg/kubebench/converter_test.go index 383b57752..3598ab56c 100644 --- a/pkg/kubebench/converter_test.go +++ b/pkg/kubebench/converter_test.go @@ -3,10 +3,11 @@ package kubebench_test import ( "encoding/json" "errors" - "github.com/aquasecurity/starboard/pkg/kubebench" "os" "testing" + "github.com/aquasecurity/starboard/pkg/kubebench" + "github.com/aquasecurity/starboard/pkg/starboard" starboardv1alpha1 "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" diff --git a/pkg/kubehunter/model.go b/pkg/kubehunter/model.go index 195ee7699..32587d0aa 100644 --- a/pkg/kubehunter/model.go +++ b/pkg/kubehunter/model.go @@ -2,8 +2,9 @@ package kubehunter import ( "encoding/json" - sec "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" "io" + + sec "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" ) func toSummary(vulnerabilities []sec.KubeHunterVulnerability) (summary sec.KubeHunterSummary) { diff --git a/pkg/operator/etc/config.go b/pkg/operator/etc/config.go index 1d10bfbdc..2dc438a0f 100644 --- a/pkg/operator/etc/config.go +++ b/pkg/operator/etc/config.go @@ -29,12 +29,7 @@ type Operator struct { } type ScannerTrivy struct { - Enabled bool `env:"OPERATOR_SCANNER_TRIVY_ENABLED" envDefault:"true"` - ImageRef string `env:"OPERATOR_SCANNER_TRIVY_IMAGE" envDefault:"aquasec/trivy:0.11.0"` -} - -func (c ScannerTrivy) GetTrivyImageRef() string { - return c.ImageRef + Enabled bool `env:"OPERATOR_SCANNER_TRIVY_ENABLED" envDefault:"true"` } type ScannerAquaCSP struct { diff --git a/pkg/operator/trivy/scanner.go b/pkg/operator/trivy/scanner.go index 502e13c8c..335751bd2 100644 --- a/pkg/operator/trivy/scanner.go +++ b/pkg/operator/trivy/scanner.go @@ -3,12 +3,12 @@ package trivy import ( "io" - "github.com/aquasecurity/starboard/pkg/ext" + "github.com/aquasecurity/starboard/pkg/starboard" - "github.com/aquasecurity/starboard/pkg/find/vulnerabilities/trivy" - "github.com/aquasecurity/starboard/pkg/operator/etc" + "github.com/aquasecurity/starboard/pkg/ext" "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" + "github.com/aquasecurity/starboard/pkg/find/vulnerabilities/trivy" "github.com/aquasecurity/starboard/pkg/operator/scanner" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -17,12 +17,12 @@ import ( type trivyScanner struct { idGenerator ext.IDGenerator - config etc.ScannerTrivy + config trivy.Config } // NewScanner constructs a new VulnerabilityScanner, which is using an official // Trivy container image to scan pod containers. -func NewScanner(idGenerator ext.IDGenerator, config etc.ScannerTrivy) scanner.VulnerabilityScanner { +func NewScanner(idGenerator ext.IDGenerator, config trivy.Config) scanner.VulnerabilityScanner { return &trivyScanner{ idGenerator: idGenerator, config: config, @@ -33,9 +33,35 @@ func (s *trivyScanner) GetPodTemplateSpec(spec corev1.PodSpec, options scanner.O initContainers := []corev1.Container{ { Name: s.idGenerator.GenerateID(), - Image: s.config.ImageRef, + Image: s.config.GetTrivyImageRef(), ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Env: []corev1.EnvVar{ + { + Name: "HTTP_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: starboard.ConfigMapName, + }, + Key: "trivy.httpProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "GITHUB_TOKEN", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: starboard.ConfigMapName, + }, + Key: "trivy.githubToken", + Optional: pointer.BoolPtr(true), + }, + }, + }, + }, Command: []string{ "trivy", }, @@ -66,14 +92,38 @@ func (s *trivyScanner) GetPodTemplateSpec(spec corev1.PodSpec, options scanner.O containers := make([]corev1.Container, len(spec.Containers)) for i, c := range spec.Containers { - var envs []corev1.EnvVar containers[i] = corev1.Container{ Name: c.Name, - Image: s.config.ImageRef, + Image: s.config.GetTrivyImageRef(), ImagePullPolicy: corev1.PullIfNotPresent, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, - Env: envs, + Env: []corev1.EnvVar{ + { + Name: "TRIVY_SEVERITY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: starboard.ConfigMapName, + }, + Key: "trivy.severity", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "HTTP_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: starboard.ConfigMapName, + }, + Key: "trivy.httpProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + }, Command: []string{ "trivy", }, diff --git a/pkg/starboard/config.go b/pkg/starboard/config.go index 6ae7b4a2f..dda70c8f4 100644 --- a/pkg/starboard/config.go +++ b/pkg/starboard/config.go @@ -4,7 +4,10 @@ import ( "context" "fmt" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" "github.com/google/go-containerregistry/pkg/name" @@ -209,8 +212,11 @@ type BuildInfo struct { // of key-value pairs. type ConfigData map[string]string -type ConfigReader interface { +// ConfigManager defines methods for managing ConfigData. +type ConfigManager interface { + EnsureDefault(ctx context.Context) error Read(ctx context.Context) (ConfigData, error) + Delete(ctx context.Context) error } // GetDefaultConfig returns the default configuration data. @@ -256,22 +262,54 @@ func GetVersionFromImageRef(imageRef string) (string, error) { return version, nil } -// NewConfigReader constructs a new ConfigReader that is using client-go -// interfaces to read ConfigData from the ConfigMap. -func NewConfigReader(client kubernetes.Interface) ConfigReader { - return &configReader{ - client: client, +// NewConfigManager constructs a new ConfigManager that is using client-go +// interfaces to manage ConfigData backed by the the ConfigMap stored in the +// Starboard's namespace. +// +// This implementation is used by the Starboard CLI, which relies on the client-go. +// The Starboard Operator should use NewControllerRuntimeConfigManager instead. +func NewConfigManager(client kubernetes.Interface, namespace string) ConfigManager { + return &configManager{ + client: client, + namespace: namespace, } } -type configReader struct { - client kubernetes.Interface +type configManager struct { + client kubernetes.Interface + namespace string } -func (c *configReader) Read(ctx context.Context) (ConfigData, error) { - cm, err := c.client.CoreV1().ConfigMaps(NamespaceName).Get(ctx, ConfigMapName, metav1.GetOptions{}) +func (c *configManager) EnsureDefault(ctx context.Context) error { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: c.namespace, + Name: ConfigMapName, + Labels: labels.Set{ + "app.kubernetes.io/managed-by": "starboard", + }, + }, + Data: GetDefaultConfig(), + } + _, err := c.client.CoreV1().ConfigMaps(c.namespace).Create(ctx, cm, metav1.CreateOptions{}) + if !apierrors.IsAlreadyExists(err) { + return err + } + return nil +} + +func (c *configManager) Read(ctx context.Context) (ConfigData, error) { + cm, err := c.client.CoreV1().ConfigMaps(c.namespace).Get(ctx, ConfigMapName, metav1.GetOptions{}) if err != nil { return nil, err } return cm.Data, nil } + +func (c *configManager) Delete(ctx context.Context) error { + err := c.client.CoreV1().ConfigMaps(c.namespace).Delete(ctx, ConfigMapName, metav1.DeleteOptions{}) + if apierrors.IsNotFound(err) { + return nil + } + return err +} diff --git a/pkg/starboard/config_test.go b/pkg/starboard/config_test.go index 3d2a61d21..083a3cd47 100644 --- a/pkg/starboard/config_test.go +++ b/pkg/starboard/config_test.go @@ -4,6 +4,8 @@ import ( "context" "testing" + "k8s.io/apimachinery/pkg/api/errors" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -104,7 +106,7 @@ func TestConfigData_GetKubeBenchImageRef(t *testing.T) { } } -func TestConfigReader_Read(t *testing.T) { +func TestConfigManager_Read(t *testing.T) { clientset := fake.NewSimpleClientset(&corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: starboard.NamespaceName, @@ -114,9 +116,77 @@ func TestConfigReader_Read(t *testing.T) { "foo": "bar", }, }) - configData, err := starboard.NewConfigReader(clientset).Read(context.TODO()) + configData, err := starboard.NewConfigManager(clientset, starboard.NamespaceName).Read(context.TODO()) require.NoError(t, err) assert.Equal(t, starboard.ConfigData{ "foo": "bar", }, configData) } + +func TestConfigManager_EnsureDefault(t *testing.T) { + + t.Run("Should create the ConfigMap with the default configuration settings", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + err := starboard.NewConfigManager(clientset, starboard.NamespaceName).EnsureDefault(context.TODO()) + require.NoError(t, err) + + cm, err := clientset.CoreV1(). + ConfigMaps(starboard.NamespaceName). + Get(context.TODO(), starboard.ConfigMapName, metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, starboard.GetDefaultConfig(), starboard.ConfigData(cm.Data)) + }) + + t.Run("Should not modify the ConfigMap if it already exists", func(t *testing.T) { + clientset := fake.NewSimpleClientset(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: starboard.NamespaceName, + Name: starboard.ConfigMapName, + }, + Data: map[string]string{ + "foo": "bar", + }, + }) + + err := starboard.NewConfigManager(clientset, starboard.NamespaceName).EnsureDefault(context.TODO()) + require.NoError(t, err) + + cm, err := clientset.CoreV1(). + ConfigMaps(starboard.NamespaceName). + Get(context.TODO(), starboard.ConfigMapName, metav1.GetOptions{}) + require.NoError(t, err) + + assert.Equal(t, map[string]string{ + "foo": "bar", + }, cm.Data) + }) + +} + +func TestConfigManager_Delete(t *testing.T) { + + t.Run("Should not return error when ConfigMap does not exist", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + err := starboard.NewConfigManager(clientset, starboard.NamespaceName).Delete(context.TODO()) + require.NoError(t, err) + }) + + t.Run("Should delete the ConfigMap", func(t *testing.T) { + clientset := fake.NewSimpleClientset(&corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: starboard.NamespaceName, + Name: starboard.ConfigMapName, + }, + Data: map[string]string{ + "foo": "bar", + }, + }) + + err := starboard.NewConfigManager(clientset, starboard.NamespaceName).Delete(context.TODO()) + require.NoError(t, err) + + _, err = clientset.CoreV1().ConfigMaps(starboard.NamespaceName).Get(context.TODO(), starboard.ConfigMapName, metav1.GetOptions{}) + assert.True(t, errors.IsNotFound(err)) + }) +}