Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ jobs:
go-version-file: go.mod

- name: Validate strict plugin contracts
run: go run github.com/GoCodeAlone/workflow/cmd/wfctl@v0.61.0 plugin validate --file plugin.json --strict-contracts
run: go run github.com/GoCodeAlone/workflow/cmd/wfctl@v0.64.3 plugin validate --file plugin.json --strict-contracts
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:

- uses: GoCodeAlone/setup-wfctl@v1
with:
version: v0.64.0
version: v0.64.3

- name: Validate plugin contract for publish (pre-build)
run: wfctl plugin validate-contract --for-publish --tag "${{ github.ref_name }}" .
Expand Down
1 change: 1 addition & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ project_name: workflow-plugin-aws

before:
hooks:
- "sh -c \"cp plugin.json cmd/workflow-plugin-aws/plugin.json\""
- "sh -c \"rm -rf .release && mkdir -p .release && cp plugin.json .release/plugin.json && cp plugin.contracts.json .release/plugin.contracts.json && sed -i.bak 's/\\\"version\\\": \\\".*\\\"/\\\"version\\\": \\\"{{ .Version }}\\\"/' .release/plugin.json && rm -f .release/plugin.json.bak\""
- "sh -c \"sed -i.bak 's|/releases/download/v[^/]*/|/releases/download/{{ .Tag }}/|g' .release/plugin.json && rm -f .release/plugin.json.bak\""
- "sh -c \"export GOPRIVATE=github.com/GoCodeAlone/*; WFCTL_VERSION=$(GOWORK=off go list -m github.com/GoCodeAlone/workflow | awk '{print $2}') && GOWORK=off go run github.com/GoCodeAlone/workflow/cmd/wfctl@${WFCTL_VERSION} plugin validate --file .release/plugin.json --strict-contracts\""
Expand Down
15 changes: 12 additions & 3 deletions cmd/workflow-plugin-aws/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,23 @@
package main

import (
_ "embed"

"github.com/GoCodeAlone/workflow-plugin-aws/internal"
sdk "github.com/GoCodeAlone/workflow/plugin/external/sdk"
)

// pluginJSON is copied from the repository root by GoReleaser before builds
// and is committed for local builds/tests.
//
//go:embed plugin.json
var pluginJSON []byte

func main() {
sdk.ServeIaCPlugin(internal.NewIaCServer(), sdk.IaCServeOptions{
Modules: internal.ModuleProviders(),
Steps: internal.StepProviders(),
BuildVersion: sdk.ResolveBuildVersion(internal.Version),
ManifestProvider: sdk.MustEmbedManifest(pluginJSON),
Modules: internal.ModuleProviders(),
Steps: internal.StepProviders(),
BuildVersion: sdk.ResolveBuildVersion(internal.Version),
})
}
82 changes: 82 additions & 0 deletions cmd/workflow-plugin-aws/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"name": "workflow-plugin-aws",
"version": "0.0.0",
"author": "GoCodeAlone",
"description": "AWS provider plugin for workflow IaC — manages ECS, EKS, RDS, ElastiCache, VPC, ALB, Route53, ECR, API Gateway, Security Groups, IAM, S3, ACM, and AutoScaling Group resources",
"license": "MIT",
"type": "external",
"tier": "community",
"minEngineVersion": "0.64.3",
"iacServices": [
"workflow.plugin.external.iac.IaCProviderRequired",
"workflow.plugin.external.iac.IaCProviderEnumerator",
"workflow.plugin.external.iac.IaCProviderDriftDetector",
"workflow.plugin.external.iac.IaCProviderCredentialRevoker",
"workflow.plugin.external.iac.IaCProviderMigrationRepairer",
"workflow.plugin.external.iac.IaCProviderValidator",
"workflow.plugin.external.iac.IaCProviderDriftConfigDetector",
"workflow.plugin.external.iac.IaCProviderRequirementMapper",
"workflow.plugin.external.iac.ResourceDriver",
"workflow.plugin.external.iac.IaCStateBackend"
],
"keywords": [
"aws",
"iac",
"infrastructure",
"ecs",
"eks",
"rds",
"vpc",
"s3",
"autoscaling"
],
"homepage": "https://github.com/GoCodeAlone/workflow-plugin-aws",
"repository": "https://github.com/GoCodeAlone/workflow-plugin-aws",
"capabilities": {
"configProvider": false,
"moduleTypes": [
"iac.provider",
"aws.credentials",
"storage.s3"
],
"stepTypes": [
"step.s3_upload"
],
"triggerTypes": [],
"iacStateBackends": [
"s3"
]
},
"downloads": [
{
"os": "linux",
"arch": "amd64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.0.0/workflow-plugin-aws-linux-amd64.tar.gz"
},
{
"os": "linux",
"arch": "arm64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.0.0/workflow-plugin-aws-linux-arm64.tar.gz"
},
{
"os": "darwin",
"arch": "amd64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.0.0/workflow-plugin-aws-darwin-amd64.tar.gz"
},
{
"os": "darwin",
"arch": "arm64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.0.0/workflow-plugin-aws-darwin-arm64.tar.gz"
},
{
"os": "windows",
"arch": "amd64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.0.0/workflow-plugin-aws-windows-amd64.tar.gz"
},
{
"os": "windows",
"arch": "arm64",
"url": "https://github.com/GoCodeAlone/workflow-plugin-aws/releases/download/v0.0.0/workflow-plugin-aws-windows-arm64.tar.gz"
}
]
}
108 changes: 98 additions & 10 deletions drivers/ecs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package drivers
import (
"context"
"fmt"
"sort"

awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ecs"
Expand Down Expand Up @@ -61,20 +62,18 @@ func (d *ECSDriver) Create(ctx context.Context, spec interfaces.ResourceSpec) (*
}
replicas := int32(intProp(spec.Config, "replicas", 1))

containerDefinitions := []ecstypes.ContainerDefinition{
containerDefinitionFromConfig(spec.Name, spec.Config),
}

// Register task definition
tdIn := &ecs.RegisterTaskDefinitionInput{
Family: awssdk.String(spec.Name),
RequiresCompatibilities: []ecstypes.Compatibility{ecstypes.CompatibilityFargate},
NetworkMode: ecstypes.NetworkModeAwsvpc,
Cpu: awssdk.String(cpu),
Memory: awssdk.String(memory),
ContainerDefinitions: []ecstypes.ContainerDefinition{
{
Name: awssdk.String(spec.Name),
Image: awssdk.String(image),
Essential: awssdk.Bool(true),
},
},
ContainerDefinitions: containerDefinitions,
}
tdOut, err := d.client.RegisterTaskDefinition(ctx, tdIn)
if err != nil {
Expand Down Expand Up @@ -148,9 +147,7 @@ func (d *ECSDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec
NetworkMode: ecstypes.NetworkModeAwsvpc,
Cpu: awssdk.String(cpu),
Memory: awssdk.String(mem),
ContainerDefinitions: []ecstypes.ContainerDefinition{
{Name: awssdk.String(ref.Name), Image: awssdk.String(image), Essential: awssdk.Bool(true)},
},
ContainerDefinitions: []ecstypes.ContainerDefinition{containerDefinitionFromConfig(ref.Name, spec.Config)},
})
if err != nil {
return nil, fmt.Errorf("ecs: re-register task def %q: %w", ref.Name, err)
Expand All @@ -165,6 +162,97 @@ func (d *ECSDriver) Update(ctx context.Context, ref interfaces.ResourceRef, spec
return ecsServiceToOutput(ref.Name, out.Service), nil
}

func containerDefinitionFromConfig(name string, cfg map[string]any) ecstypes.ContainerDefinition {
image, _ := cfg["image"].(string)
def := ecstypes.ContainerDefinition{
Name: awssdk.String(name),
Image: awssdk.String(image),
Essential: awssdk.Bool(true),
}
if command := stringSliceProp(cfg, "command"); len(command) > 0 {
def.Command = command
}
def.Environment = ecsEnvironment(cfg["env_vars"])
def.Secrets = ecsSecrets(cfg["env_vars_secret"])
def.PortMappings = ecsPortMappings(cfg["ports"])
return def
}

func ecsEnvironment(raw any) []ecstypes.KeyValuePair {
values := stringMap(raw)
keys := sortedMapKeys(values)
out := make([]ecstypes.KeyValuePair, 0, len(keys))
for _, key := range keys {
value := values[key]
out = append(out, ecstypes.KeyValuePair{Name: awssdk.String(key), Value: awssdk.String(value)})
}
return out
}

func ecsSecrets(raw any) []ecstypes.Secret {
values := stringMap(raw)
keys := sortedMapKeys(values)
out := make([]ecstypes.Secret, 0, len(keys))
for _, key := range keys {
value := values[key]
out = append(out, ecstypes.Secret{Name: awssdk.String(key), ValueFrom: awssdk.String(value)})
}
return out
}

func ecsPortMappings(raw any) []ecstypes.PortMapping {
items, ok := raw.([]any)
if !ok {
return nil
}
out := make([]ecstypes.PortMapping, 0, len(items))
for _, item := range items {
m, ok := item.(map[string]any)
if !ok {
continue
}
port := intProp(m, "port", 0)
if port <= 0 {
continue
}
protocol := ecstypes.TransportProtocolTcp
if p, _ := m["protocol"].(string); p == "udp" {
protocol = ecstypes.TransportProtocolUdp
}
out = append(out, ecstypes.PortMapping{
ContainerPort: awssdk.Int32(int32(port)),
Protocol: protocol,
})
}
return out
}

func stringMap(raw any) map[string]string {
switch values := raw.(type) {
case map[string]string:
return values
case map[string]any:
out := make(map[string]string, len(values))
for key, value := range values {
if str, ok := value.(string); ok {
out[key] = str
}
}
return out
default:
return nil
}
}

func sortedMapKeys(values map[string]string) []string {
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}

func (d *ECSDriver) Delete(ctx context.Context, ref interfaces.ResourceRef) error {
// Scale to 0 first
_, _ = d.client.UpdateService(ctx, &ecs.UpdateServiceInput{
Expand Down
81 changes: 74 additions & 7 deletions drivers/ecs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

type mockECSClient struct {
registerOut *ecs.RegisterTaskDefinitionOutput
registerIn *ecs.RegisterTaskDefinitionInput
registerErr error
createSvcOut *ecs.CreateServiceOutput
createSvcErr error
Expand All @@ -27,7 +28,8 @@ type mockECSClient struct {
deregErr error
}

func (m *mockECSClient) RegisterTaskDefinition(_ context.Context, _ *ecs.RegisterTaskDefinitionInput, _ ...func(*ecs.Options)) (*ecs.RegisterTaskDefinitionOutput, error) {
func (m *mockECSClient) RegisterTaskDefinition(_ context.Context, in *ecs.RegisterTaskDefinitionInput, _ ...func(*ecs.Options)) (*ecs.RegisterTaskDefinitionOutput, error) {
m.registerIn = in
return m.registerOut, m.registerErr
}
func (m *mockECSClient) CreateService(_ context.Context, _ *ecs.CreateServiceInput, _ ...func(*ecs.Options)) (*ecs.CreateServiceOutput, error) {
Expand Down Expand Up @@ -56,12 +58,12 @@ func TestECSDriver_Create(t *testing.T) {
},
createSvcOut: &ecs.CreateServiceOutput{
Service: &ecstypes.Service{
ServiceArn: awssdk.String(svcARN),
ServiceName: awssdk.String("my-svc"),
Status: awssdk.String("ACTIVE"),
DesiredCount: 1,
RunningCount: 0,
TaskDefinition: awssdk.String("arn:aws:ecs:us-east-1:123:task-definition/my-svc:1"),
ServiceArn: awssdk.String(svcARN),
ServiceName: awssdk.String("my-svc"),
Status: awssdk.String("ACTIVE"),
DesiredCount: 1,
RunningCount: 0,
TaskDefinition: awssdk.String("arn:aws:ecs:us-east-1:123:task-definition/my-svc:1"),
},
},
}
Expand All @@ -88,6 +90,71 @@ func TestECSDriver_Create(t *testing.T) {
}
}

func TestECSDriver_Create_CollectorConfig(t *testing.T) {
mock := &mockECSClient{
registerOut: &ecs.RegisterTaskDefinitionOutput{
TaskDefinition: &ecstypes.TaskDefinition{
TaskDefinitionArn: awssdk.String("arn:aws:ecs:us-east-1:123:task-definition/observability-collector:1"),
},
},
createSvcOut: &ecs.CreateServiceOutput{
Service: &ecstypes.Service{
ServiceArn: awssdk.String("arn:aws:ecs:us-east-1:123:service/default/observability-collector"),
ServiceName: awssdk.String("observability-collector"),
Status: awssdk.String("ACTIVE"),
DesiredCount: 1,
RunningCount: 0,
TaskDefinition: awssdk.String("arn:aws:ecs:us-east-1:123:task-definition/observability-collector:1"),
},
},
}
d := drivers.NewECSDriverWithClient(mock, "default")
_, err := d.Create(context.Background(), interfaces.ResourceSpec{
Name: "observability-collector",
Type: "infra.container_service",
Config: map[string]any{
"image": "public.ecr.aws/aws-observability/aws-otel-collector:latest",
"command": []any{"--config=env:AOT_CONFIG_CONTENT"},
"env_vars": map[string]any{
"AOT_CONFIG_CONTENT": "receivers: {}",
},
"env_vars_secret": map[string]any{
"DD_API_KEY": "arn:aws:secretsmanager:us-east-1:123:secret:datadog",
},
"ports": []any{
map[string]any{"port": 4317, "protocol": "tcp"},
map[string]any{"port": 4318, "protocol": "tcp"},
},
},
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if mock.registerIn == nil {
t.Fatal("RegisterTaskDefinition input was not captured")
}
containers := mock.registerIn.ContainerDefinitions
if len(containers) != 1 {
t.Fatalf("container definitions len = %d, want 1", len(containers))
}
c := containers[0]
if len(c.Command) != 1 {
t.Fatalf("command = %v, want one entry", c.Command)
}
if got := c.Command[0]; got != "--config=env:AOT_CONFIG_CONTENT" {
t.Fatalf("command[0] = %q", got)
}
if len(c.Environment) != 1 || awssdk.ToString(c.Environment[0].Name) != "AOT_CONFIG_CONTENT" {
t.Fatalf("environment = %+v", c.Environment)
}
if len(c.Secrets) != 1 || awssdk.ToString(c.Secrets[0].Name) != "DD_API_KEY" {
t.Fatalf("secrets = %+v", c.Secrets)
}
if len(c.PortMappings) != 2 || awssdk.ToInt32(c.PortMappings[0].ContainerPort) != 4317 {
t.Fatalf("port mappings = %+v", c.PortMappings)
}
}

func TestECSDriver_Create_MissingImage(t *testing.T) {
d := drivers.NewECSDriverWithClient(&mockECSClient{}, "default")
_, err := d.Create(context.Background(), interfaces.ResourceSpec{
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/GoCodeAlone/workflow-plugin-aws
go 1.26.0

require (
github.com/GoCodeAlone/workflow v0.64.0
github.com/GoCodeAlone/workflow v0.64.3
github.com/aws/aws-sdk-go-v2 v1.41.7
github.com/aws/aws-sdk-go-v2/config v1.32.16
github.com/aws/aws-sdk-go-v2/credentials v1.19.15
Expand Down
Loading
Loading