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 @@
-
\ No newline at end of file
+
\ 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
+}