Skip to content

Commit

Permalink
Add secrets fetching and refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
qbart committed Dec 27, 2021
1 parent c919dbd commit 18f4432
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 109 deletions.
86 changes: 86 additions & 0 deletions awscmd/ecs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package awscmd

import (
"context"
"fmt"

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

type InputEcsDeploy struct {
Region string
Cluster string
Service string
DockerImage string
}

type OuputEcsDeploy struct {
Service string
}

func EcsDeploy(ctx context.Context, input *InputEcsDeploy) (*OuputEcsDeploy, error) {
sess, err := NewSession(input.Region)
if err != nil {
return nil, err
}
svc := ecs.New(sess)

// 1. fetch running task
serviceOut, err := svc.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{
Cluster: aws.String(input.Cluster),
Services: []*string{aws.String(input.Service)},
})

if err != nil {
return nil, fmt.Errorf("Failed to fetch current running task: %w", err)
}

if len(serviceOut.Services) == 0 {
return nil, fmt.Errorf("No service definition found")
}
if len(serviceOut.Services) != 1 {
// this should not happen because we defined only 1 service in input but stays for sanity check
return nil, fmt.Errorf("Ambigious match, found more than 1 service")
}

var taskDefinition *string
for _, s := range serviceOut.Services {
taskDefinition = s.TaskDefinition
}

// 2. build new task definition
taskDefinitionOut, err := svc.DescribeTaskDefinitionWithContext(ctx, &ecs.DescribeTaskDefinitionInput{
TaskDefinition: taskDefinition,
})

containerDefinitions := []*ecs.ContainerDefinition{}
for _, container := range taskDefinitionOut.TaskDefinition.ContainerDefinitions {
container.Image = aws.String(input.DockerImage)
containerDefinitions = append(containerDefinitions, container)
}

// 3. register new task revision
registerOut, err := svc.RegisterTaskDefinitionWithContext(ctx, &ecs.RegisterTaskDefinitionInput{
ContainerDefinitions: containerDefinitions,
Family: taskDefinitionOut.TaskDefinition.Family,
})
if err != nil {
return nil, fmt.Errorf("Failed to register new task revision: %w", err)
}
arn := registerOut.TaskDefinition.TaskDefinitionArn

// 4. update ecs service with new task arn
updateOut, err := svc.UpdateServiceWithContext(ctx, &ecs.UpdateServiceInput{
Cluster: aws.String(input.Cluster),
Service: aws.String(input.Service),
TaskDefinition: arn,
})
if err != nil {
return nil, fmt.Errorf("Failed to update service with new task revision: %w", err)
}

return &OuputEcsDeploy{
Service: *updateOut.Service.ServiceName,
}, nil
}
49 changes: 49 additions & 0 deletions awscmd/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package awscmd

import (
"context"
"encoding/json"
"fmt"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/secretsmanager"
)

type InputSecretsAll struct {
Region string
ID string
}

type OutputSecretsAll struct {
Secrets map[string]string
}

func SecretsAll(ctx context.Context, input *InputSecretsAll) (*OutputSecretsAll, error) {
sess, err := NewSession(input.Region)
if err != nil {
return nil, err
}

svc := secretsmanager.New(sess, aws.NewConfig().WithRegion(input.Region))
secretsInput := &secretsmanager.GetSecretValueInput{
SecretId: aws.String(input.ID),
VersionStage: aws.String("AWSCURRENT"),
}
result, err := svc.GetSecretValue(secretsInput)
if err != nil {
return nil, fmt.Errorf("Failed to get secret value: %w", err)
}

var out OutputSecretsAll
if result.SecretString != nil {
err = json.NewDecoder(strings.NewReader(*result.SecretString)).Decode(&out.Secrets)
if err != nil {
return nil, fmt.Errorf("Failed to decode secrets into key-value map: %w", err)
}
} else {
return nil, fmt.Errorf("Failed to fetch secret value (SecretString is empty): %w", err)
}

return &out, nil
}
17 changes: 17 additions & 0 deletions awscmd/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package awscmd

import (
"fmt"

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

func NewSession(region string) (*session.Session, error) {
sess, err := session.NewSession(&aws.Config{Region: aws.String(region)})
if err != nil {
return nil, fmt.Errorf("Failed to initiate new session: %w", err)
}

return sess, nil
}
132 changes: 23 additions & 109 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,16 @@ package main

import (
"context"
"encoding/json"
"fmt"
"os"
"strings"

"github.com/Selleo/cli/awscmd"
"github.com/Selleo/cli/selleo"
"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/aws/aws-sdk-go/service/secretsmanager"
"github.com/Selleo/cli/shellcmd"
"github.com/urfave/cli/v2"
"github.com/wzshiming/ctc"
)

type AwsEcsDeployInput struct {
Region *string
Cluster *string
Service *string
DockerImage *string
}

type AwsSecretsManagerExportInput struct {
Region *string
ID *string
}

func main() {
app := &cli.App{
Commands: []*cli.Command{
Expand Down Expand Up @@ -55,37 +39,17 @@ func main() {
&cli.StringFlag{Name: "id", Usage: "Secrets ID", Required: true},
},
Action: func(c *cli.Context) error {
actionInput := AwsSecretsManagerExportInput{
Region: aws.String(c.String("region")),
ID: aws.String(c.String("id")),
input := &awscmd.InputSecretsAll{
Region: c.String("region"),
ID: c.String("id"),
}
sess, err := session.NewSession(&aws.Config{Region: actionInput.Region})
out, err := awscmd.SecretsAll(context.TODO(), input)
if err != nil {
return fmt.Errorf("Failed to initiate new session: %w", err)
return err
}

svc := secretsmanager.New(sess, aws.NewConfig().WithRegion(*actionInput.Region))
input := &secretsmanager.GetSecretValueInput{
SecretId: actionInput.ID,
VersionStage: aws.String("AWSCURRENT"),
}
result, err := svc.GetSecretValue(input)
if err != nil {
return fmt.Errorf("Failed to get secret value: %w", err)
}
if result.SecretString != nil {
var kv map[string]string
err = json.NewDecoder(strings.NewReader(*result.SecretString)).Decode(&kv)
if err != nil {
return fmt.Errorf("Failed to decode secrets into key-value map: %w", err)
}
for k, v := range kv {
escaped := strings.ReplaceAll(v, `"`, `\"`)
fmt.Fprint(c.App.Writer, "export ", k, "=", `"`, escaped, `"`, "\n")
}
} else {
return fmt.Errorf("Failed to fetch secret value (SecretString is empty): %w", err)
}
shellcmd.KeyValueToExports(c.App.Writer, out.Secrets)

return nil
},
},
Expand All @@ -105,74 +69,24 @@ func main() {
&cli.StringFlag{Name: "docker-image", Usage: "Docker image to replace task definition with", Required: true},
},
Action: func(c *cli.Context) error {
actionInput := AwsEcsDeployInput{
Region: aws.String(c.String("region")),
Cluster: aws.String(c.String("cluster")),
Service: aws.String(c.String("service")),
DockerImage: aws.String(c.String("docker-image")),
}

ses, err := session.NewSession(&aws.Config{Region: actionInput.Region})
if err != nil {
return fmt.Errorf("Failed to initiate new session: %w", err)
}
svc := ecs.New(ses)

// 1. fetch running task
serviceOut, err := svc.DescribeServicesWithContext(context.TODO(), &ecs.DescribeServicesInput{
Cluster: actionInput.Cluster,
Services: []*string{actionInput.Service},
})

if err != nil {
return fmt.Errorf("Failed to fetch current running task: %w", err)
}

if len(serviceOut.Services) == 0 {
return fmt.Errorf("No service definition found")
}
if len(serviceOut.Services) != 1 {
// this should not happen because we defined only 1 service in input but stays for sanity check
return fmt.Errorf("Ambigious match, found more than 1 service")
}

var taskDefinition *string
for _, s := range serviceOut.Services {
taskDefinition = s.TaskDefinition
input := &awscmd.InputEcsDeploy{
Region: c.String("region"),
Cluster: c.String("cluster"),
Service: c.String("service"),
DockerImage: c.String("docker-image"),
}

// 2. build new task definition
taskDefinitionOut, err := svc.DescribeTaskDefinitionWithContext(context.TODO(), &ecs.DescribeTaskDefinitionInput{
TaskDefinition: taskDefinition,
})

containerDefinitions := []*ecs.ContainerDefinition{}
for _, container := range taskDefinitionOut.TaskDefinition.ContainerDefinitions {
container.Image = actionInput.DockerImage
containerDefinitions = append(containerDefinitions, container)
}

// 3. register new task revision
registerOut, err := svc.RegisterTaskDefinitionWithContext(context.TODO(), &ecs.RegisterTaskDefinitionInput{
ContainerDefinitions: containerDefinitions,
Family: taskDefinitionOut.TaskDefinition.Family,
})
if err != nil {
return fmt.Errorf("Failed to register new task revision: %w", err)
}
arn := registerOut.TaskDefinition.TaskDefinitionArn

// 4. update ecs service with new task arn
updateOut, err := svc.UpdateServiceWithContext(context.TODO(), &ecs.UpdateServiceInput{
Cluster: actionInput.Cluster,
Service: actionInput.Service,
TaskDefinition: arn,
})
out, err := awscmd.EcsDeploy(context.TODO(), input)
if err != nil {
return fmt.Errorf("Failed to update service with new task revision: %w", err)
return err
}

fmt.Fprintf(c.App.Writer, "%sNew deployment for service `%s` created%s\n", ctc.ForegroundGreen, *updateOut.Service.ServiceName, ctc.Reset)
fmt.Fprintf(
c.App.Writer,
"%sNew deployment for service `%s` created%s\n",
ctc.ForegroundGreen,
out.Service,
ctc.Reset,
)

return nil
},
Expand Down
14 changes: 14 additions & 0 deletions shellcmd/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package shellcmd

import (
"fmt"
"io"
"strings"
)

func KeyValueToExports(w io.Writer, kv map[string]string) {
for k, v := range kv {
escaped := strings.ReplaceAll(v, `"`, `\"`)
fmt.Fprint(w, "export ", k, "=", `"`, escaped, `"`, "\n")
}
}

0 comments on commit 18f4432

Please sign in to comment.