Skip to content

Commit

Permalink
chore(stack): add task to deploy/cfn/stack package and add template f…
Browse files Browse the repository at this point in the history
…or task (#1101)

This PR adds a CloudFormation template for a task stack, and implement stack configuration methods for task.

Related #702 

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
  • Loading branch information
Lou1415926 authored Jul 9, 2020
1 parent bdb8f3a commit 6ade3a2
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 0 deletions.
5 changes: 5 additions & 0 deletions internal/pkg/deploy/cloudformation/stack/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ func NameForService(app, env, svc string) string {
func NameForEnv(app, env string) string {
return fmt.Sprintf("%s-%s", app, env)
}

// NameForTask returns the stack name for a task.
func NameForTask(task string) string {
return fmt.Sprintf("task-%s", task)
}
111 changes: 111 additions & 0 deletions internal/pkg/deploy/cloudformation/stack/task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package stack

import (
"fmt"
"github.com/aws/copilot-cli/internal/pkg/config"
"strconv"

"github.com/aws/copilot-cli/internal/pkg/deploy"
"github.com/aws/copilot-cli/internal/pkg/template"

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

const (
taskTemplatePath = "task/cf.yml"

taskNameParamKey = "TaskName"
taskCPUParamKey = "TaskCPU"
taskMemoryParamKey = "TaskMemory"
taskLogRetentionParamKey = "LogRetention"

taskContainerImageParamKey = "ContainerImage"
taskTaskRoleParamKey = "TaskRole"
taskCommandParamKey = "Command"

taskLogRetentionInDays = "1"
)

type taskStackConfig struct {
*deploy.CreateTaskResourcesInput
parser template.ReadParser
}

// NewTaskStackConfig sets up a struct that provides stack configurations for CloudFormation
// to deploy the task resources stack.
func NewTaskStackConfig(taskOpts *deploy.CreateTaskResourcesInput) *taskStackConfig {
return &taskStackConfig{
CreateTaskResourcesInput: taskOpts,
parser: template.New(),
}
}

// StackName returns the name of the CloudFormation stack for the task.
func (t *taskStackConfig) StackName() string {
return NameForTask(t.Name)
}

// Template returns the task CloudFormation template.
func (t *taskStackConfig) Template() (string, error) {
content, err := t.parser.Parse(taskTemplatePath, struct{
EnvVars map[string]string
}{
EnvVars: t.EnvVars,
})
if err != nil {
return "", fmt.Errorf("read template for task stack: %w", err)
}
return content.String(), nil
}

// Parameters returns the parameter values to be passed to the task CloudFormation template.
func (t *taskStackConfig) Parameters() ([]*cloudformation.Parameter, error) {
return []*cloudformation.Parameter{
{
ParameterKey: aws.String(taskNameParamKey),
ParameterValue: aws.String(t.Name),
},
{
ParameterKey: aws.String(taskCPUParamKey),
ParameterValue: aws.String(strconv.Itoa(t.CPU)),
},
{
ParameterKey: aws.String(taskMemoryParamKey),
ParameterValue: aws.String(strconv.Itoa(t.Memory)),
},
{
ParameterKey: aws.String(taskLogRetentionParamKey),
ParameterValue: aws.String(taskLogRetentionInDays),
},
{
ParameterKey: aws.String(taskContainerImageParamKey),
ParameterValue: aws.String(t.Image),
},
{
ParameterKey: aws.String(taskTaskRoleParamKey),
ParameterValue: aws.String(t.TaskRole),
},
{
ParameterKey: aws.String(taskCommandParamKey),
ParameterValue: aws.String(t.Command),
},
}, nil
}

// Tags returns the tags that should be applied to the task CloudFormation.
func (t *taskStackConfig) Tags() []*cloudformation.Tag {
appEnvTags := make(map[string]string)

if t.Env != config.EnvNameNone {
appEnvTags[deploy.AppTagKey] = t.App
appEnvTags[deploy.EnvTagKey] = t.Env
}

return mergeAndFlattenTags(appEnvTags, map[string]string{
deploy.TaskTagKey: t.Name,
})
}
191 changes: 191 additions & 0 deletions internal/pkg/deploy/cloudformation/stack/task_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package stack

import (
"bytes"
"errors"
"fmt"
"github.com/aws/copilot-cli/internal/pkg/config"
"github.com/aws/copilot-cli/internal/pkg/deploy"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/copilot-cli/internal/pkg/template"
"github.com/aws/copilot-cli/internal/pkg/template/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

const (
testTaskName = "my-task"
)

func TestTaskStackConfig_Template(t *testing.T) {
testCases := map[string]struct {
mockReadParser func(m *mocks.MockReadParser)

wantedTemplate string
wantedError error
}{
"should return error if unable to read": {
mockReadParser: func(m *mocks.MockReadParser) {
m.EXPECT().Parse(taskTemplatePath, gomock.Any()).Return(nil, errors.New("error reading template"))
},
wantedError: errors.New("read template for task stack: error reading template"),
},
"should return template body when present": {
mockReadParser: func(m *mocks.MockReadParser) {
m.EXPECT().Parse(taskTemplatePath, gomock.Any()).Return(&template.Content{
Buffer: bytes.NewBufferString("This is the task template"),
}, nil)
},
wantedTemplate: "This is the task template",
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockReadParser := mocks.NewMockReadParser(ctrl)
if tc.mockReadParser != nil {
tc.mockReadParser(mockReadParser)
}

taskInput := deploy.CreateTaskResourcesInput{}

taskStackConfig := &taskStackConfig{
CreateTaskResourcesInput: &taskInput,
parser: mockReadParser,
}

got, err := taskStackConfig.Template()

if tc.wantedError != nil {
require.EqualError(t, tc.wantedError, err.Error())
} else {
require.Equal(t, tc.wantedTemplate, got)
}
})
}
}

func TestTaskStackConfig_Parameters(t *testing.T) {
expectedParams := []*cloudformation.Parameter{
{
ParameterKey: aws.String(taskNameParamKey),
ParameterValue: aws.String("my-task"),
},
{
ParameterKey: aws.String(taskContainerImageParamKey),
ParameterValue: aws.String("7456.dkr.ecr.us-east-2.amazonaws.com/my-task:0.1"),
},
{
ParameterKey: aws.String(taskCPUParamKey),
ParameterValue: aws.String("256"),
},
{
ParameterKey: aws.String(taskMemoryParamKey),
ParameterValue: aws.String("512"),
},
{
ParameterKey: aws.String(taskLogRetentionParamKey),
ParameterValue: aws.String(taskLogRetentionInDays),
},
{
ParameterKey: aws.String(taskTaskRoleParamKey),
ParameterValue: aws.String("task-role"),
},
{
ParameterKey: aws.String(taskCommandParamKey),
ParameterValue: aws.String("echo hooray"),
},
}

taskInput := deploy.CreateTaskResourcesInput{
Name: "my-task",
CPU: 256,
Memory: 512,

Image: "7456.dkr.ecr.us-east-2.amazonaws.com/my-task:0.1",
TaskRole: "task-role",
Command: "echo hooray",
}

task := &taskStackConfig{
CreateTaskResourcesInput: &taskInput,
}
params, _ := task.Parameters()
require.ElementsMatch(t, expectedParams, params)
}

func TestTaskStackConfig_StackName(t *testing.T) {
taskInput := deploy.CreateTaskResourcesInput{
Name: "my-task",
}

task := &taskStackConfig{
CreateTaskResourcesInput: &taskInput,
}
got := task.StackName()
require.Equal(t, got, fmt.Sprintf("task-%s", testTaskName))
}

func TestTaskStackConfig_Tags(t *testing.T) {
testCases := map[string]struct {
input deploy.CreateTaskResourcesInput

expectedTags []*cloudformation.Tag
}{
"with app and env": {
input: deploy.CreateTaskResourcesInput{
Name: "my-task",

App: "my-app",
Env: "test",
},

expectedTags: []*cloudformation.Tag{
{
Key: aws.String(deploy.TaskTagKey),
Value: aws.String("my-task"),
},
{
Key: aws.String(deploy.AppTagKey),
Value: aws.String("my-app"),
},
{
Key: aws.String(deploy.EnvTagKey),
Value: aws.String("test"),
},
},
},
"input without app or env": {
input: deploy.CreateTaskResourcesInput{
Name: "my-task",

Env: config.EnvNameNone,
},

expectedTags: []*cloudformation.Tag{
{
Key: aws.String(deploy.TaskTagKey),
Value: aws.String("my-task"),
},
},
},
}


for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
taskStackConfig := &taskStackConfig{
CreateTaskResourcesInput: &tc.input,
}
tags := taskStackConfig.Tags()

require.ElementsMatch(t, tc.expectedTags, tags)
})
}
}
2 changes: 2 additions & 0 deletions internal/pkg/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const (
EnvTagKey = "copilot-environment"
// ServiceTagKey is tag key for Copilot svc.
ServiceTagKey = "copilot-service"
// TaskTagKey is tag key for Copilot task.
TaskTagKey = "copilot-task"
)

const (
Expand Down
21 changes: 21 additions & 0 deletions internal/pkg/deploy/task.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package deploy holds the structures to deploy infrastructure resources.
// This file defines service deployment resources.
package deploy

// CreateTaskResourcesInput holds the fields required to create a task stack.
type CreateTaskResourcesInput struct {
Name string
CPU int
Memory int

Image string
TaskRole string
Command string
EnvVars map[string]string

App string
Env string
}
Loading

0 comments on commit 6ade3a2

Please sign in to comment.