Files
weave-scope/probe/awsecs/reporter.go
Bryan Boreham 1dcdfab05a fixup: from review feedback
Fix a logic error in ECS scale-down button, bad copy/paste in
ActiveControls() and neaten the switch cases in container controls.

Co-Authored-By: Filip Barl <filip@weave.works>
2020-01-13 14:48:38 +00:00

263 lines
7.9 KiB
Go

package awsecs
import (
"fmt"
"time"
log "github.com/sirupsen/logrus"
"github.com/weaveworks/scope/common/xfer"
"github.com/weaveworks/scope/probe/controls"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/report"
)
// TaskFamily is the key that stores the task family of an ECS Task
const (
Cluster = report.ECSCluster
CreatedAt = report.ECSCreatedAt
TaskFamily = report.ECSTaskFamily
ServiceDesiredCount = report.ECSServiceDesiredCount
ServiceRunningCount = report.ECSServiceRunningCount
ScaleUp = report.ECSScaleUp
ScaleDown = report.ECSScaleDown
)
var (
taskMetadata = report.MetadataTemplates{
Cluster: {ID: Cluster, Label: "Cluster", From: report.FromLatest, Priority: 0},
CreatedAt: {ID: CreatedAt, Label: "Created at", From: report.FromLatest, Priority: 1, Datatype: report.DateTime},
TaskFamily: {ID: TaskFamily, Label: "Family", From: report.FromLatest, Priority: 2},
}
serviceMetadata = report.MetadataTemplates{
Cluster: {ID: Cluster, Label: "Cluster", From: report.FromLatest, Priority: 0},
CreatedAt: {ID: CreatedAt, Label: "Created at", From: report.FromLatest, Priority: 1, Datatype: report.DateTime},
ServiceDesiredCount: {ID: ServiceDesiredCount, Label: "Desired tasks", From: report.FromLatest, Priority: 2, Datatype: report.Number},
ServiceRunningCount: {ID: ServiceRunningCount, Label: "Running tasks", From: report.FromLatest, Priority: 3, Datatype: report.Number},
}
)
// TaskLabelInfo is used in return value of GetLabelInfo. Exported for test.
type TaskLabelInfo struct {
ContainerIDs []string
Family string
}
// GetLabelInfo returns map from cluster to map of task arns to task infos.
// Exported for test.
func GetLabelInfo(rpt report.Report) map[string]map[string]*TaskLabelInfo {
results := map[string]map[string]*TaskLabelInfo{}
log.Debug("scanning for ECS containers")
for nodeID, node := range rpt.Container.Nodes {
taskArn, ok := node.Latest.Lookup(docker.LabelPrefix + "com.amazonaws.ecs.task-arn")
if !ok {
continue
}
cluster, ok := node.Latest.Lookup(docker.LabelPrefix + "com.amazonaws.ecs.cluster")
if !ok {
continue
}
family, ok := node.Latest.Lookup(docker.LabelPrefix + "com.amazonaws.ecs.task-definition-family")
if !ok {
continue
}
taskMap, ok := results[cluster]
if !ok {
taskMap = map[string]*TaskLabelInfo{}
results[cluster] = taskMap
}
task, ok := taskMap[taskArn]
if !ok {
task = &TaskLabelInfo{ContainerIDs: []string{}, Family: family}
taskMap[taskArn] = task
}
task.ContainerIDs = append(task.ContainerIDs, nodeID)
}
log.Debugf("Got ECS container info: %v", results)
return results
}
// Reporter implements Tagger, Reporter
type Reporter struct {
ClientsByCluster map[string]EcsClient // Exported for test
cacheSize int
cacheExpiry time.Duration
clusterRegion string
handlerRegistry *controls.HandlerRegistry
probeID string
}
// Make creates a new Reporter
func Make(cacheSize int, cacheExpiry time.Duration, clusterRegion string, handlerRegistry *controls.HandlerRegistry, probeID string) Reporter {
r := Reporter{
ClientsByCluster: map[string]EcsClient{},
cacheSize: cacheSize,
cacheExpiry: cacheExpiry,
clusterRegion: clusterRegion,
handlerRegistry: handlerRegistry,
probeID: probeID,
}
handlerRegistry.Batch(nil, map[string]xfer.ControlHandlerFunc{
ScaleUp: r.controlScaleUp,
ScaleDown: r.controlScaleDown,
})
return r
}
func (r Reporter) getClient(cluster string) (EcsClient, error) {
client, ok := r.ClientsByCluster[cluster]
if !ok {
log.Debugf("Creating new ECS client")
var err error
client, err = newClient(cluster, r.cacheSize, r.cacheExpiry, r.clusterRegion)
if err != nil {
return nil, err
}
r.ClientsByCluster[cluster] = client
}
return client, nil
}
// Tag needed for Tagger
func (r Reporter) Tag(rpt report.Report) (report.Report, error) {
rpt = rpt.Copy()
clusterMap := GetLabelInfo(rpt)
for cluster, taskMap := range clusterMap {
log.Debugf("Fetching ECS info for cluster %v with %v tasks", cluster, len(taskMap))
client, err := r.getClient(cluster)
if err != nil {
return rpt, nil
}
taskArns := make([]string, 0, len(taskMap))
for taskArn := range taskMap {
taskArns = append(taskArns, taskArn)
}
ecsInfo := client.GetInfo(taskArns)
log.Debugf("Got info from ECS: %d tasks, %d services", len(ecsInfo.Tasks), len(ecsInfo.Services))
// Create all the services first
for serviceName, service := range ecsInfo.Services {
serviceID := report.MakeECSServiceNodeID(cluster, serviceName)
activeControls := []string{ScaleUp}
// Disable ScaleDown when only 1 task is desired, since
// scaling down to 0 would cause the service to disappear (#2085)
if service.DesiredCount > 1 {
activeControls = append(activeControls, ScaleDown)
}
rpt.ECSService.AddNode(report.MakeNodeWith(serviceID, map[string]string{
Cluster: cluster,
ServiceDesiredCount: fmt.Sprintf("%d", service.DesiredCount),
ServiceRunningCount: fmt.Sprintf("%d", service.RunningCount),
report.ControlProbeID: r.probeID,
}).WithLatestActiveControls(activeControls...))
}
log.Debugf("Created %v ECS service nodes", len(ecsInfo.Services))
for taskArn, info := range taskMap {
task, ok := ecsInfo.Tasks[taskArn]
if !ok {
// can happen due to partial failures, just skip it
continue
}
// new task node
taskID := report.MakeECSTaskNodeID(taskArn)
node := report.MakeNodeWith(taskID, map[string]string{
TaskFamily: info.Family,
Cluster: cluster,
CreatedAt: task.CreatedAt.Format(time.RFC3339Nano),
})
rpt.ECSTask.AddNode(node)
// parents sets to merge into all matching container nodes
parentsSets := report.MakeSets()
parentsSets = parentsSets.AddString(report.ECSTask, taskID)
if serviceName, ok := ecsInfo.TaskServiceMap[taskArn]; ok {
serviceID := report.MakeECSServiceNodeID(cluster, serviceName)
parentsSets = parentsSets.AddString(report.ECSService, serviceID)
// in addition, make service parent of task
rpt.ECSTask.Nodes[taskID] = rpt.ECSTask.Nodes[taskID].WithParent(report.ECSService, serviceID)
}
for _, containerID := range info.ContainerIDs {
if containerNode, ok := rpt.Container.Nodes[containerID]; ok {
rpt.Container.Nodes[containerID] = containerNode.WithParents(parentsSets)
} else {
log.Warnf("Got task info for non-existent container %v, this shouldn't be able to happen", containerID)
}
}
}
}
return rpt, nil
}
// Report needed for Reporter
func (Reporter) Report() (report.Report, error) {
result := report.MakeReport()
taskTopology := report.MakeTopology().WithMetadataTemplates(taskMetadata)
result.ECSTask = result.ECSTask.Merge(taskTopology)
serviceTopology := report.MakeTopology().WithMetadataTemplates(serviceMetadata)
serviceTopology.Controls.AddControls([]report.Control{
{
ID: ScaleDown,
Human: "Scale down",
Icon: "fa fa-minus",
Rank: 0,
},
{
ID: ScaleUp,
Human: "Scale up",
Icon: "fa fa-plus",
Rank: 1,
},
})
result.ECSService = result.ECSService.Merge(serviceTopology)
return result, nil
}
// Name needed for Tagger, Reporter
func (r Reporter) Name() string {
return "awsecs"
}
// Stop unregisters controls.
func (r *Reporter) Stop() {
r.handlerRegistry.Batch([]string{
ScaleUp,
ScaleDown,
}, nil)
}
func (r *Reporter) controlScaleUp(req xfer.Request) xfer.Response {
return xfer.ResponseError(r.controlScale(req, 1))
}
func (r *Reporter) controlScaleDown(req xfer.Request) xfer.Response {
return xfer.ResponseError(r.controlScale(req, -1))
}
func (r *Reporter) controlScale(req xfer.Request, amount int) error {
cluster, serviceName, ok := report.ParseECSServiceNodeID(req.NodeID)
if !ok {
return fmt.Errorf("Bad node ID")
}
client, err := r.getClient(cluster)
if err != nil {
return err
}
return client.ScaleService(serviceName, amount)
}