Skip to content

Commit

Permalink
chore: reorg code from lib to inventory package
Browse files Browse the repository at this point in the history
Signed-off-by: Bradley Jones <bradley.jones@anchore.com>
  • Loading branch information
bradleyjones committed Mar 7, 2023
1 parent 93415d3 commit 5d6299f
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 136 deletions.
79 changes: 79 additions & 0 deletions ecg/inventory/ecs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package inventory

import (
"fmt"

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

// Check if AWS are present, should be stored in ~/.aws/credentials
func checkAWSCredentials(sess *session.Session) error {
_, err := sess.Config.Credentials.Get()
if err != nil {
// TODO: Add some logs here detailing where to put the credentials
return fmt.Errorf("unable to get AWS credentials: %w", err)
}
return nil
}

func fetchClusters(client *ecs.ECS) ([]*string, error) {
input := &ecs.ListClustersInput{}

result, err := client.ListClusters(input)
if err != nil {
return nil, err
}

return result.ClusterArns, nil
}

func fetchTasksFromCluster(client *ecs.ECS, cluster string) ([]*string, error) {
input := &ecs.ListTasksInput{
Cluster: aws.String(cluster),
}

result, err := client.ListTasks(input)
if err != nil {
return nil, err
}

return result.TaskArns, nil
}

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

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

uniqueImages := make(map[string]ReportImage)

for _, task := range results.Tasks {
for _, container := range task.Containers {
digest := ""
if container.ImageDigest != nil {
digest = *container.ImageDigest
}
uniqueName := fmt.Sprintf("%s@%s", *container.Image, digest)
uniqueImages[uniqueName] = ReportImage{
Tag: *container.Image,
RepoDigest: digest,
}
}
}

// convert map of unique images to a slice
images := []ReportImage{}
for _, image := range uniqueImages {
images = append(images, image)
}

return images, nil
}
70 changes: 70 additions & 0 deletions ecg/inventory/report.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,78 @@
package inventory

import (
"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"
)

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"`
}

// GetInventoryReport is an atomic method for getting in-use image results, in parallel for multiple clusters
func GetInventoryReport(region string) (Report, error) {
sessConfig := &aws.Config{}
if region != "" {
sessConfig.Region = aws.String(region)
}
sess, err := session.NewSession(sessConfig)
if err != nil {
logger.Log.Error("Failed to create AWS session", err)
}

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

ecsClient := ecs.New(sess)

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

results := []ReportItem{}

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

// Fetch tasks in cluster
tasks, err := fetchTasksFromCluster(ecsClient, *cluster)
if err != nil {
return Report{}, 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
}
}

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,
})
}
// 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{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Results: results,
ClusterName: "",
InventoryType: "ecs",
}, nil
}
136 changes: 1 addition & 135 deletions ecg/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import (
"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/ecg/connection"
"github.com/anchore/anchore-ecs-inventory/ecg/inventory"
"github.com/anchore/anchore-ecs-inventory/ecg/logger"
Expand Down Expand Up @@ -50,7 +46,7 @@ func PeriodicallyGetInventoryReport(pollingIntervalSeconds int, anchoreDetails c
ticker := time.NewTicker(time.Duration(pollingIntervalSeconds) * time.Second)

for {
report, err := GetInventoryReport(region)
report, err := inventory.GetInventoryReport(region)
if err != nil {
log.Error("Failed to get Inventory Report", err)
} else {
Expand All @@ -65,136 +61,6 @@ func PeriodicallyGetInventoryReport(pollingIntervalSeconds int, anchoreDetails c
}
}

// GetInventoryReport is an atomic method for getting in-use image results, in parallel for multiple clusters
func GetInventoryReport(region string) (inventory.Report, error) {
sessConfig := &aws.Config{}
if region != "" {
sessConfig.Region = aws.String(region)
}
sess, err := session.NewSession(sessConfig)
if err != nil {
log.Error("Failed to create AWS session", err)
}

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

ecsClient := ecs.New(sess)

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

results := []inventory.ReportItem{}

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

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

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

results = append(results, inventory.ReportItem{
Namespace: *cluster, // 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 inventory.Report{
Timestamp: time.Now().UTC().Format(time.RFC3339),
Results: results,
ClusterName: "",
InventoryType: "ecs",
}, nil
}

func SetLogger(logger logger.Logger) {
log = logger
}

// Check if AWS are present, should be stored in ~/.aws/credentials
func checkAWSCredentials(sess *session.Session) error {
_, err := sess.Config.Credentials.Get()
if err != nil {
// TODO: Add some logs here detailing where to put the credentials
return fmt.Errorf("unable to get AWS credentials: %w", err)
}
return nil
}

func fetchClusters(client *ecs.ECS) ([]*string, error) {
input := &ecs.ListClustersInput{}

result, err := client.ListClusters(input)
if err != nil {
return nil, err
}

return result.ClusterArns, nil
}

func fetchTasksFromCluster(client *ecs.ECS, cluster string) ([]*string, error) {
input := &ecs.ListTasksInput{
Cluster: aws.String(cluster),
}

result, err := client.ListTasks(input)
if err != nil {
return nil, err
}

return result.TaskArns, nil
}

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

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

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

for _, task := range results.Tasks {
for _, container := range task.Containers {
digest := ""
if container.ImageDigest != nil {
digest = *container.ImageDigest
}
uniqueName := fmt.Sprintf("%s@%s", *container.Image, digest)
uniqueImages[uniqueName] = inventory.ReportImage{
Tag: *container.Image,
RepoDigest: digest,
}
}
}

// convert map of unique images to a slice
images := []inventory.ReportImage{}
for _, image := range uniqueImages {
images = append(images, image)
}

return images, nil
}
1 change: 0 additions & 1 deletion internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ func InitLogger(logConfig LogConfig) {
var cfg zap.Config

level, err := zap.ParseAtomicLevel(logConfig.Level)

if err != nil {
log.Printf("Invalid log level: %s, defaulting to `info`", logConfig.Level)
level = zap.NewAtomicLevelAt(zap.InfoLevel)
Expand Down

0 comments on commit 5d6299f

Please sign in to comment.