diff --git a/pkg/app/piped/cloudprovider/ecs/BUILD.bazel b/pkg/app/piped/cloudprovider/ecs/BUILD.bazel index 9082e2b771..3181daf4db 100644 --- a/pkg/app/piped/cloudprovider/ecs/BUILD.bazel +++ b/pkg/app/piped/cloudprovider/ecs/BUILD.bazel @@ -15,6 +15,7 @@ go_library( deps = [ "//pkg/app/piped/cloudprovider:go_default_library", "//pkg/config:go_default_library", + "//pkg/model:go_default_library", "@com_github_aws_aws_sdk_go_v2//aws:go_default_library", "@com_github_aws_aws_sdk_go_v2_config//:go_default_library", "@com_github_aws_aws_sdk_go_v2_credentials//stscreds:go_default_library", @@ -37,6 +38,7 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//pkg/model:go_default_library", "@com_github_aws_aws_sdk_go_v2//aws:go_default_library", "@com_github_aws_aws_sdk_go_v2_service_ecs//types:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", diff --git a/pkg/app/piped/cloudprovider/ecs/task.go b/pkg/app/piped/cloudprovider/ecs/task.go index 2316324745..bf9397f288 100644 --- a/pkg/app/piped/cloudprovider/ecs/task.go +++ b/pkg/app/piped/cloudprovider/ecs/task.go @@ -19,9 +19,10 @@ import ( "os" "strings" + "github.com/aws/aws-sdk-go-v2/service/ecs/types" "sigs.k8s.io/yaml" - "github.com/aws/aws-sdk-go-v2/service/ecs/types" + "github.com/pipe-cd/pipecd/pkg/model" ) func loadTaskDefinition(path string) (types.TaskDefinition, error) { @@ -61,3 +62,33 @@ func parseContainerImage(image string) (name, tag string) { name = paths[len(paths)-1] return } + +// FindArtifactVersions parses artifact versions from ECS task definition. +func FindArtifactVersions(taskDefinition types.TaskDefinition) ([]*model.ArtifactVersion, error) { + if len(taskDefinition.ContainerDefinitions) == 0 { + return nil, fmt.Errorf("container definition could not be empty") + } + + // Remove duplicate images. + imageMap := map[string]struct{}{} + for _, cd := range taskDefinition.ContainerDefinitions { + imageMap[*cd.Image] = struct{}{} + } + + versions := make([]*model.ArtifactVersion, 0, len(imageMap)) + for i := range imageMap { + name, tag := parseContainerImage(i) + if name == "" { + return nil, fmt.Errorf("image name could not be empty") + } + + versions = append(versions, &model.ArtifactVersion{ + Kind: model.ArtifactVersion_CONTAINER_IMAGE, + Version: tag, + Name: name, + Url: i, + }) + } + + return versions, nil +} diff --git a/pkg/app/piped/cloudprovider/ecs/task_test.go b/pkg/app/piped/cloudprovider/ecs/task_test.go index 78f6dc1ee9..ded643af4e 100644 --- a/pkg/app/piped/cloudprovider/ecs/task_test.go +++ b/pkg/app/piped/cloudprovider/ecs/task_test.go @@ -20,6 +20,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ecs/types" "github.com/stretchr/testify/assert" + + "github.com/pipe-cd/pipecd/pkg/model" ) func TestParseTaskDefinition(t *testing.T) { @@ -78,3 +80,207 @@ cpu: 256 }) } } + +func TestFindArtifactVersions(t *testing.T) { + t.Parallel() + + testcases := []struct { + name string + input []byte + expected []*model.ArtifactVersion + expectedErr bool + }{ + { + name: "ok", + input: []byte(` +{ + "family": "nginx-canary-fam-1", + "compatibilities": [ + "FARGATE" + ], + "networkMode": "awsvpc", + "memory": 512, + "cpu": 256, + "containerDefinitions" : [ + { + "image": "gcr.io/pipecd/helloworld:v1.0.0", + "name": "helloworld", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 9085, + "protocol": "tcp" + } + ] + } + ] +} +`), + expected: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_CONTAINER_IMAGE, + Version: "v1.0.0", + Name: "helloworld", + Url: "gcr.io/pipecd/helloworld:v1.0.0", + }, + }, + expectedErr: false, + }, + { + name: "missing containerDefinitions", + input: []byte(` +{ + "family": "nginx-canary-fam-1", + "compatibilities": [ + "FARGATE" + ], + "networkMode": "awsvpc", + "memory": 512, + "cpu": 256, +} +`), + expected: nil, + expectedErr: true, + }, + { + name: "missing image name", + input: []byte(` +{ + "family": "nginx-canary-fam-1", + "compatibilities": [ + "FARGATE" + ], + "networkMode": "awsvpc", + "memory": 512, + "cpu": 256, + "containerDefinitions" : [ + { + "image": "gcr.io/pipecd/:v1.0.0", + "name": "helloworld", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 9085, + "protocol": "tcp" + } + ] + } + ] +} +`), + expected: nil, + expectedErr: true, + }, + { + name: "multiple containers", + input: []byte(` +{ + "family": "nginx-canary-fam-1", + "compatibilities": [ + "FARGATE" + ], + "networkMode": "awsvpc", + "memory": 512, + "cpu": 256, + "containerDefinitions" : [ + { + "image": "gcr.io/pipecd/helloworld:v1.0.0", + "name": "helloworld", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 9085, + "protocol": "tcp" + } + ] + }, + { + "image": "gcr.io/pipecd/my-service:v1.0.0", + "name": "my-service", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 9090, + "protocol": "tcp" + } + ] + } + ] +} +`), + expected: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_CONTAINER_IMAGE, + Version: "v1.0.0", + Name: "helloworld", + Url: "gcr.io/pipecd/helloworld:v1.0.0", + }, + { + Kind: model.ArtifactVersion_CONTAINER_IMAGE, + Version: "v1.0.0", + Name: "my-service", + Url: "gcr.io/pipecd/my-service:v1.0.0", + }, + }, + expectedErr: false, + }, + { + name: "multiple containers with the same image", + input: []byte(` +{ + "family": "nginx-canary-fam-1", + "compatibilities": [ + "FARGATE" + ], + "networkMode": "awsvpc", + "memory": 512, + "cpu": 256, + "containerDefinitions" : [ + { + "image": "gcr.io/pipecd/helloworld:v1.0.0", + "name": "helloworld", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 9085, + "protocol": "tcp" + } + ] + }, + { + "image": "gcr.io/pipecd/helloworld:v1.0.0", + "name": "helloworld-02", + "portMappings": [ + { + "containerPort": 80, + "hostPort": 9091, + "protocol": "tcp" + } + ] + } + ] +} +`), + expected: []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_CONTAINER_IMAGE, + Version: "v1.0.0", + Name: "helloworld", + Url: "gcr.io/pipecd/helloworld:v1.0.0", + }, + }, + expectedErr: false, + }, + } + + for _, tc := range testcases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + td, _ := parseTaskDefinition(tc.input) + versions, err := FindArtifactVersions(td) + assert.Equal(t, tc.expectedErr, err != nil) + assert.ElementsMatch(t, tc.expected, versions) + }) + } +} diff --git a/pkg/app/piped/planner/ecs/ecs.go b/pkg/app/piped/planner/ecs/ecs.go index d0e15faab9..e2b7519108 100644 --- a/pkg/app/piped/planner/ecs/ecs.go +++ b/pkg/app/piped/planner/ecs/ecs.go @@ -62,6 +62,17 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu in.Logger.Warn("unable to determine target version", zap.Error(err)) } + out.Versions, err = determineVersions(ds.AppDir, cfg.Input.TaskDefinitionFile) + if err != nil { + in.Logger.Warn("unable to determine target versions", zap.Error(err)) + out.Versions = []*model.ArtifactVersion{ + { + Kind: model.ArtifactVersion_UNKNOWN, + Version: "unknown", + }, + } + } + autoRollback := *cfg.Input.AutoRollback // In case the strategy has been decided by trigger. @@ -133,3 +144,12 @@ func determineVersion(appDir, taskDefinitonFile string) (string, error) { return provider.FindImageTag(taskDefinition) } + +func determineVersions(appDir, taskDefinitonFile string) ([]*model.ArtifactVersion, error) { + taskDefinition, err := provider.LoadTaskDefinition(appDir, taskDefinitonFile) + if err != nil { + return nil, err + } + + return provider.FindArtifactVersions(taskDefinition) +}