diff --git a/README.md b/README.md index 643600f..c2666f1 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,54 @@ namespace-selectors: ignore-empty: false ``` +### Account Routing + +The following configuration options can determine which Anchore account +inventory reports are sent to. Without any of the following configuration the +account set in the `anchore` section will be used. + +If a mixture of static account routing and account routing by namespace label +is used then the static account routes configured in k8s-inventory config will +take precedence over any account that is specified by namespace label. + +#### Static account routing config + +Set a list of accounts and which namespaces inventory should be sent to that +account. You can override the default credentials on a per account basis, if +not set then the global credentials set in the `anchore` section will be used. + +```yaml +account-routes: + # Example + # account: # (this is the name of the anchore account e.g. admin) + # user: username + # password: password + # namespaces: # Can be a list of explicit namespaces matches or regex patterns + # - default + # - ^kube-* +``` + +#### Account routing by namespace label + +In this mode use a label set on a kubernetes namespace to determine which +Anchore account inventory data for that namespace should be sent to. It is +assumed that the credentials set in the `anchore` section can post to all +accounts. + +```yaml +# 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 or if the anchore account is not found. + # 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 Parameters This section will allow users to tune the way anchore-k8s-inventory interacts with the kubernetes API server. @@ -355,6 +403,7 @@ anchore: url: user: password: $ANCHORE_K8S_INVENTORY_ANCHORE_PASSWORD + account: http: insecure: true timeout-seconds: 10 diff --git a/anchore-k8s-inventory.yaml b/anchore-k8s-inventory.yaml index be9becc..52dd99a 100644 --- a/anchore-k8s-inventory.yaml +++ b/anchore-k8s-inventory.yaml @@ -57,7 +57,8 @@ 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 + # The name of the account to route inventory to for a namespace that is + # missing the label or if the anchore account is not found. # 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 @@ -113,6 +114,7 @@ anchore: # url: $ANCHORE_K8S_INVENTORY_ANCHORE_URL # user: $ANCHORE_K8S_INVENTORY_ANCHORE_USER password: $ANCHORE_K8S_INVENTORY_ANCHORE_PASSWORD + # account: admin # http: # insecure: true # timeout-seconds: 10 diff --git a/cmd/root.go b/cmd/root.go index d255944..a2e29c1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,11 +1,13 @@ package cmd import ( + "errors" "fmt" "os" "runtime/pprof" "github.com/anchore/k8s-inventory/pkg/mode" + "github.com/anchore/k8s-inventory/pkg/reporter" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -55,13 +57,26 @@ var rootCmd = &cobra.Command{ log.Errorf("Failed to get Image Results: %+v", err) os.Exit(1) } + anErrorOccurred := false for account, report := range reports { err = pkg.HandleReport(report, appConfig, account) + if errors.Is(err, reporter.ErrAnchoreAccountDoesNotExist) { + // Retry with default account + retryAccount := appConfig.AnchoreDetails.Account + if appConfig.AccountRouteByNamespaceLabel.DefaultAccount != "" { + retryAccount = appConfig.AccountRouteByNamespaceLabel.DefaultAccount + } + log.Warnf("Anchore Account %s does not exist, sending to default account", account) + err = pkg.HandleReport(report, appConfig, retryAccount) + } if err != nil { log.Errorf("Failed to handle Image Results: %+v", err) - os.Exit(1) + anErrorOccurred = true } } + if anErrorOccurred { + os.Exit(1) + } } }, } diff --git a/pkg/inventory/namespace.go b/pkg/inventory/namespace.go index 46c6655..12a274a 100644 --- a/pkg/inventory/namespace.go +++ b/pkg/inventory/namespace.go @@ -115,7 +115,9 @@ func FetchNamespaces( // Only return namespaces that are explicitly included if set if len(includes) > 0 { for _, ns := range includes { - nsList = append(nsList, nsMap[ns]) + if _, exists := nsMap[ns]; exists { + nsList = append(nsList, nsMap[ns]) + } } return nsList, nil } diff --git a/pkg/lib.go b/pkg/lib.go index ba91474..db21d3e 100644 --- a/pkg/lib.go +++ b/pkg/lib.go @@ -5,6 +5,7 @@ k8s go SDK import ( "encoding/json" + "errors" "fmt" "os" "regexp" @@ -75,6 +76,9 @@ func HandleReport(report inventory.Report, cfg *config.Application, account stri if anchoreDetails.IsValid() { if err := reporter.Post(report, anchoreDetails); err != nil { + if errors.Is(err, reporter.ErrAnchoreAccountDoesNotExist) { + return err + } return fmt.Errorf("unable to report Inventory to Anchore account %s: %w", account, err) } log.Infof("Inventory report sent to Anchore account %s", account) @@ -97,6 +101,15 @@ func PeriodicallyGetInventoryReport(cfg *config.Application) { } else { for account, report := range reports { err := HandleReport(report, cfg, account) + if errors.Is(err, reporter.ErrAnchoreAccountDoesNotExist) { + // Retry with default account + retryAccount := cfg.AnchoreDetails.Account + if cfg.AccountRouteByNamespaceLabel.DefaultAccount != "" { + retryAccount = cfg.AccountRouteByNamespaceLabel.DefaultAccount + } + log.Warnf("Anchore Account %s does not exist, sending to default account", account) + err = HandleReport(report, cfg, retryAccount) + } if err != nil { log.Errorf("Failed to handle Inventory Report: %w", err) } @@ -274,12 +287,14 @@ func GetAccountRoutedNamespaces(defaultAccount string, namespaces []inventory.Na for _, ns := range namespaces { for _, namespaceRegex := range route.Namespaces { if regexp.MustCompile(namespaceRegex).MatchString(ns.Name) { + log.Debugf("Namespace %s matched route from config %s", ns.Name, routeNS) accountNamespaces[ns.Name] = struct{}{} accountRoutesForAllNamespaces[routeNS] = append(accountRoutesForAllNamespaces[routeNS], ns) } } } } + // 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). @@ -288,12 +303,16 @@ func GetAccountRoutedNamespaces(defaultAccount string, namespaces []inventory.Na _, namespaceRouted := accountNamespaces[ns.Name] if namespaceLabelRouting.LabelKey != "" && !namespaceRouted { if account, ok := ns.Labels[namespaceLabelRouting.LabelKey]; ok { + log.Debugf("Namespace %s matched route from label %s", ns.Name, account) accountRoutesForAllNamespaces[account] = append(accountRoutesForAllNamespaces[account], ns) } else if !namespaceLabelRouting.IgnoreMissingLabel { accountRoutesForAllNamespaces[defaultAccount] = append(accountRoutesForAllNamespaces[defaultAccount], ns) + } else { + log.Infof("Ignoring namespace %s because it does not have the label %s", ns.Name, namespaceLabelRouting.LabelKey) } } else if !namespaceRouted { accountRoutesForAllNamespaces[defaultAccount] = append(accountRoutesForAllNamespaces[defaultAccount], ns) + log.Debugf("Namespace %s added to default account %s", ns.Name, defaultAccount) } } diff --git a/pkg/reporter/reporter.go b/pkg/reporter/reporter.go index 43e8ac5..cebd8cc 100644 --- a/pkg/reporter/reporter.go +++ b/pkg/reporter/reporter.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "strings" "time" "github.com/anchore/k8s-inventory/internal/config" @@ -19,13 +20,26 @@ import ( ) const ( - reportAPIPathV1 = "v1/enterprise/kubernetes-inventory" - reportAPIPathV2 = "v2/kubernetes-inventory" + reportAPIPathV1 = "v1/enterprise/kubernetes-inventory" + reportAPIPathV2 = "v2/kubernetes-inventory" + AnchoreAccountMissingError = "User account not found" ) -var enterpriseEndpoint = reportAPIPathV2 +var ( + ErrAnchoreAccountDoesNotExist = fmt.Errorf("user account not found") + enterpriseEndpoint = reportAPIPathV2 +) + +type AnchoreResponse struct { + Message string `json:"message"` + Httpcode int `json:"httpcode"` + Detail interface{} `json:"detail"` + AnchoreRequestID string `json:"anchore_request_id"` +} // This method does the actual Reporting (via HTTP) to Anchore +// +//nolint:funlen func Post(report inventory.Report, anchoreDetails config.AnchoreInfo) error { defer tracker.TrackFunctionTime(time.Now(), "Reporting results to Anchore for cluster: "+report.ClusterName+"") log.Debug("Validating and normalizing report before sending to Anchore") @@ -78,7 +92,21 @@ func Post(report inventory.Report, anchoreDetails config.AnchoreInfo) error { log.Info("Retrying inventory report with new endpoint: ", enterpriseEndpoint) return Post(report, anchoreDetails) } - return fmt.Errorf("failed to report data to Anchore: %+v", resp) + + // Check if account is correct + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response from Anchore: %w", err) + } + anchoreResponse := &AnchoreResponse{} + err = json.Unmarshal(respBody, anchoreResponse) + if err != nil { + return fmt.Errorf("failed to parse response from Anchore: %w", err) + } + if strings.Contains(anchoreResponse.Message, AnchoreAccountMissingError) { + return ErrAnchoreAccountDoesNotExist + } + return fmt.Errorf("failed to report data to Anchore: %s", string(respBody)) } if resp.StatusCode < 200 || resp.StatusCode > 299 { return fmt.Errorf("failed to report data to Anchore: %+v", resp)