Skip to content

Commit fc725f9

Browse files
committed
refactor: Improve application layer by restructuring dependencies
1 parent 95684a6 commit fc725f9

File tree

8 files changed

+512
-342
lines changed

8 files changed

+512
-342
lines changed

init.go

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@ import (
44
"context"
55
"errors"
66
"flag"
7+
"time"
78

8-
"github.com/aws/aws-sdk-go-v2/config"
9-
"github.com/aws/aws-sdk-go-v2/service/ecs"
10-
)
11-
12-
var (
13-
ecsClient *ecs.Client
9+
"github.com/manabusakai/tdtidy/internal/ecs"
1410
)
1511

1612
var (
@@ -19,23 +15,11 @@ var (
1915
familyPrefix = flag.String("family-prefix", "", "Specify the family name of the task definitions. If specified, filter by family name.")
2016
)
2117

22-
const (
23-
Deregister command = "deregister"
24-
Delete command = "delete"
25-
)
26-
2718
func New(ctx context.Context) (*App, error) {
28-
cfg, err := config.LoadDefaultConfig(ctx)
29-
if err != nil {
30-
return nil, err
31-
}
32-
ecsClient = ecs.NewFromConfig(cfg)
33-
3419
opt, err := initOption()
3520
if err != nil {
3621
return nil, err
3722
}
38-
3923
return &App{
4024
ctx: ctx,
4125
opt: opt,
@@ -52,9 +36,6 @@ func initOption() (*option, error) {
5236
cmd, args := args[0], args[1:]
5337
flag.CommandLine.Parse(args)
5438

55-
debug.Printf("subcommand: %s", cmd)
56-
debug.Printf("dryRun: %t, retentionPeriod: %d, familyPrefix: %q", *dryRun, *retentionPeriod, *familyPrefix)
57-
5839
if *familyPrefix == "" {
5940
familyPrefix = nil
6041
}
@@ -66,3 +47,40 @@ func initOption() (*option, error) {
6647
familyPrefix: familyPrefix,
6748
}, nil
6849
}
50+
51+
type command string
52+
53+
const (
54+
Deregister command = "deregister"
55+
Delete command = "delete"
56+
)
57+
58+
type option struct {
59+
subcommand command
60+
dryRun *bool
61+
retentionPeriod *int
62+
familyPrefix *string
63+
}
64+
65+
func (opt *option) threshold() time.Time {
66+
return time.Now().AddDate(0, 0, -(*opt.retentionPeriod)).UTC()
67+
}
68+
69+
type App struct {
70+
ctx context.Context
71+
opt *option
72+
}
73+
74+
func (app *App) Run() {
75+
debug.Printf("options: {subcommand: %s, dryRun: %t, retentionPeriod: %d, familyPrefix: %q}",
76+
app.opt.subcommand,
77+
*app.opt.dryRun,
78+
*app.opt.retentionPeriod,
79+
*app.opt.familyPrefix,
80+
)
81+
debug.Printf("threshold: %s", app.opt.threshold().Format(time.DateTime))
82+
83+
client := ecs.NewClient(app.ctx)
84+
processor := NewProcessor(client, app.opt)
85+
processor.Process()
86+
}

internal/ecs/client.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package ecs
2+
3+
import (
4+
"context"
5+
"log"
6+
"time"
7+
8+
"github.com/aws/aws-sdk-go-v2/config"
9+
"github.com/aws/aws-sdk-go-v2/service/ecs"
10+
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
11+
"github.com/samber/lo"
12+
)
13+
14+
const (
15+
// The refill rate for API actions per second.
16+
// https://docs.aws.amazon.com/AmazonECS/latest/APIReference/request-throttling.html
17+
refillRate = 1
18+
)
19+
20+
type TaskDefinition struct {
21+
Arn *string
22+
Family *string
23+
Revision int32
24+
RegisteredAt *time.Time
25+
DeregisteredAt *time.Time
26+
}
27+
28+
type TaskDefinitionStatus types.TaskDefinitionStatus
29+
30+
const (
31+
TaskDefinitionStatusActive TaskDefinitionStatus = "ACTIVE"
32+
TaskDefinitionStatusInactive TaskDefinitionStatus = "INACTIVE"
33+
)
34+
35+
type Client interface {
36+
ListTaskDefinitionStatus() []TaskDefinitionStatus
37+
ListTaskDefinitions(familyPrefix *string, status TaskDefinitionStatus) ([]TaskDefinition, error)
38+
DeregisterTaskDefinitions(tds []TaskDefinition) ([]TaskDefinition, error)
39+
DeleteTaskDefinitions(tds []TaskDefinition) ([]TaskDefinition, error)
40+
}
41+
42+
type client struct {
43+
ctx context.Context
44+
ecsClient *ecs.Client
45+
}
46+
47+
func NewClient(ctx context.Context) Client {
48+
cfg, err := config.LoadDefaultConfig(ctx)
49+
if err != nil {
50+
log.Fatal(err)
51+
}
52+
ecsClient := ecs.NewFromConfig(cfg)
53+
return &client{
54+
ctx: ctx,
55+
ecsClient: ecsClient,
56+
}
57+
}
58+
59+
func (c *client) ListTaskDefinitionStatus() []TaskDefinitionStatus {
60+
return []TaskDefinitionStatus{
61+
TaskDefinitionStatusActive,
62+
TaskDefinitionStatusInactive,
63+
}
64+
}
65+
66+
func (c *client) ListTaskDefinitions(familyPrefix *string, status TaskDefinitionStatus) ([]TaskDefinition, error) {
67+
out := make([]TaskDefinition, 0)
68+
p := ecs.NewListTaskDefinitionsPaginator(c.ecsClient, &ecs.ListTaskDefinitionsInput{
69+
FamilyPrefix: familyPrefix,
70+
Status: types.TaskDefinitionStatus(status),
71+
})
72+
for p.HasMorePages() {
73+
res, err := p.NextPage(c.ctx)
74+
if err != nil {
75+
return nil, err
76+
}
77+
for _, arn := range res.TaskDefinitionArns {
78+
res, err := c.ecsClient.DescribeTaskDefinition(c.ctx, &ecs.DescribeTaskDefinitionInput{
79+
TaskDefinition: &arn,
80+
})
81+
if err != nil {
82+
return nil, err
83+
}
84+
out = append(out, mapToTaskDefinition(res.TaskDefinition))
85+
}
86+
}
87+
return out, nil
88+
}
89+
90+
func (c *client) DeregisterTaskDefinitions(tds []TaskDefinition) ([]TaskDefinition, error) {
91+
out := make([]TaskDefinition, 0)
92+
for _, td := range tds {
93+
res, err := c.ecsClient.DeregisterTaskDefinition(c.ctx, &ecs.DeregisterTaskDefinitionInput{
94+
TaskDefinition: td.Arn,
95+
})
96+
if err != nil {
97+
return nil, err
98+
}
99+
out = append(out, mapToTaskDefinition(res.TaskDefinition))
100+
sleep()
101+
}
102+
return out, nil
103+
}
104+
105+
func (c *client) DeleteTaskDefinitions(tds []TaskDefinition) ([]TaskDefinition, error) {
106+
out := make([]TaskDefinition, 0)
107+
maxDeletions := 10
108+
chunks := lo.Chunk(tds, maxDeletions)
109+
for _, tds := range chunks {
110+
arns := lo.Map(tds, func(td TaskDefinition, _ int) string {
111+
return *td.Arn
112+
})
113+
res, err := c.ecsClient.DeleteTaskDefinitions(c.ctx, &ecs.DeleteTaskDefinitionsInput{
114+
TaskDefinitions: arns,
115+
})
116+
if err != nil {
117+
return nil, err
118+
}
119+
for _, td := range res.TaskDefinitions {
120+
out = append(out, mapToTaskDefinition(&td))
121+
}
122+
sleep()
123+
}
124+
return out, nil
125+
}
126+
127+
func mapToTaskDefinition(td *types.TaskDefinition) TaskDefinition {
128+
return TaskDefinition{
129+
Arn: td.TaskDefinitionArn,
130+
Family: td.Family,
131+
Revision: td.Revision,
132+
RegisteredAt: td.RegisteredAt,
133+
DeregisteredAt: td.DeregisteredAt,
134+
}
135+
}
136+
137+
func sleep() {
138+
time.Sleep(refillRate * time.Second)
139+
}

processor.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package tdtidy
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"time"
7+
8+
"github.com/manabusakai/tdtidy/internal/ecs"
9+
)
10+
11+
type Processor struct {
12+
client ecs.Client
13+
opt *option
14+
}
15+
16+
func NewProcessor(client ecs.Client, opt *option) *Processor {
17+
return &Processor{
18+
client: client,
19+
opt: opt,
20+
}
21+
}
22+
23+
func (p *Processor) Process() {
24+
tds := make(map[ecs.TaskDefinitionStatus][]ecs.TaskDefinition)
25+
for _, status := range p.client.ListTaskDefinitionStatus() {
26+
res, err := p.client.ListTaskDefinitions(p.opt.familyPrefix, status)
27+
if err != nil {
28+
log.Fatal(err)
29+
}
30+
tds[status] = p.filterTaskDefinitions(res)
31+
}
32+
33+
err := p.executeSubcommand(tds)
34+
if err != nil {
35+
log.Fatal(err)
36+
}
37+
}
38+
39+
func (p *Processor) filterTaskDefinitions(tds []ecs.TaskDefinition) []ecs.TaskDefinition {
40+
filteredTds := make([]ecs.TaskDefinition, 0)
41+
for _, td := range tds {
42+
if p.isValidTaskDefinition(&td, p.opt.threshold()) {
43+
filteredTds = append(filteredTds, td)
44+
}
45+
}
46+
return filteredTds
47+
}
48+
49+
func (p *Processor) isValidTaskDefinition(td *ecs.TaskDefinition, threshold time.Time) bool {
50+
// Check if RegisteredAt is missing
51+
if td.RegisteredAt == nil {
52+
return false
53+
}
54+
// If not deregistered, RegisteredAt should be before threshold
55+
if td.DeregisteredAt == nil && td.RegisteredAt.After(threshold) {
56+
return false
57+
}
58+
// If deregistered, DeregisteredAt should be before threshold
59+
if td.DeregisteredAt != nil && td.DeregisteredAt.After(threshold) {
60+
return false
61+
}
62+
return true
63+
}
64+
65+
func (p *Processor) executeSubcommand(tds map[ecs.TaskDefinitionStatus][]ecs.TaskDefinition) error {
66+
var (
67+
targetTds []ecs.TaskDefinition
68+
action func([]ecs.TaskDefinition) ([]ecs.TaskDefinition, error)
69+
)
70+
switch p.opt.subcommand {
71+
case Deregister:
72+
targetTds = tds[ecs.TaskDefinitionStatusActive]
73+
action = p.client.DeregisterTaskDefinitions
74+
case Delete:
75+
targetTds = tds[ecs.TaskDefinitionStatusInactive]
76+
action = p.client.DeleteTaskDefinitions
77+
default:
78+
return fmt.Errorf("unknown subcommand %q", p.opt.subcommand)
79+
}
80+
81+
if len(targetTds) == 0 {
82+
return nil
83+
}
84+
if *p.opt.dryRun {
85+
p.printSummary(targetTds)
86+
return nil
87+
}
88+
89+
res, err := action(targetTds)
90+
if err != nil {
91+
return err
92+
}
93+
p.printSummary(res)
94+
return nil
95+
}
96+
97+
func (p *Processor) printSummary(tds []ecs.TaskDefinition) {
98+
logPrefixes := map[command]string{
99+
Deregister: "Deregistered",
100+
Delete: "Deleted",
101+
}
102+
logPrefix, ok := logPrefixes[p.opt.subcommand]
103+
if !ok {
104+
logPrefix = "Unknown"
105+
}
106+
if *p.opt.dryRun {
107+
logPrefix = "[dry-run] " + logPrefix
108+
}
109+
for _, td := range tds {
110+
fmt.Printf("%s: %s:%d\n", logPrefix, *td.Family, td.Revision)
111+
}
112+
}

0 commit comments

Comments
 (0)