Skip to content

Commit

Permalink
feat: cage recreate (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
keroxp authored May 29, 2024
1 parent 530797f commit 8129a1a
Show file tree
Hide file tree
Showing 39 changed files with 2,060 additions and 1,112 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[*.go]
indent_size = 4
indent_size = 2
indent_style = tab
[Shakefile]
indent_size = 2
indent_style = tab
indent_style = tab
10 changes: 7 additions & 3 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ jobs:
git diff --exit-code
- run: go build
- run: go build github.com/loilo-inc/canarycage/cli/cage
- run: go test -coverprofile=coverage.txt -covermode=count
- name: Upload Coverage
run: bash <(curl -s https://codecov.io/bash)
- run: go test ./... -coverprofile=coverage.txt -covermode=count
- uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
fail_ci_if_error: true
files: coverage.txt
41 changes: 29 additions & 12 deletions cage.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
//go:generate go run github.com/golang/mock/mockgen -source $GOFILE -destination ../mocks/mock_$GOPACKAGE/$GOFILE -package mock_$GOPACKAGE
package cage

import (
"context"
"time"

"github.com/loilo-inc/canarycage/awsiface"
)
Expand All @@ -10,27 +12,42 @@ type Cage interface {
Up(ctx context.Context) (*UpResult, error)
Run(ctx context.Context, input *RunInput) (*RunResult, error)
RollOut(ctx context.Context) (*RollOutResult, error)
Recreate(ctx context.Context) (*RecreateResult, error)
}

type Time interface {
Now() time.Time
NewTimer(time.Duration) *time.Timer
}

type cage struct {
env *Envars
ecs awsiface.EcsClient
alb awsiface.AlbClient
ec2 awsiface.Ec2Client
Env *Envars
Ecs awsiface.EcsClient
Alb awsiface.AlbClient
Ec2 awsiface.Ec2Client
Time Time
MaxWait time.Duration
}

type Input struct {
Env *Envars
ECS awsiface.EcsClient
ALB awsiface.AlbClient
EC2 awsiface.Ec2Client
Env *Envars
ECS awsiface.EcsClient
ALB awsiface.AlbClient
EC2 awsiface.Ec2Client
Time Time
MaxWait time.Duration
}

func NewCage(input *Input) Cage {
if input.Time == nil {
input.Time = &timeImpl{}
}
return &cage{
env: input.Env,
ecs: input.ECS,
alb: input.ALB,
ec2: input.EC2,
Env: input.Env,
Ecs: input.ECS,
Alb: input.ALB,
Ec2: input.EC2,
Time: input.Time,
MaxWait: 5 * time.Minute,
}
}
94 changes: 88 additions & 6 deletions cli/cage/commands/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,101 @@ package commands

import (
"context"
"io"

"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/aws/aws-sdk-go-v2/service/ecs"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
cage "github.com/loilo-inc/canarycage"
"github.com/loilo-inc/canarycage/cli/cage/prompt"
"github.com/urfave/cli/v2"
"golang.org/x/xerrors"
)

type CageCommands interface {
Up() *cli.Command
RollOut() *cli.Command
Run() *cli.Command
Commands(
envars *cage.Envars,
) []*cli.Command
}

type cageCommands struct {
ctx context.Context
Prompt *prompt.Prompter
cageCliProvier cageCliProvier
}

func NewCageCommands(
stdin io.Reader,
cageCliProvier cageCliProvier,
) CageCommands {
return &cageCommands{
Prompt: prompt.NewPrompter(stdin),
cageCliProvier: cageCliProvier,
}
}

type cageCliProvier = func(envars *cage.Envars) (cage.Cage, error)

func (c *cageCommands) Commands(envars *cage.Envars) []*cli.Command {
return []*cli.Command{
c.Up(envars),
c.RollOut(envars),
c.Run(envars),
c.Recreate(envars),
}
}

func DefalutCageCliProvider(envars *cage.Envars) (cage.Cage, error) {
conf, err := config.LoadDefaultConfig(
context.Background(),
config.WithRegion(envars.Region))
if err != nil {
return nil, xerrors.Errorf("failed to load aws config: %w", err)
}
cagecli := cage.NewCage(&cage.Input{
Env: envars,
ECS: ecs.NewFromConfig(conf),
EC2: ec2.NewFromConfig(conf),
ALB: elasticloadbalancingv2.NewFromConfig(conf),
})
return cagecli, nil
}

func (c *cageCommands) requireArgs(
ctx *cli.Context,
minArgs int,
maxArgs int,
) (dir string, rest []string, err error) {
if ctx.NArg() < minArgs {
return "", nil, xerrors.Errorf("invalid number of arguments. expected at least %d", minArgs)
} else if ctx.NArg() > maxArgs {
return "", nil, xerrors.Errorf("invalid number of arguments. expected at most %d", maxArgs)
}
dir = ctx.Args().First()
rest = ctx.Args().Tail()
return
}

func NewCageCommands(ctx context.Context) CageCommands {
return &cageCommands{ctx: ctx}
func (c *cageCommands) setupCage(
envars *cage.Envars,
dir string,
) (cage.Cage, error) {
td, svc, err := cage.LoadDefinitionsFromFiles(dir)
if err != nil {
return nil, err
}
cage.MergeEnvars(envars, &cage.Envars{
Cluster: *svc.Cluster,
Service: *svc.ServiceName,
TaskDefinitionInput: td,
ServiceDefinitionInput: svc,
})
if err := cage.EnsureEnvars(envars); err != nil {
return nil, err
}
cagecli, err := c.cageCliProvier(envars)
if err != nil {
return nil, err
}
return cagecli, nil
}
112 changes: 112 additions & 0 deletions cli/cage/commands/command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package commands_test

import (
"fmt"
"strings"
"testing"

"github.com/golang/mock/gomock"
cage "github.com/loilo-inc/canarycage"
"github.com/loilo-inc/canarycage/cli/cage/commands"
"github.com/loilo-inc/canarycage/mocks/mock_cage"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)

func TestCommands(t *testing.T) {
region := "ap-notheast-1"
cluster := "cluster"
service := "service"
stdinService := fmt.Sprintf("%s\n%s\n%s\n%s\n", region, cluster, service, "yes")
stdinTask := fmt.Sprintf("%s\n%s\n%s\n", region, cluster, "yes")
setup := func(t *testing.T, input string) (*cli.App, *mock_cage.MockCage) {
ctrl := gomock.NewController(t)
stdin := strings.NewReader(input)
cagecli := mock_cage.NewMockCage(ctrl)
app := cli.NewApp()
app.Commands = commands.NewCageCommands(stdin, func(envars *cage.Envars) (cage.Cage, error) {
return cagecli, nil
}).Commands(&cage.Envars{CI: input == ""})
return app, cagecli
}
t.Run("rollout", func(t *testing.T) {
t.Run("basic", func(t *testing.T) {
app, cagecli := setup(t, stdinService)
cagecli.EXPECT().RollOut(gomock.Any()).Return(&cage.RollOutResult{}, nil)
err := app.Run([]string{"cage", "rollout", "--region", "ap-notheast-1", "../../../fixtures"})
assert.NoError(t, err)
})
t.Run("basic/ci", func(t *testing.T) {
app, cagecli := setup(t, "")
cagecli.EXPECT().RollOut(gomock.Any()).Return(&cage.RollOutResult{}, nil)
err := app.Run([]string{"cage", "rollout", "--region", "ap-notheast-1", "../../../fixtures"})
assert.NoError(t, err)
})
t.Run("error", func(t *testing.T) {
app, cagecli := setup(t, stdinService)
cagecli.EXPECT().RollOut(gomock.Any()).Return(&cage.RollOutResult{}, fmt.Errorf("error"))
err := app.Run([]string{"cage", "rollout", "--region", "ap-notheast-1", "../../../fixtures"})
assert.EqualError(t, err, "error")
})
})
t.Run("recreate", func(t *testing.T) {
t.Run("basic", func(t *testing.T) {
app, cagecli := setup(t, stdinService)
cagecli.EXPECT().Recreate(gomock.Any()).Return(&cage.RecreateResult{}, nil)
err := app.Run([]string{"cage", "recreate", "--region", "ap-notheast-1", "../../../fixtures"})
assert.NoError(t, err)
})
t.Run("basic/ci", func(t *testing.T) {
app, cagecli := setup(t, "")
cagecli.EXPECT().Recreate(gomock.Any()).Return(&cage.RecreateResult{}, nil)
err := app.Run([]string{"cage", "recreate", "--region", "ap-notheast-1", "../../../fixtures"})
assert.NoError(t, err)
})
t.Run("error", func(t *testing.T) {
app, cagecli := setup(t, stdinService)
cagecli.EXPECT().Recreate(gomock.Any()).Return(nil, fmt.Errorf("error"))
err := app.Run([]string{"cage", "recreate", "--region", "ap-notheast-1", "../../../fixtures"})
assert.EqualError(t, err, "error")
})
})
t.Run("up", func(t *testing.T) {
t.Run("basic", func(t *testing.T) {
app, cagecli := setup(t, stdinService)
cagecli.EXPECT().Up(gomock.Any()).Return(&cage.UpResult{}, nil)
err := app.Run([]string{"cage", "up", "--region", "ap-notheast-1", "../../../fixtures"})
assert.NoError(t, err)
})
t.Run("basic/ci", func(t *testing.T) {
app, cagecli := setup(t, "")
cagecli.EXPECT().Up(gomock.Any()).Return(&cage.UpResult{}, nil)
err := app.Run([]string{"cage", "up", "--region", "ap-notheast-1", "../../../fixtures"})
assert.NoError(t, err)
})
t.Run("error", func(t *testing.T) {
app, cagecli := setup(t, stdinService)
cagecli.EXPECT().Up(gomock.Any()).Return(nil, fmt.Errorf("error"))
err := app.Run([]string{"cage", "up", "--region", "ap-notheast-1", "../../../fixtures"})
assert.EqualError(t, err, "error")
})
})
t.Run("run", func(t *testing.T) {
t.Run("basic", func(t *testing.T) {
app, cagecli := setup(t, stdinTask)
cagecli.EXPECT().Run(gomock.Any(), gomock.Any()).Return(&cage.RunResult{}, nil)
err := app.Run([]string{"cage", "run", "--region", "ap-notheast-1", "../../../fixtures", "container", "exec"})
assert.NoError(t, err)
})
t.Run("basic/ci", func(t *testing.T) {
app, cagecli := setup(t, "")
cagecli.EXPECT().Run(gomock.Any(), gomock.Any()).Return(&cage.RunResult{}, nil)
err := app.Run([]string{"cage", "run", "--region", "ap-notheast-1", "../../../fixtures", "container", "exec"})
assert.NoError(t, err)
})
t.Run("error", func(t *testing.T) {
app, cagecli := setup(t, stdinTask)
cagecli.EXPECT().Run(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error"))
err := app.Run([]string{"cage", "run", "--region", "ap-notheast-1", "../../../fixtures", "container", "exec"})
assert.EqualError(t, err, "error")
})
})
}
3 changes: 3 additions & 0 deletions cli/cage/commands/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package commands

type CommandsExport = cageCommands
44 changes: 2 additions & 42 deletions cli/cage/commands/flags.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package commands

import (
"context"

"github.com/apex/log"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/loilo-inc/canarycage"
cage "github.com/loilo-inc/canarycage"
"github.com/urfave/cli/v2"
)

Expand All @@ -15,6 +11,7 @@ func RegionFlag(dest *string) *cli.StringFlag {
EnvVars: []string{cage.RegionKey},
Usage: "aws region for ecs. if not specified, try to load from aws sessions automatically",
Destination: dest,
Required: true,
}
}
func ClusterFlag(dest *string) *cli.StringFlag {
Expand Down Expand Up @@ -51,40 +48,3 @@ func CanaryTaskIdleDurationFlag(dest *int) *cli.IntFlag {
Value: 10,
}
}

func (c *cageCommands) aggregateEnvars(
ctx *cli.Context,
envars *cage.Envars,
) {
cfg, err := config.LoadDefaultConfig(context.Background())
if err != nil {
log.Fatalf(err.Error())
}

if envars.Region != "" {
log.Infof("🗺 region was set: %s", envars.Region)
}

if cfg.Region != "" {
log.Infof("🗺 region was loaded from default config: %s", cfg.Region)
} else {
log.Fatalf("🙄 region must specified by --region flag or aws session")
}

if ctx.NArg() > 0 {
dir := ctx.Args().Get(0)
td, svc, err := cage.LoadDefinitionsFromFiles(dir)
if err != nil {
log.Fatalf(err.Error())
}
cage.MergeEnvars(envars, &cage.Envars{
Cluster: *svc.Cluster,
Service: *svc.ServiceName,
TaskDefinitionInput: td,
ServiceDefinitionInput: svc,
})
}
if err := cage.EnsureEnvars(envars); err != nil {
log.Fatalf(err.Error())
}
}
Loading

0 comments on commit 8129a1a

Please sign in to comment.