Skip to content

Commit

Permalink
feat: add account routing for namespaces
Browse files Browse the repository at this point in the history
Allow account routing by config for namespaces to specific accounts by
namespaces name using either exact match or by regex pattern

Signed-off-by: Bradley Jones <[email protected]>
  • Loading branch information
bradleyjones committed May 2, 2024
1 parent f8e0ba6 commit 518a73c
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 53 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 50
COVERAGE_THRESHOLD := 48

CLUSTER_NAME=anchore-k8s-inventory-testing

Expand Down
9 changes: 9 additions & 0 deletions anchore-k8s-inventory.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ namespace-selectors:

ignore-empty: false

account-routes:
# Example
# account:
# user: username
# password: password
# namespaces:
# - default
# - ^kube-*

# Kubernetes API configuration parameters (should not need tuning)
kubernetes:
# Sets the request timeout for kubernetes API requests
Expand Down
12 changes: 7 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,20 @@ var rootCmd = &cobra.Command{
case mode.PeriodicPolling:
pkg.PeriodicallyGetInventoryReport(appConfig)
default:
report, err := pkg.GetInventoryReport(appConfig)
reports, err := pkg.GetInventoryReports(appConfig)
if appConfig.Dev.ProfileCPU {
pprof.StopCPUProfile()
}
if err != nil {
log.Errorf("Failed to get Image Results: %+v", err)
os.Exit(1)
}
err = pkg.HandleReport(report, appConfig)
if err != nil {
log.Errorf("Failed to handle Image Results: %+v", err)
os.Exit(1)
for account, report := range reports {
err = pkg.HandleReport(report, appConfig, account)
if err != nil {
log.Errorf("Failed to handle Image Results: %+v", err)
os.Exit(1)
}
}
}
},
Expand Down
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type Application struct {
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"`
RunMode mode.Mode
Expand All @@ -69,6 +70,14 @@ type NamespaceSelector struct {
IgnoreEmpty bool `mapstructure:"ignore-empty"`
}

type AccountRoutes map[string]AccountRouteDetails

type AccountRouteDetails struct {
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Namespaces []string `mapstructure:"namespaces"`
}

// KubernetesAPI details the configuration for interacting with the k8s api server
type KubernetesAPI struct {
RequestTimeoutSeconds int64 `mapstructure:"request-timeout-seconds"`
Expand Down Expand Up @@ -128,6 +137,7 @@ func setNonCliDefaultValues(v *viper.Viper) {
v.SetDefault("missing-registry-override", "")
v.SetDefault("missing-tag-policy.policy", "digest")
v.SetDefault("missing-tag-policy.tag", "UNKNOWN")
v.SetDefault("account-routes", AccountRoutes{})
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 @@ -31,6 +31,7 @@ namespaceselectors:
include: []
exclude: []
ignoreempty: false
accountroutes: {}
missingregistryoverride: ""
missingtagpolicy:
policy: digest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespaceselectors:
include: []
exclude: []
ignoreempty: false
accountroutes: {}
missingregistryoverride: ""
missingtagpolicy:
policy: ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ namespaceselectors:
include: []
exclude: []
ignoreempty: false
accountroutes: {}
missingregistryoverride: ""
missingtagpolicy:
policy: digest
Expand Down
153 changes: 130 additions & 23 deletions pkg/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"os"
"regexp"
"time"

"github.com/anchore/k8s-inventory/pkg/reporter"
Expand All @@ -33,6 +34,8 @@ type channels struct {
stopper chan struct{}
}

type AccountRoutedReports map[string]inventory.Report

func reportToStdout(report inventory.Report) error {
enc := json.NewEncoder(os.Stdout)
// prevent > and < from being escaped in the payload
Expand All @@ -44,19 +47,37 @@ func reportToStdout(report inventory.Report) error {
return nil
}

func HandleReport(report inventory.Report, cfg *config.Application) error {
func HandleReport(report inventory.Report, cfg *config.Application, account string) error {
if cfg.VerboseInventoryReports {
err := reportToStdout(report)
if err != nil {
log.Errorf("Failed to output Inventory Report: %w", err)
}
}

if cfg.AnchoreDetails.IsValid() {
if err := reporter.Post(report, cfg.AnchoreDetails); err != nil {
return fmt.Errorf("unable to report Inventory to Anchore: %w", err)
anchoreDetails := cfg.AnchoreDetails
// Look for account credentials in the account routes first then fall back to the global anchore credentials
if account == "" {
return fmt.Errorf("account name is required")
}
anchoreDetails.Account = account
if cfg.AccountRoutes != nil {
if route, ok := cfg.AccountRoutes[account]; ok {
log.Debugf("Using account details specified from account-routes config for account %s", account)
anchoreDetails.User = route.User
anchoreDetails.Password = route.Password
} else {
log.Debugf("Using default account details for account %s", account)
}
} else {
log.Debugf("Using default account details for account %s", account)
}

if anchoreDetails.IsValid() {
if err := reporter.Post(report, anchoreDetails); err != nil {
return fmt.Errorf("unable to report Inventory to Anchore account %s: %w", account, err)
}
log.Info("Inventory report sent to Anchore")
log.Infof("Inventory report sent to Anchore account %s", account)
} else {
log.Info("Anchore details not specified, not reporting inventory")
}
Expand All @@ -70,13 +91,15 @@ func PeriodicallyGetInventoryReport(cfg *config.Application) {
ticker := time.NewTicker(time.Duration(cfg.PollingIntervalSeconds) * time.Second)

for {
report, err := GetInventoryReport(cfg)
reports, err := GetInventoryReports(cfg)
if err != nil {
log.Errorf("Failed to get Inventory Report: %w", err)
} else {
err := HandleReport(report, cfg)
if err != nil {
log.Errorf("Failed to handle Inventory Report: %w", err)
for account, report := range reports {
err := HandleReport(report, cfg, account)
if err != nil {
log.Errorf("Failed to handle Inventory Report: %w", err)
}
}
}

Expand Down Expand Up @@ -117,23 +140,21 @@ func launchWorkerPool(
}
}

// GetInventoryReport is an atomic method for getting in-use image results, in parallel for multiple namespaces
// GetInventoryReportForNamespaces is an atomic method for getting in-use image results, in parallel for multiple namespaces
//
//nolint:funlen
func GetInventoryReport(cfg *config.Application) (inventory.Report, error) {
log.Info("Starting image inventory collection")
func GetInventoryReportForNamespaces(
cfg *config.Application,
namespaces []inventory.Namespace,
) (inventory.Report, error) {
log.Info("Starting image inventory collection for specific namespaces")
log.Debugf("Namespaces: %v", namespaces)

kubeconfig, err := client.GetKubeConfig(cfg)
if err != nil {
return inventory.Report{}, err
}

ch := channels{
reportItem: make(chan ReportItem),
errors: make(chan error),
stopper: make(chan struct{}, 1),
}

clientset, err := client.GetClientSet(kubeconfig)
if err != nil {
return inventory.Report{}, fmt.Errorf("failed to get k8s client set: %w", err)
Expand All @@ -142,11 +163,10 @@ func GetInventoryReport(cfg *config.Application) (inventory.Report, error) {
Clientset: clientset,
}

namespaces, err := inventory.FetchNamespaces(client,
cfg.Kubernetes.RequestBatchSize, cfg.Kubernetes.RequestTimeoutSeconds,
cfg.NamespaceSelectors.Exclude, cfg.NamespaceSelectors.Include)
if err != nil {
return inventory.Report{}, err
ch := channels{
reportItem: make(chan ReportItem),
errors: make(chan error),
stopper: make(chan struct{}, 1),
}

queue := make(chan inventory.Namespace, len(namespaces)) // fill the queue of namespaces to process
Expand Down Expand Up @@ -215,6 +235,93 @@ func GetInventoryReport(cfg *config.Application) (inventory.Report, error) {
}, nil
}

func GetAllNamespaces(cfg *config.Application) ([]inventory.Namespace, error) {
kubeconfig, err := client.GetKubeConfig(cfg)
if err != nil {
return []inventory.Namespace{}, err
}

clientset, err := client.GetClientSet(kubeconfig)
if err != nil {
return []inventory.Namespace{}, fmt.Errorf("failed to get k8s client set: %w", err)
}
client := client.Client{
Clientset: clientset,
}

namespaces, err := inventory.FetchNamespaces(client,
cfg.Kubernetes.RequestBatchSize, cfg.Kubernetes.RequestTimeoutSeconds,
cfg.NamespaceSelectors.Exclude, cfg.NamespaceSelectors.Include)
if err != nil {
return []inventory.Namespace{}, err
}

log.Infof("Found %d namespaces", len(namespaces))

return namespaces, nil
}

func GetAccountRoutedNamespaces(defaultAccount string, namespaces []inventory.Namespace, accountRoutes config.AccountRoutes) map[string][]inventory.Namespace {
accountRoutesForAllNamespaces := make(map[string][]inventory.Namespace)

accountNamespaces := make(map[string]struct{})
for routeNS, route := range accountRoutes {
for _, ns := range namespaces {
for _, namespaceRegex := range route.Namespaces {
if regexp.MustCompile(namespaceRegex).MatchString(ns.Name) {
accountNamespaces[ns.Name] = struct{}{}
accountRoutesForAllNamespaces[routeNS] = append(accountRoutesForAllNamespaces[routeNS], ns)
}
}
}
}
// Add namespaces that are not in any account route to the default account
for _, ns := range namespaces {
if _, ok := accountNamespaces[ns.Name]; !ok {
accountRoutesForAllNamespaces[defaultAccount] = append(accountRoutesForAllNamespaces[defaultAccount], ns)
}
}

return accountRoutesForAllNamespaces
}

func GetInventoryReports(cfg *config.Application) (AccountRoutedReports, error) {
log.Info("Starting image inventory collection")

reports := AccountRoutedReports{}

namespaces, _ := GetAllNamespaces(cfg)

if len(cfg.AccountRoutes) == 0 {
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)

for account, namespaces := range accountRoutesForAllNamespaces {
nsNames := make([]string, 0)
for _, ns := range namespaces {
nsNames = append(nsNames, ns.Name)
}
log.Infof("Namespaces for account %s : %s", account, nsNames)
}

// Get inventory reports for each account
for account, namespaces := range accountRoutesForAllNamespaces {
accountReport, err := GetInventoryReportForNamespaces(cfg, namespaces)
if err != nil {
return AccountRoutedReports{}, err
}
reports[account] = accountReport
}
}

return reports, nil
}

func processNamespace(
clientset *kubernetes.Clientset,
cfg *config.Application,
Expand Down
Loading

0 comments on commit 518a73c

Please sign in to comment.