diff --git a/anchore-k8s-inventory.yaml b/anchore-k8s-inventory.yaml index c354214..be9becc 100644 --- a/anchore-k8s-inventory.yaml +++ b/anchore-k8s-inventory.yaml @@ -52,6 +52,17 @@ account-routes: # - default # - ^kube-* +# Route namespaces to anchore accounts by a label on the namespace +account-route-by-namespace-label: + # The name of the namespace label that will be used to route the contents of + # that namespace to the Anchore account matching the value of the label + key: # e.g anchore.io/account.name + # The name of the account to route inventory to for a namespace that is missing the label + # If not set then it will default to the account specified in the anchore credentials + default-account: # e.g. admin + # If true will exclude inventorying namespaces that are missing the specified label + ignore-namespace-missing-label: false + # Kubernetes API configuration parameters (should not need tuning) kubernetes: # Sets the request timeout for kubernetes API requests diff --git a/internal/config/config.go b/internal/config/config.go index aebd0cf..2faa6a0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,15 +40,16 @@ type Application struct { Quiet bool `mapstructure:"quiet"` Log Logging `mapstructure:"log"` CliOptions CliOnlyOptions - Dev Development `mapstructure:"dev"` - KubeConfig KubeConf `mapstructure:"kubeconfig"` - Kubernetes KubernetesAPI `mapstructure:"kubernetes"` - Namespaces []string `mapstructure:"namespaces"` - KubernetesRequestTimeoutSeconds int64 `mapstructure:"kubernetes-request-timeout-seconds"` - NamespaceSelectors NamespaceSelector `mapstructure:"namespace-selectors"` - AccountRoutes AccountRoutes `mapstructure:"account-routes"` - MissingRegistryOverride string `mapstructure:"missing-registry-override"` - MissingTagPolicy MissingTagConf `mapstructure:"missing-tag-policy"` + Dev Development `mapstructure:"dev"` + KubeConfig KubeConf `mapstructure:"kubeconfig"` + Kubernetes KubernetesAPI `mapstructure:"kubernetes"` + Namespaces []string `mapstructure:"namespaces"` + KubernetesRequestTimeoutSeconds int64 `mapstructure:"kubernetes-request-timeout-seconds"` + NamespaceSelectors NamespaceSelector `mapstructure:"namespace-selectors"` + AccountRoutes AccountRoutes `mapstructure:"account-routes"` + AccountRouteByNamespaceLabel AccountRouteByNamespaceLabel `mapstructure:"account-route-by-namespace-label"` + MissingRegistryOverride string `mapstructure:"missing-registry-override"` + MissingTagPolicy MissingTagConf `mapstructure:"missing-tag-policy"` RunMode mode.Mode Mode string `mapstructure:"mode"` IgnoreNotRunning bool `mapstructure:"ignore-not-running"` @@ -78,6 +79,12 @@ type AccountRouteDetails struct { Namespaces []string `mapstructure:"namespaces"` } +type AccountRouteByNamespaceLabel struct { + LabelKey string `mapstructure:"key"` + DefaultAccount string `mapstructure:"default-account"` + IgnoreMissingLabel bool `mapstructure:"ignore-missing-label"` +} + // KubernetesAPI details the configuration for interacting with the k8s api server type KubernetesAPI struct { RequestTimeoutSeconds int64 `mapstructure:"request-timeout-seconds"` @@ -138,6 +145,7 @@ func setNonCliDefaultValues(v *viper.Viper) { v.SetDefault("missing-tag-policy.policy", "digest") v.SetDefault("missing-tag-policy.tag", "UNKNOWN") v.SetDefault("account-routes", AccountRoutes{}) + v.SetDefault("account-route-by-namespace-label", AccountRouteByNamespaceLabel{}) v.SetDefault("namespaces", []string{}) v.SetDefault("namespace-selectors.include", []string{}) v.SetDefault("namespace-selectors.exclude", []string{}) diff --git a/internal/config/test-fixtures/snapshot/TestDefaultConfigString.golden b/internal/config/test-fixtures/snapshot/TestDefaultConfigString.golden index eac6d20..8aeefbd 100644 --- a/internal/config/test-fixtures/snapshot/TestDefaultConfigString.golden +++ b/internal/config/test-fixtures/snapshot/TestDefaultConfigString.golden @@ -32,6 +32,10 @@ namespaceselectors: exclude: [] ignoreempty: false accountroutes: {} +accountroutebynamespacelabel: + labelkey: "" + defaultaccount: "" + ignoremissinglabel: false missingregistryoverride: "" missingtagpolicy: policy: digest diff --git a/internal/config/test-fixtures/snapshot/TestEmptyConfigString.golden b/internal/config/test-fixtures/snapshot/TestEmptyConfigString.golden index 6c59c18..a4f4174 100644 --- a/internal/config/test-fixtures/snapshot/TestEmptyConfigString.golden +++ b/internal/config/test-fixtures/snapshot/TestEmptyConfigString.golden @@ -32,6 +32,10 @@ namespaceselectors: exclude: [] ignoreempty: false accountroutes: {} +accountroutebynamespacelabel: + labelkey: "" + defaultaccount: "" + ignoremissinglabel: false missingregistryoverride: "" missingtagpolicy: policy: "" diff --git a/internal/config/test-fixtures/snapshot/TestSensitiveConfigString.golden b/internal/config/test-fixtures/snapshot/TestSensitiveConfigString.golden index ce1c5f4..2f4097c 100644 --- a/internal/config/test-fixtures/snapshot/TestSensitiveConfigString.golden +++ b/internal/config/test-fixtures/snapshot/TestSensitiveConfigString.golden @@ -32,6 +32,10 @@ namespaceselectors: exclude: [] ignoreempty: false accountroutes: {} +accountroutebynamespacelabel: + labelkey: "" + defaultaccount: "" + ignoremissinglabel: false missingregistryoverride: "" missingtagpolicy: policy: digest diff --git a/pkg/lib.go b/pkg/lib.go index ecfe2a4..ba91474 100644 --- a/pkg/lib.go +++ b/pkg/lib.go @@ -261,9 +261,14 @@ func GetAllNamespaces(cfg *config.Application) ([]inventory.Namespace, error) { return namespaces, nil } -func GetAccountRoutedNamespaces(defaultAccount string, namespaces []inventory.Namespace, accountRoutes config.AccountRoutes) map[string][]inventory.Namespace { +func GetAccountRoutedNamespaces(defaultAccount string, namespaces []inventory.Namespace, + accountRoutes config.AccountRoutes, namespaceLabelRouting config.AccountRouteByNamespaceLabel) map[string][]inventory.Namespace { accountRoutesForAllNamespaces := make(map[string][]inventory.Namespace) + if namespaceLabelRouting.DefaultAccount != "" { + defaultAccount = namespaceLabelRouting.DefaultAccount + } + accountNamespaces := make(map[string]struct{}) for routeNS, route := range accountRoutes { for _, ns := range namespaces { @@ -275,9 +280,19 @@ func GetAccountRoutedNamespaces(defaultAccount string, namespaces []inventory.Na } } } - // Add namespaces that are not in any account route to the default account + // If there is a namespace label routing, add namespaces to the account routes based on the label, + // if the namespace has not already been added to an account route set via explicit configuration in + // accountRoutes config. (This overrides the label routing for the case where the label cannot be changed). + // Otherwise, add namespaces that are not in any account route to the default account unless disabled. for _, ns := range namespaces { - if _, ok := accountNamespaces[ns.Name]; !ok { + _, namespaceRouted := accountNamespaces[ns.Name] + if namespaceLabelRouting.LabelKey != "" && !namespaceRouted { + if account, ok := ns.Labels[namespaceLabelRouting.LabelKey]; ok { + accountRoutesForAllNamespaces[account] = append(accountRoutesForAllNamespaces[account], ns) + } else if !namespaceLabelRouting.IgnoreMissingLabel { + accountRoutesForAllNamespaces[defaultAccount] = append(accountRoutesForAllNamespaces[defaultAccount], ns) + } + } else if !namespaceRouted { accountRoutesForAllNamespaces[defaultAccount] = append(accountRoutesForAllNamespaces[defaultAccount], ns) } } @@ -292,14 +307,14 @@ func GetInventoryReports(cfg *config.Application) (AccountRoutedReports, error) namespaces, _ := GetAllNamespaces(cfg) - if len(cfg.AccountRoutes) == 0 { + if len(cfg.AccountRoutes) == 0 && cfg.AccountRouteByNamespaceLabel.LabelKey == "" { allNamespacesReport, err := GetInventoryReportForNamespaces(cfg, namespaces) if err != nil { return AccountRoutedReports{}, err } reports[cfg.AnchoreDetails.Account] = allNamespacesReport } else { - accountRoutesForAllNamespaces := GetAccountRoutedNamespaces(cfg.AnchoreDetails.Account, namespaces, cfg.AccountRoutes) + accountRoutesForAllNamespaces := GetAccountRoutedNamespaces(cfg.AnchoreDetails.Account, namespaces, cfg.AccountRoutes, cfg.AccountRouteByNamespaceLabel) for account, namespaces := range accountRoutesForAllNamespaces { nsNames := make([]string, 0) diff --git a/pkg/lib_test.go b/pkg/lib_test.go index 5cfac6b..b41dd86 100644 --- a/pkg/lib_test.go +++ b/pkg/lib_test.go @@ -12,15 +12,31 @@ import ( var ( TestNamespace1 = inventory.Namespace{ Name: "ns1", + Labels: map[string]string{ + "anchore.io/account": "account1", + }, } TestNamespace2 = inventory.Namespace{ Name: "ns2", + Labels: map[string]string{ + "anchore.io/account": "account2", + }, } TestNamespace3 = inventory.Namespace{ Name: "ns3", + Labels: map[string]string{ + "anchore.io/account": "account3", + }, } TestNamespace4 = inventory.Namespace{ Name: "ns4", + Labels: map[string]string{ + "anchore.io/account": "account4", + }, + } + TestNamespace5 = inventory.Namespace{ + Name: "ns5-no-label", + Labels: map[string]string{}, } TestNamespaces = []inventory.Namespace{ TestNamespace1, @@ -32,9 +48,10 @@ var ( func TestGetAccountRoutedNamespaces(t *testing.T) { type args struct { - defaultAccount string - namespaces []inventory.Namespace - accountRoutes config.AccountRoutes + defaultAccount string + namespaces []inventory.Namespace + accountRoutes config.AccountRoutes + namespaceLabelRouting config.AccountRouteByNamespaceLabel } tests := []struct { name string @@ -44,9 +61,10 @@ func TestGetAccountRoutedNamespaces(t *testing.T) { { name: "no account routes all to default", args: args{ - defaultAccount: "admin", - namespaces: TestNamespaces, - accountRoutes: config.AccountRoutes{}, + defaultAccount: "admin", + namespaces: TestNamespaces, + accountRoutes: config.AccountRoutes{}, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{}, }, want: map[string][]inventory.Namespace{ "admin": TestNamespaces, @@ -71,6 +89,7 @@ func TestGetAccountRoutedNamespaces(t *testing.T) { Namespaces: []string{"ns4"}, }, }, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{}, }, want: map[string][]inventory.Namespace{ "account1": {TestNamespace1}, @@ -89,15 +108,117 @@ func TestGetAccountRoutedNamespaces(t *testing.T) { Namespaces: []string{"ns.*"}, }, }, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{}, }, want: map[string][]inventory.Namespace{ "account1": TestNamespaces, }, }, + { + name: "namespaces to accounts that match a label only", + args: args{ + defaultAccount: "admin", + namespaces: TestNamespaces, + accountRoutes: config.AccountRoutes{}, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{ + LabelKey: "anchore.io/account", + DefaultAccount: "default", + IgnoreMissingLabel: false, + }, + }, + want: map[string][]inventory.Namespace{ + "account1": {TestNamespace1}, + "account2": {TestNamespace2}, + "account3": {TestNamespace3}, + "account4": {TestNamespace4}, + }, + }, + { + name: "namespaces to accounts that match a label only with namespace missing label (default account not set)", + args: args{ + defaultAccount: "admin", + namespaces: append(TestNamespaces, TestNamespace5), + accountRoutes: config.AccountRoutes{}, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{ + LabelKey: "anchore.io/account", + DefaultAccount: "", + IgnoreMissingLabel: false, + }, + }, + want: map[string][]inventory.Namespace{ + "account1": {TestNamespace1}, + "account2": {TestNamespace2}, + "account3": {TestNamespace3}, + "account4": {TestNamespace4}, + "admin": {TestNamespace5}, + }, + }, + { + name: "namespaces to accounts that match a label only with namespace missing label (default account set)", + args: args{ + defaultAccount: "admin", + namespaces: append(TestNamespaces, TestNamespace5), + accountRoutes: config.AccountRoutes{}, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{ + LabelKey: "anchore.io/account", + DefaultAccount: "defaultoverride", + IgnoreMissingLabel: false, + }, + }, + want: map[string][]inventory.Namespace{ + "account1": {TestNamespace1}, + "account2": {TestNamespace2}, + "account3": {TestNamespace3}, + "account4": {TestNamespace4}, + "defaultoverride": {TestNamespace5}, + }, + }, + { + name: "namespaces to accounts that match a label only with namespace missing label set to ignore", + args: args{ + defaultAccount: "admin", + namespaces: append(TestNamespaces, TestNamespace5), + accountRoutes: config.AccountRoutes{}, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{ + LabelKey: "anchore.io/account", + DefaultAccount: "", + IgnoreMissingLabel: true, + }, + }, + want: map[string][]inventory.Namespace{ + "account1": {TestNamespace1}, + "account2": {TestNamespace2}, + "account3": {TestNamespace3}, + "account4": {TestNamespace4}, + }, + }, + { + name: "mix of account routes and label routing", + args: args{ + defaultAccount: "admin", + namespaces: TestNamespaces, + accountRoutes: config.AccountRoutes{ + "explicitaccount1": config.AccountRouteDetails{ + Namespaces: []string{"ns1"}, + }, + }, + namespaceLabelRouting: config.AccountRouteByNamespaceLabel{ + LabelKey: "anchore.io/account", + DefaultAccount: "default", + IgnoreMissingLabel: false, + }, + }, + want: map[string][]inventory.Namespace{ + "explicitaccount1": {TestNamespace1}, + "account2": {TestNamespace2}, + "account3": {TestNamespace3}, + "account4": {TestNamespace4}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := GetAccountRoutedNamespaces(tt.args.defaultAccount, tt.args.namespaces, tt.args.accountRoutes) + got := GetAccountRoutedNamespaces(tt.args.defaultAccount, tt.args.namespaces, tt.args.accountRoutes, tt.args.namespaceLabelRouting) assert.Equal(t, tt.want, got) }) }