Skip to content

Commit

Permalink
chore(ecs): describe job and generate run task request from job workl…
Browse files Browse the repository at this point in the history
…oad (aws#2246)

This PR implement the function to generate a run task request from a deployed job, and the functions to support it.

This PR is rebased from aws#2201 (which is not merged yet). The real changes to this PR are at 
- `ecs/run_task_request.go` (where the information needed to generated a `task run` command is grabbed from a Copilot job)
- `ecs/run_task_request_test.go`
- `aws/stepfunctions/*` (where the client to make API calls to stepfunctions is implemented)

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 committed May 10, 2021
1 parent c1f9857 commit 904dd44
Show file tree
Hide file tree
Showing 11 changed files with 707 additions and 4 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ gen-mocks: tools
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/cloudformation/mocks/mock_cloudformation.go -source=./internal/pkg/aws/cloudformation/interfaces.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/cloudformation/stackset/mocks/mock_stackset.go -source=./internal/pkg/aws/cloudformation/stackset/stackset.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/ssm/mocks/mock_ssm.go -source=./internal/pkg/aws/ssm/ssm.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/aws/stepfunctions/mocks/mock_stepfunctions.go -source=./internal/pkg/aws/stepfunctions/stepfunctions.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/addon/mocks/mock_addons.go -source=./internal/pkg/addon/addons.go
${GOBIN}/mockgen -package=exec -source=./internal/pkg/exec/exec.go -destination=./internal/pkg/exec/mock_exec.go
${GOBIN}/mockgen -package=mocks -destination=./internal/pkg/deploy/mocks/mock_deploy.go -source=./internal/pkg/deploy/deploy.go
Expand Down
4 changes: 4 additions & 0 deletions internal/pkg/aws/resourcegroups/resourcegroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
)

const (
ResourceTypeStateMachine = "states:stateMachine"
)

type api interface {
GetResources(input *resourcegroupstaggingapi.GetResourcesInput) (*resourcegroupstaggingapi.GetResourcesOutput, error)
}
Expand Down
50 changes: 50 additions & 0 deletions internal/pkg/aws/stepfunctions/mocks/mock_stepfunctions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions internal/pkg/aws/stepfunctions/stepfunctions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package stepfunctions provides a client to make API requests to Amazon Step Functions.
package stepfunctions

import (
"fmt"

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

type api interface {
DescribeStateMachine(input *sfn.DescribeStateMachineInput) (*sfn.DescribeStateMachineOutput, error)
}

// StepFunctions wraps an AWS StepFunctions client.
type StepFunctions struct {
client api
}

// New returns StepFunctions configured against the input session.
func New(s *session.Session) *StepFunctions {
return &StepFunctions{
client: sfn.New(s),
}
}

// StateMachineDefinition returns the JSON-based state machine definition.
func (s *StepFunctions) StateMachineDefinition(stateMachineARN string) (string, error) {
out, err := s.client.DescribeStateMachine(&sfn.DescribeStateMachineInput{
StateMachineArn: aws.String(stateMachineARN),
})
if err != nil {
return "", fmt.Errorf("describe state machine: %w", err)
}

return aws.StringValue(out.Definition), nil
}
70 changes: 70 additions & 0 deletions internal/pkg/aws/stepfunctions/stepfunctions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package stepfunctions provides a client to make API requests to Amazon Step Functions.
package stepfunctions

import (
"errors"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/sfn"

"github.com/stretchr/testify/require"

"github.com/aws/copilot-cli/internal/pkg/aws/stepfunctions/mocks"
"github.com/golang/mock/gomock"
)

func TestStepFunctions_StateMachineDefinition(t *testing.T) {
testCases := map[string]struct {
inStateMachineARN string

mockStepFunctionsClient func(m *mocks.Mockapi)

wantedError error
wantedDefinition string
}{
"fail to describe state machine": {
inStateMachineARN: "ninth inning",
mockStepFunctionsClient: func(m *mocks.Mockapi) {
m.EXPECT().DescribeStateMachine(&sfn.DescribeStateMachineInput{
StateMachineArn: aws.String("ninth inning"),
}).Return(nil, errors.New("some error"))
},
wantedError: errors.New("describe state machine: some error"),
},
"success": {
inStateMachineARN: "ninth inning",
mockStepFunctionsClient: func(m *mocks.Mockapi) {
m.EXPECT().DescribeStateMachine(&sfn.DescribeStateMachineInput{
StateMachineArn: aws.String("ninth inning"),
}).Return(&sfn.DescribeStateMachineOutput{
Definition: aws.String("{\n \"Version\": \"42\",\n \"Comment\": \"very important comment\"\n}"),
}, nil)
},
wantedDefinition: "{\n \"Version\": \"42\",\n \"Comment\": \"very important comment\"\n}",
},
}

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

mockStepFunctionsClient := mocks.NewMockapi(ctrl)
tc.mockStepFunctionsClient(mockStepFunctionsClient)
sfn := StepFunctions{
client: mockStepFunctionsClient,
}

out, err := sfn.StateMachineDefinition(tc.inStateMachineARN)
if tc.wantedError != nil {
require.EqualError(t, tc.wantedError, err.Error())
} else {
require.Equal(t, tc.wantedDefinition, out)
}
})
}
}
128 changes: 124 additions & 4 deletions internal/pkg/ecs/ecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
package ecs

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

"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/copilot-cli/internal/pkg/aws/stepfunctions"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/copilot-cli/internal/pkg/aws/ecs"
Expand Down Expand Up @@ -38,6 +42,10 @@ type ecsClient interface {
NetworkConfiguration(cluster, serviceName string) (*ecs.NetworkConfiguration, error)
}

type stepFunctionsClient interface {
StateMachineDefinition(stateMachineARN string) (string, error)
}

// ServiceDesc contains the description of an ECS service.
type ServiceDesc struct {
Name string
Expand All @@ -47,15 +55,17 @@ type ServiceDesc struct {

// Client retrieves Copilot information from ECS endpoint.
type Client struct {
rgGetter resourceGetter
ecsClient ecsClient
rgGetter resourceGetter
ecsClient ecsClient
StepFuncClient stepFunctionsClient
}

// New inits a new Client.
func New(sess *session.Session) *Client {
return &Client{
rgGetter: resourcegroups.New(sess),
ecsClient: ecs.New(sess),
rgGetter: resourcegroups.New(sess),
ecsClient: ecs.New(sess),
StepFuncClient: stepfunctions.New(sess),
}
}

Expand Down Expand Up @@ -220,6 +230,83 @@ func (c Client) NetworkConfiguration(app, env, svc string) (*ecs.NetworkConfigur
return c.ecsClient.NetworkConfiguration(clusterARN, svcName)
}

// NetworkConfigurationForJob returns the network configuration of the job.
func (c Client) NetworkConfigurationForJob(app, env, job string) (*ecs.NetworkConfiguration, error) {
jobARN, err := c.stateMachineARN(app, env, job)
if err != nil {
return nil, err
}

raw, err := c.StepFuncClient.StateMachineDefinition(jobARN)
if err != nil {
return nil, fmt.Errorf("get state machine definition for job %s: %w", job, err)
}

var config NetworkConfiguration
err = json.Unmarshal([]byte(raw), &config)
if err != nil {
return nil, fmt.Errorf("unmarshal state machine definition: %w", err)
}

return (*ecs.NetworkConfiguration)(&config), nil
}

// NetworkConfiguration wraps an ecs.NetworkConfiguration struct.
type NetworkConfiguration ecs.NetworkConfiguration

// UnmarshalJSON implements custom logic to unmarshal only the network configuration from a state machine definition.
// Example state machine definition:
// "Version": "1.0",
// "Comment": "Run AWS Fargate task",
// "StartAt": "Run Fargate Task",
// "States": {
// "Run Fargate Task": {
// "Type": "Task",
// "Resource": "arn:aws:states:::ecs:runTask.sync",
// "Parameters": {
// "LaunchType": "FARGATE",
// "PlatformVersion": "1.4.0",
// "Cluster": "cluster",
// "TaskDefinition": "def",
// "PropagateTags": "TASK_DEFINITION",
// "Group.$": "$$.Execution.Name",
// "NetworkConfiguration": {
// "AwsvpcConfiguration": {
// "Subnets": ["sbn-1", "sbn-2"],
// "AssignPublicIp": "ENABLED",
// "SecurityGroups": ["sg-1", "sg-2"]
// }
// }
// },
// "End": true
// }
func (n *NetworkConfiguration) UnmarshalJSON(b []byte) error {
var f interface{}
err := json.Unmarshal(b, &f)
if err != nil {
return err
}

states := f.(map[string]interface{})["States"].(map[string]interface{})
parameters := states["Run Fargate Task"].(map[string]interface{})["Parameters"].(map[string]interface{})
networkConfig := parameters["NetworkConfiguration"].(map[string]interface{})["AwsvpcConfiguration"].(map[string]interface{})

var subnets []string
for _, subnet := range networkConfig["Subnets"].([]interface{}) {
subnets = append(subnets, subnet.(string))
}

var securityGroups []string
for _, sg := range networkConfig["SecurityGroups"].([]interface{}) {
securityGroups = append(securityGroups, sg.(string))
}

n.Subnets = subnets
n.SecurityGroups = securityGroups
n.AssignPublicIp = networkConfig["AssignPublicIp"].(string)
return nil
}

func (c Client) listActiveCopilotTasks(opts listActiveCopilotTasksOpts) ([]*ecs.Task, error) {
var tasks []*ecs.Task
if opts.TaskGroup != "" {
Expand Down Expand Up @@ -309,3 +396,36 @@ func (c Client) serviceARN(app, env, svc string) (*ecs.ServiceArn, error) {
serviceArn := ecs.ServiceArn(services[0].ARN)
return &serviceArn, nil
}

func (c Client) stateMachineARN(app, env, job string) (string, error) {
resources, err := c.rgGetter.GetResourcesByTags(resourcegroups.ResourceTypeStateMachine, map[string]string{
deploy.AppTagKey: app,
deploy.EnvTagKey: env,
deploy.ServiceTagKey: job,
})
if err != nil {
return "", fmt.Errorf("get state machine resource by tags for job %s: %w", job, err)
}

var stateMachineARN string
targetName := fmt.Sprintf(fmtStateMachineName, app, env, job)
for _, r := range resources {
parsedARN, err := arn.Parse(r.ARN)
if err != nil {
continue
}
parts := strings.Split(parsedARN.Resource, ":")
if len(parts) != 2 {
continue
}
if parts[1] == targetName {
stateMachineARN = r.ARN
break
}
}

if stateMachineARN == "" {
return "", fmt.Errorf("state machine for job %s not found", job)
}
return stateMachineARN, nil
}

0 comments on commit 904dd44

Please sign in to comment.