Skip to content

Commit

Permalink
feat: account routing by namespace label
Browse files Browse the repository at this point in the history
Signed-off-by: Bradley Jones <[email protected]>
  • Loading branch information
bradleyjones committed May 10, 2024
1 parent 07b4ac2 commit 132bfbb
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 21 deletions.
11 changes: 11 additions & 0 deletions anchore-k8s-inventory.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 17 additions & 9 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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{})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ namespaceselectors:
exclude: []
ignoreempty: false
accountroutes: {}
accountroutebynamespacelabel:
labelkey: ""
defaultaccount: ""
ignoremissinglabel: false
missingregistryoverride: ""
missingtagpolicy:
policy: digest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ namespaceselectors:
exclude: []
ignoreempty: false
accountroutes: {}
accountroutebynamespacelabel:
labelkey: ""
defaultaccount: ""
ignoremissinglabel: false
missingregistryoverride: ""
missingtagpolicy:
policy: ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ namespaceselectors:
exclude: []
ignoreempty: false
accountroutes: {}
accountroutebynamespacelabel:
labelkey: ""
defaultaccount: ""
ignoremissinglabel: false
missingregistryoverride: ""
missingtagpolicy:
policy: digest
Expand Down
25 changes: 20 additions & 5 deletions pkg/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
}
Expand All @@ -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)
Expand Down
135 changes: 128 additions & 7 deletions pkg/lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -71,6 +89,7 @@ func TestGetAccountRoutedNamespaces(t *testing.T) {
Namespaces: []string{"ns4"},
},
},
namespaceLabelRouting: config.AccountRouteByNamespaceLabel{},
},
want: map[string][]inventory.Namespace{
"account1": {TestNamespace1},
Expand All @@ -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)
})
}
Expand Down

0 comments on commit 132bfbb

Please sign in to comment.