Skip to content

Commit

Permalink
feat: post to enterprise per cluster
Browse files Browse the repository at this point in the history
Some additional code reorg to separate Report types and posting vs
inventory gathering.

Signed-off-by: Bradley Jones <bradley.jones@anchore.com>
  • Loading branch information
bradleyjones committed Mar 8, 2023
1 parent c23a108 commit 1746dee
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 76 deletions.
12 changes: 7 additions & 5 deletions pkg/inventory/ecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecs"

"github.com/anchore/anchore-ecs-inventory/pkg/reporter"
)

// Check if AWS are present, should be stored in ~/.aws/credentials
Expand Down Expand Up @@ -42,18 +44,18 @@ func fetchTasksFromCluster(client *ecs.ECS, cluster string) ([]*string, error) {
return result.TaskArns, nil
}

func fetchImagesFromTasks(client *ecs.ECS, cluster string, tasks []*string) ([]ReportImage, error) {
func fetchImagesFromTasks(client *ecs.ECS, cluster string, tasks []*string) ([]reporter.ReportImage, error) {
input := &ecs.DescribeTasksInput{
Cluster: aws.String(cluster),
Tasks: tasks,
}

results, err := client.DescribeTasks(input)
if err != nil {
return []ReportImage{}, err
return []reporter.ReportImage{}, err
}

uniqueImages := make(map[string]ReportImage)
uniqueImages := make(map[string]reporter.ReportImage)

for _, task := range results.Tasks {
for _, container := range task.Containers {
Expand All @@ -62,15 +64,15 @@ func fetchImagesFromTasks(client *ecs.ECS, cluster string, tasks []*string) ([]R
digest = *container.ImageDigest
}
uniqueName := fmt.Sprintf("%s@%s", *container.Image, digest)
uniqueImages[uniqueName] = ReportImage{
uniqueImages[uniqueName] = reporter.ReportImage{
Tag: *container.Image,
RepoDigest: digest,
}
}
}

// convert map of unique images to a slice
images := []ReportImage{}
images := []reporter.ReportImage{}
for _, image := range uniqueImages {
images = append(images, image)
}
Expand Down
98 changes: 67 additions & 31 deletions pkg/inventory/report.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,46 @@
package inventory

import (
"encoding/json"
"fmt"
"os"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecs"

"github.com/anchore/anchore-ecs-inventory/internal/logger"
"github.com/anchore/anchore-ecs-inventory/pkg/connection"
"github.com/anchore/anchore-ecs-inventory/pkg/reporter"
)

type Report struct {
Timestamp string `json:"timestamp,omitempty"` // Should be generated using time.Now.UTC() and formatted according to RFC Y-M-DTH:M:SZ
Results []ReportItem `json:"results"`
ClusterName string `json:"cluster_name,omitempty"` // NOTE: The key here is ClusterName to match the Anchore API but it's actually the region
InventoryType string `json:"inventory_type"`
// Output the JSON formatted report to stdout
func reportToStdout(report reporter.Report) error {
enc := json.NewEncoder(os.Stdout)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(report); err != nil {
return fmt.Errorf("unable to show inventory: %w", err)
}
return nil
}

func HandleReport(report reporter.Report, anchoreDetails connection.AnchoreInfo) error {
if anchoreDetails.IsValid() {
if err := reporter.Post(report, anchoreDetails); err != nil {
return fmt.Errorf("unable to report Inventory to Anchore: %w", err)
}
} else {
logger.Log.Debug("Anchore details not specified, not reporting inventory")
}

// Encode the report to JSON and output to stdout (maintains same behaviour as when multiple presenters were supported)
return reportToStdout(report)
}

// GetInventoryReport is an atomic method for getting in-use image results, in parallel for multiple clusters
func GetInventoryReport(region string) (Report, error) {
func GetInventoryReportsForRegion(region string, anchoreDetails connection.AnchoreInfo) error {
sessConfig := &aws.Config{}
if region != "" {
sessConfig.Region = aws.String(region)
Expand All @@ -30,49 +52,63 @@ func GetInventoryReport(region string) (Report, error) {

err = checkAWSCredentials(sess)
if err != nil {
return Report{}, err
return err
}

ecsClient := ecs.New(sess)

clusters, err := fetchClusters(ecsClient)
if err != nil {
return Report{}, err
return err
}

results := []ReportItem{}

for _, cluster := range clusters {
logger.Log.Debug("Found cluster", "cluster", *cluster)

// Fetch tasks in cluster
tasks, err := fetchTasksFromCluster(ecsClient, *cluster)
report, err := GetInventoryReportForCluster(*cluster, ecsClient)
if err != nil {
return Report{}, err
return err
}

images := []ReportImage{}
// Must be at least one task to continue
if len(tasks) == 0 {
logger.Log.Debug("No tasks found in cluster", "cluster", *cluster)
} else {
images, err = fetchImagesFromTasks(ecsClient, *cluster, tasks)
if err != nil {
return Report{}, err
}
err = HandleReport(report, anchoreDetails)
if err != nil {
return err
}
}

results = append(results, ReportItem{
Namespace: *cluster, // NOTE The key is Namespace to match the Anchore API but it's actually the cluster ARN
Images: images,
})
return nil
}

// GetInventoryReportForCluster is an atomic method for getting in-use image results, for a cluster
func GetInventoryReportForCluster(cluster string, ecsClient *ecs.ECS) (reporter.Report, error) {
logger.Log.Debug("Found cluster", "cluster", cluster)

// Fetch tasks in cluster
tasks, err := fetchTasksFromCluster(ecsClient, cluster)
if err != nil {
return reporter.Report{}, err
}

images := []reporter.ReportImage{}
// Must be at least one task to continue
if len(tasks) == 0 {
logger.Log.Debug("No tasks found in cluster", "cluster", cluster)
} else {
images, err = fetchImagesFromTasks(ecsClient, cluster, tasks)
if err != nil {
return reporter.Report{}, err
}
}

results := []reporter.ReportItem{}
results = append(results, reporter.ReportItem{
Namespace: "", // NOTE The key is Namespace to match the Anchore API but it's actually the cluster ARN
Images: images,
})
// NOTE: clusterName not used for ECS as the clusternARN (used as the namespace in results payload) provides sufficient
// unique location data (account, region, clustername)
return Report{
return reporter.Report{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Results: results,
ClusterName: "",
ClusterName: cluster,
InventoryType: "ecs",
}, nil
}
38 changes: 2 additions & 36 deletions pkg/lib.go
Original file line number Diff line number Diff line change
@@ -1,59 +1,25 @@
package pkg

import (
"encoding/json"
"fmt"
"os"
"time"

"github.com/anchore/anchore-ecs-inventory/pkg/connection"
"github.com/anchore/anchore-ecs-inventory/pkg/inventory"
"github.com/anchore/anchore-ecs-inventory/pkg/logger"
"github.com/anchore/anchore-ecs-inventory/pkg/reporter"
)

var log logger.Logger

// Output the JSON formatted report to stdout
func reportToStdout(report inventory.Report) error {
enc := json.NewEncoder(os.Stdout)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(report); err != nil {
return fmt.Errorf("unable to show inventory: %w", err)
}
return nil
}

func HandleReport(report inventory.Report, anchoreDetails connection.AnchoreInfo) error {
if anchoreDetails.IsValid() {
if err := reporter.Post(report, anchoreDetails); err != nil {
return fmt.Errorf("unable to report Inventory to Anchore: %w", err)
}
} else {
log.Debug("Anchore details not specified, not reporting inventory")
}

// Encode the report to JSON and output to stdout (maintains same behaviour as when multiple presenters were supported)
return reportToStdout(report)
}

// PeriodicallyGetInventoryReport periodically retrieve image results and report/output them according to the configuration.
// Note: Errors do not cause the function to exit, since this is periodically running
func PeriodicallyGetInventoryReport(pollingIntervalSeconds int, anchoreDetails connection.AnchoreInfo, region string) {
// Fire off a ticker that reports according to a configurable polling interval
ticker := time.NewTicker(time.Duration(pollingIntervalSeconds) * time.Second)

for {
report, err := inventory.GetInventoryReport(region)
err := inventory.GetInventoryReportsForRegion(region, anchoreDetails)
if err != nil {
log.Error("Failed to get Inventory Report", err)
} else {
err := HandleReport(report, anchoreDetails)
if err != nil {
log.Error("Failed to handle Inventory Report", err)
}
log.Error("Failed to get Inventory Reports for region", err)
}

// Wait at least as long as the ticker
Expand Down
8 changes: 8 additions & 0 deletions pkg/reporter/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package reporter

type Report struct {
Timestamp string `json:"timestamp,omitempty"` // Should be generated using time.Now.UTC() and formatted according to RFC Y-M-DTH:M:SZ
Results []ReportItem `json:"results"`
ClusterName string `json:"cluster_name,omitempty"` // NOTE: The key here is ClusterName to match the Anchore API but it's actually the region
InventoryType string `json:"inventory_type"`
}
3 changes: 1 addition & 2 deletions pkg/reporter/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import (

"github.com/anchore/anchore-ecs-inventory/internal/logger"
"github.com/anchore/anchore-ecs-inventory/pkg/connection"
"github.com/anchore/anchore-ecs-inventory/pkg/inventory"
)

const ReportAPIPath = "v1/enterprise/inventories"

// This method does the actual Reporting (via HTTP) to Anchore
//
//nolint:gosec
func Post(report inventory.Report, anchoreDetails connection.AnchoreInfo) error {
func Post(report Report, anchoreDetails connection.AnchoreInfo) error {
logger.Log.Debug("Reporting results to Anchore")
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: anchoreDetails.HTTP.Insecure},
Expand Down
4 changes: 2 additions & 2 deletions pkg/inventory/reportitem.go → pkg/reporter/reportitem.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package inventory
package reporter

import (
"fmt"
)

// ReportItem represents a cluster and all it's unique images
type ReportItem struct {
Namespace string `json:"namespace,omitempty"` // NOTE The key is Namespace to match the Anchore API but it's actually the cluster ARN
Namespace string `json:"namespace,omitempty"` // NOTE The key is Namespace to match the Anchore API but it's actually passed as empty string
Images []ReportImage `json:"images"`
}

Expand Down

0 comments on commit 1746dee

Please sign in to comment.