diff --git a/.github/assets/Operator-Design.svg b/.github/assets/Operator-Design.svg index 813363c..966d50e 100644 --- a/.github/assets/Operator-Design.svg +++ b/.github/assets/Operator-Design.svg @@ -1,4 +1,4 @@ -KayronGithubinfrastructurereleasecontainerreferencev1.8.3v1.9.0ECSGithubstagingtestingCloudFormationrdsecsvpcregistrypolicynetworkcachescopev1.8.3v1.9.0ec2cloudformationtemplate \ No newline at end of file +Kayroninfrastructurepreviewcontainerreferenceregistrypolicycachescopecloudformationtemplaterelease \ No newline at end of file diff --git a/README.md b/README.md index 7925afb..7e4f207 100644 --- a/README.md +++ b/README.md @@ -147,167 +147,263 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration ```yaml === RUN Test_Operator_Integration { - "time": "2025-08-21 18:19:32", + "time": "2025-09-17 15:31:59", "level": "debug", "message": "resetting operator cache", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/delete.go:9" } { - "time": "2025-08-21 18:19:34", + "time": "2025-09-17 15:32:01", "level": "debug", "message": "resolved ref for github repository", "environment": "testing", - "ref": "2fd5cb440fe5e6b9ff205ea94c07a77a331fc79e", + "ref": "176ffefa272b210eb3be269887d665a682dbd548", "repository": "https://github.com/0xSplits/releases", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/release/ensure.go:65" + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/release/ensure.go:66" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", - "deploy": "v0.1.5", + "deploy": "branch=preview", "github": "infrastructure", + "preview": "false", "provider": "cloudformation", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", - "deploy": "v0.1.9", + "deploy": "release=v0.1.1", + "docker": "splits-lite", + "github": "splits-lite", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" +} +{ + "time": "2025-09-17 15:32:02", + "level": "debug", + "message": "caching release artifact", + "deploy": "branch=fancy-feature-branch", + "docker": "splits-lite", + "github": "splits-lite", + "preview": "true", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" +} +{ + "time": "2025-09-17 15:32:02", + "level": "debug", + "message": "caching release artifact", + "deploy": "branch=preview", "docker": "kayron", "github": "kayron", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching release artifact", - "deploy": "v0.1.18", + "deploy": "release=v0.2.2", "docker": "specta", "github": "specta", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/cache/create.go:17" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "instrumented worker handler", "handler": "release", - "latency": "2.2552785s", + "latency": "3.727451s", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" +} +{ + "time": "2025-09-17 15:32:02", + "level": "debug", + "message": "caching current state", + "current": "d5fa88afd502edc9052f89c956618b2cb567d984", + "github": "infrastructure", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/template/ensure.go:35" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:02", + "level": "debug", + "message": "instrumented worker handler", + "handler": "template", + "latency": "173.083µs", + "success": "true", + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" +} +{ + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching desired state", - "desired": "v0.1.18", - "github": "specta", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:35" + "desired": "v0.1.1", + "github": "splits-lite", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:02", "level": "debug", "message": "caching desired state", - "desired": "v0.1.9", - "github": "kayron", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:35" + "desired": "v0.2.2", + "github": "specta", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:03", "level": "debug", "message": "caching desired state", - "desired": "v0.1.5", + "desired": "d5fa88afd502edc9052f89c956618b2cb567d984", "github": "infrastructure", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:35" + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:03", "level": "debug", - "message": "caching current state", - "current": "v0.1.5", - "github": "infrastructure", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/template/ensure.go:35" + "message": "caching desired state", + "desired": "eb9f56e195f25218499897a6c6d79c62261faba5", + "github": "kayron", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:03", + "level": "debug", + "message": "caching desired state", + "desired": "e307253e62c2da119579e2505db0b6185642ab9b", + "github": "splits-lite", + "preview": "true", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/reference/ensure.go:45" +} +{ + "time": "2025-09-17 15:32:03", "level": "debug", "message": "instrumented worker handler", "handler": "reference", - "latency": "494.083µs", + "latency": "367.2485ms", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:35", + "time": "2025-09-17 15:32:04", "level": "debug", - "message": "instrumented worker handler", - "handler": "template", - "latency": "741.125µs", - "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "message": "caching current state", + "current": "bc7891268e44f62e0aebbe339c0850b61d52c417", + "docker": "splits-lite", + "preview": "false", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "caching current state", - "current": "v0.1.9", + "current": "bc7891268e44f62e0aebbe339c0850b61d52c417", + "docker": "splits-lite", + "preview": "true", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" +} +{ + "time": "2025-09-17 15:32:04", + "level": "debug", + "message": "caching current state", + "current": "eb9f56e195f25218499897a6c6d79c62261faba5", "docker": "kayron", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "caching current state", - "current": "v0.1.18", + "current": "v0.2.2", "docker": "specta", + "preview": "false", "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/container/cache.go:10" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:04", "level": "debug", "message": "instrumented worker handler", "handler": "container", - "latency": "2.0316385s", + "latency": "1.705606292s", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" +} +{ + "time": "2025-09-17 15:32:05", + "level": "debug", + "message": "executed image check", + "exists": "true", + "image": "splits-lite", + "preview": "true", + "tag": "e307253e62c2da119579e2505db0b6185642ab9b", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/registry/ensure.go:45" +} +{ + "time": "2025-09-17 15:32:05", + "level": "debug", + "message": "executed image check", + "exists": "true", + "image": "splits-lite", + "preview": "false", + "tag": "v0.1.1", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/registry/ensure.go:45" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:05", "level": "debug", "message": "instrumented worker handler", "handler": "registry", - "latency": "55.416µs", + "latency": "875.278125ms", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:05", "level": "info", "message": "continuing reconciliation loop", "reason": "detected state drift", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:47" + "release": "splits-lite", + "version": "v0.1.1", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:68" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:05", + "level": "info", + "message": "continuing reconciliation loop", + "domain": "1d0fd508.lite.testing.splits.org", + "reason": "detected state drift", + "release": "splits-lite", + "version": "e307253e62c2da119579e2505db0b6185642ab9b", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/policy/ensure.go:68" +} +{ + "time": "2025-09-17 15:32:05", "level": "debug", "message": "instrumented worker handler", "handler": "policy", - "latency": "112.334µs", + "latency": "523.166µs", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:05", "level": "debug", "message": "resolved ref for github repository", "environment": "testing", - "ref": "v0.1.5", + "ref": "d5fa88afd502edc9052f89c956618b2cb567d984", "repository": "https://github.com/0xSplits/infrastructure", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/ensure.go:22" + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/ensure.go:24" } { - "time": "2025-08-21 18:19:37", + "time": "2025-09-17 15:32:05", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -315,7 +411,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -323,7 +419,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -331,7 +427,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -339,7 +435,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:38", + "time": "2025-09-17 15:32:06", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -347,7 +443,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:39", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -355,7 +451,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:39", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -363,7 +459,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:39", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -371,7 +467,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 15:32:07", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -379,7 +475,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -387,7 +483,15 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 15:32:08", + "level": "debug", + "message": "uploading cloudformation template", + "bucket": "splits-cf-templates", + "key": "testing/splits-lite/splits-lite.yaml", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" +} +{ + "time": "2025-09-17 15:32:08", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -395,7 +499,7 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "uploading cloudformation template", "bucket": "splits-cf-templates", @@ -403,33 +507,34 @@ go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/infrastructure/aws.go:26" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "instrumented worker handler", "handler": "infrastructure", - "latency": "3.557394083s", + "latency": "3.615857209s", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 15:32:08", "level": "info", "message": "updating cloudformation stack", "name": "server-test", - "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/cloudformation/ensure.go:44" + "url": "https://splits-cf-templates.s3.us-west-2.amazonaws.com/testing/index.yaml", + "caller": "/Users/xh3b4sd/project/0xSplits/kayron/pkg/operator/cloudformation/ensure.go:30" } { - "time": "2025-08-21 18:19:40", + "time": "2025-09-17 15:32:08", "level": "debug", "message": "instrumented worker handler", "handler": "cloudformation", - "latency": "190.917µs", + "latency": "190.125µs", "success": "true", - "caller": "/Users/xh3b4sd/project/0xSplits/workit/handler/metrics/ensure.go:55" + "caller": "/Users/xh3b4sd/go/pkg/mod/github.com/0x!splits/workit@v0.6.0/handler/metrics/ensure.go:55" } ---- PASS: Test_Operator_Integration (7.85s) +--- PASS: Test_Operator_Integration (9.93s) PASS -ok github.com/0xSplits/kayron/pkg/operator 9.139s +ok github.com/0xSplits/kayron/pkg/operator 11.224s ``` ### Releases diff --git a/go.mod b/go.mod index 3f1b154..9542ff9 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,10 @@ module github.com/0xSplits/kayron go 1.24.0 -toolchain go1.24.6 - require ( github.com/0xSplits/otelgo v0.1.2 github.com/0xSplits/roghfs v0.1.0 - github.com/0xSplits/workit v0.6.0 + github.com/0xSplits/workit v0.7.0 github.com/aws/aws-sdk-go-v2 v1.39.0 github.com/aws/aws-sdk-go-v2/config v1.31.8 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.66.2 @@ -18,6 +16,7 @@ require ( github.com/distribution/reference v0.6.0 github.com/goccy/go-yaml v1.18.0 github.com/google/go-cmp v0.7.0 + github.com/google/go-containerregistry v0.20.6 github.com/google/go-github/v73 v73.0.0 github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 @@ -58,10 +57,8 @@ require ( github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/go-containerregistry v0.20.6 github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -71,20 +68,20 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/otlptranslator v0.0.2 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.17.0 // indirect - github.com/puzpuzpuz/xsync/v4 v4.1.0 // indirect + github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/vbatts/tar-split v0.12.1 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.60.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index 47655f4..cd8b516 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/0xSplits/otelgo v0.1.2 h1:QjbUMNNQcUsnkOmZ35bc3Fbhz7u0PA611LYrh4aOpPk github.com/0xSplits/otelgo v0.1.2/go.mod h1:YnmWxWUT7xaMQTF3FBmN8GuvIMafSJ/lYhlyIBlr99w= github.com/0xSplits/roghfs v0.1.0 h1:E3BB8+w+X3g64ezuJ13FssmfNorqy/fx2RGWBFtVifw= github.com/0xSplits/roghfs v0.1.0/go.mod h1:KVlXti9dNWj2YtskpRjZNFqccSKsh5K+UJs73GMXTJs= -github.com/0xSplits/workit v0.6.0 h1:h2LrDdkOTuokSOwt0E3dt8N90fsMsoNd3CCsyI3U0f8= -github.com/0xSplits/workit v0.6.0/go.mod h1:+SQ35oJXLBigYeA1VSrKkJNaX4KBLE+M4PuoBZT1xs0= +github.com/0xSplits/workit v0.7.0 h1:kBa6bJ/9mjUIJYhi+MzBI5kY1iEfSyAYGH+H7wrSMoY= +github.com/0xSplits/workit v0.7.0/go.mod h1:9vuuL6Lr+KfZuOKMihtymd11+8Yq5woBqRqra29gd6U= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/aws/aws-sdk-go-v2 v1.39.0 h1:xm5WV/2L4emMRmMjHFykqiA4M/ra0DJVSWUkDyBjbg4= @@ -88,8 +88,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 h1:cLN4IBkmkYZNnk7EAJ0BHIethd+J6LqxFNw5mSiI2bM= -github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -122,14 +120,14 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= -github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U= -github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0= +github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -154,8 +152,8 @@ github.com/xh3b4sd/logger v0.11.1 h1:aTK4ygh7aPv1jq54J8bx+zjH6A8RYdkKAgOZYw867C0 github.com/xh3b4sd/logger v0.11.1/go.mod h1:MC7Dp7RC3tZ182KlvSulGcRQVX/D2l+WlCSGLF1mvO8= github.com/xh3b4sd/tracer v1.0.0 h1:mr9uYCx/Ry2w1wdJz0V0Kq71/KeF+hUQjbZQJCxm3Zw= github.com/xh3b4sd/tracer v1.0.0/go.mod h1:nfZeNH5RRfqE6ctQroIfY75b2NRlJHl2g+HP7ddvHrM= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= @@ -170,8 +168,8 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -180,8 +178,8 @@ golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/cache/create.go b/pkg/cache/create.go index ecdbe78..856326b 100644 --- a/pkg/cache/create.go +++ b/pkg/cache/create.go @@ -20,6 +20,7 @@ func (c *Cache) Create(rel release.Slice) error { "docker", x.Docker.String(), "github", x.Github.String(), "deploy", x.Deploy.String(), + "preview", x.Deploy.Preview.String(), "provider", x.Provider.String(), ) diff --git a/pkg/cache/object.go b/pkg/cache/object.go index 9b41520..c02acc0 100644 --- a/pkg/cache/object.go +++ b/pkg/cache/object.go @@ -30,6 +30,34 @@ type Object struct { kin kind } +// Domain returns the hash based testing domain for preview deployments, or an +// empty string for any other main release and non-testing environment. +func (o Object) Domain(env string) string { + // Note that we filter the domain name creation by preview deployments, + // because at the time of writing we do not have any convenient way to tell + // whether this release artifact is exposed to the internet via DNS. Right now + // we only know that for certain in case of preview deployments, because their + // sole purpose is to be exposed to the internet. + + if !bool(o.Release.Deploy.Preview) { + return "" + } + + return fmt.Sprintf("%s.%s.%s.splits.org", + o.Release.Labels.Hash.Lower(), + + // Note that this is a dirty hack to make preview deployments work today for + // existing services that already work using certain incosnistencies between + // repository and domain names. E.g. we have "splits-lite" in Github, but + // use just "lite.testing.splits.org". A better way of doing this would be + // to allow for some kind of domain configuration in the release definition, + // so that we can remove this magical string replacement below. + strings.TrimPrefix(o.Release.Docker.String(), "splits-"), + + env, + ) +} + func (o Object) Name() string { if o.kin == Infrastructure { return o.Release.Github.String() diff --git a/pkg/cache/previews.go b/pkg/cache/previews.go new file mode 100644 index 0000000..4258efb --- /dev/null +++ b/pkg/cache/previews.go @@ -0,0 +1,20 @@ +package cache + +// Previews returns all cached service release artifacts that are defined as +// preview deployments. +func (c *Cache) Previews(doc string) []Object { + { + c.mut.Lock() + defer c.mut.Unlock() + } + + var lis []Object + + for _, x := range c.ser { + if bool(x.Release.Deploy.Preview) && x.Release.Docker.String() == doc { + lis = append(lis, x) + } + } + + return lis +} diff --git a/pkg/cache/services.go b/pkg/cache/services.go index 8a1da84..3b7f04e 100644 --- a/pkg/cache/services.go +++ b/pkg/cache/services.go @@ -1,5 +1,7 @@ package cache +// Services returns all cached service release artifacts, including those of any +// preview deployments. func (c *Cache) Services() []Object { { c.mut.Lock() diff --git a/pkg/cache/update.go b/pkg/cache/update.go index 2e33c86..5862f2e 100644 --- a/pkg/cache/update.go +++ b/pkg/cache/update.go @@ -12,5 +12,6 @@ func (c *Cache) Update(obj Object) { if obj.kin == Service { c.ser[obj.ind].Artifact = c.ser[obj.ind].Artifact.Merge(obj.Artifact) + c.ser[obj.ind].Release.Deploy.Preview = obj.Release.Deploy.Preview } } diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go new file mode 100644 index 0000000..2b175de --- /dev/null +++ b/pkg/hash/hash.go @@ -0,0 +1,70 @@ +package hash + +import ( + "crypto/sha256" + "encoding/hex" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type Hash struct { + // dsh is the dashed prefix version of Upp. + // + // -1D0FD508 + // + dsh []byte + // low is the lower case version of Upp. + // + // 1d0fd508 + // + low []byte + // upp is the upper case version of this hash. + // + // 1D0FD508 + // + upp []byte +} + +func New(str string) Hash { + var hsh string + { + hsh = newHsh(str) + } + + var low string + var upp string + { + low = cases.Lower(language.English).String(hsh) + upp = cases.Upper(language.English).String(hsh) + } + + return Hash{ + dsh: []byte("-" + upp), + low: []byte(low), + upp: []byte(upp), + } +} + +func (h Hash) Dashed() string { + return string(h.dsh) +} + +func (h Hash) Empty() bool { + return h.dsh == nil && h.low == nil && h.upp == nil +} + +func (h Hash) Lower() string { + return string(h.low) +} + +func (h Hash) Upper() string { + return string(h.upp) +} + +func newHsh(str string) string { + sum := sha256.Sum256([]byte(str)) + enc := hex.EncodeToString(sum[:]) + + return enc[:8] +} diff --git a/pkg/operator/chain.go b/pkg/operator/chain.go index a661fc1..3d16760 100644 --- a/pkg/operator/chain.go +++ b/pkg/operator/chain.go @@ -15,6 +15,12 @@ func (o *Operator) Chain() [][]handler.Ensure { // reconciliation loops. {o.release}, + // Inject any potential preview deployments into our internal list of + // release definitions so that we can render and expose any additional + // development services during testing. Note that this worker handler is + // only active within the testing environment. + {o.preview}, + // Run the next steps in parallel in order to find the current and // desired state of the release artifacts that we are tasked to // managed. diff --git a/pkg/operator/cloudformation/active.go b/pkg/operator/cloudformation/active.go new file mode 100644 index 0000000..a5e86cb --- /dev/null +++ b/pkg/operator/cloudformation/active.go @@ -0,0 +1,6 @@ +package cloudformation + +// Active defines this worker handler to always be executed. +func (c *CloudFormation) Active() bool { + return true +} diff --git a/pkg/operator/cloudformation/ensure.go b/pkg/operator/cloudformation/ensure.go index a808a17..a200bf2 100644 --- a/pkg/operator/cloudformation/ensure.go +++ b/pkg/operator/cloudformation/ensure.go @@ -83,16 +83,22 @@ func (c *CloudFormation) temPar(rel []cache.Object) []types.Parameter { } // Inject all desired artifact versions into the parameters that we are just - // about to deploy. Injecting those parameters after all user inputs have been - // applied above guarantees that only the release versions as defined in the - // release source repository will ever be applied. + // about to deploy, but only for main release definitions, not for preview + // deployments. Injecting the template parameters after all user inputs have + // been applied above guarantees that only the release versions as defined in + // the release source repository will ever be applied. for _, x := range rel { + if bool(x.Release.Deploy.Preview) { + continue + } + par = append(par, types.Parameter{ ParameterKey: aws.String(x.Parameter()), ParameterValue: aws.String(x.Version()), }) } + return par } diff --git a/pkg/operator/container/active.go b/pkg/operator/container/active.go new file mode 100644 index 0000000..b188353 --- /dev/null +++ b/pkg/operator/container/active.go @@ -0,0 +1,6 @@ +package container + +// Active defines this worker handler to always be executed. +func (c *Container) Active() bool { + return true +} diff --git a/pkg/operator/container/cache.go b/pkg/operator/container/cache.go index 717b30d..586ccd8 100644 --- a/pkg/operator/container/cache.go +++ b/pkg/operator/container/cache.go @@ -4,13 +4,14 @@ func (c *Container) cache(ima []image) { for _, x := range c.cac.Services() { var tag string { - tag = curTag(ima, x.Release.Docker.String()) + tag = curTag(ima, x.Release.Labels.Hash.Upper(), x.Release.Docker.String()) } c.log.Log( "level", "debug", "message", "caching current state", "docker", x.Release.Docker.String(), + "preview", x.Release.Deploy.Preview.String(), "current", musStr(tag), ) @@ -32,9 +33,9 @@ func (c *Container) cache(ima []image) { } } -func curTag(ima []image, ser string) string { +func curTag(ima []image, hsh string, doc string) string { for _, x := range ima { - if x.ser == ser { + if x.pre == hsh && x.ser == doc { return x.tag } } diff --git a/pkg/operator/container/image.go b/pkg/operator/container/image.go index cca8cc7..e7efa8f 100644 --- a/pkg/operator/container/image.go +++ b/pkg/operator/container/image.go @@ -11,6 +11,9 @@ import ( ) type image struct { + // pre is the "preview" resource tag attached to any given ECS service, if + // any, e.g. 1D0FD508. + pre string // ser is the "service" resource tag attached to any given ECS service, e.g. // alloy or specta. ser string @@ -57,6 +60,7 @@ func (c *Container) image(tas []task) ([]image, error) { // only responsible for their own execution index within the image slice. ima[i] = image{ + pre: t.pre, ser: t.ser, tag: tag, } diff --git a/pkg/operator/container/task.go b/pkg/operator/container/task.go index 4413b94..f309a0c 100644 --- a/pkg/operator/container/task.go +++ b/pkg/operator/container/task.go @@ -15,6 +15,9 @@ type task struct { // arn is the filtered task definition ARN that any given ECS service is // running right now. arn string + // pre is the "preview" resource tag attached to any given ECS service, if + // any, e.g. 1D0FD508. + pre string // ser is the "service" resource tag attached to any given ECS service, e.g. // alloy or specta. ser string @@ -57,12 +60,26 @@ func (c *Container) task(det []detail) ([]task, error) { } for _, x := range out.Services { - var tag string + if aws.ToString(x.Status) != "ACTIVE" { + // There might be inactive or draining services with our desired service + // labels in case we updated CloudFormation stacks multiple times during + // with preview deployments during testing. We only want to consider the + // current state of those stacks that are still active, because the + // inactive versions have most likely been deleted already. + + { + continue + } + } + + var pre string + var ser string { - tag = serTag(x.Tags) + pre = serTag(x.Tags, "preview") + ser = serTag(x.Tags, "service") } - if tag == "" { + if ser == "" { c.log.Log( "level", "warning", "message", "skipping reconciliation for ECS service", @@ -81,7 +98,8 @@ func (c *Container) task(det []detail) ([]task, error) { tas[i] = task{ arn: *x.TaskDefinition, - ser: tag, + pre: pre, + ser: ser, } } @@ -121,10 +139,10 @@ func (c *Container) task(det []detail) ([]task, error) { return fil, nil } -func serTag(tag []types.Tag) string { +func serTag(tag []types.Tag, key string) string { for _, x := range tag { - if *x.Key == "service" { - return *x.Value + if aws.ToString(x.Key) == key { + return aws.ToString(x.Value) } } diff --git a/pkg/operator/infrastructure/active.go b/pkg/operator/infrastructure/active.go new file mode 100644 index 0000000..89b7066 --- /dev/null +++ b/pkg/operator/infrastructure/active.go @@ -0,0 +1,6 @@ +package infrastructure + +// Active defines this worker handler to always be executed. +func (i *Infrastructure) Active() bool { + return true +} diff --git a/pkg/operator/infrastructure/ensure.go b/pkg/operator/infrastructure/ensure.go index 28f2dc4..38313bc 100644 --- a/pkg/operator/infrastructure/ensure.go +++ b/pkg/operator/infrastructure/ensure.go @@ -4,9 +4,11 @@ import ( "fmt" "io/fs" "path/filepath" + "strings" "github.com/0xSplits/kayron/pkg/cache" "github.com/0xSplits/kayron/pkg/constant" + "github.com/0xSplits/kayron/pkg/preview" "github.com/0xSplits/roghfs" "github.com/spf13/afero" "github.com/xh3b4sd/tracer" @@ -52,6 +54,8 @@ func (i *Infrastructure) Ensure() error { } } + // Skip everything that is not a YAML file. + var ext string { ext = filepath.Ext(fil.Name()) @@ -60,6 +64,11 @@ func (i *Infrastructure) Ensure() error { } } + var nam string + { + nam = strings.TrimSuffix(fil.Name(), ext) + } + var byt []byte { byt, err = afero.ReadFile(gfs, pat) @@ -68,6 +77,20 @@ func (i *Infrastructure) Ensure() error { } } + // Before uploading our templates to S3, we have to inject any preview + // deployments configured for the service release that matches this + // particular template by file name. + // + // "splits-lite" == Release.Docker.String() + // + + { + byt, err = i.renPre(nam, byt) + if err != nil { + return tracer.Mask(err) + } + } + { err = i.putObj(pat, byt) if err != nil { @@ -87,3 +110,41 @@ func (i *Infrastructure) Ensure() error { return nil } + +func (i *Infrastructure) renPre(nam string, byt []byte) ([]byte, error) { + var err error + + // If there are no preview deployments defined inside our release artifacts, + // then we do not have to inject anything, but instead return the same + // template bytes early that we just received as input. + + var rel []cache.Object + { + rel = i.cac.Previews(nam) + } + + if len(rel) == 0 { + return byt, nil + } + + // At this point we have preview deployments defined by at least one release + // artifact. So we create a preview renderer and extend the raw template bytes + // that we received as input data above. + + var pre *preview.Preview + { + pre = preview.New(preview.Config{ + Env: i.env, + Inp: byt, + }) + } + + { + byt, err = pre.Render(rel) + if err != nil { + return nil, tracer.Mask(err) + } + } + + return byt, nil +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 6885a52..c488b36 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -9,6 +9,7 @@ import ( "github.com/0xSplits/kayron/pkg/operator/container" "github.com/0xSplits/kayron/pkg/operator/infrastructure" "github.com/0xSplits/kayron/pkg/operator/policy" + "github.com/0xSplits/kayron/pkg/operator/preview" "github.com/0xSplits/kayron/pkg/operator/reference" "github.com/0xSplits/kayron/pkg/operator/registry" "github.com/0xSplits/kayron/pkg/operator/release" @@ -35,6 +36,7 @@ type Operator struct { container *container.Container infrastructure *infrastructure.Infrastructure policy *policy.Policy + preview *preview.Preview reference *reference.Reference release *release.Release registry *registry.Registry @@ -65,7 +67,8 @@ func New(c Config) *Operator { cloudFormation: cloudformation.New(cloudformation.Config{Aws: c.Aws, Cac: c.Cac, Dry: c.Dry, Env: c.Env, Log: c.Log, Met: c.Met}), container: container.New(container.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log}), infrastructure: infrastructure.New(infrastructure.Config{Aws: c.Aws, Cac: c.Cac, Dry: c.Dry, Env: c.Env, Log: c.Log}), - policy: policy.New(policy.Config{Cac: c.Cac, Log: c.Log}), + policy: policy.New(policy.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), + preview: preview.New(preview.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), reference: reference.New(reference.Config{Cac: c.Cac, Env: c.Env, Log: c.Log}), release: release.New(release.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log, Sta: c.Sta}), registry: registry.New(registry.Config{Aws: c.Aws, Cac: c.Cac, Env: c.Env, Log: c.Log}), diff --git a/pkg/operator/operator_integration_test.go b/pkg/operator/operator_integration_test.go index b3e5392..bca1d69 100644 --- a/pkg/operator/operator_integration_test.go +++ b/pkg/operator/operator_integration_test.go @@ -27,6 +27,7 @@ import ( // KAYRON_GITHUB_TOKEN=todo go test -tags=integration ./pkg/operator -v -race -run Test_Operator_Integration func Test_Operator_Integration(t *testing.T) { var env envvar.Env + { env = envvar.Env{ CloudformationStack: "server-test", diff --git a/pkg/operator/policy/active.go b/pkg/operator/policy/active.go new file mode 100644 index 0000000..909e20d --- /dev/null +++ b/pkg/operator/policy/active.go @@ -0,0 +1,6 @@ +package policy + +// Active defines this worker handler to always be executed. +func (p *Policy) Active() bool { + return true +} diff --git a/pkg/operator/policy/ensure.go b/pkg/operator/policy/ensure.go index a2cf2b7..919c32d 100644 --- a/pkg/operator/policy/ensure.go +++ b/pkg/operator/policy/ensure.go @@ -57,20 +57,29 @@ func (p *Policy) ensure(rel []cache.Object) error { // 4. the container image for the desired state must be pushed // + var drf bool + for _, x := range rel { if !bool(x.Release.Deploy.Suspend) && x.Artifact.Drift() && x.Artifact.Valid() { + { + drf = true + } + p.log.Log( "level", "info", "message", "continuing reconciliation loop", "reason", "detected state drift", "release", x.Name(), + "domain", x.Domain(p.env.Environment), "version", x.Artifact.Reference.Desired, ) - - return nil } } + if drf { + return nil + } + // At this point all service releases were found to be up to date this time // around. This means that we do not have to do any more work for this // particular reconciliation loop. And so we return the control flow error diff --git a/pkg/operator/policy/ensure_test.go b/pkg/operator/policy/ensure_test.go index 0729f9d..56f3e36 100644 --- a/pkg/operator/policy/ensure_test.go +++ b/pkg/operator/policy/ensure_test.go @@ -6,6 +6,7 @@ import ( "github.com/0xSplits/kayron/pkg/cache" "github.com/0xSplits/kayron/pkg/cancel" + "github.com/0xSplits/kayron/pkg/envvar" "github.com/0xSplits/kayron/pkg/release/artifact" "github.com/0xSplits/kayron/pkg/release/artifact/condition" "github.com/0xSplits/kayron/pkg/release/artifact/reference" @@ -177,6 +178,9 @@ func Test_Operator_Policy_Ensure(t *testing.T) { { pol = New(Config{ Cac: cac, + Env: envvar.Env{ + Environment: "testing", + }, Log: log, }) } diff --git a/pkg/operator/policy/policy.go b/pkg/operator/policy/policy.go index 7c8b128..b8f24c0 100644 --- a/pkg/operator/policy/policy.go +++ b/pkg/operator/policy/policy.go @@ -7,17 +7,20 @@ import ( "fmt" "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/envvar" "github.com/xh3b4sd/logger" "github.com/xh3b4sd/tracer" ) type Config struct { Cac *cache.Cache + Env envvar.Env Log logger.Interface } type Policy struct { cac *cache.Cache + env envvar.Env log logger.Interface } @@ -25,12 +28,16 @@ func New(c Config) *Policy { if c.Cac == nil { tracer.Panic(tracer.Mask(fmt.Errorf("%T.Cac must not be empty", c))) } + if c.Env.Environment == "" { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Env must not be empty", c))) + } if c.Log == nil { tracer.Panic(tracer.Mask(fmt.Errorf("%T.Log must not be empty", c))) } return &Policy{ cac: c.Cac, + env: c.Env, log: c.Log, } } diff --git a/pkg/operator/preview/active.go b/pkg/operator/preview/active.go new file mode 100644 index 0000000..8b1ce5a --- /dev/null +++ b/pkg/operator/preview/active.go @@ -0,0 +1,8 @@ +package preview + +// Active defines this worker handler to only be executed within the testing +// environment, because we do not allow preview deployments to be injected in +// e.g. staging nor production. +func (p *Preview) Active() bool { + return p.env.Environment == "testing" +} diff --git a/pkg/operator/preview/ensure.go b/pkg/operator/preview/ensure.go new file mode 100644 index 0000000..abb2927 --- /dev/null +++ b/pkg/operator/preview/ensure.go @@ -0,0 +1,76 @@ +package preview + +import ( + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" + "github.com/xh3b4sd/choreo/parallel" + "github.com/xh3b4sd/tracer" +) + +func (p *Preview) Ensure() error { + // Get the list of cached releases so that we can lookup their respective + // artifact references for any potential preview deployment settings. + + var rel []cache.Object + { + rel = p.cac.Releases() + } + + fnc := func(_ int, o cache.Object) error { + var err error + + // If this release has preview deployments disabled, then ignore this cache + // object and move on to the next one. + + if !bool(o.Release.Deploy.Preview) { + return nil + } + + // If this release has preview deployments enabled, then compute the preview + // releases, so that we can inject them into the internal cache below. + + var exp release.Slice + { + exp, err = p.pre.Expand(o.Release) + if err != nil { + return tracer.Mask(err) + } + } + + // Extend the cache for all expanded preview deployments. + + { + err := p.cac.Create(exp) + if err != nil { + return tracer.Mask(err) + } + } + + // Mark the expanded release artifact as non-preview. The Deploy.Preview + // flag of the release.Struct acts as a signal to expand our release + // definitions internally. Once expanded, we redefine the purpose of this + // preview flag to maintain our understanding of how to deploy "real" + // service releases. In other words, we turn one release into many, while + // muting the one that instructed the many for the preview mechanism. + + { + o.Release.Deploy.Preview = preview.Bool(false) + } + + { + p.cac.Update(o) + } + + return nil + } + + { + err := parallel.Slice(rel, fnc) + if err != nil { + return tracer.Mask(err) + } + } + + return nil +} diff --git a/pkg/operator/preview/preview.go b/pkg/operator/preview/preview.go new file mode 100644 index 0000000..8ed4434 --- /dev/null +++ b/pkg/operator/preview/preview.go @@ -0,0 +1,54 @@ +// Package preview injects preview deployments into our internal release +// artifact cache. This operator function enables us to expose additional +// development services within the testing environment only. +package preview + +import ( + "fmt" + + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/envvar" + "github.com/0xSplits/kayron/pkg/preview" + "github.com/xh3b4sd/logger" + "github.com/xh3b4sd/tracer" +) + +type Config struct { + Cac *cache.Cache + Env envvar.Env + Log logger.Interface +} + +type Preview struct { + cac *cache.Cache + env envvar.Env + log logger.Interface + pre *preview.Preview +} + +func New(c Config) *Preview { + if c.Cac == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Cac must not be empty", c))) + } + if c.Env.Environment == "" { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Env must not be empty", c))) + } + if c.Log == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Log must not be empty", c))) + } + + var pre *preview.Preview + { + pre = preview.New(preview.Config{ + Env: c.Env, + Inp: []byte{}, + }) + } + + return &Preview{ + cac: c.Cac, + env: c.Env, + log: c.Log, + pre: pre, + } +} diff --git a/pkg/operator/reference/active.go b/pkg/operator/reference/active.go new file mode 100644 index 0000000..a8b85ed --- /dev/null +++ b/pkg/operator/reference/active.go @@ -0,0 +1,6 @@ +package reference + +// Active defines this worker handler to always be executed. +func (r *Reference) Active() bool { + return true +} diff --git a/pkg/operator/reference/ensure.go b/pkg/operator/reference/ensure.go index 2a3e70c..3192fcf 100644 --- a/pkg/operator/reference/ensure.go +++ b/pkg/operator/reference/ensure.go @@ -7,8 +7,6 @@ import ( ) func (r *Reference) Ensure() error { - var err error - // Get the list of cached releases so that we can lookup their respective // artifact references concurrently, if necessary. This includes // infrastructure and service releases. @@ -23,9 +21,14 @@ func (r *Reference) Ensure() error { // not define a branch deployment strategy. fnc := func(i int, x cache.Object) error { - ref, err := r.desRef(x.Release) - if err != nil { - return tracer.Mask(err) + var err error + + var ref string + { + ref, err = r.desRef(x.Release) + if err != nil { + return tracer.Mask(err) + } } if ref == "" { @@ -36,6 +39,7 @@ func (r *Reference) Ensure() error { "level", "debug", "message", "caching desired state", "github", x.Release.Github.String(), + "preview", x.Release.Deploy.Preview.String(), "desired", musStr(ref), ) @@ -51,7 +55,7 @@ func (r *Reference) Ensure() error { } { - err = parallel.Slice(rel, fnc) + err := parallel.Slice(rel, fnc) if err != nil { return tracer.Mask(err) } diff --git a/pkg/operator/reference/github.go b/pkg/operator/reference/github.go index 264d0d6..c61679c 100644 --- a/pkg/operator/reference/github.go +++ b/pkg/operator/reference/github.go @@ -10,10 +10,18 @@ import ( func (r *Reference) desRef(rel release.Struct) (string, error) { // Return the commit sha if the branch deployment strategy is selected. Note // that branches may be referenced in releases while they are not yet tracked, - // or not tracked anymore inside of Github. This may happen predominantly - // during testing when preparing or finishing releases and their dependencies. + // or not tracked anymore inside of Github. This may happen predominantly during + // testing when preparing or finishing releases and their dependencies. Note + // that we do not lookup branch references for preview deployments, if those + // references got already filled in the release labels. E.g. we may have looked + // up the latest commit sha for a preview deployment in an earlier stage of this + // reconciliation loop already. if !rel.Deploy.Branch.Empty() { + if bool(rel.Deploy.Preview) && rel.Labels.Head != "" { + return rel.Labels.Head, nil + } + sha, err := r.comSha(rel.Github.String(), rel.Deploy.Branch.String()) if err != nil { return "", tracer.Mask(err) diff --git a/pkg/operator/registry/active.go b/pkg/operator/registry/active.go new file mode 100644 index 0000000..aeaa41f --- /dev/null +++ b/pkg/operator/registry/active.go @@ -0,0 +1,6 @@ +package registry + +// Active defines this worker handler to always be executed. +func (r *Registry) Active() bool { + return true +} diff --git a/pkg/operator/registry/ensure.go b/pkg/operator/registry/ensure.go index 4fcce1b..5c12f0a 100644 --- a/pkg/operator/registry/ensure.go +++ b/pkg/operator/registry/ensure.go @@ -42,22 +42,23 @@ func (r *Registry) Ensure() error { } } - { - x.Artifact.Condition.Success = exi - } - - { - r.cac.Update(x) - } - r.log.Log( "level", "debug", "message", "executed image check", "image", x.Release.Docker.String(), + "preview", x.Release.Deploy.Preview.String(), "tag", des, "exists", strconv.FormatBool(exi), ) + { + x.Artifact.Condition.Success = exi + } + + { + r.cac.Update(x) + } + return nil } diff --git a/pkg/operator/release/active.go b/pkg/operator/release/active.go new file mode 100644 index 0000000..97d4401 --- /dev/null +++ b/pkg/operator/release/active.go @@ -0,0 +1,6 @@ +package release + +// Active defines this worker handler to always be executed. +func (r *Release) Active() bool { + return true +} diff --git a/pkg/operator/release/resolver/resolver.go b/pkg/operator/release/resolver/resolver.go index 1bdf6c5..34f914c 100644 --- a/pkg/operator/release/resolver/resolver.go +++ b/pkg/operator/release/resolver/resolver.go @@ -1,3 +1,7 @@ +// Package resolver tries to provide the environment specific Git reference for +// the release source repository. This Git reference tells us which version of +// our service releases to consider, based on the environment Kayron is running +// in. package resolver import ( diff --git a/pkg/operator/template/active.go b/pkg/operator/template/active.go new file mode 100644 index 0000000..4f40bec --- /dev/null +++ b/pkg/operator/template/active.go @@ -0,0 +1,6 @@ +package template + +// Active defines this worker handler to always be executed. +func (t *Template) Active() bool { + return true +} diff --git a/pkg/preview/error.go b/pkg/preview/error.go new file mode 100644 index 0000000..990046b --- /dev/null +++ b/pkg/preview/error.go @@ -0,0 +1,9 @@ +package preview + +import ( + "github.com/xh3b4sd/tracer" +) + +var containerImageFormatError = &tracer.Error{ + Description: "This critical error indicates that the provided container image was unrecognizable, which means that the operator does not know how to proceed safely.", +} diff --git a/pkg/preview/expand.go b/pkg/preview/expand.go new file mode 100644 index 0000000..45f57c4 --- /dev/null +++ b/pkg/preview/expand.go @@ -0,0 +1,123 @@ +package preview + +import ( + "context" + "sort" + "strings" + + "github.com/0xSplits/kayron/pkg/hash" + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" + "github.com/google/go-github/v73/github" + "github.com/xh3b4sd/tracer" +) + +var ( + // filter is a collection of branch name prefixes that we want to ignore when + // expanding a service release into preview releases. E.g. we do not want to + // deploy preview releases for dependabot branches. + filter = []string{ + "dependabot/", + } +) + +func (p *Preview) Expand(rel release.Struct) (release.Slice, error) { + var err error + + opt := &github.PullRequestListOptions{ + State: "open", + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + var pul []*github.PullRequest + { + pul, _, err = p.git.PullRequests.List(context.Background(), p.own, rel.Github.String(), opt) + if err != nil { + return nil, tracer.Mask(err) + } + } + + var lis release.Slice + { + lis = expand(rel, pul) + } + + return lis, nil +} + +func expand(rel release.Struct, pul []*github.PullRequest) release.Slice { + // Before sorting, filter pull requests by branch names that we definitely + // want to consider for preview releases. E.g. drop all dependabot branches + // before sorting. + + var fil []*github.PullRequest + + for _, x := range pul { + if !hasPre(x.GetHead().GetRef(), filter) { + fil = append(fil, x) + } + } + + // Sort our filtered pull requests from oldest to newest, so that new pull + // requests do not change the order of pull request specific preview + // deployments. This is relevant because the order of preview releases created + // below will define the priority settings of the ALB's listener rules. + + sort.Slice(fil, func(i, j int) bool { + return fil[i].GetCreatedAt().Before(fil[j].GetCreatedAt().Time) + }) + + var lis release.Slice + for _, x := range fil { + var pre release.Struct + { + pre = rel + } + + var bra string + var ref string + { + bra = x.GetHead().GetRef() + ref = x.GetHead().GetSHA() + } + + { + pre.Deploy = deploy.Struct{ + Branch: branch.String(bra), + Preview: preview.Bool(true), + } + } + + // Make sure to inject the preview deployment hash into the release labels. + // This is used to identify the correct current state of deployed container + // image tags, as well as rendering the correct CloudFormation templates. + // Here we also optimize the branch reference lookup by assigning the head + // label, because we have this latest commit sha for the preview branch here + // already. + + { + pre.Labels.Hash = hash.New(bra) + pre.Labels.Head = ref + } + + { + lis = append(lis, pre) + } + } + + return lis +} + +func hasPre(str string, pre []string) bool { + for _, x := range pre { + if strings.HasPrefix(str, x) { + return true + } + } + + return false +} diff --git a/pkg/preview/expand_test.go b/pkg/preview/expand_test.go new file mode 100644 index 0000000..73fecad --- /dev/null +++ b/pkg/preview/expand_test.go @@ -0,0 +1,149 @@ +package preview + +import ( + "fmt" + "testing" + "time" + + "github.com/0xSplits/kayron/pkg/hash" + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" + "github.com/0xSplits/kayron/pkg/release/schema/release/docker" + "github.com/0xSplits/kayron/pkg/release/schema/release/labels" + "github.com/google/go-cmp/cmp" + "github.com/google/go-github/v73/github" +) + +func Test_Preview_Expand(t *testing.T) { + testCases := []struct { + rel release.Struct + pul []*github.PullRequest + exp release.Slice + }{ + // Case 000 + { + rel: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("main"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + pul: []*github.PullRequest{ + {CreatedAt: tesTim(3), Head: tesBra("b/3")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/foo-bar")}, + {CreatedAt: tesTim(5), Head: tesBra("b/5")}, + }, + exp: release.Slice{ + { + Deploy: deploy.Struct{ + Branch: branch.String("b/3"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/3"), + }, + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/5"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/5"), + }, + }, + }, + }, + // Case 001 + { + rel: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("main"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + }, + pul: []*github.PullRequest{ + {CreatedAt: tesTim(3), Head: tesBra("b/3")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/foo-bar")}, + {CreatedAt: tesTim(5), Head: tesBra("b/5")}, + {CreatedAt: tesTim(7), Head: tesBra("b/7")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/another-one")}, + {CreatedAt: tesTim(4), Head: tesBra("dependabot/b/5")}, + {CreatedAt: tesTim(9), Head: tesBra("b/9")}, + }, + exp: release.Slice{ + { + Deploy: deploy.Struct{ + Branch: branch.String("b/3"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/3"), + }, + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/5"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/5"), + }, + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/7"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/7"), + }, + }, + { + Deploy: deploy.Struct{ + Branch: branch.String("b/9"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("b/9"), + }, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + exp := expand(tc.rel, tc.pul) + + var opt []cmp.Option + { + opt = []cmp.Option{ + cmp.AllowUnexported(hash.Hash{}), + } + } + + if dif := cmp.Diff(tc.exp, exp, opt...); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} + +func tesBra(nam string) *github.PullRequestBranch { + return &github.PullRequestBranch{Ref: github.Ptr(nam)} +} + +func tesTim(sec int64) *github.Timestamp { + return &github.Timestamp{Time: time.Unix(sec, 0)} +} diff --git a/pkg/preview/image.go b/pkg/preview/image.go new file mode 100644 index 0000000..de30e15 --- /dev/null +++ b/pkg/preview/image.go @@ -0,0 +1,29 @@ +package preview + +import ( + "bytes" + "regexp" + + "github.com/xh3b4sd/tracer" +) + +var ( + exp = regexp.MustCompile(`.*\/.*:(\$\{[^}]+\}|[A-Za-z0-9_][A-Za-z0-9._-]{0,127})($|["']?[ \t]*#.*|["'])`) +) + +func repIma(lin []byte, tag []byte) ([]byte, error) { + var sub [][]byte + { + sub = exp.FindSubmatch(lin) + if len(sub) < 2 { + return nil, tracer.Mask(containerImageFormatError, tracer.Context{Key: "input", Value: string(lin)}) + } + } + + var out []byte + { + out = bytes.ReplaceAll(lin, sub[1], tag) + } + + return out, nil +} diff --git a/pkg/preview/image_test.go b/pkg/preview/image_test.go new file mode 100644 index 0000000..78d8f79 --- /dev/null +++ b/pkg/preview/image_test.go @@ -0,0 +1,59 @@ +package preview + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Preview_repIma(t *testing.T) { + testCases := []struct { + lin []byte + tag []byte + out []byte + }{ + // Case 000 + { + lin: []byte(`Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}"`), + tag: []byte(`v0.2.0`), + out: []byte(`Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:v0.2.0"`), + }, + // Case 001 + { + lin: []byte(` Image: 'ecr.amazonaws.com/splits-lite:${LiteVersion}' # hello world`), + tag: []byte(`v1.0.0`), + out: []byte(` Image: 'ecr.amazonaws.com/splits-lite:v1.0.0' # hello world`), + }, + // Case 002 + { + lin: []byte(` Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:v0.1.0"`), + tag: []byte(`bc7891268e44f62e0aebbe339c0850b61d52c417`), + out: []byte(` Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417"`), + }, + // Case 003 + { + lin: []byte(`Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417' # comment`), + tag: []byte(`v3.5.0-bc789126`), + out: []byte(`Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:v3.5.0-bc789126' # comment`), + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var out []byte + { + out, err = repIma(tc.lin, tc.tag) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + if dif := cmp.Diff(tc.out, out); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/preview/preview.go b/pkg/preview/preview.go new file mode 100644 index 0000000..9ecf79f --- /dev/null +++ b/pkg/preview/preview.go @@ -0,0 +1,61 @@ +package preview + +import ( + "fmt" + + "github.com/0xSplits/kayron/pkg/envvar" + "github.com/0xSplits/kayron/pkg/scanner" + "github.com/0xSplits/roghfs" + "github.com/google/go-github/v73/github" + "github.com/xh3b4sd/tracer" +) + +type Config struct { + Env envvar.Env + Inp []byte +} + +type Preview struct { + git *github.Client + inp []byte + own string + sca *scanner.Scanner +} + +func New(c Config) *Preview { + if c.Env.Environment == "" { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Env must not be empty", c))) + } + if c.Inp == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Inp must not be empty", c))) + } + + var err error + + var git *github.Client + { + git = github.NewClient(nil).WithAuthToken(c.Env.GithubToken) + } + + var own string + { + own, _, err = roghfs.Parse(c.Env.ReleaseSource) + if err != nil { + tracer.Panic(tracer.Mask(err)) + } + } + + var sca *scanner.Scanner + { + sca = scanner.New(scanner.Config{ + Inp: c.Inp, + }) + } + + return &Preview{ + git: git, + inp: c.Inp, + own: own, + sca: sca, + } +} diff --git a/pkg/preview/priority.go b/pkg/preview/priority.go new file mode 100644 index 0000000..2f547e4 --- /dev/null +++ b/pkg/preview/priority.go @@ -0,0 +1,21 @@ +package preview + +import ( + "bytes" + + "github.com/goccy/go-yaml" + "github.com/xh3b4sd/tracer" +) + +func lisPri(lin []byte) (int, error) { + var pri map[string]int + + { + err := yaml.Unmarshal(bytes.TrimSpace(lin), &pri) + if err != nil { + return 0, tracer.Mask(err) + } + } + + return pri["Priority"], nil +} diff --git a/pkg/preview/render.go b/pkg/preview/render.go new file mode 100644 index 0000000..5723020 --- /dev/null +++ b/pkg/preview/render.go @@ -0,0 +1,146 @@ +package preview + +import ( + "bytes" + "fmt" + + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/hash" + "github.com/xh3b4sd/tracer" +) + +func (p *Preview) Render(pre []cache.Object) ([]byte, error) { + var err error + + var res Resource + { + res = Resource{ + Ser: p.sca.Search([]byte(" Service:")), + Tas: p.sca.Search([]byte(" TaskDefinition:")), + Dom: p.sca.Search([]byte(" DomainRecord:")), + Tar: p.sca.Search([]byte(" TargetGroup:")), + Lis: p.sca.Search([]byte(" ListenerRule:")), + } + } + + var out []byte + { + out = append(p.inp, '\n') + } + + // Derive the base of our listener rule priority range from the provided + // template defining a AWS::ElasticLoadBalancingV2::ListenerRule resource. + + var pri int + { + pri, err = lisPri(res.Lis.Search([]byte(" Priority:")).Bytes()) + if err != nil { + return nil, tracer.Mask(err) + } + } + + for _, x := range pre { + var hsh hash.Hash + { + hsh = x.Release.Labels.Hash + } + + var dom string + { + dom = x.Domain("${Environment}") + } + + var ima []byte + { + ima, err = repIma(res.Tas.Search([]byte(" Image:")).Bytes(), []byte(x.Artifact.Reference.Desired)) + if err != nil { + return nil, tracer.Mask(err) + } + } + + var tag []byte + { + tag, err = appTag(res.Ser.Search([]byte(" Tags:")).Bytes(), hsh.Upper()) + if err != nil { + return nil, tracer.Mask(err) + } + } + + // Increment the listener rule priority per preview release. This must be + // done before the call to render(), because the base priority resolved + // above is already taken by the main release defining our preview + // deployments. + + { + pri++ + } + + { + out = append(out, p.render(res, dom, pri, hsh, ima, tag)...) + } + } + + return out, nil +} + +func (p *Preview) render(res Resource, dom string, pri int, hsh hash.Hash, ima []byte, tag []byte) []byte { + { + res.Ser = res.Ser.Append([]byte(" Service:"), []byte(hsh.Upper())) + res.Ser = res.Ser.Append([]byte(" ServiceName:"), []byte(hsh.Dashed())) + res.Ser = res.Ser.Append([]byte(" TaskDefinition:"), []byte(hsh.Upper())) + res.Ser = res.Ser.Append([]byte(" - TargetGroupArn:"), []byte(hsh.Upper())) + res.Ser = res.Ser.Delete([]byte(" ServiceRegistries:")) + res.Ser = res.Ser.Delete([]byte(" Tags:"), tag...) + } + + { + res.Tas = res.Tas.Append([]byte(" TaskDefinition:"), []byte(hsh.Upper())) + res.Tas = res.Tas.Append([]byte(" Family:"), []byte(hsh.Dashed())) + res.Tas = res.Tas.Delete([]byte(" Image:"), ima...) + } + + { + res.Dom = res.Dom.Append([]byte(" DomainRecord:"), []byte(hsh.Upper())) + res.Dom = res.Dom.Delete([]byte(" Name:"), fmt.Appendf(nil, ` Name: !Sub "%s"`, dom)...) + } + + { + res.Tar = res.Tar.Append([]byte(" TargetGroup:"), []byte(hsh.Upper())) + } + + { + res.Lis = res.Lis.Append([]byte(" ListenerRule:"), []byte(hsh.Upper())) + res.Lis = res.Lis.Append([]byte(" TargetGroupArn:"), []byte(hsh.Upper())) + res.Lis = res.Lis.Delete([]byte(" Values:"), fmt.Appendf(nil, " Values:\n - !Sub \"%s\"", dom)...) + res.Lis = res.Lis.Delete([]byte(" Priority:"), fmt.Appendf(nil, " Priority: %d # Host header = %s", pri, dom)...) + } + + var out []byte + { + out = append(out, header(hsh)...) + out = append(out, '\n', '\n') + out = append(out, res.Ser.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Tas.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Dom.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Tar.Bytes()...) + out = append(out, '\n', '\n') + out = append(out, res.Lis.Bytes()...) + out = append(out, '\n', '\n') + } + + return out +} + +func header(hsh hash.Hash) []byte { + return bytes.Join( + [][]byte{ + []byte(" #"), + []byte(" # AUTO GENERATED PREVIEW DEPLOYMENT " + hsh.Upper()), + []byte(" #"), + }, + []byte("\n"), + ) +} diff --git a/pkg/preview/render_test.go b/pkg/preview/render_test.go new file mode 100644 index 0000000..38f4b70 --- /dev/null +++ b/pkg/preview/render_test.go @@ -0,0 +1,113 @@ +package preview + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/0xSplits/kayron/pkg/cache" + "github.com/0xSplits/kayron/pkg/envvar" + "github.com/0xSplits/kayron/pkg/hash" + "github.com/0xSplits/kayron/pkg/release/artifact" + "github.com/0xSplits/kayron/pkg/release/artifact/reference" + "github.com/0xSplits/kayron/pkg/release/schema/release" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" + "github.com/0xSplits/kayron/pkg/release/schema/release/docker" + "github.com/0xSplits/kayron/pkg/release/schema/release/labels" + "github.com/google/go-cmp/cmp" +) + +func Test_Preview_Render(t *testing.T) { + testCases := []struct { + obj []cache.Object + }{ + // Case 000 + { + obj: []cache.Object{ + { + Artifact: artifact.Struct{ + Reference: reference.Struct{ + Desired: "bc7891268e44f62e0aebbe339c0850b61d52c417", + }, + }, + Release: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("fancy-feature-branch"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("fancy-feature-branch"), + }, + }, + }, + { + Artifact: artifact.Struct{ + Reference: reference.Struct{ + Desired: "02b42b7ec63d4078767cb3b7cb0d34fde91b6237", + }, + }, + Release: release.Struct{ + Deploy: deploy.Struct{ + Branch: branch.String("dependabot/another-one"), + Preview: preview.Bool(true), + }, + Docker: docker.String("lite"), + Labels: labels.Struct{ + Hash: hash.New("dependabot/another-one"), + }, + }, + }, + }, + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var pre *Preview + { + pre = New(Config{ + Env: envvar.Env{ + Environment: "testing", + GithubToken: "foo", + ReleaseSource: "https://github.com/0xSplits/releases", + }, + Inp: inp, + }) + } + + var res []byte + { + res, err = pre.Render(tc.obj) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/preview/resource.go b/pkg/preview/resource.go new file mode 100644 index 0000000..00d4d3d --- /dev/null +++ b/pkg/preview/resource.go @@ -0,0 +1,11 @@ +package preview + +import "github.com/0xSplits/kayron/pkg/scanner" + +type Resource struct { + Ser *scanner.Scanner + Tas *scanner.Scanner + Dom *scanner.Scanner + Tar *scanner.Scanner + Lis *scanner.Scanner +} diff --git a/pkg/preview/tags.go b/pkg/preview/tags.go new file mode 100644 index 0000000..14c28f2 --- /dev/null +++ b/pkg/preview/tags.go @@ -0,0 +1,26 @@ +package preview + +import ( + "bytes" +) + +func appTag(lin []byte, hsh string) ([]byte, error) { + var pre []byte + { + pre = bytes.Join( + [][]byte{ + nil, + []byte(" - Key: \"preview\""), + []byte(" Value: \"" + hsh + "\""), + }, + []byte("\n"), + ) + } + + var byt []byte + { + byt = append(lin, pre...) + } + + return byt, nil +} diff --git a/pkg/preview/testdata/000/inp.yaml.golden b/pkg/preview/testdata/000/inp.yaml.golden new file mode 100644 index 0000000..d550cbb --- /dev/null +++ b/pkg/preview/testdata/000/inp.yaml.golden @@ -0,0 +1,308 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "CloudFormation template for the Splits Lite containers and their load balancer configuration" + +Parameters: + Environment: + Description: "the name of the deployed environment" + Type: String + + DiscoveryStack: + Description: "the name of the discovery stack" + Type: String + + FargateStack: + Description: "the name of the server stack" + Type: String + + NetworkStack: + Description: "the name of the networking stack" + Type: String + + Secret: + Description: "the name of the environment/stack specific secret" + Type: String + + LiteCertificate: + Description: "the ARN of the SSL certificate for the Splits Lite containers" + Type: String + + LiteDomain: + Description: "the domain name of the Splits Lite DNS" + Type: String + + LitePort: + Description: "the server port of the Splits Lite containers" + Type: Number + + LiteVersion: + Description: "the Docker image tag for the Splits Lite containers" + Type: String + +Conditions: + IsProd: !Equals [!Ref Environment, production] + +Resources: + Cluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Ref AWS::StackName + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" + DependsOn: + - Cluster + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/fargate/${AWS::StackName}/lite" + RetentionInDays: 7 + Tags: + - Key: "environment" + Value: !Ref Environment + + TaskExecutionRole: + Type: AWS::IAM::Role + Properties: + Path: "/" + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: ecs-tasks.amazonaws.com + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: secrets-access + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}*" + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecord: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Ref LiteDomain + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Ref LiteDomain + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 12000 # Host header = lite.${Environment}.splits.org + + ListenerCertificate: + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: !Ref LiteCertificate + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + # ProdListenerRule registers the target group of the splits lite containers + # under the shorthand production domain lite.splits.org, but only for the + # production environment. Note that in order to make this work, we are also + # registering the respective SSL certificate in the load balancer, so that the + # TLS handshake may succeed. + ProdListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Condition: IsProd + Properties: + Actions: + - Type: "forward" + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: "host-header" + HostHeaderConfig: + Values: + - "lite.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 16 # Host header = lite.splits.org + + ProdListenerCertificate: + Condition: IsProd + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: "arn:aws:acm:us-west-2:995626699990:certificate/4e1a3a6b-c119-4594-a6c9-7f667401d78e" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: "the security group for the Splits Lite containers in the Splits Lite stack" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromAlloyToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the Alloy to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: "tcp" + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-AlloySecurityGroupId" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: tcp + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteEgress: + Type: AWS::EC2::SecurityGroupEgress + Properties: + Description: "the egress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + IpProtocol: tcp + ToPort: !Ref LitePort + FromPort: !Ref LitePort + DestinationSecurityGroupId: !Ref SecurityGroup + Tags: + - Key: "environment" + Value: !Ref Environment + + # ServiceDiscovery exposes the private DNS for the Splits Lite service via + # Cloud Map within the environment specific VPC. Multiple tasks of the same + # service resolve to their respective private IPs, which can be accessed with + # the appropriate security group configuration. + # + # dig +short lite.splits.local + # + # 10.50.117.100 + # 10.50.114.203 + # 10.50.113.188 + # + ServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: "lite" + NamespaceId: + Fn::ImportValue: !Sub "${DiscoveryStack}-DiscoveryNamespaceId" + DnsConfig: + DnsRecords: + - Type: A + TTL: 60 diff --git a/pkg/preview/testdata/000/out.yaml.golden b/pkg/preview/testdata/000/out.yaml.golden new file mode 100644 index 0000000..50ceb21 --- /dev/null +++ b/pkg/preview/testdata/000/out.yaml.golden @@ -0,0 +1,530 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: "CloudFormation template for the Splits Lite containers and their load balancer configuration" + +Parameters: + Environment: + Description: "the name of the deployed environment" + Type: String + + DiscoveryStack: + Description: "the name of the discovery stack" + Type: String + + FargateStack: + Description: "the name of the server stack" + Type: String + + NetworkStack: + Description: "the name of the networking stack" + Type: String + + Secret: + Description: "the name of the environment/stack specific secret" + Type: String + + LiteCertificate: + Description: "the ARN of the SSL certificate for the Splits Lite containers" + Type: String + + LiteDomain: + Description: "the domain name of the Splits Lite DNS" + Type: String + + LitePort: + Description: "the server port of the Splits Lite containers" + Type: Number + + LiteVersion: + Description: "the Docker image tag for the Splits Lite containers" + Type: String + +Conditions: + IsProd: !Equals [!Ref Environment, production] + +Resources: + Cluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Ref AWS::StackName + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" + DependsOn: + - Cluster + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub "/fargate/${AWS::StackName}/lite" + RetentionInDays: 7 + Tags: + - Key: "environment" + Value: !Ref Environment + + TaskExecutionRole: + Type: AWS::IAM::Role + Properties: + Path: "/" + AssumeRolePolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: ecs-tasks.amazonaws.com + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyName: secrets-access + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - secretsmanager:GetSecretValue + Resource: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}*" + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecord: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Ref LiteDomain + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Ref LiteDomain + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 12000 # Host header = lite.${Environment}.splits.org + + ListenerCertificate: + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: !Ref LiteCertificate + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + # ProdListenerRule registers the target group of the splits lite containers + # under the shorthand production domain lite.splits.org, but only for the + # production environment. Note that in order to make this work, we are also + # registering the respective SSL certificate in the load balancer, so that the + # TLS handshake may succeed. + ProdListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Condition: IsProd + Properties: + Actions: + - Type: "forward" + TargetGroupArn: !Ref TargetGroup + Conditions: + - Field: "host-header" + HostHeaderConfig: + Values: + - "lite.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 16 # Host header = lite.splits.org + + ProdListenerCertificate: + Condition: IsProd + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: "arn:aws:acm:us-west-2:995626699990:certificate/4e1a3a6b-c119-4594-a6c9-7f667401d78e" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + + SecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: "the security group for the Splits Lite containers in the Splits Lite stack" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromAlloyToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the Alloy to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: "tcp" + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-AlloySecurityGroupId" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: "the ingress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: !Ref SecurityGroup + IpProtocol: tcp + FromPort: !Ref LitePort + ToPort: !Ref LitePort + SourceSecurityGroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + Tags: + - Key: "environment" + Value: !Ref Environment + + FromLoadBalancerToLiteEgress: + Type: AWS::EC2::SecurityGroupEgress + Properties: + Description: "the egress rule to allow traffic from the ELB to the Splits Lite containers" + GroupId: + Fn::ImportValue: !Sub "${NetworkStack}-ELBSecurityGroupID" + IpProtocol: tcp + ToPort: !Ref LitePort + FromPort: !Ref LitePort + DestinationSecurityGroupId: !Ref SecurityGroup + Tags: + - Key: "environment" + Value: !Ref Environment + + # ServiceDiscovery exposes the private DNS for the Splits Lite service via + # Cloud Map within the environment specific VPC. Multiple tasks of the same + # service resolve to their respective private IPs, which can be accessed with + # the appropriate security group configuration. + # + # dig +short lite.splits.local + # + # 10.50.117.100 + # 10.50.114.203 + # 10.50.113.188 + # + ServiceDiscovery: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: "lite" + NamespaceId: + Fn::ImportValue: !Sub "${DiscoveryStack}-DiscoveryNamespaceId" + DnsConfig: + DnsRecords: + - Type: A + TTL: 60 + + # + # AUTO GENERATED PREVIEW DEPLOYMENT 1D0FD508 + # + + Service1D0FD508: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite-1D0FD508" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition1D0FD508 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup1D0FD508 + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" + - Key: "preview" + Value: "1D0FD508" + DependsOn: + - Cluster + + TaskDefinition1D0FD508: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite-1D0FD508" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:bc7891268e44f62e0aebbe339c0850b61d52c417" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecord1D0FD508: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Sub "1d0fd508.lite.${Environment}.splits.org" + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroup1D0FD508: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRule1D0FD508: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroup1D0FD508 + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Sub "1d0fd508.lite.${Environment}.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 12001 # Host header = 1d0fd508.lite.${Environment}.splits.org + + # + # AUTO GENERATED PREVIEW DEPLOYMENT F4436797 + # + + ServiceF4436797: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite-F4436797" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinitionF4436797 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroupF4436797 + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" + - Key: "preview" + Value: "F4436797" + DependsOn: + - Cluster + + TaskDefinitionF4436797: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite-F4436797" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:02b42b7ec63d4078767cb3b7cb0d34fde91b6237" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment + + DomainRecordF4436797: + Type: AWS::Route53::RecordSet + Properties: + HostedZoneId: + Fn::ImportValue: !Sub "${NetworkStack}-EnvironmentHostedZoneId" + Name: !Sub "f4436797.lite.${Environment}.splits.org" + Type: A + AliasTarget: + DNSName: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerBaseDnsName" + HostedZoneId: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerCanonicalHostedZoneId" + + TargetGroupF4436797: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Port: !Ref LitePort + Protocol: "HTTP" + VpcId: + Fn::ImportValue: !Sub "${NetworkStack}-VpcID" + TargetType: "ip" + HealthCheckPath: "/" + HealthCheckProtocol: "HTTP" + Matcher: + HttpCode: 200 + Tags: + - Key: "environment" + Value: !Ref Environment + + ListenerRuleF4436797: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref TargetGroupF4436797 + Conditions: + - Field: host-header + HostHeaderConfig: + Values: + - !Sub "f4436797.lite.${Environment}.splits.org" + ListenerArn: + Fn::ImportValue: !Sub "${FargateStack}-ApplicationLoadBalancerListenerArn" + Priority: 12002 # Host header = f4436797.lite.${Environment}.splits.org diff --git a/pkg/release/loader/loader_test.go b/pkg/release/loader/loader_test.go index 0730aeb..19fe82b 100644 --- a/pkg/release/loader/loader_test.go +++ b/pkg/release/loader/loader_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/0xSplits/kayron/pkg/hash" "github.com/0xSplits/kayron/pkg/release/schema" "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" @@ -15,6 +16,7 @@ import ( "github.com/0xSplits/kayron/pkg/release/schema/release/labels" "github.com/0xSplits/kayron/pkg/release/schema/release/provider" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/afero" "github.com/xh3b4sd/tracer" ) @@ -244,7 +246,14 @@ func Test_Loader(t *testing.T) { } } - if dif := cmp.Diff(tc.sch, sch); dif != "" { + var opt []cmp.Option + { + opt = []cmp.Option{ + cmpopts.IgnoreUnexported(hash.Hash{}), + } + } + + if dif := cmp.Diff(tc.sch, sch, opt...); dif != "" { t.Fatalf("-expected +actual:\n%s", dif) } }) diff --git a/pkg/release/schema/release/deploy/preview/bool.go b/pkg/release/schema/release/deploy/preview/bool.go new file mode 100644 index 0000000..d1d3801 --- /dev/null +++ b/pkg/release/schema/release/deploy/preview/bool.go @@ -0,0 +1,18 @@ +package preview + +import "strconv" + +// Bool enables preview deployments for this service. +type Bool bool + +func (b Bool) Empty() bool { + return !bool(b) +} + +func (b Bool) String() string { + return strconv.FormatBool(bool(b)) +} + +func (b Bool) Verify() error { + return nil +} diff --git a/pkg/release/schema/release/deploy/struct.go b/pkg/release/schema/release/deploy/struct.go index 96566a7..4d0a502 100644 --- a/pkg/release/schema/release/deploy/struct.go +++ b/pkg/release/schema/release/deploy/struct.go @@ -1,7 +1,10 @@ package deploy import ( + "fmt" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/branch" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/suspend" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/webhook" @@ -9,40 +12,50 @@ import ( ) // Struct defines exactly one mutually exclusive declaration of either Branch, -// Release, Suspend or Webhook as required deployment instruction. +// Release, Suspend or Webhook as required deployment instruction. Struct may +// also define Preview as a testing environment only deployment mechanism for +// pull requests, which does not influence the main deployment strategy +// configuration mentioned above. type Struct struct { Branch branch.String `yaml:"branch,omitempty"` + Preview preview.Bool `yaml:"preview,omitempty"` Release release.String `yaml:"release,omitempty"` Suspend suspend.Bool `yaml:"suspend,omitempty"` Webhook webhook.Slice `yaml:"webhook,omitempty"` } func (s Struct) Empty() bool { - return s.Branch.Empty() && s.Release.Empty() && s.Suspend.Empty() && s.Webhook.Empty() + return s.Branch.Empty() && s.Preview.Empty() && s.Release.Empty() && s.Suspend.Empty() && s.Webhook.Empty() } func (s Struct) String() string { if !s.Branch.Empty() { - return s.Branch.String() + return fmt.Sprintf("branch=%s", s.Branch.String()) } + // Note that Struct.Preview is not a deployment strategy in all environments, + // so Struct.Preview does not contribute to the name/string representation of + // this deployment strategy. + if !s.Release.Empty() { - return s.Release.String() + return fmt.Sprintf("release=%s", s.Release.String()) } if !s.Suspend.Empty() { - return s.Suspend.String() + return fmt.Sprintf("suspend=%s", s.Suspend.String()) } if !s.Webhook.Empty() { - return s.Webhook.String() + return fmt.Sprintf("webhook=%s", s.Webhook.String()) } return "" } func (s Struct) Verify() error { - // Reject deployment configurations that define more than one strategy. + // Reject deployment configurations that define more than one strategy. Note + // that s.Preview is not a deployment strategy and is therefore not considered + // for this check. { lis := enabled(s.Branch, s.Release, s.Suspend, s.Webhook) if len(lis) > 1 { @@ -57,6 +70,13 @@ func (s Struct) Verify() error { } } + if !s.Preview.Empty() { + err := s.Preview.Verify() + if err != nil { + return tracer.Mask(err) + } + } + if !s.Release.Empty() { err := s.Release.Verify() if err != nil { diff --git a/pkg/release/schema/release/deploy/suspend/bool.go b/pkg/release/schema/release/deploy/suspend/bool.go index a16b006..9f92dc5 100644 --- a/pkg/release/schema/release/deploy/suspend/bool.go +++ b/pkg/release/schema/release/deploy/suspend/bool.go @@ -2,7 +2,7 @@ package suspend import "strconv" -// Bool disables any further reconciliation of this service indefinitely. +// Bool disables any further reconciliation of this release indefinitely. type Bool bool func (b Bool) Empty() bool { diff --git a/pkg/release/schema/release/error.go b/pkg/release/schema/release/error.go index dee57c4..7b8d703 100644 --- a/pkg/release/schema/release/error.go +++ b/pkg/release/schema/release/error.go @@ -33,6 +33,18 @@ func IsServiceDeployEmpty(err error) bool { // // +var releaseDeployPreviewError = &tracer.Error{ + Description: "The release configuration does not allow preview deployments for infrastructure providers.", +} + +func IsServiceDeployPreview(err error) bool { + return errors.Is(err, releaseDeployPreviewError) +} + +// +// +// + var releaseGithubEmptyError = &tracer.Error{ Description: "The release configuration requires a github repository to be provided.", } diff --git a/pkg/release/schema/release/labels/struct.go b/pkg/release/schema/release/labels/struct.go index 08131ae..64f8f88 100644 --- a/pkg/release/schema/release/labels/struct.go +++ b/pkg/release/schema/release/labels/struct.go @@ -1,5 +1,9 @@ package labels +import ( + "github.com/0xSplits/kayron/pkg/hash" +) + // Struct contains runtime specific internals annotated inside the schema // loader. type Struct struct { @@ -7,16 +11,22 @@ type Struct struct { // of a specific config file. E.g. given a config file with 3 service // definitions, the last block has the index 2. Block int + // Hash contains the hashed branch name for any service release of a preview + // deployment. + Hash hash.Hash + // Head is the latest Git Reference for any service release of a preview + // deployment. + Head string // Source is the absolute source file path of the .yaml definition as loaded // from the underlying file system. This label may help to make error messages // more useful. Source string } -func (m Struct) Empty() bool { - return m.Source == "" +func (s Struct) Empty() bool { + return s.Block == 0 && s.Hash.Empty() && s.Head == "" && s.Source == "" } -func (m Struct) Verify() error { +func (s Struct) Verify() error { return nil } diff --git a/pkg/release/schema/release/provider/error.go b/pkg/release/schema/release/provider/error.go new file mode 100644 index 0000000..f907279 --- /dev/null +++ b/pkg/release/schema/release/provider/error.go @@ -0,0 +1,19 @@ +package provider + +import ( + "errors" + + "github.com/xh3b4sd/tracer" +) + +// +// +// + +var providerNameError = &tracer.Error{ + Description: "The provider configuration requires the provider name to be \"cloudformation\".", +} + +func IsProviderName(err error) bool { + return errors.Is(err, providerNameError) +} diff --git a/pkg/release/schema/release/provider/string.go b/pkg/release/schema/release/provider/string.go index 101c72a..4fe8887 100644 --- a/pkg/release/schema/release/provider/string.go +++ b/pkg/release/schema/release/provider/string.go @@ -1,5 +1,10 @@ package provider +import ( + "github.com/0xSplits/kayron/pkg/constant" + "github.com/xh3b4sd/tracer" +) + type String string func (s String) Empty() bool { @@ -11,6 +16,9 @@ func (s String) String() string { } func (s String) Verify() error { - // TODO + if s != constant.Cloudformation { + return tracer.Mask(providerNameError) + } + return nil } diff --git a/pkg/release/schema/release/struct.go b/pkg/release/schema/release/struct.go index 62c736b..1f448d8 100644 --- a/pkg/release/schema/release/struct.go +++ b/pkg/release/schema/release/struct.go @@ -72,6 +72,10 @@ func (s Struct) verify() error { } if !s.Provider.Empty() { + if bool(s.Deploy.Preview) { + return tracer.Mask(releaseDeployPreviewError) + } + err := s.Provider.Verify() if err != nil { return tracer.Mask(err) diff --git a/pkg/release/schema/schema_test.go b/pkg/release/schema/schema_test.go index 943ba49..8aff951 100644 --- a/pkg/release/schema/schema_test.go +++ b/pkg/release/schema/schema_test.go @@ -6,6 +6,7 @@ import ( "github.com/0xSplits/kayron/pkg/release/schema/release" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy" + "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/preview" "github.com/0xSplits/kayron/pkg/release/schema/release/deploy/webhook" "github.com/0xSplits/kayron/pkg/release/schema/release/docker" "github.com/0xSplits/kayron/pkg/release/schema/release/github" @@ -191,6 +192,24 @@ func Test_Schema_Verify_failure(t *testing.T) { }, mat: release.IsServiceLabelsEmpty, }, + // Case 009, one provider, preview deployments + { + sch: Schema{ + Release: release.Slice{ + { + Github: github.String("infrastructure"), + Provider: provider.String("cloudformation"), + Deploy: deploy.Struct{ + Preview: preview.Bool(true), + }, + Labels: labels.Struct{ + Source: "foo", + }, + }, + }, + }, + mat: release.IsServiceDeployPreview, + }, } for i, tc := range testCases { diff --git a/pkg/scanner/append.go b/pkg/scanner/append.go new file mode 100644 index 0000000..9efebb1 --- /dev/null +++ b/pkg/scanner/append.go @@ -0,0 +1,66 @@ +package scanner + +import ( + "bufio" + "bytes" +) + +func (s *Scanner) Append(pre []byte, suf []byte) *Scanner { + var buf *bufio.Scanner + { + buf = bufio.NewScanner(bytes.NewReader(s.inp)) + } + + var blo [][]byte + for buf.Scan() { + var lin []byte + { + lin = append([]byte(nil), buf.Bytes()...) // copy to prevent buffer overwrites + } + + if bytes.HasPrefix(lin, pre) { + lin = insert(lin, suf) + } + + { + blo = append(blo, lin) + } + } + + var inp []byte + { + inp = bytes.Join(blo, []byte("\n")) + } + + return New(Config{ + Inp: inp, + }) +} + +func insert(lin []byte, suf []byte) []byte { + var las byte + { + las = lin[len(lin)-1] + } + + // double quote 0x22 + // single quote 0x27 + // colon 0x3A + + if las == 0x22 || las == 0x27 || las == 0x3A { + return merge(lin[:len(lin)-1], suf, []byte{las}) + } + + return append(lin, suf...) +} + +func merge(pre []byte, mid []byte, suf []byte) []byte { + out := make([]byte, len(pre)+len(mid)+len(suf)) + + num := copy(out, pre) + num += copy(out[num:], mid) + + copy(out[num:], suf) + + return out +} diff --git a/pkg/scanner/append_test.go b/pkg/scanner/append_test.go new file mode 100644 index 0000000..dc19d80 --- /dev/null +++ b/pkg/scanner/append_test.go @@ -0,0 +1,86 @@ +package scanner + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Scanner_Append(t *testing.T) { + testCases := []struct { + pre string + suf string + }{ + // Case 000 + { + pre: " Value:", + suf: "-1234", + }, + // Case 001 + { + pre: " Value:", + suf: ".0xFa73", + }, + // Case 002 + { + pre: " ServiceName:", + suf: "-1d0fd508", + }, + // Case 003 + { + pre: " TaskDefinition:", + suf: "-e3eae11", + }, + // Case 004 + { + pre: " TaskDefinition:", + suf: "-XHEKSOUDL", + }, + // Case 005 + { + pre: " Service:", + suf: "FancyFeatureBranch", + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/append/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/append/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var sca *Scanner + { + sca = New(Config{ + Inp: inp, + }) + } + + var res []byte + { + res = sca.Append([]byte(tc.pre), []byte(tc.suf)).Bytes() + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/scanner/bytes.go b/pkg/scanner/bytes.go new file mode 100644 index 0000000..e58dbd0 --- /dev/null +++ b/pkg/scanner/bytes.go @@ -0,0 +1,6 @@ +package scanner + +// Bytes returns the input bytes that this scanner is conigured with. +func (s *Scanner) Bytes() []byte { + return s.inp +} diff --git a/pkg/scanner/delete.go b/pkg/scanner/delete.go new file mode 100644 index 0000000..c5147d2 --- /dev/null +++ b/pkg/scanner/delete.go @@ -0,0 +1,58 @@ +package scanner + +import ( + "bufio" + "bytes" +) + +// Delete tries to drop the entire YAML block identified by the given key line, +// e.g. " Service:". A new scanner configured without the found YAML block as +// input bytes is returned. Optionally a substituion slice may be defined, which +// causes Delete to replace the matched block with the given structure. +func (s *Scanner) Delete(key []byte, sub ...byte) *Scanner { + var buf *bufio.Scanner + { + buf = bufio.NewScanner(bytes.NewReader(s.inp)) + } + + var blo [][]byte + var drp bool + var rep bool + var end int + var sta int + for buf.Scan() { + var lin []byte + { + lin = append([]byte(nil), buf.Bytes()...) // copy to prevent buffer overwrites + } + + if drp { + end = spaces(lin) + } + + if drp && end <= sta && len(lin) != 0 { + drp = false + } + + if bytes.HasPrefix(lin, key) { + drp = true + sta = spaces(lin) + } + + if !drp { + blo = append(blo, lin) + } else if !rep && sub != nil { + rep = true + blo = append(blo, sub) + } + } + + var inp []byte + { + inp = bytes.Join(blo, []byte("\n")) + } + + return New(Config{ + Inp: inp, + }) +} diff --git a/pkg/scanner/delete_test.go b/pkg/scanner/delete_test.go new file mode 100644 index 0000000..a7ed77c --- /dev/null +++ b/pkg/scanner/delete_test.go @@ -0,0 +1,91 @@ +package scanner + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Scanner_Delete(t *testing.T) { + testCases := []struct { + pre string + sub []byte + }{ + // Case 000 + { + pre: " Service:", + sub: nil, + }, + // Case 001 + { + pre: " TaskDefinition:", + sub: nil, + }, + // Case 002 + { + pre: "Resources:", + sub: nil, + }, + // Case 003, real production example + { + pre: " ServiceRegistries:", + sub: nil, + }, + // Case 004 + { + pre: " Image:", + sub: nil, + }, + // Case 005 + { + pre: " Image:", + sub: []byte(" Image: registry/image:tag"), + }, + // Case 006 + { + pre: " ContainerDefinitions:", + sub: []byte(" ContainerDefinitions:\n Foo: 1\n Bar: 2"), + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/delete/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/delete/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var sca *Scanner + { + sca = New(Config{ + Inp: inp, + }) + } + + var res []byte + { + res = sca.Delete([]byte(tc.pre), tc.sub...).Bytes() + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/scanner/scanner.go b/pkg/scanner/scanner.go new file mode 100644 index 0000000..47d6129 --- /dev/null +++ b/pkg/scanner/scanner.go @@ -0,0 +1,26 @@ +// Package scanner is a multi line block scanner for YAML input bytes. +package scanner + +import ( + "fmt" + + "github.com/xh3b4sd/tracer" +) + +type Config struct { + Inp []byte +} + +type Scanner struct { + inp []byte +} + +func New(c Config) *Scanner { + if c.Inp == nil { + tracer.Panic(tracer.Mask(fmt.Errorf("%T.Inp must not be empty", c))) + } + + return &Scanner{ + inp: c.Inp, + } +} diff --git a/pkg/scanner/search.go b/pkg/scanner/search.go new file mode 100644 index 0000000..7451dc3 --- /dev/null +++ b/pkg/scanner/search.go @@ -0,0 +1,68 @@ +package scanner + +import ( + "bufio" + "bytes" + "unicode" +) + +// Search tries to find the entire YAML block identified by the given key line, +// e.g. " Service:". A new scanner configured with the found YAML block as +// input bytes is returned. +func (s *Scanner) Search(key []byte) *Scanner { + var buf *bufio.Scanner + { + buf = bufio.NewScanner(bytes.NewReader(s.inp)) + } + + var blo [][]byte + var fou bool + var end int + var sta int + for buf.Scan() { + var lin []byte + { + lin = append([]byte(nil), buf.Bytes()...) // copy to prevent buffer overwrites + } + + if fou { + end = spaces(lin) + } + + if fou && end <= sta && len(lin) != 0 { + break + } + + if !fou { + fou = bytes.HasPrefix(lin, key) + sta = spaces(lin) + } + + if fou { + blo = append(blo, lin) + } + } + + var inp []byte + { + inp = bytes.Join(blo, []byte("\n")) + } + + return New(Config{ + Inp: inp, + }) +} + +func spaces(b []byte) int { + var cou int + + for _, x := range b { + if unicode.IsSpace(rune(x)) { + cou++ + } else { + break + } + } + + return cou +} diff --git a/pkg/scanner/search_test.go b/pkg/scanner/search_test.go new file mode 100644 index 0000000..f250ed3 --- /dev/null +++ b/pkg/scanner/search_test.go @@ -0,0 +1,79 @@ +package scanner + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func Test_Scanner_Search(t *testing.T) { + testCases := []struct { + key string + }{ + // Case 000 + { + key: " Service:", + }, + // Case 001 + { + key: " TaskDefinition:", + }, + // Case 002 + { + key: "Resources:", + }, + // Case 003 + { + key: " ServiceRegistries:", + }, + // Case 004 + { + key: " Image:", + }, + // Case 005 + { + key: " Tags:", + }, + } + + for i, tc := range testCases { + t.Run(fmt.Sprintf("%03d", i), func(t *testing.T) { + var err error + + var inp []byte + { + inp, err = os.ReadFile(fmt.Sprintf("./testdata/search/%03d/inp.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var out []byte + { + out, err = os.ReadFile(fmt.Sprintf("./testdata/search/%03d/out.yaml.golden", i)) + if err != nil { + t.Fatal("expected", nil, "got", err) + } + } + + var sca *Scanner + { + sca = New(Config{ + Inp: inp, + }) + } + + var res []byte + { + res = sca.Search([]byte(tc.key)).Bytes() + } + + if dif := cmp.Diff(bytes.TrimSpace(out), bytes.TrimSpace(res)); dif != "" { + t.Fatalf("-expected +actual:\n%s", dif) + } + }) + } +} diff --git a/pkg/scanner/testdata/append/000/inp.yaml.golden b/pkg/scanner/testdata/append/000/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/append/000/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/append/000/out.yaml.golden b/pkg/scanner/testdata/append/000/out.yaml.golden new file mode 100644 index 0000000..e3d8e94 --- /dev/null +++ b/pkg/scanner/testdata/append/000/out.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0-1234" diff --git a/pkg/scanner/testdata/append/001/inp.yaml.golden b/pkg/scanner/testdata/append/001/inp.yaml.golden new file mode 100644 index 0000000..c0bac0e --- /dev/null +++ b/pkg/scanner/testdata/append/001/inp.yaml.golden @@ -0,0 +1,23 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + + + + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: 'v0.2.0' diff --git a/pkg/scanner/testdata/append/001/out.yaml.golden b/pkg/scanner/testdata/append/001/out.yaml.golden new file mode 100644 index 0000000..396e35d --- /dev/null +++ b/pkg/scanner/testdata/append/001/out.yaml.golden @@ -0,0 +1,23 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + + + + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: 'v0.2.0.0xFa73' diff --git a/pkg/scanner/testdata/append/002/inp.yaml.golden b/pkg/scanner/testdata/append/002/inp.yaml.golden new file mode 100644 index 0000000..d3a96f2 --- /dev/null +++ b/pkg/scanner/testdata/append/002/inp.yaml.golden @@ -0,0 +1,18 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/append/002/out.yaml.golden b/pkg/scanner/testdata/append/002/out.yaml.golden new file mode 100644 index 0000000..a6aaa8e --- /dev/null +++ b/pkg/scanner/testdata/append/002/out.yaml.golden @@ -0,0 +1,18 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite-1d0fd508" + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/append/003/inp.yaml.golden b/pkg/scanner/testdata/append/003/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/append/003/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/003/out.yaml.golden b/pkg/scanner/testdata/append/003/out.yaml.golden new file mode 100644 index 0000000..95fbc33 --- /dev/null +++ b/pkg/scanner/testdata/append/003/out.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition-e3eae11 + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/004/inp.yaml.golden b/pkg/scanner/testdata/append/004/inp.yaml.golden new file mode 100644 index 0000000..0c82dd9 --- /dev/null +++ b/pkg/scanner/testdata/append/004/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref "TaskDefinition" + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/004/out.yaml.golden b/pkg/scanner/testdata/append/004/out.yaml.golden new file mode 100644 index 0000000..3a23df2 --- /dev/null +++ b/pkg/scanner/testdata/append/004/out.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref "TaskDefinition-XHEKSOUDL" + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/005/inp.yaml.golden b/pkg/scanner/testdata/append/005/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/append/005/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/append/005/out.yaml.golden b/pkg/scanner/testdata/append/005/out.yaml.golden new file mode 100644 index 0000000..aac98b1 --- /dev/null +++ b/pkg/scanner/testdata/append/005/out.yaml.golden @@ -0,0 +1,32 @@ + ServiceFancyFeatureBranch: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/delete/000/inp.yaml.golden b/pkg/scanner/testdata/delete/000/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/delete/000/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/000/out.yaml.golden b/pkg/scanner/testdata/delete/000/out.yaml.golden new file mode 100644 index 0000000..1ef0541 --- /dev/null +++ b/pkg/scanner/testdata/delete/000/out.yaml.golden @@ -0,0 +1,12 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/001/inp.yaml.golden b/pkg/scanner/testdata/delete/001/inp.yaml.golden new file mode 100644 index 0000000..f35d7f7 --- /dev/null +++ b/pkg/scanner/testdata/delete/001/inp.yaml.golden @@ -0,0 +1,19 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/001/out.yaml.golden b/pkg/scanner/testdata/delete/001/out.yaml.golden new file mode 100644 index 0000000..d6f4b84 --- /dev/null +++ b/pkg/scanner/testdata/delete/001/out.yaml.golden @@ -0,0 +1,16 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/002/inp.yaml.golden b/pkg/scanner/testdata/delete/002/inp.yaml.golden new file mode 100644 index 0000000..66d360c --- /dev/null +++ b/pkg/scanner/testdata/delete/002/inp.yaml.golden @@ -0,0 +1,19 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + + Type: AWS::ECS::Service + + TaskDefinition: + + + + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/002/out.yaml.golden b/pkg/scanner/testdata/delete/002/out.yaml.golden new file mode 100644 index 0000000..911288a --- /dev/null +++ b/pkg/scanner/testdata/delete/002/out.yaml.golden @@ -0,0 +1,5 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/delete/003/inp.yaml.golden b/pkg/scanner/testdata/delete/003/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/delete/003/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/delete/003/out.yaml.golden b/pkg/scanner/testdata/delete/003/out.yaml.golden new file mode 100644 index 0000000..1cd6743 --- /dev/null +++ b/pkg/scanner/testdata/delete/003/out.yaml.golden @@ -0,0 +1,29 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/delete/004/inp.yaml.golden b/pkg/scanner/testdata/delete/004/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/delete/004/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/004/out.yaml.golden b/pkg/scanner/testdata/delete/004/out.yaml.golden new file mode 100644 index 0000000..31546d4 --- /dev/null +++ b/pkg/scanner/testdata/delete/004/out.yaml.golden @@ -0,0 +1,31 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/005/inp.yaml.golden b/pkg/scanner/testdata/delete/005/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/delete/005/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/005/out.yaml.golden b/pkg/scanner/testdata/delete/005/out.yaml.golden new file mode 100644 index 0000000..760c846 --- /dev/null +++ b/pkg/scanner/testdata/delete/005/out.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: registry/image:tag + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/006/inp.yaml.golden b/pkg/scanner/testdata/delete/006/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/delete/006/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/delete/006/out.yaml.golden b/pkg/scanner/testdata/delete/006/out.yaml.golden new file mode 100644 index 0000000..a633e8a --- /dev/null +++ b/pkg/scanner/testdata/delete/006/out.yaml.golden @@ -0,0 +1,17 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + Foo: 1 + Bar: 2 + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/search/000/inp.yaml.golden b/pkg/scanner/testdata/search/000/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/search/000/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/search/000/out.yaml.golden b/pkg/scanner/testdata/search/000/out.yaml.golden new file mode 100644 index 0000000..05791d0 --- /dev/null +++ b/pkg/scanner/testdata/search/000/out.yaml.golden @@ -0,0 +1,2 @@ + Service: + Type: AWS::ECS::Service diff --git a/pkg/scanner/testdata/search/001/inp.yaml.golden b/pkg/scanner/testdata/search/001/inp.yaml.golden new file mode 100644 index 0000000..bfedc9b --- /dev/null +++ b/pkg/scanner/testdata/search/001/inp.yaml.golden @@ -0,0 +1,23 @@ + + + +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + TaskDefinition: + + Type: AWS::ECS::TaskDefinition + + Cluster: + Type: AWS::ECS::Cluster + + Service: + + + + Type: AWS::ECS::Service + +Outputs: + + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/search/001/out.yaml.golden b/pkg/scanner/testdata/search/001/out.yaml.golden new file mode 100644 index 0000000..a7182d4 --- /dev/null +++ b/pkg/scanner/testdata/search/001/out.yaml.golden @@ -0,0 +1,3 @@ + TaskDefinition: + + Type: AWS::ECS::TaskDefinition diff --git a/pkg/scanner/testdata/search/002/inp.yaml.golden b/pkg/scanner/testdata/search/002/inp.yaml.golden new file mode 100644 index 0000000..fc0c9c6 --- /dev/null +++ b/pkg/scanner/testdata/search/002/inp.yaml.golden @@ -0,0 +1,15 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition + +Outputs: + ServiceVersion: + Value: "v0.2.0" diff --git a/pkg/scanner/testdata/search/002/out.yaml.golden b/pkg/scanner/testdata/search/002/out.yaml.golden new file mode 100644 index 0000000..b07d9f4 --- /dev/null +++ b/pkg/scanner/testdata/search/002/out.yaml.golden @@ -0,0 +1,9 @@ +Resources: + Cluster: + Type: AWS::ECS::Cluster + + Service: + Type: AWS::ECS::Service + + TaskDefinition: + Type: AWS::ECS::TaskDefinition diff --git a/pkg/scanner/testdata/search/003/inp.yaml.golden b/pkg/scanner/testdata/search/003/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/search/003/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/search/003/out.yaml.golden b/pkg/scanner/testdata/search/003/out.yaml.golden new file mode 100644 index 0000000..dab60df --- /dev/null +++ b/pkg/scanner/testdata/search/003/out.yaml.golden @@ -0,0 +1,3 @@ + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn diff --git a/pkg/scanner/testdata/search/004/inp.yaml.golden b/pkg/scanner/testdata/search/004/inp.yaml.golden new file mode 100644 index 0000000..8197729 --- /dev/null +++ b/pkg/scanner/testdata/search/004/inp.yaml.golden @@ -0,0 +1,32 @@ + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Sub "${AWS::StackName}-lite" + RequiresCompatibilities: + - FARGATE + Cpu: 512 + Memory: 1024 + NetworkMode: awsvpc + ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn + TaskRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/splits-ecs-role" + ContainerDefinitions: + - Name: !Sub "${AWS::StackName}-lite" + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" + PortMappings: + - ContainerPort: !Ref LitePort + Protocol: "tcp" + Essential: true + Secrets: + - Name: ALCHEMY_API_KEY + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:ALCHEMY_API_KEY::" + - Name: WALLETCONNECT_PROJECT_ID + ValueFrom: !Sub "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${Secret}:WALLETCONNECT_PROJECT_ID::" + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Sub "/fargate/${AWS::StackName}/lite" + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Tags: + - Key: "environment" + Value: !Ref Environment diff --git a/pkg/scanner/testdata/search/004/out.yaml.golden b/pkg/scanner/testdata/search/004/out.yaml.golden new file mode 100644 index 0000000..5d5d32c --- /dev/null +++ b/pkg/scanner/testdata/search/004/out.yaml.golden @@ -0,0 +1 @@ + Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/splits-lite:${LiteVersion}" diff --git a/pkg/scanner/testdata/search/005/inp.yaml.golden b/pkg/scanner/testdata/search/005/inp.yaml.golden new file mode 100644 index 0000000..bf01872 --- /dev/null +++ b/pkg/scanner/testdata/search/005/inp.yaml.golden @@ -0,0 +1,32 @@ + Service: + Type: AWS::ECS::Service + Properties: + Cluster: !Ref Cluster + ServiceName: !Sub "${AWS::StackName}-lite" + DesiredCount: 1 + LaunchType: "FARGATE" + TaskDefinition: !Ref TaskDefinition + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: "DISABLED" + SecurityGroups: + - !Ref SecurityGroup + Subnets: + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet1ID" + - Fn::ImportValue: !Sub "${NetworkStack}-PrivateSubnet2ID" + LoadBalancers: + - TargetGroupArn: !Ref TargetGroup + ContainerName: !Sub "${AWS::StackName}-lite" + ContainerPort: !Ref LitePort + ServiceRegistries: + - ContainerName: !Sub "${AWS::StackName}-lite" + RegistryArn: !GetAtt ServiceDiscovery.Arn + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name + DependsOn: + - Cluster + - TaskDefinition + - ListenerRule diff --git a/pkg/scanner/testdata/search/005/out.yaml.golden b/pkg/scanner/testdata/search/005/out.yaml.golden new file mode 100644 index 0000000..d84f802 --- /dev/null +++ b/pkg/scanner/testdata/search/005/out.yaml.golden @@ -0,0 +1,5 @@ + Tags: + - Key: "environment" + Value: !Ref Environment + - Key: "service" + Value: "splits-lite" # must match repo name diff --git a/pkg/stack/search.go b/pkg/stack/search.go index 639b535..43975c2 100644 --- a/pkg/stack/search.go +++ b/pkg/stack/search.go @@ -58,7 +58,7 @@ func (s *Stack) Search() (types.Stack, error) { func hasEnv(tags []types.Tag, env string) bool { for _, x := range tags { - if x.Key != nil && x.Value != nil && *x.Key == "environment" && *x.Value == env { + if aws.ToString(x.Key) == "environment" && aws.ToString(x.Value) == env { return true } } diff --git a/pkg/worker/handler/image/active.go b/pkg/worker/handler/image/active.go new file mode 100644 index 0000000..1d4fbb4 --- /dev/null +++ b/pkg/worker/handler/image/active.go @@ -0,0 +1,6 @@ +package image + +// Active defines this worker handler to always be executed. +func (h *Handler) Active() bool { + return true +}