Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: account routing by namespace label #213

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading