diff --git a/cmd/workflow-plugin-aws/plugin.json b/cmd/workflow-plugin-aws/plugin.json index c79f23d..615458e 100644 --- a/cmd/workflow-plugin-aws/plugin.json +++ b/cmd/workflow-plugin-aws/plugin.json @@ -6,7 +6,7 @@ "license": "MIT", "type": "external", "tier": "community", - "minEngineVersion": "0.69.1", + "minEngineVersion": "0.73.0", "iacServices": [ "workflow.plugin.external.iac.IaCProviderRequired", "workflow.plugin.external.iac.IaCProviderEnumerator", @@ -19,6 +19,7 @@ "workflow.plugin.external.iac.IaCProviderRegionLister", "workflow.plugin.external.iac.IaCProviderOwnership", "workflow.plugin.external.iac.ResourceDriver", + "workflow.plugin.external.iac.IaCProviderRunner", "workflow.plugin.external.iac.IaCStateBackend" ], "keywords": [ diff --git a/go.mod b/go.mod index 201694c..755517d 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/GoCodeAlone/workflow-plugin-aws go 1.26.0 require ( - github.com/GoCodeAlone/workflow v0.69.7 - github.com/aws/aws-sdk-go-v2 v1.41.9 + github.com/GoCodeAlone/workflow v0.73.0 + github.com/aws/aws-sdk-go-v2 v1.41.11 github.com/aws/aws-sdk-go-v2/config v1.32.16 github.com/aws/aws-sdk-go-v2/credentials v1.19.15 github.com/aws/aws-sdk-go-v2/service/acm v1.32.1 @@ -45,11 +45,12 @@ require ( github.com/Workiva/go-datastructures v1.1.7 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/armon/go-metrics v0.4.1 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect + github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.74.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect @@ -58,7 +59,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect - github.com/aws/smithy-go v1.26.0 // indirect + github.com/aws/smithy-go v1.27.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bytedance/gopkg v0.1.4 // indirect @@ -180,24 +181,24 @@ require ( go.mongodb.org/mongo-driver v1.17.9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/sdk v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/arch v0.27.0 // indirect - golang.org/x/crypto v0.51.0 // indirect + golang.org/x/crypto v0.52.0 // indirect golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.54.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect + golang.org/x/sys v0.45.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.45.0 // indirect @@ -205,8 +206,8 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.70.0 // indirect + modernc.org/libc v1.72.3 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.47.0 // indirect + modernc.org/sqlite v1.51.0 // indirect ) diff --git a/go.sum b/go.sum index a93dcb5..38a1c7c 100644 --- a/go.sum +++ b/go.sum @@ -24,6 +24,8 @@ github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.10.0 h1:+2M/ecyCxDiXfJ github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.10.0/go.mod h1:tlVH1mA5yuU8CB7R7+HXIRaBixZoNid6h+5tew5u3FU= github.com/GoCodeAlone/workflow v0.69.7 h1:LgRTJtbicyOeucyQmHw/F7rjfYP8T15C01p7jNm6kP0= github.com/GoCodeAlone/workflow v0.69.7/go.mod h1:nWB662ILBUUjL2NBlj7RchyiI4CZ2+UxnpQcbIA2tWE= +github.com/GoCodeAlone/workflow v0.73.0 h1:hi8PuYujNhcMQ5H/+6e0nAPvmDdjPPzmX7nqnsMMm1k= +github.com/GoCodeAlone/workflow v0.73.0/go.mod h1:i/9ZTfR8YYlmr0+hvIZi4cYw7SxzkQRLlzYQCrRD/2k= github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318= @@ -50,8 +52,12 @@ github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJ github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4= github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo= +github.com/aws/aws-sdk-go-v2 v1.41.11 h1:9PRf7jyTMEUM6fuNRAJa2mO/skJfrF50rENJwf2LXqw= +github.com/aws/aws-sdk-go-v2 v1.41.11/go.mod h1:iiUX27gOXRuYaoeUVXhUpPwjJHzISfPAjjcuhUbLSVs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 h1:oRtsqWgxbpeXrOlxOoQStx2M9WNbIkPq4C4Xn1or6bc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12/go.mod h1:Zg0Oe9qT+9wcezlm1a64wGJp2qZdRElVxo/seJf7jYU= github.com/aws/aws-sdk-go-v2/config v1.32.16 h1:Q0iQ7quUgJP0F/SCRTieScnaMdXr9h/2+wze1u3cNeM= github.com/aws/aws-sdk-go-v2/config v1.32.16/go.mod h1:duCCnJEFqpt2RC6no1iK6q+8HpwOAkiUua0pY507dQc= github.com/aws/aws-sdk-go-v2/credentials v1.19.15 h1:fyvgWTszojq8hEnMi8PPBTvZdTtEVmAVyo+NFLHBhH4= @@ -60,8 +66,12 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 h1:IOGsJ1xVWhsi+ZO7/NW8Ou github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22/go.mod h1:b+hYdbU+jGKfXE8kKM6g1+h+L/Go3vMvzlxBsiuGsxg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27 h1:8sPbKi1/KRHwl5oR3qN9mUXestCeHuaRutxylnr/eVY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.27/go.mod h1:QV9IVIopJ1dpQUno0f9VYDUwOEjj8u0iEJ4JiZVre3Y= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27 h1:9d8AoASQY9UwrOSmiJ7uSM0MGUPFhnenwSvpaFfat2c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.27/go.mod h1:x0rldpsnUQaQIs4Rh+Vwm9Z/0vI6BxadGtsgJfZFb8s= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 h1:FPXsW9+gMuIeKmz7j6ENWcWtBGTe1kH8r9thNt5Uxx4= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23/go.mod h1:7J8iGMdRKk6lw2C+cMIphgAnT8uTwBwNOsGkyOCm80U= github.com/aws/aws-sdk-go-v2/service/acm v1.32.1 h1:KAK08un+8LhHlG6OEUmDTqFpQth2tYA+6EX0NNocgl4= @@ -72,6 +82,8 @@ github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.16 h1:9ePxWacy github.com/aws/aws-sdk-go-v2/service/applicationautoscaling v1.41.16/go.mod h1:gR1tnThD1DBemyG1rmZ9U5+WbfGoiLUaZDvsQ6wbAjM= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.2 h1:mleWBVIxwceEzyItUVoqMFiv6TmOP6ECPoN6WB/VWXc= github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.2/go.mod h1:cMApt548kNgu87UsBTNWVv+fpzjbUTFRSFjD1688SBs= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.74.4 h1:4GBRq2ZWJkOy6S4HRNuJJpaZ5KXPNIDe/QJysoyXglI= +github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.74.4/go.mod h1:Op4lD9kBH1InUC+DxIALi0ALAASdpY71vRdtevYFVrs= github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 h1:98Miqj16un1WLNyM1RjVDhXYumhqZrQfAeG8i4jPG6o= github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0/go.mod h1:T6ndRfdhnXLIY5oKBHjYZDVj706los2zGdpThppquvA= github.com/aws/aws-sdk-go-v2/service/ecr v1.44.2 h1:USCQWra7IXZiH25796EZizvSRmJeS5wJNrdv70JIk0o= @@ -114,6 +126,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.0 h1:ks8KBcZPh3PYISr5dAiXCM5/Thcu github.com/aws/aws-sdk-go-v2/service/sts v1.42.0/go.mod h1:pFw33T0WLvXU3rw1WBkpMlkgIn54eCB5FYLhjDc9Foo= github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.27.0 h1:ZoFioDKJxkSIW2otF9T0aPtNlUwhdVCcuZh/rzH9Hus= +github.com/aws/smithy-go v1.27.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -654,18 +668,27 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8V go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -692,6 +715,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -756,6 +781,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -835,8 +862,10 @@ k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7 k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= @@ -847,16 +876,21 @@ modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= +modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= +modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= +modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U= +modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/contracts/aws.pb.go b/internal/contracts/aws.pb.go index a8147c9..195ee6f 100644 --- a/internal/contracts/aws.pb.go +++ b/internal/contracts/aws.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.4 -// protoc v3.21.12 +// protoc-gen-go v1.36.11 +// protoc v7.35.0 // source: internal/contracts/aws.proto package contracts @@ -33,9 +33,17 @@ type AWSProviderConfig struct { // secret_access_key is the AWS secret access key for static credentials. SecretAccessKey string `protobuf:"bytes,3,opt,name=secret_access_key,json=secretAccessKey,proto3" json:"secret_access_key,omitempty"` // ecs_cluster is the default ECS cluster name used by the ECS driver. - EcsCluster string `protobuf:"bytes,4,opt,name=ecs_cluster,json=ecsCluster,proto3" json:"ecs_cluster,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + EcsCluster string `protobuf:"bytes,4,opt,name=ecs_cluster,json=ecsCluster,proto3" json:"ecs_cluster,omitempty"` + // ecs_subnet_ids are default subnet IDs for provider-ephemeral ECS RunTask. + EcsSubnetIds []string `protobuf:"bytes,5,rep,name=ecs_subnet_ids,json=ecsSubnetIds,proto3" json:"ecs_subnet_ids,omitempty"` + // ecs_security_group_ids are default security groups for provider-ephemeral ECS RunTask. + EcsSecurityGroupIds []string `protobuf:"bytes,6,rep,name=ecs_security_group_ids,json=ecsSecurityGroupIds,proto3" json:"ecs_security_group_ids,omitempty"` + // ecs_task_execution_role_arn is the optional ECS task execution role ARN for runner jobs. + EcsTaskExecutionRoleArn string `protobuf:"bytes,7,opt,name=ecs_task_execution_role_arn,json=ecsTaskExecutionRoleArn,proto3" json:"ecs_task_execution_role_arn,omitempty"` + // ecs_runner_log_group is the CloudWatch Logs group used by runner jobs. + EcsRunnerLogGroup string `protobuf:"bytes,8,opt,name=ecs_runner_log_group,json=ecsRunnerLogGroup,proto3" json:"ecs_runner_log_group,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AWSProviderConfig) Reset() { @@ -96,29 +104,49 @@ func (x *AWSProviderConfig) GetEcsCluster() string { return "" } +func (x *AWSProviderConfig) GetEcsSubnetIds() []string { + if x != nil { + return x.EcsSubnetIds + } + return nil +} + +func (x *AWSProviderConfig) GetEcsSecurityGroupIds() []string { + if x != nil { + return x.EcsSecurityGroupIds + } + return nil +} + +func (x *AWSProviderConfig) GetEcsTaskExecutionRoleArn() string { + if x != nil { + return x.EcsTaskExecutionRoleArn + } + return "" +} + +func (x *AWSProviderConfig) GetEcsRunnerLogGroup() string { + if x != nil { + return x.EcsRunnerLogGroup + } + return "" +} + var File_internal_contracts_aws_proto protoreflect.FileDescriptor -var file_internal_contracts_aws_proto_rawDesc = string([]byte{ - 0x0a, 0x1c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x61, 0x63, 0x74, 0x73, 0x2f, 0x61, 0x77, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, - 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x73, - 0x2e, 0x61, 0x77, 0x73, 0x2e, 0x76, 0x31, 0x22, 0x9c, 0x01, 0x0a, 0x11, 0x41, 0x57, 0x53, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, - 0x06, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, - 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, - 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, - 0x63, 0x65, 0x73, 0x73, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x73, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x41, 0x63, 0x63, 0x65, - 0x73, 0x73, 0x4b, 0x65, 0x79, 0x12, 0x1f, 0x0a, 0x0b, 0x65, 0x63, 0x73, 0x5f, 0x63, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x65, 0x63, 0x73, 0x43, - 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x42, 0x49, 0x5a, 0x47, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x47, 0x6f, 0x43, 0x6f, 0x64, 0x65, 0x41, 0x6c, 0x6f, 0x6e, 0x65, - 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x2d, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x2d, 0x61, 0x77, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x73, 0x3b, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -}) +const file_internal_contracts_aws_proto_rawDesc = "" + + "\n" + + "\x1cinternal/contracts/aws.proto\x12\x17workflow.plugins.aws.v1\"\xe6\x02\n" + + "\x11AWSProviderConfig\x12\x16\n" + + "\x06region\x18\x01 \x01(\tR\x06region\x12\"\n" + + "\raccess_key_id\x18\x02 \x01(\tR\vaccessKeyId\x12*\n" + + "\x11secret_access_key\x18\x03 \x01(\tR\x0fsecretAccessKey\x12\x1f\n" + + "\vecs_cluster\x18\x04 \x01(\tR\n" + + "ecsCluster\x12$\n" + + "\x0eecs_subnet_ids\x18\x05 \x03(\tR\fecsSubnetIds\x123\n" + + "\x16ecs_security_group_ids\x18\x06 \x03(\tR\x13ecsSecurityGroupIds\x12<\n" + + "\x1becs_task_execution_role_arn\x18\a \x01(\tR\x17ecsTaskExecutionRoleArn\x12/\n" + + "\x14ecs_runner_log_group\x18\b \x01(\tR\x11ecsRunnerLogGroupBIZGgithub.com/GoCodeAlone/workflow-plugin-aws/internal/contracts;contractsb\x06proto3" var ( file_internal_contracts_aws_proto_rawDescOnce sync.Once diff --git a/internal/contracts/aws.proto b/internal/contracts/aws.proto index f5c1c9b..4c39a49 100644 --- a/internal/contracts/aws.proto +++ b/internal/contracts/aws.proto @@ -16,4 +16,12 @@ message AWSProviderConfig { string secret_access_key = 3; // ecs_cluster is the default ECS cluster name used by the ECS driver. string ecs_cluster = 4; + // ecs_subnet_ids are default subnet IDs for provider-ephemeral ECS RunTask. + repeated string ecs_subnet_ids = 5; + // ecs_security_group_ids are default security groups for provider-ephemeral ECS RunTask. + repeated string ecs_security_group_ids = 6; + // ecs_task_execution_role_arn is the optional ECS task execution role ARN for runner jobs. + string ecs_task_execution_role_arn = 7; + // ecs_runner_log_group is the CloudWatch Logs group used by runner jobs. + string ecs_runner_log_group = 8; } diff --git a/internal/iacserver.go b/internal/iacserver.go index f45210d..c1a4bd5 100644 --- a/internal/iacserver.go +++ b/internal/iacserver.go @@ -52,6 +52,7 @@ type awsIaCServer struct { pb.UnimplementedIaCProviderOwnershipServer pb.UnimplementedResourceDriverServer pb.UnimplementedIaCStateBackendServer + pb.UnimplementedIaCProviderRunnerServer provider *provider.AWSProvider @@ -91,6 +92,7 @@ var ( _ pb.IaCProviderRequirementMapperServer = (*awsIaCServer)(nil) _ pb.IaCProviderRegionListerServer = (*awsIaCServer)(nil) _ pb.IaCProviderOwnershipServer = (*awsIaCServer)(nil) + _ pb.IaCProviderRunnerServer = (*awsIaCServer)(nil) // awsIaCServer also SERVES the typed IaC state-backend contract (s3 // backend). The SDK serve hook auto-registers this via type-assertion at // plugin startup — see cmd/workflow-plugin-aws/main.go. diff --git a/internal/iacserver_mapper_test.go b/internal/iacserver_mapper_test.go index d108b29..c566f2a 100644 --- a/internal/iacserver_mapper_test.go +++ b/internal/iacserver_mapper_test.go @@ -185,8 +185,8 @@ func TestPluginManifestAdvertisesRequirementMapper(t *testing.T) { if err := json.Unmarshal(data, &manifest); err != nil { t.Fatalf("parse plugin.json: %v", err) } - if manifest.MinEngineVersion != "0.69.1" { - t.Fatalf("minEngineVersion = %q, want 0.69.1", manifest.MinEngineVersion) + if manifest.MinEngineVersion != "0.73.0" { + t.Fatalf("minEngineVersion = %q, want 0.73.0", manifest.MinEngineVersion) } for _, want := range []string{ "workflow.plugin.external.iac.IaCProviderRequirementMapper", diff --git a/internal/runner_server.go b/internal/runner_server.go new file mode 100644 index 0000000..fc8b9a3 --- /dev/null +++ b/internal/runner_server.go @@ -0,0 +1,108 @@ +package internal + +import ( + "context" + + "github.com/GoCodeAlone/workflow/interfaces" + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" +) + +func (s *awsIaCServer) RunJob(ctx context.Context, req *pb.JobSpec) (*pb.JobHandle, error) { + handle, err := s.provider.RunJob(ctx, jobSpecFromPB(req)) + if err != nil { + return nil, err + } + return jobHandleToPB(handle), nil +} + +func (s *awsIaCServer) JobStatus(ctx context.Context, req *pb.JobHandle) (*pb.JobStatusReply, error) { + reply, err := s.provider.JobStatus(ctx, jobHandleFromPB(req)) + if err != nil { + return nil, err + } + return jobStatusToPB(reply), nil +} + +func (s *awsIaCServer) JobLogs(req *pb.JobHandle, stream pb.IaCProviderRunner_JobLogsServer) error { + return s.provider.JobLogs(stream.Context(), jobHandleFromPB(req), awsRunnerLogSink{stream: stream}) +} + +type awsRunnerLogSink struct { + stream pb.IaCProviderRunner_JobLogsServer +} + +func (s awsRunnerLogSink) WriteLogChunk(chunk interfaces.LogChunk) error { + return s.stream.Send(&pb.LogChunk{ + Data: append([]byte(nil), chunk.Data...), + Source: chunk.Source, + Eof: chunk.EOF, + }) +} + +func jobSpecFromPB(req *pb.JobSpec) interfaces.JobSpec { + if req == nil { + return interfaces.JobSpec{} + } + return interfaces.JobSpec{ + Name: req.GetName(), + Kind: req.GetKind(), + Image: req.GetImage(), + RunCommand: req.GetRunCommand(), + EnvVars: copyStringMap(req.GetEnvVars()), + EnvVarsSecret: copyStringMap(req.GetEnvVarsSecret()), + Cron: req.GetCron(), + } +} + +func jobHandleFromPB(req *pb.JobHandle) interfaces.JobHandle { + if req == nil { + return interfaces.JobHandle{} + } + return interfaces.JobHandle{ + ID: req.GetId(), + Name: req.GetName(), + Provider: req.GetProvider(), + Metadata: copyStringMap(req.GetMetadata()), + } +} + +func jobHandleToPB(handle *interfaces.JobHandle) *pb.JobHandle { + if handle == nil { + return nil + } + return &pb.JobHandle{ + Id: handle.ID, + Name: handle.Name, + Provider: handle.Provider, + Metadata: copyStringMap(handle.Metadata), + } +} + +func jobStatusToPB(reply *interfaces.JobStatusReply) *pb.JobStatusReply { + if reply == nil { + return nil + } + return &pb.JobStatusReply{ + Handle: jobHandleToPB(&reply.Handle), + State: jobStateToPB(reply.State), + ExitCode: int32(reply.ExitCode), + Message: reply.Message, + } +} + +func jobStateToPB(state interfaces.JobState) pb.JobState { + switch state { + case interfaces.JobStatePending: + return pb.JobState_JOB_STATE_PENDING + case interfaces.JobStateRunning: + return pb.JobState_JOB_STATE_RUNNING + case interfaces.JobStateSucceeded: + return pb.JobState_JOB_STATE_SUCCEEDED + case interfaces.JobStateFailed: + return pb.JobState_JOB_STATE_FAILED + case interfaces.JobStateCancelled: + return pb.JobState_JOB_STATE_CANCELLED + default: + return pb.JobState_JOB_STATE_UNSPECIFIED + } +} diff --git a/internal/runner_test.go b/internal/runner_test.go new file mode 100644 index 0000000..a95a531 --- /dev/null +++ b/internal/runner_test.go @@ -0,0 +1,19 @@ +package internal + +import ( + "testing" + + pb "github.com/GoCodeAlone/workflow/plugin/external/proto" + "github.com/GoCodeAlone/workflow/plugin/external/sdk" + "google.golang.org/grpc" +) + +func TestAWSRunnerServerAutoRegisters(t *testing.T) { + server := grpc.NewServer() + if err := sdk.RegisterAllIaCProviderServices(server, NewIaCServer()); err != nil { + t.Fatalf("RegisterAllIaCProviderServices: %v", err) + } + if _, ok := server.GetServiceInfo()[pb.IaCProviderRunner_ServiceDesc.ServiceName]; !ok { + t.Fatalf("IaCProviderRunner service was not registered") + } +} diff --git a/plugin.json b/plugin.json index f0f58e2..ebf260a 100644 --- a/plugin.json +++ b/plugin.json @@ -6,7 +6,7 @@ "license": "MIT", "type": "external", "tier": "community", - "minEngineVersion": "0.69.1", + "minEngineVersion": "0.73.0", "required_secrets": [ { "name": "AWS_ACCESS_KEY_ID", @@ -33,6 +33,7 @@ "workflow.plugin.external.iac.IaCProviderRegionLister", "workflow.plugin.external.iac.IaCProviderOwnership", "workflow.plugin.external.iac.ResourceDriver", + "workflow.plugin.external.iac.IaCProviderRunner", "workflow.plugin.external.iac.IaCStateBackend" ], "keywords": [ diff --git a/provider/provider.go b/provider/provider.go index 6e80c0e..bbb6d22 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -4,6 +4,7 @@ package provider import ( "context" "fmt" + "strings" "sync" "time" @@ -33,6 +34,8 @@ type AWSProvider struct { driverMap map[string]interfaces.ResourceDriver ownershipClient ownershipTaggingClient + runnerClient awsRunnerClient + runnerConfig awsRunnerConfig } // NewAWSProvider creates a new AWS provider. @@ -66,6 +69,9 @@ func (p *AWSProvider) AWSConfigSnapshot() (awssdk.Config, bool) { // - region // - access_key_id, secret_access_key // - ecs_cluster +// - ecs_subnet_ids, ecs_security_group_ids +// - ecs_task_execution_role_arn +// - ecs_runner_log_group // // Supported config keys under the nested `credentials:` block (preferred // shape — mirrors the standalone-module path from plan-2 Tasks 4-6): @@ -131,6 +137,21 @@ func (p *AWSProvider) Initialize(ctx context.Context, config map[string]any) err p.ownershipClient = resourcegroupstaggingapi.NewFromConfig(cfg) ecsCluster, _ := config["ecs_cluster"].(string) + if ecsCluster == "" { + ecsCluster = "default" + } + p.runnerConfig = awsRunnerConfig{ + cluster: ecsCluster, + region: region, + subnetIDs: stringSliceConfig(config["ecs_subnet_ids"]), + securityGroupIDs: stringSliceConfig(config["ecs_security_group_ids"]), + taskExecutionRoleARN: stringConfig(config["ecs_task_execution_role_arn"]), + logGroup: stringConfig(config["ecs_runner_log_group"]), + } + if p.runnerConfig.logGroup == "" { + p.runnerConfig.logGroup = "/workflow/provider-ephemeral" + } + p.runnerClient = newRealAWSRunnerClient(cfg) p.registerDrivers(cfg, ecsCluster, region) p.initialized = true return nil @@ -190,6 +211,37 @@ func (p *AWSProvider) ResourceDriver(resourceType string) (interfaces.ResourceDr return d, nil } +func stringConfig(raw any) string { + v, _ := raw.(string) + return v +} + +func stringSliceConfig(raw any) []string { + switch values := raw.(type) { + case []string: + return append([]string(nil), values...) + case []any: + out := make([]string, 0, len(values)) + for _, value := range values { + if s, ok := value.(string); ok && s != "" { + out = append(out, s) + } + } + return out + case string: + var out []string + for _, part := range strings.Split(values, ",") { + part = strings.TrimSpace(part) + if part != "" { + out = append(out, part) + } + } + return out + default: + return nil + } +} + // Plan computes required create/update/delete actions for the desired state. func (p *AWSProvider) Plan(ctx context.Context, desired []interfaces.ResourceSpec, current []interfaces.ResourceState) (*interfaces.IaCPlan, error) { p.mu.RLock() @@ -387,11 +439,18 @@ func (p *AWSProvider) ResolveSizing(resourceType string, size interfaces.Size, h } // SupportedCanonicalKeys returns the full canonical IaC key set plus the -// AWS-specific keys accepted by this provider (access_key_id, secret_access_key, -// ecs_cluster). +// AWS-specific keys accepted by this provider. func (p *AWSProvider) SupportedCanonicalKeys() []string { canonical := interfaces.CanonicalKeys() - awsSpecific := []string{"access_key_id", "secret_access_key", "ecs_cluster"} + awsSpecific := []string{ + "access_key_id", + "secret_access_key", + "ecs_cluster", + "ecs_subnet_ids", + "ecs_security_group_ids", + "ecs_task_execution_role_arn", + "ecs_runner_log_group", + } result := make([]string, 0, len(canonical)+len(awsSpecific)) result = append(result, canonical...) result = append(result, awsSpecific...) diff --git a/provider/provider_test.go b/provider/provider_test.go index d11d4d3..f74f23e 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -130,8 +130,16 @@ func TestAWSProvider_SupportedCanonicalKeys(t *testing.T) { t.Errorf("SupportedCanonicalKeys missing canonical key %q", required) } } - // Must also include the AWS-specific credential and cluster keys. - for _, required := range []string{"access_key_id", "secret_access_key", "ecs_cluster"} { + // Must also include the AWS-specific credential, cluster, and runner keys. + for _, required := range []string{ + "access_key_id", + "secret_access_key", + "ecs_cluster", + "ecs_subnet_ids", + "ecs_security_group_ids", + "ecs_task_execution_role_arn", + "ecs_runner_log_group", + } { if !keySet[required] { t.Errorf("SupportedCanonicalKeys missing AWS-specific key %q", required) } diff --git a/provider/runner.go b/provider/runner.go new file mode 100644 index 0000000..e4b8eaa --- /dev/null +++ b/provider/runner.go @@ -0,0 +1,369 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + "sort" + "strings" + "time" + + "github.com/GoCodeAlone/workflow/interfaces" + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + logtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" +) + +type awsRunnerConfig struct { + cluster string + region string + subnetIDs []string + securityGroupIDs []string + taskExecutionRoleARN string + logGroup string +} + +type awsRunnerClient interface { + RegisterTaskDefinition(ctx context.Context, in *ecs.RegisterTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.RegisterTaskDefinitionOutput, error) + RunTask(ctx context.Context, in *ecs.RunTaskInput, optFns ...func(*ecs.Options)) (*ecs.RunTaskOutput, error) + DescribeTasks(ctx context.Context, in *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) + GetLogEvents(ctx context.Context, in *cloudwatchlogs.GetLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogEventsOutput, error) +} + +type realAWSRunnerClient struct { + ecs *ecs.Client + logs *cloudwatchlogs.Client +} + +func newRealAWSRunnerClient(cfg awssdk.Config) awsRunnerClient { + return &realAWSRunnerClient{ + ecs: ecs.NewFromConfig(cfg), + logs: cloudwatchlogs.NewFromConfig(cfg), + } +} + +func (c *realAWSRunnerClient) RegisterTaskDefinition(ctx context.Context, in *ecs.RegisterTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.RegisterTaskDefinitionOutput, error) { + return c.ecs.RegisterTaskDefinition(ctx, in, optFns...) +} + +func (c *realAWSRunnerClient) RunTask(ctx context.Context, in *ecs.RunTaskInput, optFns ...func(*ecs.Options)) (*ecs.RunTaskOutput, error) { + return c.ecs.RunTask(ctx, in, optFns...) +} + +func (c *realAWSRunnerClient) DescribeTasks(ctx context.Context, in *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + return c.ecs.DescribeTasks(ctx, in, optFns...) +} + +func (c *realAWSRunnerClient) GetLogEvents(ctx context.Context, in *cloudwatchlogs.GetLogEventsInput, optFns ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogEventsOutput, error) { + return c.logs.GetLogEvents(ctx, in, optFns...) +} + +var _ interfaces.IaCProviderRunner = (*AWSProvider)(nil) + +func (p *AWSProvider) RunJob(ctx context.Context, spec interfaces.JobSpec) (*interfaces.JobHandle, error) { + p.mu.RLock() + client := p.runnerClient + cfg := p.runnerConfig + p.mu.RUnlock() + if client == nil || cfg.cluster == "" || cfg.region == "" { + return nil, fmt.Errorf("aws runner: provider is not initialized") + } + if strings.TrimSpace(spec.Image) == "" { + return nil, fmt.Errorf("aws runner: image is required") + } + if strings.TrimSpace(spec.RunCommand) == "" { + return nil, fmt.Errorf("aws runner: run_command is required") + } + if len(cfg.subnetIDs) > 0 && cfg.taskExecutionRoleARN == "" { + return nil, fmt.Errorf("aws runner: ecs_task_execution_role_arn is required when ecs_subnet_ids selects Fargate") + } + + name := awsJobName(spec.Name) + tdOut, err := client.RegisterTaskDefinition(ctx, awsTaskDefinitionInput(name, spec, cfg)) + if err != nil { + return nil, fmt.Errorf("aws runner: register task definition %q: %w", name, err) + } + taskDefARN := awssdk.ToString(tdOut.TaskDefinition.TaskDefinitionArn) + runOut, err := client.RunTask(ctx, awsRunTaskInput(name, taskDefARN, cfg)) + if err != nil { + return nil, fmt.Errorf("aws runner: run task %q: %w", name, err) + } + if len(runOut.Failures) > 0 { + return nil, fmt.Errorf("aws runner: run task %q: %s", name, awsTaskFailures(runOut.Failures)) + } + if len(runOut.Tasks) == 0 { + return nil, fmt.Errorf("aws runner: run task %q returned no tasks", name) + } + taskARN := awssdk.ToString(runOut.Tasks[0].TaskArn) + return &interfaces.JobHandle{ + ID: taskARN, + Name: name, + Provider: "aws", + Metadata: map[string]string{ + "cluster": cfg.cluster, + "task_arn": taskARN, + "task_definition_arn": taskDefARN, + "container": name, + "log_group": cfg.logGroup, + "log_stream": awsLogStream(name, taskARN), + }, + }, nil +} + +func (p *AWSProvider) JobStatus(ctx context.Context, handle interfaces.JobHandle) (*interfaces.JobStatusReply, error) { + p.mu.RLock() + client := p.runnerClient + defaultCluster := p.runnerConfig.cluster + p.mu.RUnlock() + if client == nil { + return nil, fmt.Errorf("aws runner: provider is not initialized") + } + cluster := handle.Metadata["cluster"] + if cluster == "" { + cluster = defaultCluster + } + taskARN := handle.Metadata["task_arn"] + if taskARN == "" { + taskARN = handle.ID + } + if cluster == "" || taskARN == "" { + return nil, fmt.Errorf("aws runner: cluster and task_arn metadata are required") + } + out, err := client.DescribeTasks(ctx, &ecs.DescribeTasksInput{ + Cluster: awssdk.String(cluster), + Tasks: []string{taskARN}, + }) + if err != nil { + return nil, fmt.Errorf("aws runner: describe task %q: %w", taskARN, err) + } + if len(out.Tasks) == 0 { + return nil, fmt.Errorf("aws runner: task %q not found", taskARN) + } + state, exitCode, message := awsTaskState(out.Tasks[0]) + return &interfaces.JobStatusReply{Handle: handle, State: state, ExitCode: exitCode, Message: message}, nil +} + +func (p *AWSProvider) JobLogs(ctx context.Context, handle interfaces.JobHandle, sink interfaces.LogCaptureSink) error { + p.mu.RLock() + client := p.runnerClient + p.mu.RUnlock() + if client == nil { + return fmt.Errorf("aws runner: provider is not initialized") + } + if sink == nil { + return nil + } + logGroup := handle.Metadata["log_group"] + logStream := handle.Metadata["log_stream"] + if logGroup == "" || logStream == "" { + return sink.WriteLogChunk(interfaces.LogChunk{EOF: true}) + } + var nextToken *string + for { + in := &cloudwatchlogs.GetLogEventsInput{ + LogGroupName: awssdk.String(logGroup), + LogStreamName: awssdk.String(logStream), + StartFromHead: awssdk.Bool(true), + Limit: awssdk.Int32(1000), + NextToken: nextToken, + } + out, err := client.GetLogEvents(ctx, in) + if err != nil { + return fmt.Errorf("aws runner: get log events %q/%q: %w", logGroup, logStream, err) + } + for _, event := range out.Events { + msg := awssdk.ToString(event.Message) + if msg == "" { + continue + } + if !strings.HasSuffix(msg, "\n") { + msg += "\n" + } + if err := sink.WriteLogChunk(interfaces.LogChunk{Data: []byte(msg), Source: "stdout"}); err != nil { + return err + } + } + if out.NextForwardToken == nil || (nextToken != nil && awssdk.ToString(out.NextForwardToken) == awssdk.ToString(nextToken)) { + break + } + nextToken = out.NextForwardToken + } + return sink.WriteLogChunk(interfaces.LogChunk{EOF: true}) +} + +func awsTaskDefinitionInput(name string, spec interfaces.JobSpec, cfg awsRunnerConfig) *ecs.RegisterTaskDefinitionInput { + launchCompat := ecstypes.CompatibilityEc2 + networkMode := ecstypes.NetworkModeBridge + if len(cfg.subnetIDs) > 0 { + launchCompat = ecstypes.CompatibilityFargate + networkMode = ecstypes.NetworkModeAwsvpc + } + in := &ecs.RegisterTaskDefinitionInput{ + Family: awssdk.String(name), + RequiresCompatibilities: []ecstypes.Compatibility{launchCompat}, + NetworkMode: networkMode, + Cpu: awssdk.String("256"), + Memory: awssdk.String("512"), + ContainerDefinitions: []ecstypes.ContainerDefinition{{ + Name: awssdk.String(name), + Image: awssdk.String(spec.Image), + Essential: awssdk.Bool(true), + EntryPoint: []string{"/bin/sh", "-c"}, + Command: []string{spec.RunCommand}, + Environment: awsJobEnvironment(spec.EnvVars), + Secrets: awsJobSecrets(spec.EnvVarsSecret), + LogConfiguration: &ecstypes.LogConfiguration{ + LogDriver: ecstypes.LogDriverAwslogs, + Options: map[string]string{ + "awslogs-group": cfg.logGroup, + "awslogs-region": cfg.region, + "awslogs-stream-prefix": name, + "awslogs-create-group": "true", + }, + }, + }}, + } + if cfg.taskExecutionRoleARN != "" { + in.ExecutionRoleArn = awssdk.String(cfg.taskExecutionRoleARN) + } + return in +} + +func awsRunTaskInput(name, taskDefARN string, cfg awsRunnerConfig) *ecs.RunTaskInput { + in := &ecs.RunTaskInput{ + Cluster: awssdk.String(cfg.cluster), + TaskDefinition: awssdk.String(taskDefARN), + StartedBy: awssdk.String(awsStartedBy(name)), + LaunchType: ecstypes.LaunchTypeEc2, + } + if len(cfg.subnetIDs) > 0 { + in.LaunchType = ecstypes.LaunchTypeFargate + in.NetworkConfiguration = &ecstypes.NetworkConfiguration{ + AwsvpcConfiguration: &ecstypes.AwsVpcConfiguration{ + Subnets: append([]string(nil), cfg.subnetIDs...), + SecurityGroups: append([]string(nil), cfg.securityGroupIDs...), + AssignPublicIp: ecstypes.AssignPublicIpEnabled, + }, + } + } + return in +} + +func awsJobEnvironment(values map[string]string) []ecstypes.KeyValuePair { + keys := sortedAWSMapKeys(values) + out := make([]ecstypes.KeyValuePair, 0, len(keys)) + for _, key := range keys { + out = append(out, ecstypes.KeyValuePair{Name: awssdk.String(key), Value: awssdk.String(values[key])}) + } + return out +} + +func awsJobSecrets(values map[string]string) []ecstypes.Secret { + keys := sortedAWSMapKeys(values) + out := make([]ecstypes.Secret, 0, len(keys)) + for _, key := range keys { + out = append(out, ecstypes.Secret{Name: awssdk.String(key), ValueFrom: awssdk.String(values[key])}) + } + return out +} + +func sortedAWSMapKeys(values map[string]string) []string { + keys := make([]string, 0, len(values)) + for key := range values { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +var nonAWSJobName = regexp.MustCompile(`[^a-z0-9-]+`) + +func awsJobName(name string) string { + name = strings.ToLower(strings.TrimSpace(name)) + name = nonAWSJobName.ReplaceAllString(name, "-") + name = strings.Trim(name, "-") + if name == "" { + name = "provider-ephemeral-job" + } + suffix := fmt.Sprintf("-%d", time.Now().UnixNano()) + maxBase := 64 - len(suffix) + if len(name) > maxBase { + name = strings.TrimRight(name[:maxBase], "-") + } + return name + suffix +} + +func awsStartedBy(name string) string { + if len(name) <= 36 { + return name + } + return name[:36] +} + +func awsLogStream(containerName, taskARN string) string { + taskID := taskARN + if i := strings.LastIndex(taskID, "/"); i >= 0 && i < len(taskID)-1 { + taskID = taskID[i+1:] + } + return containerName + "/" + containerName + "/" + taskID +} + +func awsTaskFailures(failures []ecstypes.Failure) string { + parts := make([]string, 0, len(failures)) + for _, failure := range failures { + reason := awssdk.ToString(failure.Reason) + arn := awssdk.ToString(failure.Arn) + if arn != "" && reason != "" { + parts = append(parts, arn+": "+reason) + } else if reason != "" { + parts = append(parts, reason) + } else if arn != "" { + parts = append(parts, arn) + } + } + return strings.Join(parts, "; ") +} + +func awsTaskState(task ecstypes.Task) (interfaces.JobState, int, string) { + status := strings.ToUpper(awssdk.ToString(task.LastStatus)) + exitCode := 0 + hasExitCode := false + message := awssdk.ToString(task.StoppedReason) + for _, container := range task.Containers { + if container.ExitCode != nil { + exitCode = int(*container.ExitCode) + hasExitCode = true + } + if message == "" { + message = awssdk.ToString(container.Reason) + } + } + switch status { + case "PROVISIONING", "PENDING", "ACTIVATING": + return interfaces.JobStatePending, exitCode, message + case "RUNNING": + return interfaces.JobStateRunning, exitCode, message + case "STOPPED", "DEACTIVATING", "DEPROVISIONING": + if !hasExitCode { + return interfaces.JobStateFailed, 1, message + } + if exitCode == 0 { + return interfaces.JobStateSucceeded, exitCode, message + } + return interfaces.JobStateFailed, exitCode, message + default: + return interfaces.JobStateUnknown, exitCode, message + } +} + +func awsLogMessages(events []logtypes.OutputLogEvent) []string { + out := make([]string, 0, len(events)) + for _, event := range events { + if msg := awssdk.ToString(event.Message); msg != "" { + out = append(out, msg) + } + } + return out +} diff --git a/provider/runner_test.go b/provider/runner_test.go new file mode 100644 index 0000000..7fc50d5 --- /dev/null +++ b/provider/runner_test.go @@ -0,0 +1,216 @@ +package provider + +import ( + "context" + "strings" + "testing" + + "github.com/GoCodeAlone/workflow/interfaces" + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs" + logtypes "github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs/types" + "github.com/aws/aws-sdk-go-v2/service/ecs" + ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types" +) + +type fakeAWSRunnerClient struct { + registerIn *ecs.RegisterTaskDefinitionInput + runIn *ecs.RunTaskInput + describeIn *ecs.DescribeTasksInput + logsIn *cloudwatchlogs.GetLogEventsInput + task ecstypes.Task + events []logtypes.OutputLogEvent +} + +func (f *fakeAWSRunnerClient) RegisterTaskDefinition(_ context.Context, in *ecs.RegisterTaskDefinitionInput, _ ...func(*ecs.Options)) (*ecs.RegisterTaskDefinitionOutput, error) { + f.registerIn = in + return &ecs.RegisterTaskDefinitionOutput{TaskDefinition: &ecstypes.TaskDefinition{ + TaskDefinitionArn: awssdk.String("arn:aws:ecs:us-east-1:123:task-definition/" + awssdk.ToString(in.Family) + ":1"), + }}, nil +} + +func (f *fakeAWSRunnerClient) RunTask(_ context.Context, in *ecs.RunTaskInput, _ ...func(*ecs.Options)) (*ecs.RunTaskOutput, error) { + f.runIn = in + return &ecs.RunTaskOutput{Tasks: []ecstypes.Task{{ + TaskArn: awssdk.String("arn:aws:ecs:us-east-1:123:cluster/default/task/task-1"), + }}}, nil +} + +func (f *fakeAWSRunnerClient) DescribeTasks(_ context.Context, in *ecs.DescribeTasksInput, _ ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) { + f.describeIn = in + return &ecs.DescribeTasksOutput{Tasks: []ecstypes.Task{f.task}}, nil +} + +func (f *fakeAWSRunnerClient) GetLogEvents(_ context.Context, in *cloudwatchlogs.GetLogEventsInput, _ ...func(*cloudwatchlogs.Options)) (*cloudwatchlogs.GetLogEventsOutput, error) { + f.logsIn = in + return &cloudwatchlogs.GetLogEventsOutput{Events: f.events}, nil +} + +func TestAWSRunnerRunJobCreatesFargateTask(t *testing.T) { + client := &fakeAWSRunnerClient{} + p := &AWSProvider{ + initialized: true, + runnerClient: client, + runnerConfig: awsRunnerConfig{ + cluster: "default", + region: "us-east-1", + subnetIDs: []string{"subnet-1"}, + securityGroupIDs: []string{"sg-1"}, + taskExecutionRoleARN: "arn:aws:iam::123:role/ecsTaskExecutionRole", + logGroup: "/workflow/provider-ephemeral", + }, + } + + handle, err := p.RunJob(context.Background(), interfaces.JobSpec{ + Name: "Migrate DB!", + Image: "123.dkr.ecr.us-east-1.amazonaws.com/app:migrate", + RunCommand: "bin/migrate up", + EnvVars: map[string]string{"PLAIN": "value"}, + EnvVarsSecret: map[string]string{"DATABASE_URL": "arn:aws:ssm:us-east-1:123:parameter/db"}, + }) + if err != nil { + t.Fatalf("RunJob returned error: %v", err) + } + if handle.Provider != "aws" || handle.Metadata["task_arn"] == "" || handle.Metadata["log_stream"] == "" { + t.Fatalf("handle = %+v", handle) + } + if client.registerIn.NetworkMode != ecstypes.NetworkModeAwsvpc || + client.registerIn.RequiresCompatibilities[0] != ecstypes.CompatibilityFargate { + t.Fatalf("task def network/compat = %s/%v", client.registerIn.NetworkMode, client.registerIn.RequiresCompatibilities) + } + container := client.registerIn.ContainerDefinitions[0] + if awssdk.ToString(container.Image) != "123.dkr.ecr.us-east-1.amazonaws.com/app:migrate" { + t.Fatalf("image = %q", awssdk.ToString(container.Image)) + } + if len(container.EntryPoint) != 2 || container.EntryPoint[0] != "/bin/sh" || container.EntryPoint[1] != "-c" || + len(container.Command) != 1 || container.Command[0] != "bin/migrate up" { + t.Fatalf("entrypoint=%v command=%v", container.EntryPoint, container.Command) + } + if !hasAWSEnv(container.Environment, "PLAIN", "value") { + t.Fatalf("missing env: %#v", container.Environment) + } + if !hasAWSSecret(container.Secrets, "DATABASE_URL", "arn:aws:ssm:us-east-1:123:parameter/db") { + t.Fatalf("missing secret: %#v", container.Secrets) + } + if client.runIn.LaunchType != ecstypes.LaunchTypeFargate || + client.runIn.NetworkConfiguration == nil || + len(client.runIn.NetworkConfiguration.AwsvpcConfiguration.Subnets) != 1 { + t.Fatalf("run task input = %#v", client.runIn) + } +} + +func TestAWSRunnerRunJobRequiresExecutionRoleForFargate(t *testing.T) { + p := &AWSProvider{ + initialized: true, + runnerClient: &fakeAWSRunnerClient{}, + runnerConfig: awsRunnerConfig{ + cluster: "default", + region: "us-east-1", + subnetIDs: []string{"subnet-1"}, + logGroup: "/workflow/provider-ephemeral", + }, + } + _, err := p.RunJob(context.Background(), interfaces.JobSpec{ + Name: "job", + Image: "repo/app:latest", + RunCommand: "echo ok", + }) + if err == nil || !strings.Contains(err.Error(), "ecs_task_execution_role_arn is required") { + t.Fatalf("RunJob error = %v", err) + } +} + +func TestAWSRunnerStatusAndLogs(t *testing.T) { + exit := int32(7) + client := &fakeAWSRunnerClient{ + task: ecstypes.Task{ + LastStatus: awssdk.String("STOPPED"), + StoppedReason: awssdk.String("Essential container exited"), + Containers: []ecstypes.Container{{ + ExitCode: &exit, + }}, + }, + events: []logtypes.OutputLogEvent{{Message: awssdk.String("migration failed")}}, + } + p := &AWSProvider{runnerClient: client, runnerConfig: awsRunnerConfig{cluster: "default"}} + handle := interfaces.JobHandle{ + ID: "task-1", + Metadata: map[string]string{ + "cluster": "default", + "task_arn": "task-1", + "log_group": "/workflow/provider-ephemeral", + "log_stream": "job/job/task-1", + }, + } + + status, err := p.JobStatus(context.Background(), handle) + if err != nil { + t.Fatalf("JobStatus returned error: %v", err) + } + if status.State != interfaces.JobStateFailed || status.ExitCode != 7 { + t.Fatalf("status = %+v", status) + } + + sink := &runnerSink{} + if err := p.JobLogs(context.Background(), handle, sink); err != nil { + t.Fatalf("JobLogs returned error: %v", err) + } + if string(sink.data) != "migration failed\n" || !sink.eof { + t.Fatalf("sink data=%q eof=%v", string(sink.data), sink.eof) + } +} + +func TestAWSRunnerStoppedTaskWithoutExitCodeFails(t *testing.T) { + client := &fakeAWSRunnerClient{ + task: ecstypes.Task{ + LastStatus: awssdk.String("STOPPED"), + StoppedReason: awssdk.String("CannotPullContainerError"), + }, + } + p := &AWSProvider{runnerClient: client, runnerConfig: awsRunnerConfig{cluster: "default"}} + status, err := p.JobStatus(context.Background(), interfaces.JobHandle{ + ID: "task-1", + Metadata: map[string]string{"cluster": "default", "task_arn": "task-1"}, + }) + if err != nil { + t.Fatalf("JobStatus returned error: %v", err) + } + if status.State != interfaces.JobStateFailed || status.ExitCode != 1 { + t.Fatalf("status = %+v", status) + } +} + +func hasAWSEnv(values []ecstypes.KeyValuePair, key, value string) bool { + for _, env := range values { + if awssdk.ToString(env.Name) == key && awssdk.ToString(env.Value) == value { + return true + } + } + return false +} + +func hasAWSSecret(values []ecstypes.Secret, key, value string) bool { + for _, secret := range values { + if awssdk.ToString(secret.Name) == key && awssdk.ToString(secret.ValueFrom) == value { + return true + } + } + return false +} + +type runnerSink struct { + data []byte + eof bool +} + +func (s *runnerSink) WriteLogChunk(chunk interfaces.LogChunk) error { + if chunk.EOF { + s.eof = true + return nil + } + if strings.Contains(strings.ToLower(chunk.Source), "stderr") { + return nil + } + s.data = append(s.data, chunk.Data...) + return nil +}