From dac42153e5125f0f21d11415b8d82dbc72d63f1b Mon Sep 17 00:00:00 2001 From: Santiago Beroch Date: Thu, 16 Oct 2025 10:22:56 -0300 Subject: [PATCH 1/7] provisioner examples retrieval Signed-off-by: Santiago Beroch --- .github/workflows/scheduled-generated.yml | 2 + README.md | 9 +- config.toml | 6 + content/en/examples/_index.md | 4 + .../examples/resource-provisioners/_index.md | 9 + .../community/dapr-pubsub/score-compose.md | 13 + .../community/dapr-pubsub/score-k8s.md | 14 + .../dapr-state-store/score-compose.md | 13 + .../community/dapr-state-store/score-k8s.md | 13 + .../dapr-subscription/score-compose.md | 13 + .../community/dapr-subscription/score-k8s.md | 13 + .../community/dns/score-compose.md | 19 + .../community/dns/score-k8s.md | 19 + .../community/environment/score-compose.md | 18 + .../community/environment/score-k8s.md | 18 + .../score-compose.md | 13 + .../horizontal-pod-autoscaler/score-k8s.md | 13 + .../community/redis/score-k8s.md | 31 + .../community/route/score-k8s.md | 16 + .../community/service/score-compose.md | 13 + .../community/service/score-k8s.md | 14 + .../resource-provisioners.md | 1 + gen/examples-site/gen-example-pages.js | 5 +- ...transform-default-resource-provisioners.js | 1 + .../community/.devcontainer/devcontainer.json | 29 + .../.devcontainer/installMoreTools.sh | 27 + .../community/.github/workflows/ci.yaml | 30 + .../community/.github/workflows/release.yaml | 35 + .../community/.gitignore | 5 + .../community/.scripts/setup-kind-cluster.sh | 39 + .../community/.scripts/test-provisioners.sh | 27 + .../resource-provisioners/community/LICENSE | 201 ++ .../resource-provisioners/community/README.md | 47 + .../10-redis-dapr-pubsub.provisioners.yaml | 59 + .../10-rabbitmq-dapr-pubsub.provisioners.yaml | 108 ++ .../10-redis-dapr-pubsub.provisioners.yaml | 157 ++ .../community/dapr-pubsub/score.yaml | 22 + ...0-redis-dapr-state-store.provisioners.yaml | 59 + ...0-redis-dapr-state-store.provisioners.yaml | 157 ++ .../community/dapr-state-store/score.yaml | 22 + .../10-dapr-subscription.provisioners.yaml | 29 + .../10-dapr-subscription.provisioners.yaml | 29 + .../community/dapr-subscription/score.yaml | 25 + .../10-dns-in-codespace.provisioners.yaml | 15 + .../10-dns-with-url.provisioners.yaml | 13 + .../community/dns/score-compose/README.md | 2 + .../10-dns-in-codespace.provisioners.yaml | 15 + .../10-dns-with-url.provisioners.yaml | 13 + .../community/dns/score-k8s/README.md | 2 + .../community/dns/score.yaml | 22 + .../community/environment/.env | 1 + .../score-compose/10-env.provisioners.yaml | 11 + .../environment/score-compose/README.md | 2 + .../score-k8s/10-env.provisioners.yaml | 11 + .../community/environment/score-k8s/README.md | 2 + .../community/environment/score.yaml | 17 + .../score-compose/10-hpa.provisioners.yaml | 3 + .../score-k8s/10-hpa.provisioners.yaml | 34 + .../horizontal-pod-autoscaler/score.yaml | 15 + .../10-redis-helm-template.provisioners.yaml | 24 + .../10-redis-helm-upgrade.provisioners.yaml | 22 + .../community/redis/score-k8s/README.md | 12 + .../community/redis/score.yaml | 16 + .../10-ingress-route.provisioners.yaml | 37 + ...ngress-with-netpol-route.provisioners.yaml | 62 + ...ay-httproute-with-netpol.provisioners.yaml | 67 + ...shared-gateway-httproute.provisioners.yaml | 48 + .../community/route/score.yaml | 22 + .../community/score-compose.md | 43 + .../community/score-k8s.md | 43 + .../community/service/score-backend.yaml | 8 + .../10-service.provisioners.yaml | 11 + .../community/service/score-frontend.yaml | 13 + .../10-service-with-netpol.provisioners.yaml | 64 + .../score-k8s/10-service.provisioners.yaml | 11 + .../default/score-compose/check_version.go | 50 + .../score-compose/check_version_test.go | 72 + .../score-compose/default.provisioners.yaml | 996 ++++++++++ .../fixtures/provisioners.custom.golden | 284 +++ .../default/score-compose/generate.go | 526 ++++++ .../score-compose/generate_examples_test.go | 300 +++ .../default/score-compose/generate_test.go | 1633 +++++++++++++++++ .../default/score-compose/init.go | 316 ++++ .../default/score-compose/init_test.go | 331 ++++ .../default/score-compose/provisioners.go | 124 ++ .../score-compose/provisioners_test.go | 117 ++ .../default/score-compose/resources.go | 198 ++ .../default/score-compose/resources_test.go | 163 ++ .../default/score-compose/root.go | 65 + .../default/score-compose/root_test.go | 261 +++ .../default/score-compose/run.go | 364 ++++ .../default/score-compose/run_test.go | 430 +++++ .../provisioners.list.valid.json.golden | 377 ++++ .../provisioners.list.valid.table.golden | 61 + .../default/score-k8s/default.go | 22 + .../default/score-k8s/default_test.go | 30 + .../score-k8s/zz-default.provisioners.yaml | 1390 ++++++++++++++ package.json | 6 +- 98 files changed, 10190 insertions(+), 4 deletions(-) create mode 100644 content/en/examples/resource-provisioners/_index.md create mode 100644 content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose.md create mode 100644 content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/dapr-state-store/score-compose.md create mode 100644 content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/dapr-subscription/score-compose.md create mode 100644 content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/dns/score-compose.md create mode 100644 content/en/examples/resource-provisioners/community/dns/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/environment/score-compose.md create mode 100644 content/en/examples/resource-provisioners/community/environment/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose.md create mode 100644 content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/redis/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/route/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/service/score-compose.md create mode 100644 content/en/examples/resource-provisioners/community/service/score-k8s.md create mode 100644 gen/examples-site/examples-category-content/resource-provisioners.md create mode 100644 gen/examples-site/transform-default-resource-provisioners.js create mode 100644 gen/external-content/resource-provisioners/community/.devcontainer/devcontainer.json create mode 100755 gen/external-content/resource-provisioners/community/.devcontainer/installMoreTools.sh create mode 100644 gen/external-content/resource-provisioners/community/.github/workflows/ci.yaml create mode 100644 gen/external-content/resource-provisioners/community/.github/workflows/release.yaml create mode 100644 gen/external-content/resource-provisioners/community/.gitignore create mode 100755 gen/external-content/resource-provisioners/community/.scripts/setup-kind-cluster.sh create mode 100755 gen/external-content/resource-provisioners/community/.scripts/test-provisioners.sh create mode 100644 gen/external-content/resource-provisioners/community/LICENSE create mode 100644 gen/external-content/resource-provisioners/community/README.md create mode 100644 gen/external-content/resource-provisioners/community/dapr-pubsub/score-compose/10-redis-dapr-pubsub.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dapr-pubsub/score-k8s/10-rabbitmq-dapr-pubsub.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dapr-pubsub/score-k8s/10-redis-dapr-pubsub.provisioners.yaml create mode 100755 gen/external-content/resource-provisioners/community/dapr-pubsub/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/dapr-state-store/score-compose/10-redis-dapr-state-store.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dapr-state-store/score-k8s/10-redis-dapr-state-store.provisioners.yaml create mode 100755 gen/external-content/resource-provisioners/community/dapr-state-store/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/dapr-subscription/score-compose/10-dapr-subscription.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dapr-subscription/score-k8s/10-dapr-subscription.provisioners.yaml create mode 100755 gen/external-content/resource-provisioners/community/dapr-subscription/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/dns/score-compose/10-dns-in-codespace.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dns/score-compose/10-dns-with-url.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dns/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/community/dns/score-k8s/10-dns-in-codespace.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dns/score-k8s/10-dns-with-url.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/dns/score-k8s/README.md create mode 100755 gen/external-content/resource-provisioners/community/dns/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/environment/.env create mode 100644 gen/external-content/resource-provisioners/community/environment/score-compose/10-env.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/environment/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/community/environment/score-k8s/10-env.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/environment/score-k8s/README.md create mode 100755 gen/external-content/resource-provisioners/community/environment/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/10-hpa.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/10-hpa.provisioners.yaml create mode 100755 gen/external-content/resource-provisioners/community/horizontal-pod-autoscaler/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/redis/score-k8s/10-redis-helm-template.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/redis/score-k8s/10-redis-helm-upgrade.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/redis/score-k8s/README.md create mode 100755 gen/external-content/resource-provisioners/community/redis/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-route.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-with-netpol-route.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute-with-netpol.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/route/score.yaml create mode 100644 gen/external-content/resource-provisioners/community/score-compose.md create mode 100644 gen/external-content/resource-provisioners/community/score-k8s.md create mode 100755 gen/external-content/resource-provisioners/community/service/score-backend.yaml create mode 100644 gen/external-content/resource-provisioners/community/service/score-compose/10-service.provisioners.yaml create mode 100755 gen/external-content/resource-provisioners/community/service/score-frontend.yaml create mode 100644 gen/external-content/resource-provisioners/community/service/score-k8s/10-service-with-netpol.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/community/service/score-k8s/10-service.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/score-compose/check_version.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/check_version_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden create mode 100644 gen/external-content/resource-provisioners/default/score-compose/generate.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/generate_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/init.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/init_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/provisioners.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/resources.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/resources_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/root.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/root_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/run.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/run_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden create mode 100644 gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden create mode 100644 gen/external-content/resource-provisioners/default/score-k8s/default.go create mode 100644 gen/external-content/resource-provisioners/default/score-k8s/default_test.go create mode 100644 gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml diff --git a/.github/workflows/scheduled-generated.yml b/.github/workflows/scheduled-generated.yml index 6dd70b60..d626f7fa 100644 --- a/.github/workflows/scheduled-generated.yml +++ b/.github/workflows/scheduled-generated.yml @@ -21,6 +21,8 @@ jobs: run: | git remote add -f -t main --no-tags examples https://github.com/score-spec/examples.git git remote add -f -t main --no-tags community-provisioners https://github.com/score-spec/community-provisioners.git + git remote add -f -t main --no-tags score-compose https://github.com/score-spec/score-compose.git + git remote add -f -t main --no-tags score-k8s https://github.com/score-spec/score-k8s.git # Set github actions bot as the committer, see https://github.com/actions/checkout/pull/1184 git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" diff --git a/README.md b/README.md index e18794b3..08ff48de 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,8 @@ Example output. ```bash styles/style-guide.md - 40:71 error Did you really mean Vale.Spelling - 'inclusivity'? + 40:71 error Did you really mean Vale.Spelling + 'inclusivity'? ✖ 1 error, 0 warnings and 0 suggestions in 1 file. ``` @@ -104,9 +104,14 @@ The commands for the initial integration of the repos are: ```bash git remote add -f -t main --no-tags examples https://github.com/score-spec/examples.git git remote add -f -t main --no-tags community-provisioners https://github.com/score-spec/community-provisioners.git +git remote add -f -t main --no-tags score-compose https://github.com/score-spec/score-compose.git +git remote add -f -t main --no-tags score-k8s https://github.com/score-spec/score-k8s.git git read-tree --prefix=gen/external-content/score/specification -u examples/main:specification git read-tree --prefix=gen/external-content/score/resources/default-provisioners -u examples/main:resources git read-tree --prefix=gen/external-content/score/resources/community-provisioners -u community-provisioners/main +git read-tree --prefix=gen/external-content/resource-provisioners/community -u community-provisioners/main +git read-tree --prefix=gen/external-content/resource-provisioners/default/score-compose -u score-compose/main:internal/command +git read-tree --prefix=gen/external-content/resource-provisioners/default/score-k8s -u score-k8s/main:internal/provisioners/default git add gen/external-content git commit -s -S -m "Integrating external content" ``` diff --git a/config.toml b/config.toml index bd62a53e..dfe373b9 100644 --- a/config.toml +++ b/config.toml @@ -161,9 +161,15 @@ url = "https://github.com/score-spec/examples/blob/main" [[Params.exampleLibraryGitHubUrls]] name = "score/resources/community-provisioners" url = "https://github.com/score-spec/community-provisioners/blob/main" +[[Params.exampleLibraryGitHubUrls]] +name = "resource-provisioners/community" +url = "https://github.com/score-spec/community-provisioners/blob/main" [[Params.exampleTypeLabels]] name = "score" labels = ["Specification", "Resources", "Provisioner"] +[[Params.exampleTypeLabels]] +name = "resources-provisioners" +labels = ["Source", "Implementation", "Provisioner Type", "Resource Type", "Flavor", "Tool"] # User interface configuration [Params.ui] diff --git a/content/en/examples/_index.md b/content/en/examples/_index.md index 38f846db..a4e6a3c5 100644 --- a/content/en/examples/_index.md +++ b/content/en/examples/_index.md @@ -21,6 +21,10 @@ Explore a curated set of example files covering a range of entities for working {{< link-card-item text="Find some Score files examples illustrating how to use the Score specification as well as how to use the resources provisioners with both `score-compose` and `score-k8s`." url="/examples/score" linkLabel="Get examples" >}} {{< /multi-link-card >}} +{{< multi-link-card title="Resources provisioners" >}} +{{< link-card-item text="Find some Score files examples illustrating how to use the resources provisioners with both `score-compose` and `score-k8s`." url="/examples/resource-provisioners" linkLabel="Get examples" >}} +{{< /multi-link-card >}} + {{< multi-link-card title="More examples" >}} {{< link-card-item text="Find more advanced examples illustrating how to deploy a NodeJS application talking to a PostgreSQL database, how to deploy Backstage and Nginx, how to use Dapr with your workloads, etc. with both `score-compose` and `score-k8s`." url="/docs/examples" linkLabel="Get examples" >}} {{< /multi-link-card >}} diff --git a/content/en/examples/resource-provisioners/_index.md b/content/en/examples/resource-provisioners/_index.md new file mode 100644 index 00000000..ac4b6e62 --- /dev/null +++ b/content/en/examples/resource-provisioners/_index.md @@ -0,0 +1,9 @@ +--- +title: "Resource Provisioners" +draft: false +type: examples +--- + +The examples below illustrate how to use resources provisioners for each Score implementation. + +--- diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose.md new file mode 100644 index 00000000..feff8df0 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Dapr Pubsub" +flavor: "Community" + +--- + +{{% example-file filename="10-redis-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s.md new file mode 100644 index 00000000..451c547c --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s.md @@ -0,0 +1,14 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Dapr Pubsub" +flavor: "Community" + +--- + +{{% example-file filename="10-rabbitmq-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-redis-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose.md b/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose.md new file mode 100644 index 00000000..a5a17ba4 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Dapr State Store" +flavor: "Community" + +--- + +{{% example-file filename="10-redis-dapr-state-store.provisioners.yaml" dir="resource-provisioners/community/dapr-state-store/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s.md b/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s.md new file mode 100644 index 00000000..eba79a2a --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Dapr State Store" +flavor: "Community" + +--- + +{{% example-file filename="10-redis-dapr-state-store.provisioners.yaml" dir="resource-provisioners/community/dapr-state-store/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose.md b/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose.md new file mode 100644 index 00000000..a16dffd9 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Dapr Subscription" +flavor: "Community" + +--- + +{{% example-file filename="10-dapr-subscription.provisioners.yaml" dir="resource-provisioners/community/dapr-subscription/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s.md b/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s.md new file mode 100644 index 00000000..d999b850 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Dapr Subscription" +flavor: "Community" + +--- + +{{% example-file filename="10-dapr-subscription.provisioners.yaml" dir="resource-provisioners/community/dapr-subscription/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-compose.md b/content/en/examples/resource-provisioners/community/dns/score-compose.md new file mode 100644 index 00000000..5c6ca03e --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dns/score-compose.md @@ -0,0 +1,19 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'Prerequisites for `dns-in-codespace`: +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' +hasMore: false +parent: "Dns" +flavor: "Community" + +--- + +Prerequisites for `dns-in-codespace`: + +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. + +{{% example-file filename="10-dns-in-codespace.provisioners.yaml" dir="resource-provisioners/community/dns/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-k8s.md b/content/en/examples/resource-provisioners/community/dns/score-k8s.md new file mode 100644 index 00000000..7a07decd --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dns/score-k8s.md @@ -0,0 +1,19 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'Prerequisites for `dns-in-codespace`: +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' +hasMore: false +parent: "Dns" +flavor: "Community" + +--- + +Prerequisites for `dns-in-codespace`: + +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. + +{{% example-file filename="10-dns-in-codespace.provisioners.yaml" dir="resource-provisioners/community/dns/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/environment/score-compose.md b/content/en/examples/resource-provisioners/community/environment/score-compose.md new file mode 100644 index 00000000..b37f760f --- /dev/null +++ b/content/en/examples/resource-provisioners/community/environment/score-compose.md @@ -0,0 +1,18 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'Prerequisites: +- Have `python` installed, this provisioner is using Python to load the `.env` file.' +hasMore: false +parent: "Environment" +flavor: "Community" + +--- + +Prerequisites: + +- Have `python` installed, this provisioner is using Python to load the `.env` file. + +{{% example-file filename="10-env.provisioners.yaml" dir="resource-provisioners/community/environment/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/environment/score-k8s.md b/content/en/examples/resource-provisioners/community/environment/score-k8s.md new file mode 100644 index 00000000..d10b219a --- /dev/null +++ b/content/en/examples/resource-provisioners/community/environment/score-k8s.md @@ -0,0 +1,18 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'Prerequisites: +- Have `python` installed, this provisioner is using Python to load the `.env` file.' +hasMore: false +parent: "Environment" +flavor: "Community" + +--- + +Prerequisites: + +- Have `python` installed, this provisioner is using Python to load the `.env` file. + +{{% example-file filename="10-env.provisioners.yaml" dir="resource-provisioners/community/environment/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose.md b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose.md new file mode 100644 index 00000000..ec1605c8 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Horizontal Pod Autoscaler" +flavor: "Community" + +--- + +{{% example-file filename="10-hpa.provisioners.yaml" dir="resource-provisioners/community/horizontal-pod-autoscaler/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s.md b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s.md new file mode 100644 index 00000000..b4b795ee --- /dev/null +++ b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Horizontal Pod Autoscaler" +flavor: "Community" + +--- + +{{% example-file filename="10-hpa.provisioners.yaml" dir="resource-provisioners/community/horizontal-pod-autoscaler/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/redis/score-k8s.md b/content/en/examples/resource-provisioners/community/redis/score-k8s.md new file mode 100644 index 00000000..19271176 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/redis/score-k8s.md @@ -0,0 +1,31 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'Prerequisites: +- Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have `yq` installed locally.' +hasMore: true +parent: "Redis" +flavor: "Community" + +--- + +## For `10-redis-helm-template.provisioners.yaml` + +Prerequisites: + +- Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have `yq` installed locally. + +## For `10-redis-helm-upgrade.provisioners.yaml` + +Prerequisites: + +- Have `helm` installed locally, this provisioner installs the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have access to a cluster where the Helm chart will be installed. + - If you don't have one, you can deploy a `Kind` cluster locally by running this script: `.scripts/setup-kind-cluster.sh`. + +{{% example-file filename="10-redis-helm-template.provisioners.yaml" dir="resource-provisioners/community/redis/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-redis-helm-upgrade.provisioners.yaml" dir="resource-provisioners/community/redis/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s.md b/content/en/examples/resource-provisioners/community/route/score-k8s.md new file mode 100644 index 00000000..a624a65e --- /dev/null +++ b/content/en/examples/resource-provisioners/community/route/score-k8s.md @@ -0,0 +1,16 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Route" +flavor: "Community" + +--- + +{{% example-file filename="10-ingress-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-ingress-with-netpol-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-shared-gateway-httproute-with-netpol.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-shared-gateway-httproute.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/service/score-compose.md b/content/en/examples/resource-provisioners/community/service/score-compose.md new file mode 100644 index 00000000..e8f11b6f --- /dev/null +++ b/content/en/examples/resource-provisioners/community/service/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Service" +flavor: "Community" + +--- + +{{% example-file filename="10-service.provisioners.yaml" dir="resource-provisioners/community/service/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/service/score-k8s.md b/content/en/examples/resource-provisioners/community/service/score-k8s.md new file mode 100644 index 00000000..112b44de --- /dev/null +++ b/content/en/examples/resource-provisioners/community/service/score-k8s.md @@ -0,0 +1,14 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Service" +flavor: "Community" + +--- + +{{% example-file filename="10-service-with-netpol.provisioners.yaml" dir="resource-provisioners/community/service/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} +{{% example-file filename="10-service.provisioners.yaml" dir="resource-provisioners/community/service/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/gen/examples-site/examples-category-content/resource-provisioners.md b/gen/examples-site/examples-category-content/resource-provisioners.md new file mode 100644 index 00000000..83a16719 --- /dev/null +++ b/gen/examples-site/examples-category-content/resource-provisioners.md @@ -0,0 +1 @@ +The examples below illustrate how to use resources provisioners for each Score implementation. diff --git a/gen/examples-site/gen-example-pages.js b/gen/examples-site/gen-example-pages.js index fd55a374..cdd27877 100644 --- a/gen/examples-site/gen-example-pages.js +++ b/gen/examples-site/gen-example-pages.js @@ -55,7 +55,10 @@ ${categoryIndexContent} //For each folder, check if this is the last nesting level: for (const folder of folders) { //Discard readme and other files: - if (!isDirectory(`${sourceFolder}/${categoryFolder}/${folder}`)) { + if ( + !isDirectory(`${sourceFolder}/${categoryFolder}/${folder}`) || + shouldIgnoreFolder(folder) + ) { continue; } const isLastNestingLevel = !fs diff --git a/gen/examples-site/transform-default-resource-provisioners.js b/gen/examples-site/transform-default-resource-provisioners.js new file mode 100644 index 00000000..39f0aedf --- /dev/null +++ b/gen/examples-site/transform-default-resource-provisioners.js @@ -0,0 +1 @@ +console.log("TODO"); diff --git a/gen/external-content/resource-provisioners/community/.devcontainer/devcontainer.json b/gen/external-content/resource-provisioners/community/.devcontainer/devcontainer.json new file mode 100644 index 00000000..9ae3be89 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "Score Dev Container", + "image": "mcr.microsoft.com/devcontainers/base:noble", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "moby": true, + "version": "latest" + }, + "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": { + "version": "latest", + "helm": "latest", + "minikube": "latest" + }, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "postCreateCommand": "bash .devcontainer/installMoreTools.sh", + "customizations": { + "vscode": { + "extensions": [ + "redhat.vscode-yaml" + ], + "settings": { + "yaml.schemas": { + "https://raw.githubusercontent.com/score-spec/spec/main/score-v1b1.json": "score.yaml" + } + } + } + } +} \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/.devcontainer/installMoreTools.sh b/gen/external-content/resource-provisioners/community/.devcontainer/installMoreTools.sh new file mode 100755 index 00000000..0c0d25e7 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/.devcontainer/installMoreTools.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +mkdir install-more-tools +cd install-more-tools + +SCORE_COMPOSE_VERSION=$(curl -sL https://api.github.com/repos/score-spec/score-compose/releases/latest | jq -r .tag_name) +wget https://github.com/score-spec/score-compose/releases/download/${SCORE_COMPOSE_VERSION}/score-compose_${SCORE_COMPOSE_VERSION}_linux_amd64.tar.gz +tar -xvf score-compose_${SCORE_COMPOSE_VERSION}_linux_amd64.tar.gz +chmod +x score-compose +sudo mv score-compose /usr/local/bin + +SCORE_K8S_VERSION=$(curl -sL https://api.github.com/repos/score-spec/score-k8s/releases/latest | jq -r .tag_name) +wget https://github.com/score-spec/score-k8s/releases/download/${SCORE_K8S_VERSION}/score-k8s_${SCORE_K8S_VERSION}_linux_amd64.tar.gz +tar -xvf score-k8s_${SCORE_K8S_VERSION}_linux_amd64.tar.gz +chmod +x score-k8s +sudo mv score-k8s /usr/local/bin + +KIND_VERSION=$(curl -sL https://api.github.com/repos/kubernetes-sigs/kind/releases/latest | jq -r .tag_name) +curl -Lo ./kind https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64 +chmod +x ./kind +sudo mv ./kind /usr/local/bin/kind + +sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq +sudo chmod +x /usr/bin/yq + +cd .. +rm -rf install-more-tools \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/.github/workflows/ci.yaml b/gen/external-content/resource-provisioners/community/.github/workflows/ci.yaml new file mode 100644 index 00000000..b8975828 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/.github/workflows/ci.yaml @@ -0,0 +1,30 @@ +name: ci +permissions: + contents: read +on: + pull_request: + push: + branches: + - main +jobs: + test-provisioners: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: setup-kind-cluster + run: | + .scripts/setup-kind-cluster.sh + - uses: score-spec/setup-score@v2 + with: + file: score-compose + version: latest + token: ${{ secrets.GITHUB_TOKEN }} + - uses: score-spec/setup-score@v2 + with: + file: score-k8s + version: latest + token: ${{ secrets.GITHUB_TOKEN }} + - name: test-provisioners + run: | + .scripts/test-provisioners.sh \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/.github/workflows/release.yaml b/gen/external-content/resource-provisioners/community/.github/workflows/release.yaml new file mode 100644 index 00000000..b36052e9 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/.github/workflows/release.yaml @@ -0,0 +1,35 @@ +name: release +permissions: write-all +on: + push: + tags: + - 'v*' +jobs: + release: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: oras-project/setup-oras@v1 + - name: oras login + run: | + echo ${{ secrets.GITHUB_TOKEN }} | oras login ghcr.io -u ${{ github.actor }} --password-stdin + - name: oras push score-compose + run: | + mkdir compose + provisioners=$(find -name *.provisioners.yaml | grep /score-compose) + for provisioner in $provisioners; + do + cp $provisioner compose/ + done + cd compose/ + oras push ghcr.io/${{ github.repository_owner }}/score-compose-community-provisioners:${{ github.ref_name }},latest $(ls .) + - name: oras push score-k8s + run: | + mkdir k8s + provisioners=$(find -name *.provisioners.yaml | grep /score-k8s) + for provisioner in $provisioners; + do + cp $provisioner k8s/ + done + cd k8s/ + oras push ghcr.io/${{ github.repository_owner }}/score-k8s-community-provisioners:${{ github.ref_name }},latest $(ls .) diff --git a/gen/external-content/resource-provisioners/community/.gitignore b/gen/external-content/resource-provisioners/community/.gitignore new file mode 100644 index 00000000..a53b9f57 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/.gitignore @@ -0,0 +1,5 @@ +compose.yaml +manifests.yaml +.score-compose/ +.score-k8s/ +score.yaml diff --git a/gen/external-content/resource-provisioners/community/.scripts/setup-kind-cluster.sh b/gen/external-content/resource-provisioners/community/.scripts/setup-kind-cluster.sh new file mode 100755 index 00000000..5df5af9d --- /dev/null +++ b/gen/external-content/resource-provisioners/community/.scripts/setup-kind-cluster.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -o errexit + +cat <&2 + HELM_TEMPLATE_OUTPUT_TEMP_FILE=$(mktemp) + helm template ${SERVICE} bitnami/redis --set replica.replicaCount=1 > ${HELM_TEMPLATE_OUTPUT_TEMP_FILE} + MANIFESTS_IN_JSON=$(yq ea '[.]' -o json -I=0 ${HELM_TEMPLATE_OUTPUT_TEMP_FILE}) + OUTPUTS='{"resource_outputs":{"host":"%s-master", "port":"6379", "username":"", "password":"🔐💬%s_redis-password💬🔐"},"manifests":%s}' + printf "$OUTPUTS" "$SERVICE" "$SERVICE" "$MANIFESTS_IN_JSON" + expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/redis/score-k8s/10-redis-helm-upgrade.provisioners.yaml b/gen/external-content/resource-provisioners/community/redis/score-k8s/10-redis-helm-upgrade.provisioners.yaml new file mode 100644 index 00000000..185ecc33 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/redis/score-k8s/10-redis-helm-upgrade.provisioners.yaml @@ -0,0 +1,22 @@ +- uri: cmd://bash#helm-upgrade-redis + type: redis + description: Deploys the bitnami/redis Helm chart in an existing cluster. + args: + - -c + - | + STDIN=$(cat) + SERVICE=$(echo $STDIN | yq eval -p json '.resource_id' | yq '. |= sub("\.", "-")') + NAMESPACE=$(echo $STDIN | yq eval -p json '.namespace') + if [ -z "$NAMESPACE" ]; then + NAMESPACE="default" + fi + set -eu -o pipefail + helm repo add bitnami https://charts.bitnami.com/bitnami >&2 + helm upgrade -i ${SERVICE} bitnami/redis --set replica.replicaCount=1 -n ${NAMESPACE} --wait >&2 + OUTPUTS='{"resource_outputs":{"host":"%s-master", "port":"6379", "username":"", "password":"🔐💬%s_redis-password💬🔐"},"manifests":[]}' + printf "$OUTPUTS" "$SERVICE" "$SERVICE" + expected_outputs: + - host + - port + - username + - password diff --git a/gen/external-content/resource-provisioners/community/redis/score-k8s/README.md b/gen/external-content/resource-provisioners/community/redis/score-k8s/README.md new file mode 100644 index 00000000..d1ad8775 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/redis/score-k8s/README.md @@ -0,0 +1,12 @@ +## For `10-redis-helm-template.provisioners.yaml` + +Prerequisites: +- Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have `yq` installed locally. + +## For `10-redis-helm-upgrade.provisioners.yaml` + +Prerequisites: +- Have `helm` installed locally, this provisioner installs the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have access to a cluster where the Helm chart will be installed. + - If you don't have one, you can deploy a `Kind` cluster locally by running this script: `.scripts/setup-kind-cluster.sh`. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/redis/score.yaml b/gen/external-content/resource-provisioners/community/redis/score.yaml new file mode 100755 index 00000000..c6c7b29d --- /dev/null +++ b/gen/external-content/resource-provisioners/community/redis/score.yaml @@ -0,0 +1,16 @@ +apiVersion: score.dev/v1b1 +metadata: + name: my-workload +containers: + my-container: + image: busybox + command: ["/bin/sh"] + args: ["-c", "while true; do echo $REDIS_HOST; sleep 5; done"] + variables: + REDIS_HOST: ${resources.my-redis.host} + REDIS_PORT: ${resources.my-redis.port} + REDIS_USERNAME: ${resources.my-redis.username} + REDIS_PASSWORD: ${resources.my-redis.password} +resources: + my-redis: + type: redis \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-route.provisioners.yaml b/gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-route.provisioners.yaml new file mode 100644 index 00000000..142eed61 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-route.provisioners.yaml @@ -0,0 +1,37 @@ +- uri: template://community-provisioners/ingress-route + type: route + description: Provisions an Ingress route on a shared nginx instance. + supported_params: + - path + - host + - port + init: | + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + state: | + routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + manifests: | + - apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: {{ .State.routeName }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + spec: + ingressClassName: nginx + rules: + - host: {{ .Params.host | quote }} + http: + paths: + - path: {{ .Params.path | quote }} + pathType: Prefix + backend: + service: + name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} + port: + number: {{ .Params.port }} diff --git a/gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-with-netpol-route.provisioners.yaml b/gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-with-netpol-route.provisioners.yaml new file mode 100644 index 00000000..835755d4 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/route/score-k8s/10-ingress-with-netpol-route.provisioners.yaml @@ -0,0 +1,62 @@ +- uri: template://community-provisioners/ingress-with-net-pol-route + type: route + description: Provisions an Ingress route on a shared nginx instance, and a NetworkPolicy between them. + supported_params: + - path + - host + - port + init: | + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + targetPort: {{ $port.TargetPort }} + state: | + routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + manifests: | + - apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: {{ .State.routeName }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + spec: + ingressClassName: nginx + rules: + - host: {{ .Params.host | quote }} + http: + paths: + - path: {{ .Params.path | quote }} + pathType: Prefix + backend: + service: + name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} + port: + number: {{ .Params.port }} + - apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: ingress-to-{{ .SourceWorkload }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ .SourceWorkload }} + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: ingress-nginx + podSelector: + matchLabels: + app.kubernetes.io/name: ingress-nginx + ports: + - protocol: TCP + port: {{ .Init.targetPort }} diff --git a/gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute-with-netpol.provisioners.yaml b/gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute-with-netpol.provisioners.yaml new file mode 100644 index 00000000..fd4095d1 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute-with-netpol.provisioners.yaml @@ -0,0 +1,67 @@ +- uri: template://community-provisioners/route-with-shared-gateway-with-netpol + type: route + description: Generates an HTTPRoute attached to a default Gateway in default Namespace, and a NetworkPolicy between them. + supported_params: + - path + - host + - port + init: | + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + targetPort: {{ $port.TargetPort }} + gatewayNamespace: default + gatewayName: default + state: | + routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + manifests: | + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: {{ .State.routeName }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.routeName }} + app.kubernetes.io/instance: {{ .State.routeName }} + spec: + parentRefs: + - name: {{ .Init.gatewayName }} + namespace: {{ .Init.gatewayNamespace }} + hostnames: + - {{ .Params.host | quote }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ .Params.path | quote }} + backendRefs: + - name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} + port: {{ .Params.port }} + - apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: gateway-to-{{ .SourceWorkload }} + spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ .SourceWorkload }} + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: nginx-gateway + podSelector: + matchLabels: + app.kubernetes.io/name: nginx-gateway-fabric + ports: + - protocol: TCP + port: {{ .Init.targetPort }} diff --git a/gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute.provisioners.yaml b/gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute.provisioners.yaml new file mode 100644 index 00000000..a88eed2e --- /dev/null +++ b/gen/external-content/resource-provisioners/community/route/score-k8s/10-shared-gateway-httproute.provisioners.yaml @@ -0,0 +1,48 @@ +- uri: template://community-provisioners/route-with-shared-gateway + type: route + description: Generates an HTTPRoute attached to a default Gateway in default Namespace. + supported_params: + - path + - host + - port + init: | + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + gatewayNamespace: default + gatewayName: default + state: | + routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + manifests: | + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: {{ .State.routeName }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.routeName }} + app.kubernetes.io/instance: {{ .State.routeName }} + spec: + parentRefs: + - name: {{ .Init.gatewayName }} + namespace: {{ .Init.gatewayNamespace }} + hostnames: + - {{ .Params.host | quote }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ .Params.path | quote }} + backendRefs: + - name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} + port: {{ .Params.port }} diff --git a/gen/external-content/resource-provisioners/community/route/score.yaml b/gen/external-content/resource-provisioners/community/route/score.yaml new file mode 100644 index 00000000..be4a8f08 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/route/score.yaml @@ -0,0 +1,22 @@ +apiVersion: score.dev/v1b1 +metadata: + name: my-workload +containers: + my-container: + image: ghcr.io/stefanprodan/podinfo:latest + variables: + PODINFO_UI_MESSAGE: "Hello, ${resources.dns.host}!" +resources: + dns: + type: dns + route: + type: route + params: + host: ${resources.dns.host} + path: / + port: 8080 +service: + ports: + tcp: + port: 8080 + targetPort: 9898 \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/score-compose.md b/gen/external-content/resource-provisioners/community/score-compose.md new file mode 100644 index 00000000..b92c5e22 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/score-compose.md @@ -0,0 +1,43 @@ +Initialize your local workspace, by importing a specific community provisioner: + +```bash +score-commpose init --provisioners REPLACE-ME-WITH-ACTUAL-PROVISIONER-FILE-URL.yaml +``` + +_Note: you need to replace `REPLACE-ME-WITH-ACTUAL-PROVISIONER-FILE-URL.yaml` by the actual provisioner file you want to use and import. More information [here](https://docs.score.dev/docs/score-implementation/score-compose/resources-provisioners/#install-provisioner-files)._ + +Get the provisioners definition: + +```bash +score-compose provisioners list +``` + +Generate the platform specific manifests: + +```bash +score-commpose generate score.yaml +``` + +See the resource outputs: + +```bash +score-commpose resources list +``` + +You can run the following command on each resource listed with the previous command to get their `outputs`: + +```bash +score-commpose resources get-outputs +``` + +Deploy the generated manifests: + +```bash +docker compose up -d +``` + +See the running containers: + +```bash +docker ps +``` diff --git a/gen/external-content/resource-provisioners/community/score-k8s.md b/gen/external-content/resource-provisioners/community/score-k8s.md new file mode 100644 index 00000000..c9e876c8 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/score-k8s.md @@ -0,0 +1,43 @@ +Initialize your local workspace, by importing a specific community provisioner: + +```bash +score-k8s init --provisioners REPLACE-ME-WITH-ACTUAL-PROVISIONER-FILE-URL.yaml +``` + +_Note: you need to replace `REPLACE-ME-WITH-ACTUAL-PROVISIONER-FILE-URL.yaml` by the actual provisioner file you want to use and import. More information [here](https://docs.score.dev/docs/score-implementation/score-k8s/resources-provisioners/#install-provisioner-files)._ + +Get the provisioners definition: + +```bash +score-k8s provisioners list +``` + +Generate the platform specific manifests: + +```bash +score-k8s generate score.yaml +``` + +See the resource outputs: + +```bash +score-k8s resources list +``` + +You can run the following command on each resource listed with the previous command to get their `outputs`: + +```bash +score-k8s resources get-outputs +``` + +Deploy the generated manifests: + +```bash +kubectl apply -f manifests.yaml +``` + +See the running containers: + +```bash +kubectl get all +``` diff --git a/gen/external-content/resource-provisioners/community/service/score-backend.yaml b/gen/external-content/resource-provisioners/community/service/score-backend.yaml new file mode 100755 index 00000000..1779778e --- /dev/null +++ b/gen/external-content/resource-provisioners/community/service/score-backend.yaml @@ -0,0 +1,8 @@ +apiVersion: score.dev/v1b1 +metadata: + name: backend +containers: + my-container: + image: busybox + command: ["/bin/sh"] + args: ["-c", "while true; do echo Hello Backend; sleep 5; done"] \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/service/score-compose/10-service.provisioners.yaml b/gen/external-content/resource-provisioners/community/service/score-compose/10-service.provisioners.yaml new file mode 100644 index 00000000..6ac10b4b --- /dev/null +++ b/gen/external-content/resource-provisioners/community/service/score-compose/10-service.provisioners.yaml @@ -0,0 +1,11 @@ +- uri: template://community-provisioners/static-service + type: service + description: Outputs the name of the Workload dependency if it exists in the list of Workloads. + init: | + name: {{ splitList "." .Id | last }} + outputs: | + {{ $w := (index .WorkloadServices .Init.name) }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + name: {{ $w.ServiceName | quote }} + expected_outputs: + - name diff --git a/gen/external-content/resource-provisioners/community/service/score-frontend.yaml b/gen/external-content/resource-provisioners/community/service/score-frontend.yaml new file mode 100755 index 00000000..d60f0491 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/service/score-frontend.yaml @@ -0,0 +1,13 @@ +apiVersion: score.dev/v1b1 +metadata: + name: frontend +containers: + my-container: + image: busybox + command: ["/bin/sh"] + args: ["-c", "while true; do echo $BACKEND_SVC; sleep 5; done"] + variables: + BACKEND_SVC: http://${resources.backend.name} +resources: + backend: + type: service \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/community/service/score-k8s/10-service-with-netpol.provisioners.yaml b/gen/external-content/resource-provisioners/community/service/score-k8s/10-service-with-netpol.provisioners.yaml new file mode 100644 index 00000000..2a6151e1 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/service/score-k8s/10-service-with-netpol.provisioners.yaml @@ -0,0 +1,64 @@ +- uri: template://community-provisioners/static-service-with-netpol + type: service + description: Outputs the name of the Workload dependency if it exists in the list of Workloads, and generate NetworkPolicies between them. + init: | + name: {{ splitList "." .Id | last }} + outputs: | + {{ $w := (index .WorkloadServices .Init.name) }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + name: {{ .Init.name }} + expected_outputs: + - name + manifests: | + - apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: {{ .Init.name }}-from-{{ .SourceWorkload }}-ingress + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ .Init.name }} + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: {{ .SourceWorkload }} + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Namespace | default "default" }} + ports: + {{- range $k, $port := (index .WorkloadServices .Init.name).Ports }} + - protocol: TCP + port: {{ $port.TargetPort }} + {{- end }} + - apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: {{ .SourceWorkload }}-to-{{ .Init.name }}-egress + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + spec: + podSelector: + matchLabels: + app.kubernetes.io/name: {{ .SourceWorkload }} + policyTypes: + - Egress + egress: + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: {{ .Init.name }} + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Namespace | default "default" }} + ports: + {{- range $k, $port := (index .WorkloadServices .Init.name).Ports }} + - protocol: TCP + port: {{ $port.TargetPort }} + {{- end }} diff --git a/gen/external-content/resource-provisioners/community/service/score-k8s/10-service.provisioners.yaml b/gen/external-content/resource-provisioners/community/service/score-k8s/10-service.provisioners.yaml new file mode 100644 index 00000000..7202e173 --- /dev/null +++ b/gen/external-content/resource-provisioners/community/service/score-k8s/10-service.provisioners.yaml @@ -0,0 +1,11 @@ +- uri: template://community-provisioners/static-service + type: service + description: Outputs the name of the Workload dependency if it exists in the list of Workloads. + init: | + name: {{ splitList "." .Id | last }} + outputs: | + {{ $w := (index .WorkloadServices .Init.name) }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + name: {{ $w.ServiceName | quote }} + expected_outputs: + - name diff --git a/gen/external-content/resource-provisioners/default/score-compose/check_version.go b/gen/external-content/resource-provisioners/default/score-compose/check_version.go new file mode 100644 index 00000000..48b9cff4 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/check_version.go @@ -0,0 +1,50 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "github.com/spf13/cobra" + + "github.com/score-spec/score-compose/internal/version" +) + +var checkVersionCmd = &cobra.Command{ + Use: "check-version [constraint]", + Short: "Assert that the version of score-compose matches the required constraint", + Long: `score-compose is commonly used in Makefiles and CI pipelines which may depend on a particular functionality +or a particular default provisioner provided by score-compose init. This command provides a common way to check that +the version of score-compose matches a required version. +`, + Example: ` + # check that the version is exactly 1.2.3 + score-compose check-version =v1.2.3 + + # check that the version is 1.3.0 or greater + score-compose check-version >v1.2 + + # check that the version is equal or greater to 1.2.3 + score-compose check-version >=1.2.3`, + Args: cobra.ExactArgs(1), + SilenceErrors: true, + CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + return version.AssertVersion(args[0], version.Version) + }, +} + +func init() { + rootCmd.AddCommand(checkVersionCmd) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/check_version_test.go b/gen/external-content/resource-provisioners/default/score-compose/check_version_test.go new file mode 100644 index 00000000..07f7d909 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/check_version_test.go @@ -0,0 +1,72 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckVersionHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `score-compose is commonly used in Makefiles and CI pipelines which may depend on a particular functionality +or a particular default provisioner provided by score-compose init. This command provides a common way to check that +the version of score-compose matches a required version. + +Usage: + score-compose check-version [constraint] [flags] + +Examples: + + # check that the version is exactly 1.2.3 + score-compose check-version =v1.2.3 + + # check that the version is 1.3.0 or greater + score-compose check-version >v1.2 + + # check that the version is equal or greater to 1.2.3 + score-compose check-version >=1.2.3 + +Flags: + -h, --help help for check-version + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) + + stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "check-version"}) + assert.NoError(t, err) + assert.Equal(t, stdout, stdout2) + assert.Equal(t, "", stderr) +} + +func TestCheckVersionPass(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">=0.0.0"}) + assert.NoError(t, err) + assert.Equal(t, stdout, "") + assert.Equal(t, "", stderr) +} + +func TestCheckVersionFail(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">99"}) + assert.EqualError(t, err, "current version 0.0.0 does not match requested constraint >99") + assert.Equal(t, stdout, "") + assert.Equal(t, "", stderr) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml b/gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml new file mode 100644 index 00000000..165dd5bf --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml @@ -0,0 +1,996 @@ +# This file is generated by score-compose when score-compose init is called. This presents the default set of +# template provisioners provided for a basic set of resource types. This file can be overridden by adding new +# *.provisioners.yaml files in this directory or by editing this file in place and checking it into the source code. + +# The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources +# system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source +# attribute that we can reference. +- uri: template://default-provisioners/volume + # By default, match all classes and ids of volume. If you want to override this, create another provisioner definition + # with a higher priority. + type: volume + description: Creates a persistent volume that can be mounted on a workload. + init: | + randomVolumeName: {{ .Id | replace "." "-" }}-{{ randAlphaNum 6 }} + # Store the random volume name if we haven't chosen one yet, otherwise use the one that exists already + state: | + name: {{ dig "name" .Init.randomVolumeName .State }} + # Return a source value with the volume name. This can be used in volume resource references now. + outputs: | + type: volume + source: {{ .State.name }} + # Add a volume to the docker compose file. We assume our name is unique here. We also apply a label to help ensure + # that we can track the volume back to the workload and resource that created it. + volumes: | + {{ .State.name }}: + name: {{ .State.name }} + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} + expected_outputs: + - source + - type + + +# The default provisioner for service resources, this expects a workload and port name and will return the hostname and +# port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency +# relationship yet. +- uri: template://default-provisioners/service-port + type: service-port + description: Outputs a hostname and port for connecting to another workload. + outputs: | + {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} + {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} + {{ $w := (index .WorkloadServices .Params.workload) }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + {{ $p := (index $w.Ports .Params.port) }} + {{ if not $p }}{{ fail "unknown service port" }}{{ end }} + hostname: {{ $w.ServiceName | quote }} + port: {{ $p.TargetPort }} + expected_outputs: + - hostname + - port + supported_params: + - workload + - port + +# The default redis provisioner adds a redis service to the project which returns a host, port, username, and password. +- uri: template://default-provisioners/redis + # By default, match all redis types regardless of class and id. If you want to override this, create another + # provisioner definition with a higher priority. + type: redis + description: Provisions a dedicated Redis instance. + # Init template has the default port and a random service name and password if needed later + init: | + port: 6379 + randomServiceName: redis-{{ randAlphaNum 6 }} + randomPassword: {{ randAlphaNum 16 | quote }} + # The only state we need to persist is the chosen random service name and password + state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + # Return the outputs schema that consumers expect + outputs: | + host: {{ .State.serviceName }} + port: {{ .Init.port }} + username: default + password: {{ .State.password | quote }} + # write the config file to the mounts directory + files: | + {{ .State.serviceName }}/redis.conf: | + requirepass {{ .State.password }} + port {{ .Init.port }} + save 60 1 + loglevel warning + # add a volume for persistence of the redis data + volumes: | + {{ .State.serviceName }}-data: + name: {{ .State.serviceName }}-data + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} + # And the redis service itself with volumes bound in + services: | + {{ .State.serviceName }}: + labels: + dev.score.compose.res.uid: {{ .Uid }} + image: mirror.gcr.io/redis:7-alpine + restart: always + entrypoint: ["redis-server"] + command: ["/usr/local/etc/redis/redis.conf"] + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ .State.serviceName }}/redis.conf + target: /usr/local/etc/redis/redis.conf + read_only: true + - type: volume + source: {{ .State.serviceName }}-data + target: /data + volume: + nocopy: true + info_logs: | + - "{{.Uid}}: To connect to redis: \"docker run -it --network {{ .ComposeProjectName }}_default --rm redis redis-cli -h {{ .State.serviceName | squote }} -a {{ .State.password | squote }}\"" + expected_outputs: + - host + - port + - username + - password + +# The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on +# startup. +- uri: template://default-provisioners/postgres + # By default, match all redis types regardless of class and id. If you want to override this, create another + # provisioner definition with a higher priority. + type: postgres + description: Provisions a dedicated database on a shared PostgreSQL instance. + # Init template has the random service name and password if needed later + init: | + randomServiceName: pg-{{ randAlphaNum 6 }} + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-postgres-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + # The state for each database resource is a unique db name and credentials + state: | + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + # All instances agree on the shared state since there is no concurrency here + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + # The outputs are the core database outputs. We output both name and database for broader compatibility. + outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 5432 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} + # Write out an idempotent create script per database + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | + SELECT 'CREATE DATABASE "{{ .State.database }}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{{ .State.database }}')\gexec + SELECT $$CREATE USER "{{ .State.username }}" WITH PASSWORD '{{ .State.password }}'$$ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .State.username }}')\gexec + GRANT ALL PRIVILEGES ON DATABASE "{{ .State.database }}" TO "{{ .State.username }}"; + \connect "{{ .State.database }}"; + GRANT ALL ON SCHEMA public TO "{{ .State.username }}"; + # Ensure the data volume exists + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + # Create 2 services, the first is the database itself, the second is the init container which runs the scripts + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/postgres:17-alpine + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 5432 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + interval: 2s + timeout: 2s + retries: 15 + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: mirror.gcr.io/postgres:17-alpine + entrypoint: ["/bin/sh"] + environment: + POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + command: + - "-c" + - | + cd /db-scripts + ls db-*.sql | xargs cat | psql "postgresql://postgres:$${POSTGRES_PASSWORD}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:5432/postgres" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /db-scripts + info_logs: | + - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -U {{ .State.username }} --dbname {{ .State.database }}\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" + {{ end }} + expected_outputs: + - host + - port + - name + - database + - username + - password + + +- uri: template://default-provisioners/postgres-instance + + type: postgres-instance + description: Provisions a dedicated PostgreSQL instance. + # Init template has the random service name and password if needed later + init: | + randomServiceName: pg-{{ randAlphaNum 6 }} + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-postgres-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + # The state for each database resource is a unique db name and credentials + state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + database: "postgres" + username: "postgres" + password: {{ dig "password" .Init.randomPassword .State | quote }} + # The outputs are the core database outputs. We output both name and database for broader compatibility. + outputs: | + host: {{ .State.serviceName }} + port: 5432 + username: postgres + password: {{ .State.password }} + # Ensure the data volume exists + volumes: | + {{ .State.serviceName }}-data: + driver: local + # Create 2 services, the first is the database itself, the second is the init container which runs the scripts + services: | + {{ .State.serviceName }}: + image: mirror.gcr.io/postgres:17-alpine + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: {{ .State.password | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 5432 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .State.serviceName }}-data + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + + info_logs: | + - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ .State.serviceName }} -U {{ .State.username }} --dbname {{ .State.database }}\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" + {{ end }} + expected_outputs: + - host + - port + - username + - password + +# This resource provides a minio based S3 bucket with AWS-style credentials. +# This provides some common and well known outputs that can be used with any generic AWS s3 client. +# If the provider has a publish port annotation, it can expose a management port on the local network for debugging and +# connectivity. +- uri: template://default-provisioners/s3 + type: s3 + description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. + # The init template contains some initial seed data that can be used it needed. + init: | + randomServiceName: minio-{{ randAlphaNum 6 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + randomBucket: bucket-{{ randAlpha 8 | lower }} + randomAccessKeyId: {{ randAlphaNum 20 | quote }} + randomSecretKey: {{ randAlphaNum 40 | quote }} + sk: default-provisioners-minio-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} + # The only instance state is the bucket name, for now we provision a single aws key across s3 resources. + state: | + bucket: {{ dig "bucket" .Init.randomBucket .State | quote }} + # The shared state contains the chosen service name and credentials + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instanceUsername: {{ dig .Init.sk "instanceUsername" .Init.randomUsername .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" .Init.randomAccessKeyId .Shared | quote }} + instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" .Init.randomSecretKey .Shared | quote }} + publishPort: {{ with (dig .Init.sk "publishPort" 0 .Shared) }}{{ if ne . 0 }}{{ . }}{{ else }}{{ $.Init.publishPort }}{{ end }}{{ end }} + # the outputs that we can expose + outputs: | + bucket: {{ .State.bucket }} + access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} + secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + endpoint: http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 + # for compatibility with Humanitec's existing s3 resource + region: "us-east-1" + aws_access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} + aws_secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + # we store 2 files, 1 is always the same and overridden, the other is per bucket + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/00-svcacct.sh: | + set -eu + mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} + mc admin user svcacct info myminio {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} || mc admin user svcacct add myminio {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} --access-key {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} --secret-key {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/10-bucket-{{ .State.bucket }}.sh: | + set -eu + mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} + mc mb -p myminio/{{ .State.bucket }} + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + # 2 services, the minio one, and the init container which ensures the service account and buckets exist + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: quay.io/minio/minio + command: ["server", "/data", "--console-address", ":9001"] + restart: always + {{ if ne .Init.publishPort 0 }} + ports: + - target: 9001 + published: {{ .Init.publishPort }} + {{ end }} + healthcheck: + test: ["CMD-SHELL", "mc alias set myminio http://localhost:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }}"] + interval: 2s + timeout: 2s + retries: 15 + environment: + MINIO_ROOT_USER: {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} + MINIO_ROOT_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /data + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: quay.io/minio/minio + entrypoint: ["/bin/bash"] + command: + - "-c" + - "for s in $$(ls /setup-scripts -1); do sh /setup-scripts/$$s; done" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts + target: /setup-scripts + info_logs: | + - "{{.Uid}}: To connect with a minio client: use the myminio alias at \"docker run -it --network {{ .ComposeProjectName }}_default --rm --entrypoint /bin/bash quay.io/minio/minio -c 'mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceAccessKeyId" "" .Shared }} {{ dig .Init.sk "instanceSecretKey" "" .Shared }}; bash'\"" + {{ if ne .Init.publishPort 0 }} + - "{{.Uid}}: Or enter {{ dig .Init.sk "instanceUsername" "" .Shared }} / {{ dig .Init.sk "instancePassword" "" .Shared }} at https://localhost:{{ .Init.publishPort }}" + {{ end }} + expected_outputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key + +# The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins. +- uri: template://default-provisioners/rabbitmq + type: amqp + description: Provisions a dedicated RabbitMQ vhost on a shared instance. + init: | + randomServiceName: rabbitmq-{{ randAlphaNum 6 }} + randomVHost: vhost-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-rabbitmq + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} + publishManagementPort: {{ dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi }} + state: | + vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 5672 + vhost: {{ .State.vhost }} + username: {{ .State.username }} + password: {{ .State.password }} + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instanceErlangCookie: {{ dig .Init.sk "instanceErlangCookie" (randAlpha 20) .Shared }} + {{ $publishPorts := (list) }} + {{ if ne .Init.publishPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 5672 "published" .Init.publishPort)) }}{{ end }} + {{ $x := (dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi) }} + {{ if ne .Init.publishManagementPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 15672 "published" .Init.publishManagementPort)) }}{{ end }} + publishPorts: {{ $publishPorts | toJson }} + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.vhost }}.sh: | + while ! rabbitmqctl list_vhosts > /dev/null 2>&1; do + sleep 1 + done + rabbitmqctl list_vhosts | grep {{ .State.vhost }} || rabbitmqctl add_vhost {{ .State.vhost }} + rabbitmqctl list_users | grep {{ .State.username }} || rabbitmqctl add_user {{ .State.username }} {{ .State.password }} + rabbitmqctl set_user_tags {{ .State.username }} administrator + rabbitmqctl set_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" + rabbitmqctl set_topic_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/rabbitmq:3-management-alpine + restart: always + environment: + RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + ports: {{ dig .Init.sk "publishPorts" "" .Shared | toJson}} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/rabbitmq + healthcheck: + test: ["CMD-SHELL", "rabbitmq-diagnostics -q check_port_connectivity"] + interval: 2s + timeout: 5s + retries: 15 + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: mirror.gcr.io/rabbitmq:3-management-alpine + entrypoint: ["/bin/sh"] + environment: + RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} + command: + - "-c" + - | + set -exu + for s in /db-scripts/*.sh; do source $$s; done + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + labels: + dev.score.compose.labels.is-init-container: "true" + network_mode: service:{{ dig .Init.sk "instanceServiceName" "" .Shared }} + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /db-scripts + info_logs: | + {{ if ne .Init.publishManagementPort 0 }} + - "{{.Uid}}: Browse the rabbitmq UI at \"http://localhost:{{ .Init.publishManagementPort }}\"" + {{ end }} + expected_outputs: + - host + - port + - vhost + - username + - password + +# The default dns provisioner just outputs localhost as the hostname every time. +# This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This +# can be replaced by a new provisioner in the future. +- uri: template://default-provisioners/dns + type: dns + description: Outputs a *.localhost domain as the hostname. + init: | + randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost + state: | + instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} + outputs: | + host: {{ .State.instanceHostname }} + expected_outputs: + - host + +# The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. +# It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps. +- uri: template://default-provisioners/route + type: route + description: Provisions a ingress route on a shared Nginx instance. + init: | + randomServiceName: routing-{{ randAlphaNum 6 }} + sk: default-provisioners-routing-instance + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + + shared: | + {{ .Init.sk }}: + instancePort: {{ dig .Init.sk "instancePort" 8080 .Shared }} + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + {{ $targetHost := (index .WorkloadServices .SourceWorkload).ServiceName }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ $port := index $ports (print .Params.port) }} + {{ $targetPort := $port.TargetPort }} + {{ $target := (printf "%s:%d" $targetHost $targetPort) }} + {{ $hBefore := dig .Init.sk "hosts" (dict) .Shared }} + {{ $rBefore := dig .Params.host (dict) $hBefore }} + {{ $pathType := dig "compose.score.dev/route-provisioner-path-type" "Prefix" (dig "annotations" (dict) (.Metadata | default (dict))) }} + {{ $inner := dict "path" .Params.path "target" $target "port" $targetPort "path_type" $pathType }} + {{ $rAfter := (merge $rBefore (dict .Uid $inner)) }} + {{ $hAfter := (merge $hBefore (dict .Params.host $rAfter)) }} + hosts: {{ $hAfter | toRawJson }} + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf: | + worker_processes 1; + worker_rlimit_nofile 8192; + events { + worker_connections 4096; + } + http { + resolver 127.0.0.11; + + {{ range $h, $r := (dig .Init.sk "hosts" "" .Shared) }} + server { + listen 80; + listen [::]:80; + server_name {{ $h }}; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; + proxy_set_header Proxy ""; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + proxy_buffers 16 4k; + proxy_buffer_size 2k; + client_max_body_size 10m; + {{ dig "compose.score.dev/route-provisioner-server-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 4 }} + + location = /favicon.ico { + return 204; + access_log off; + log_not_found off; + } + + {{ range $k, $v := $r }} + # the basic path variant, "/" or "/one/two" + location ~ ^{{ index $v "path" }}$ { + set $backend {{ index $v "target" }}; + proxy_pass http://$backend; + {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} + } + + # The prefix match variants are included by default but can be excluded via 'compose.score.dev/route-provisioner-path-type' annotation + {{ if eq (index $v "path_type") "Prefix" }} + location ~ ^{{ index $v "path" | trimSuffix "/" }}/.* { + set $backend {{ index $v "target" }}; + proxy_pass http://$backend; + {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} + } + {{ end }} + {{ end }} + } + {{ end }} + } + + services: | + {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/nginx:1-alpine + restart: always + ports: + - published: {{ $p }} + target: 80 + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf + target: /etc/nginx/nginx.conf + readOnly: true + info_logs: | + {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} + - "{{.Uid}}: To connect to this route, http://{{ .Params.host }}:{{ $p }}{{ .Params.path }} (make sure {{ .Params.host }} resolves to localhost)" + supported_params: + - host + - port + - path + +# The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string. +- uri: template://default-provisioners/mongodb + type: mongodb + description: Provisions a dedicated MongoDB database. + init: | + port: 27017 + randomServiceName: mongo-{{ randAlphaNum 6 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.serviceName }} + port: {{ .Init.port }} + username: {{ .State.username | quote }} + password: {{ .State.password | quote }} + connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.serviceName }}:{{ .Init.port }}/" + volumes: | + {{ .State.serviceName }}-data: + name: {{ .State.serviceName }}-data + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} + services: | + {{ .State.serviceName }}: + labels: + dev.score.compose.res.uid: {{ .Uid }} + image: mirror.gcr.io/mongo:8 + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: {{ .State.username | quote }} + MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | quote }} + healthcheck: + test: ["CMD-SHELL", "echo 'db.runCommand(\"ping\").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD"] + interval: 2s + timeout: 5s + retries: 15 + start_period: 10s + volumes: + - type: volume + source: {{ .State.serviceName }}-data + target: /data/db + volume: + nocopy: true + info_logs: | + - "{{.Uid}}: To connect to mongo: \"docker exec -ti {{ .ComposeProjectName }}-{{ .State.serviceName }}-1 mongosh -u {{ .State.username }} -p {{ .State.password }}\"" + expected_outputs: + - host + - port + - username + - password + - connection + +# The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on +# startup. +- uri: template://default-provisioners/mysql + # By default, match all mysql types regardless of class and id. If you want to override this, create another + # provisioner definition with a higher priority. + type: mysql + description: Provisions a dedicated MySQL database on a shared instance. + # Init template has the random service name and password if needed later + init: | + randomServiceName: mysql-{{ randAlphaNum 6 }} + randomDatabase: db{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + randomRootPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-mysql-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + # The state for each database resource is a unique db name and credentials + state: | + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + # All instances agree on the shared state since there is no concurrency here + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + instanceRootPassword: {{ dig .Init.sk "instanceRootPassword" .Init.randomRootPassword .Shared | quote }} + # The outputs are the core database outputs. We output both name and database for broader compatibility. + outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 3306 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} + # Write out an idempotent create script per database + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | + CREATE DATABASE IF NOT EXISTS {{ .State.database }}; + USE {{ .State.database }}; + CREATE USER IF NOT EXISTS '{{ .State.username }}'@'localhost' IDENTIFIED BY '{{ .State.password }}'; + GRANT ALL PRIVILEGES ON {{ .State.database }} TO '{{ .State.username }}'@'localhost'; + FLUSH PRIVILEGES; + # Ensure the data volume exists + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + # Create an services with the database itself + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/mysql:8 + restart: always + environment: + MYSQL_DATABASE: {{ .State.database }} + MYSQL_USER: {{ .State.username }} + MYSQL_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + MYSQL_ROOT_PASSWORD: {{ dig .Init.sk "instanceRootPassword" "" .Shared | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 3306 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /docker-entrypoint-initdb.d/ + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p$${MYSQL_ROOT_PASSWORD}"] + interval: 5s + timeout: 3s + retries: 10 + info_logs: | + - "{{.Uid}}: To connect to mysql, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mysql:8 mysql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -u {{ .State.username }} -p\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your mysql client to \" + mysql://{{ .State.username }}:{{ .State.password }}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:{{ .Init.publishPort }}/{{ .State.database }}\"" + {{ end }} + expected_outputs: + - host + - port + - name + - database + - username + - password + +- uri: template://default-provisioners/kafka-topic + type: kafka-topic + description: Provisions a dedicated Kafka topic on a shared Kafka broker. + init: | + brokerPort: 9092 + ctrlPort: 9093 + state: | + topic: {{ dig "topic" (print "topic-" (randAlphaNum 6)) .State | quote }} + shared: | + shared_kafka_instance_name: {{ dig "shared_kafka_instance_name" (print "kafka-" (randAlphaNum 6)) .Shared | quote }} + services: | + {{ .Shared.shared_kafka_instance_name }}: + image: bitnami/kafka:latest + restart: always + environment: + KAFKA_CFG_NODE_ID: "0" + KAFKA_CFG_PROCESS_ROLES: controller,broker + KAFKA_CFG_LISTENERS: "PLAINTEXT://:{{ .Init.brokerPort }},CONTROLLER://:{{ .Init.ctrlPort }}" + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "0@{{ .Shared.shared_kafka_instance_name }}:{{ .Init.ctrlPort }}" + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "false" + healthcheck: + test: ["CMD", "kafka-topics.sh", "--list", "--bootstrap-server=localhost:{{ .Init.brokerPort }}"] + interval: 2s + timeout: 2s + retries: 10 + {{ $publishPort := (dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi) }} + {{ if ne $publishPort 0 }} + ports: + - target: {{ .Init.brokerPort }} + published: {{ $publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .Shared.shared_kafka_instance_name }}-data + target: /bitnami/kafka + {{ .State.topic }}-init: + image: bitnami/kafka:latest + entrypoint: ["/bin/sh"] + command: ["-c", "kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --describe || kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --create --partitions=3"] + network_mode: "service:{{ .Shared.shared_kafka_instance_name }}" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ .Shared.shared_kafka_instance_name }}: + condition: service_healthy + restart: true + volumes: | + {{ .Shared.shared_kafka_instance_name }}-data: + driver: local + outputs: | + host: {{ .Shared.shared_kafka_instance_name }} + port: "{{ .Init.brokerPort }}" + name: {{ .State.topic }} + num_partitions: 3 + expected_outputs: + - host + - port + - name + - num_partitions + +# The default elasticsearch provisioner adds a elasticsearch instance. +- uri: template://default-provisioners/elasticsearch + # By default, match all elasticsearch types regardless of class and id. + # If you want to override this, create another provisioner definition with a higher priority. + type: elasticsearch + description: Provisions a dedicated Elastic Search instance. + # Init template has the random service name and password if needed later + init: | + serviceName: elasticsearch + randomPassword: {{ randAlphaNum 16 | quote }} + clusterName: cluster-ecs-{{ randAlphaNum 6 }} + username: elastic + sk: default-provisioners-elasticsearch-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "9200" .Metadata | quote }} + license: {{ dig "annotations" "compose.score.dev/license" "basic" .Metadata | quote }} + stackVersion: {{ dig "annotations" "compose.score.dev/stack-version" "8.14.0" .Metadata | quote }} + esMemLimit: {{ dig "annotations" "compose.score.dev/es-mem-limit" "1073741824" .Metadata | quote }} + # The state for each elasticsearch resource is a unique host, port, and credentials + state: | + clusterName: {{ dig "clusterName" .Init.clusterName .State | quote }} + username: {{ dig "username" .Init.username .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + host: {{ dig "host" .Init.serviceName .State | quote }} + outputs: | + host: {{ .State.host }} + port: {{ .Init.publishPort }} + username: {{ .State.username | quote }} + password: {{ .State.password | quote }} + # Ensure the data volume exists + volumes: | + ecscerts: + driver: local + ecsdata: + driver: local + # Create 2 services, the first is the setup container which creates the certificates, the second is the elasticsearch itself + services: | + setup: + image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} + volumes: + - type: volume + source: ecscerts + target: /usr/share/elasticsearch/config/certs + user: "0" + command: + - "bash" + - "-c" + - | + if [ ! -f config/certs/ca.zip ]; then + echo "Creating CA"; + bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; + unzip config/certs/ca.zip -d config/certs; + fi; + if [ ! -f config/certs/certs.zip ]; then + echo "Creating certs"; + echo -ne \ + "instances:\n"\ + " - name: {{ .State.host }}\n"\ + " dns:\n"\ + " - {{ .State.host }}\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + > config/certs/instances.yml; + bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; + unzip config/certs/certs.zip -d config/certs; + fi; + echo "Setting file permissions" + chown -R root:root config/certs; + find . -type d -exec chmod 750 \{\} \;; + find . -type f -exec chmod 640 \{\} \;; + echo "Waiting for Elasticsearch availability"; + until curl -s --cacert config/certs/ca/ca.crt https://{{ .State.host }}:9200 | grep -q "missing authentication credentials"; do sleep 10; done; + echo "All done!"; + healthcheck: + test: ["CMD-SHELL", "[ -f config/certs/{{ .State.host }}/{{ .State.host }}.crt ]"] + interval: 1s + timeout: 5s + retries: 120 + {{ .State.host }}: + depends_on: + setup: + condition: service_healthy + image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} + labels: + co.elastic.logs/module: elasticsearch + volumes: + - type: volume + source: ecscerts + target: /usr/share/elasticsearch/config/certs + - type: volume + source: ecsdata + target: /usr/share/elasticsearch/data + ports: + - target: 9200 + published: {{ .Init.publishPort }} + environment: + - node.name={{ .State.host }} + - cluster.name={{ .State.clusterName }} + - discovery.type=single-node + - bootstrap.memory_lock=true + - ELASTIC_PASSWORD={{ .State.password }} + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key + - xpack.security.http.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt + - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.enabled=true + - xpack.security.transport.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key + - xpack.security.transport.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt + - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.verification_mode=certificate + - xpack.license.self_generated.type={{ .Init.license }} + mem_limit: {{ .Init.esMemLimit }} + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", + ] + interval: 10s + timeout: 10s + retries: 120 + info_logs: | + - "{{.Uid}}: To connect to elasticsearch:\n + download certificate from container per command like: \n + \tdocker cp [CONTAINER-NAME]:/usr/share/elasticsearch/config/certs/ca/ca.crt /tmp/ \n + and than check connection per culr like: \n + \tcurl --cacert /tmp/ca.crt -u {{ .State.username }}:{{ .State.password }} https://localhost:{{ .Init.publishPort }}" + expected_outputs: + - host + - port + - username + - password + +- uri: template://default-provisioners/mssql + type: mssql + description: Provisions a dedicated database on a shared MS SQL server instance. + init: | + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-mssql + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + state: | + service: {{ .Init.sk }} + database: master + username: sa + password: {{ dig "password" .Init.randomPassword .State | quote }} + publishPort: {{ .Init.publishPort }} + outputs: | + server: {{ .State.service }} + port: {{ .State.publishPort }} + connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};" + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} + volumes: | + {{ .Init.sk }}-data: + driver: local + services: | + {{ .Init.sk }}: + image: mcr.microsoft.com/mssql/server:latest + restart: always + environment: + ACCEPT_EULA: "Y" + MSSQL_ENABLE_HADR: "1" + MSSQL_AGENT_ENABLED: "1" + MSSQL_SA_PASSWORD: {{ .State.password }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 1433 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .Init.sk }}-data + target: /var/opt/mssql + info_logs: | + - "{{.Uid}}: To connect to mssql: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mcr.microsoft.com/mssql/server:latest mysql /opt/mssql-tools/bin/sqlcmd -S localhost -U {{ .State.username }} -p {{ .State.password | squote }}\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your mssql client to \" + Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};\"" + {{ end }} + expected_outputs: + - server + - port + - connection + - database + - username + - password diff --git a/gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden b/gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden new file mode 100644 index 00000000..8cdb8f94 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden @@ -0,0 +1,284 @@ +## TEMPLATE PROVISIONERS ## + +- uri: template://test-provisioners/without-class-without-params + type: template-without-class-without-params + expected_outputs: + - o1 + - o2 + +- uri: template://test-provisioners/with-class-without-params + type: template-with-class-without-params + class: c1 + expected_outputs: + - o1 + - o2 + +- uri: template://test-provisioners/without-class-with-params-in-outputs + type: template-without-class-with-params-in-outputs + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + +- uri: template://test-provisioners/with-class-with-params-in-outputs + type: template-with-class-with-params-in-outputs + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + +- uri: template://test-provisioners/without-class-with-params-in-shared + type: template-without-class-with-params-in-shared + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - psh1 + +- uri: template://test-provisioners/with-class-with-params-in-shared + type: template-with-class-with-params-in-shared + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - psh1 + +- uri: template://test-provisioners/without-class-with-params-in-state + type: template-without-class-with-params-in-state + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - psh1 + +- uri: template://test-provisioners/with-class-with-params-in-state + type: template-with-class-with-params-in-state + class: c1 + outputs: | + o1: o1 + o2: o2 + state: | + {{ if not .Params.p1 }}{{ fail "expected 'p1' param for the target workload name" }}{{ end }} + {{ if not .Params.pst1 }}{{ fail "expected 'pst1' param for the target workload name" }}{{ end }} + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - pst1 + +- uri: template://test-provisioners/without-class-with-params-in-shared-outputs + type: template-without-class-with-params-in-shared-outputs + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - psh1 + +- uri: template://test-provisioners/with-class-with-params-in-shared-outputs + type: template-with-class-with-params-in-shared-outputs + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - psh1 + +- uri: template://test-provisioners/without-class-with-params-in-state-outputs + type: template-without-class-with-params-in-state-outputs + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + +- uri: template://test-provisioners/with-class-with-params-in-state-outputs + type: template-with-class-with-params-in-state-outputs + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + +- uri: template://test-provisioners/without-class-with-params-in-state-outputs-state-shared + type: template-without-class-with-params-in-state-outputs-shared + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + - psh1 + +- uri: template://test-provisioners/with-class-with-params-in-state-outputs-shared + type: template-with-class-with-params-in-state-outputs-shared + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + - psh1 + +- uri: template://test-provisioners/without-class-without-params-with-description + type: template-without-class-without-params-with-description + expected_outputs: + - o1 + - o2 + description: without-class-without-params-with-description + + +## CMD PROVISIONERS ## + +- uri: cmd://test-provisioners/without-class-without-params + type: cmd-without-class-without-params + expected_outputs: + - o1 + - o2 + +- uri: cmd://test-provisioners/with-class-without-params + type: cmd-with-class-without-params + class: c1 + expected_outputs: + - o1 + - o2 + +- uri: cmd://test-provisioners/without-class-with-params-in-outputs + type: cmd-without-class-with-params-in-outputs + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + +- uri: cmd://test-provisioners/with-class-with-params-in-outputs + type: cmd-with-class-with-params-in-outputs + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + +- uri: cmd://test-provisioners/without-class-with-params-in-shared + type: cmd-without-class-with-params-in-shared + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - psh1 + +- uri: cmd://test-provisioners/with-class-with-params-in-shared + type: cmd-with-class-with-params-in-shared + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - psh1 + +- uri: cmd://test-provisioners/without-class-with-params-in-state + type: cmd-without-class-with-params-in-state + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - psh1 + +- uri: cmd://test-provisioners/without-class-with-params-in-shared-outputs + type: cmd-without-class-with-params-in-shared-outputs + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - psh1 + +- uri: cmd://test-provisioners/with-class-with-params-in-shared-outputs + type: cmd-with-class-with-params-in-shared-outputs + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - psh1 + +- uri: cmd://test-provisioners/without-class-with-params-in-state-outputs + type: cmd-without-class-with-params-in-state-outputs + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + +- uri: cmd://test-provisioners/with-class-with-params-in-state-outputs + type: cmd-with-class-with-params-in-state-outputs + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + +- uri: cmd://test-provisioners/without-class-with-params-in-state-outputs-state-shared + type: cmd-without-class-with-params-in-state-outputs-shared + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + - psh1 + +- uri: cmd://test-provisioners/with-class-with-params-in-state-outputs-shared + type: cmd-with-class-with-params-in-state-outputs-shared + class: c1 + expected_outputs: + - o1 + - o2 + supported_params: + - p1 + - po1 + - pst1 + - psh1 + +- uri: cmd://test-provisioners/without-class-without-params-with-description + type: cmd-without-class-without-params-with-description + expected_outputs: + - o1 + - o2 + description: cmd-without-class-without-params-with-description \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/score-compose/generate.go b/gen/external-content/resource-provisioners/default/score-compose/generate.go new file mode 100644 index 00000000..58ebfac7 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/generate.go @@ -0,0 +1,526 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "fmt" + "log/slog" + "os" + "slices" + "strconv" + "strings" + + "dario.cat/mergo" + composeloader "github.com/compose-spec/compose-go/v2/loader" + "github.com/compose-spec/compose-go/v2/types" + "github.com/score-spec/score-go/framework" + "github.com/score-spec/score-go/loader" + "github.com/score-spec/score-go/schema" + score "github.com/score-spec/score-go/types" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + + "github.com/score-spec/score-compose/internal/compose" + "github.com/score-spec/score-compose/internal/patching" + "github.com/score-spec/score-compose/internal/project" + "github.com/score-spec/score-compose/internal/provisioners" + "github.com/score-spec/score-compose/internal/provisioners/envprov" + provloader "github.com/score-spec/score-compose/internal/provisioners/loader" +) + +const ( + generateCmdOverridesFileFlag = "overrides-file" + generateCmdOverridePropertyFlag = "override-property" + generateCmdImageFlag = "image" + generateCmdBuildFlag = "build" + generateCmdOutputFlag = "output" + generateCmdEnvFileFlag = "env-file" + generateCmdPublishFlag = "publish" +) + +var generateCommand = &cobra.Command{ + Use: "generate", + Args: cobra.ArbitraryArgs, + Short: "Convert one or more Score files into a Docker compose manifest", + Long: `The generate command will convert Score files in the current Score compose project into a combined Docker compose +manifest. All resources and links between Workloads will be resolved and provisioned as required. + +By default this command looks for score.yaml in the current directory, but can take explicit file names as positional +arguments. + +"score-compose init" MUST be run first. An error will be thrown if the project directory is not present. + +`, + Example: ` + # Specify Score files + score-compose generate score.yaml *.score.yaml + + # Regenerate without adding new score files + score-compose generate + + # Provide overrides when one score file is provided + score-compose generate score.yaml --override-file=./overrides.score.yaml --override-property=metadata.key=value + + # Publish a port exposed by a workload for local testing + score-compose generate score.yaml --publish 8080:my-workload:80 + + # Publish a port from a resource host and port for local testing, the middle expression is RESOURCE_ID.OUTPUT_KEY + score-compose generate score.yaml --publish 5432:postgres#my-workload.db.host:5432`, + + // don't print the errors - we print these ourselves in main() + SilenceErrors: true, + + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + inputFiles := args + slices.Sort(inputFiles) + slog.Debug("Input Score files", "files", inputFiles) + + // first load all the score files, parse them with a dummy yaml decoder to find the workload name, reject any + // with invalid or duplicate names. + workloadNames, workloadSpecs, err := loadRawScoreFiles(inputFiles) + if err != nil { + return err + } + slog.Debug("Input Workload names", "names", workloadNames) + + // Forbid --image and --build when multiple score files are provided + if v, _ := cmd.Flags().GetString(generateCmdImageFlag); v != "" && len(workloadNames) > 1 { + return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdImageFlag) + } + if v, _ := cmd.Flags().GetStringArray(generateCmdBuildFlag); len(v) > 0 && len(workloadNames) > 1 { + return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdBuildFlag) + } + + // Now read and apply any overrides files to the score files + if v, _ := cmd.Flags().GetString(generateCmdOverridesFileFlag); v != "" { + if len(workloadNames) == 0 { + return fmt.Errorf("--%s cannot be used without providing a score file", generateCmdOverridesFileFlag) + } else if len(workloadNames) > 1 { + return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdOverridesFileFlag) + } + if err := parseAndApplyOverrideFile(v, generateCmdOverridesFileFlag, workloadSpecs[workloadNames[0]]); err != nil { + return err + } + } + + // Now read, parse, and apply any override properties to the score files + if v, _ := cmd.Flags().GetStringArray(generateCmdOverridePropertyFlag); len(v) > 0 { + if len(workloadNames) == 0 { + return fmt.Errorf("--%s cannot be used without providing a score file", generateCmdOverridesFileFlag) + } else if len(workloadNames) > 1 { + return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdOverridesFileFlag) + } + for _, overridePropertyEntry := range v { + if workloadSpecs[workloadNames[0]], err = parseAndApplyOverrideProperty(overridePropertyEntry, generateCmdOverridePropertyFlag, workloadSpecs[workloadNames[0]]); err != nil { + return err + } + } + } + + sd, ok, err := project.LoadStateDirectory(".") + if err != nil { + return fmt.Errorf("failed to load existing state directory: %w", err) + } else if !ok { + return fmt.Errorf("state directory does not exist, please run \"score-compose init\" first") + } + slog.Info(fmt.Sprintf("Loaded state directory with docker compose project '%s'", sd.State.Extras.ComposeProjectName)) + + currentState := &sd.State + + // Now validate with score spec + for i, workloadName := range workloadNames { + spec := workloadSpecs[workloadName] + inputFile := inputFiles[i] + + // Ensure transforms are applied (be a good citizen) + if changes, err := schema.ApplyCommonUpgradeTransforms(spec); err != nil { + return fmt.Errorf("failed to upgrade spec: %w", err) + } else if len(changes) > 0 { + for _, change := range changes { + slog.Info(fmt.Sprintf("Applying backwards compatible upgrade to '%s': %s", workloadName, change)) + } + } + if err := schema.Validate(spec); err != nil { + return fmt.Errorf("validation errors in workload '%s': %w", workloadName, err) + } + slog.Info(fmt.Sprintf("Validated workload '%s'", workloadName)) + + var out score.Workload + if err := loader.MapSpec(&out, spec); err != nil { + return fmt.Errorf("failed to convert '%s' to structure: %w", workloadName, err) + } + + // Gather container build contexts, these will be stored and added to the generated compose output later + containerBuildContexts := make(map[string]types.BuildConfig) + if v, _ := cmd.Flags().GetStringArray(generateCmdBuildFlag); len(v) > 0 { + for _, buildFlag := range v { + parts := strings.SplitN(buildFlag, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid --%s '%s': expected 2 =-separated parts", generateCmdBuildFlag, buildFlag) + } else if _, ok := out.Containers[parts[0]]; !ok { + return fmt.Errorf("invalid --%s '%s': unknown container '%s'", generateCmdBuildFlag, buildFlag, parts[0]) + } + if strings.HasPrefix(parts[1], "{") { + var intermediate interface{} + if err := yaml.Unmarshal([]byte(parts[1]), &intermediate); err != nil { + return fmt.Errorf("invalid --%s '%s': %w", generateCmdBuildFlag, buildFlag, err) + } + var out types.BuildConfig + if err := composeloader.Transform(intermediate, &out); err != nil { + return fmt.Errorf("invalid --%s '%s': %w", generateCmdBuildFlag, buildFlag, err) + } + containerBuildContexts[parts[0]] = out + } else { + containerBuildContexts[parts[0]] = types.BuildConfig{Context: parts[1]} + } + } + } + + // Apply image if container image is . + for containerName, container := range out.Containers { + if container.Image == "." { + if v, _ := cmd.Flags().GetString(generateCmdImageFlag); v != "" { + container.Image = v + slog.Info(fmt.Sprintf("Set container image for workload '%s' container '%s' to %s from --%s", workloadName, containerName, v, generateCmdImageFlag)) + out.Containers[containerName] = container + } else if _, ok := containerBuildContexts[containerName]; !ok { + return fmt.Errorf("failed to convert '%s' because container '%s' has no image and neither --%s nor --%s was provided", workloadName, containerName, generateCmdImageFlag, generateCmdBuildFlag) + } + } + } + + currentState, err = currentState.WithWorkload(&out, &inputFile, project.WorkloadExtras{BuildConfigs: containerBuildContexts}) + if err != nil { + return fmt.Errorf("failed to add workload '%s': %w", workloadName, err) + } + } + + if len(currentState.Workloads) == 0 { + return fmt.Errorf("the project is empty, please provide a score file to generate from") + } + + loadedProvisioners, err := provloader.LoadProvisionersFromDirectory(sd.Path, provloader.DefaultSuffix) + if err != nil { + return fmt.Errorf("failed to load provisioners: %w", err) + } else if len(loadedProvisioners) > 0 { + slog.Info(fmt.Sprintf("Successfully loaded %d resource provisioners", len(loadedProvisioners))) + } + + // append the env var provisioner + environmentProvisioner := new(envprov.Provisioner) + loadedProvisioners = append(loadedProvisioners, environmentProvisioner) + + currentState, err = currentState.WithPrimedResources() + if err != nil { + return fmt.Errorf("failed to prime resources: %w", err) + } + + superProject := &types.Project{ + Name: sd.State.Extras.ComposeProjectName, + Services: make(types.Services, 0), + Volumes: map[string]types.VolumeConfig{}, + Networks: map[string]types.NetworkConfig{}, + } + + currentState, err = provisioners.ProvisionResources(context.Background(), currentState, loadedProvisioners, superProject) + if err != nil { + return fmt.Errorf("failed to provision: %w", err) + } else if len(currentState.Resources) > 0 { + slog.Info(fmt.Sprintf("Provisioned %d resources", len(currentState.Resources))) + } + + waitServiceName, hasWaitService := injectWaitService(superProject) + + for workloadName, workloadState := range currentState.Workloads { + + slog.Info(fmt.Sprintf("Converting workload '%s' to Docker compose", workloadName)) + converted, err := compose.ConvertSpec(currentState, &workloadState.Spec) + if err != nil { + return fmt.Errorf("failed to convert workload '%s' to Docker compose: %w", workloadName, err) + } + + for serviceName, service := range converted.Services { + if _, ok := superProject.Services[serviceName]; ok { + return fmt.Errorf("failed to add converted workload '%s': duplicate service name '%s'", workloadName, serviceName) + } + if hasWaitService { + if service.DependsOn == nil { + service.DependsOn = make(types.DependsOnConfig) + } + service.DependsOn[waitServiceName] = types.ServiceDependency{Condition: "service_completed_successfully", Required: true} + } + superProject.Services[serviceName] = service + } + for volumeName, volume := range converted.Volumes { + if _, ok := superProject.Volumes[volumeName]; ok { + return fmt.Errorf("failed to add converted workload '%s': duplicate volume name '%s'", workloadName, volumeName) + } + superProject.Volumes[volumeName] = volume + } + for networkName, network := range converted.Networks { + if _, ok := superProject.Networks[networkName]; ok { + return fmt.Errorf("failed to add converted workload '%s': duplicated network name '%s'", workloadName, networkName) + } + superProject.Networks[networkName] = network + } + } + + for i, content := range currentState.Extras.PatchingTemplates { + slog.Info(fmt.Sprintf("Applying patching template %d", i+1)) + superProject, err = patching.PatchServices(currentState, superProject, content) + if err != nil { + return fmt.Errorf("failed to patch template %d: %w", i+1, err) + } + } + + if v, err := cmd.Flags().GetStringArray(generateCmdPublishFlag); err != nil { + return fmt.Errorf("failed to read publish property: %w", err) + } else { + for i, k := range v { + parts := strings.Split(k, ":") + if len(parts) <= 2 { + return fmt.Errorf("--%s[%d] expected 3 :-separated parts", generateCmdPublishFlag, i) + } + // raw host port + rhp := parts[0] + // raw ref + rr := strings.Join(parts[1:len(parts)-1], ":") + // raw container port + rcp := parts[len(parts)-1] + + hp, err := strconv.Atoi(rhp) + if err != nil { + return fmt.Errorf("--%s[%d] could not parse host port '%s' as integer", generateCmdPublishFlag, i, rhp) + } else if hp <= 1 { + return fmt.Errorf("--%s[%d] host port must be > 1", generateCmdPublishFlag, i) + } + + cp, err := strconv.Atoi(rcp) + if err != nil { + return fmt.Errorf("--%s[%d] could not parse container port '%s' as integer", generateCmdPublishFlag, i, rcp) + } else if cp <= 1 { + return fmt.Errorf("--%s[%d] container port must be > 1", generateCmdPublishFlag, i) + } + + if strings.Contains(rr, "#") { + parts := strings.Split(rr, ".") + if len(parts) < 2 { + return fmt.Errorf("--%s[%d] must match RES_UID.OUTPUT", generateCmdPublishFlag, i) + } + rr = strings.Join(parts[0:len(parts)-1], ".") + outputKey := parts[len(parts)-1] + + resUid := parseResourceUid(rr) + res, ok := currentState.Resources[resUid] + if !ok { + return fmt.Errorf("--%s[%d] failed to find a resource with uid '%s'", generateCmdPublishFlag, i, rr) + } + + if v, ok := res.Outputs[outputKey]; !ok { + return fmt.Errorf("--%s[%d] resource '%s' has no output '%s'", generateCmdPublishFlag, i, resUid, outputKey) + } else if sv, ok := v.(string); !ok { + return fmt.Errorf("--%s[%d] resource '%s' output '%s' is not a string", generateCmdPublishFlag, i, resUid, outputKey) + } else if config, ok := superProject.Services[sv]; !ok { + return fmt.Errorf("--%s[%d] host '%s' does not exist", generateCmdPublishFlag, i, sv) + } else { + config.Ports = append(config.Ports, types.ServicePortConfig{ + Published: strconv.Itoa(hp), + Target: uint32(cp), + }) + superProject.Services[sv] = config + slog.Info(fmt.Sprintf("Published port %d of service '%s' to host port %d", cp, sv, hp)) + } + } else if _, ok := currentState.Workloads[rr]; !ok { + return fmt.Errorf("--%s[%d] failed to find a workload named '%s'", generateCmdPublishFlag, i, rr) + } else { + for sv, config := range superProject.Services { + if config.Hostname == rr { + config.Ports = append(config.Ports, types.ServicePortConfig{ + Published: strconv.Itoa(hp), + Target: uint32(cp), + }) + superProject.Services[sv] = config + slog.Info(fmt.Sprintf("Published port %d of service '%s' to host port %d", cp, sv, hp)) + } + } + } + } + } + + sd.State = *currentState + if err := sd.Persist(); err != nil { + return fmt.Errorf("failed to persist updated state directory: %w", err) + } + + raw, _ := yaml.Marshal(superProject) + + v, _ := cmd.Flags().GetString(generateCmdOutputFlag) + if v == "" { + return fmt.Errorf("no output file specified") + } else if v == "-" { + _, _ = fmt.Fprint(cmd.OutOrStdout(), string(raw)) + } else if err := os.WriteFile(v+".temp", raw, 0644); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } else if err := os.Rename(v+".temp", v); err != nil { + return fmt.Errorf("failed to complete writing output file: %w", err) + } + + if v, _ := cmd.Flags().GetString(generateCmdEnvFileFlag); v != "" { + content := new(strings.Builder) + for k := range environmentProvisioner.Accessed() { + _, _ = content.WriteString(k) + _, _ = content.WriteRune('=') + _, _ = content.WriteRune('\n') + } + slog.Info(fmt.Sprintf("Writing env var file to '%s'", v)) + if err := os.WriteFile(v, []byte(content.String()), 0644); err != nil { + return fmt.Errorf("failed to write env var file: %w", err) + } + } + return nil + }, +} + +func parseResourceUid(raw string) framework.ResourceUid { + parts := strings.SplitN(raw, "#", 2) + firstParts := strings.SplitN(parts[0], ".", 2) + secondParts := strings.SplitN(parts[1], ".", 2) + resType := firstParts[0] + var resClass *string + if len(firstParts) > 1 { + resClass = &firstParts[1] + } + if len(secondParts) == 1 { + return framework.NewResourceUid("", "", resType, resClass, &parts[1]) + } + return framework.NewResourceUid(secondParts[0], secondParts[1], resType, resClass, nil) +} + +// loadRawScoreFiles loads raw score specs as yaml from the given files and finds all the workload names. It throws +// errors if it failed to read, load, or if names are duplicated. +func loadRawScoreFiles(fileNames []string) ([]string, map[string]map[string]interface{}, error) { + workloadNames := make([]string, 0, len(fileNames)) + workloadToRawScore := make(map[string]map[string]interface{}, len(fileNames)) + + for _, fileName := range fileNames { + var out map[string]interface{} + raw, err := os.ReadFile(fileName) + if err != nil { + return nil, nil, fmt.Errorf("failed to read '%s': %w", fileName, err) + } else if err := yaml.Unmarshal(raw, &out); err != nil { + return nil, nil, fmt.Errorf("failed to decode '%s' as yaml: %w", fileName, err) + } + + var workloadName string + if meta, ok := out["metadata"].(map[string]interface{}); ok { + workloadName, _ = meta["name"].(string) + if _, ok := workloadToRawScore[workloadName]; ok { + return nil, nil, fmt.Errorf("workload name '%s' in file '%s' is used more than once", workloadName, fileName) + } + } + workloadNames = append(workloadNames, workloadName) + workloadToRawScore[workloadName] = out + } + return workloadNames, workloadToRawScore, nil +} + +func init() { + generateCommand.Flags().StringP(generateCmdOutputFlag, "o", "compose.yaml", "The output file to write the composed compose file to") + generateCommand.Flags().String(generateCmdOverridesFileFlag, "", "An optional file of Score overrides to merge in") + generateCommand.Flags().StringArray(generateCmdOverridePropertyFlag, []string{}, "An optional set of path=key overrides to set or remove") + generateCommand.Flags().String(generateCmdImageFlag, "", "An optional container image to use for any container with image == '.'") + generateCommand.Flags().StringArray(generateCmdBuildFlag, []string{}, "An optional build context to use for the given container --build=container=./dir or --build=container={\"context\":\"./dir\"}") + generateCommand.Flags().String(generateCmdEnvFileFlag, "", "Location to store a skeleton .env file for compose - this will override existing content") + generateCommand.Flags().StringArray(generateCmdPublishFlag, []string{}, "An optional set of HOST_PORT::CONTAINER_PORT to publish on the host system.") + rootCmd.AddCommand(generateCommand) +} + +func parseAndApplyOverrideFile(entry string, flagName string, spec map[string]interface{}) error { + if raw, err := os.ReadFile(entry); err != nil { + return fmt.Errorf("--%s '%s' is invalid, failed to read file: %w", flagName, entry, err) + } else { + slog.Info(fmt.Sprintf("Applying overrides from %s to workload", entry)) + var out map[string]interface{} + if err := yaml.Unmarshal(raw, &out); err != nil { + return fmt.Errorf("--%s '%s' is invalid: failed to decode yaml: %w", flagName, entry, err) + } else if err := mergo.Merge(&spec, out, mergo.WithOverride); err != nil { + return fmt.Errorf("--%s '%s' failed to apply: %w", flagName, entry, err) + } + } + return nil +} + +func parseAndApplyOverrideProperty(entry string, flagName string, spec map[string]interface{}) (map[string]interface{}, error) { + parts := strings.SplitN(entry, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("--%s '%s' is invalid, expected a =-separated path and value", flagName, entry) + } + if parts[1] == "" { + slog.Info(fmt.Sprintf("Overriding '%s' in workload", parts[0])) + after, err := framework.OverridePathInMap(spec, framework.ParseDotPathParts(parts[0]), true, nil) + if err != nil { + return nil, fmt.Errorf("--%s '%s' could not be applied: %w", flagName, entry, err) + } + return after, nil + } else { + var value interface{} + if err := yaml.Unmarshal([]byte(parts[1]), &value); err != nil { + return nil, fmt.Errorf("--%s '%s' is invalid, failed to unmarshal value as json: %w", flagName, entry, err) + } + slog.Info(fmt.Sprintf("Overriding '%s' in workload", parts[0])) + after, err := framework.OverridePathInMap(spec, framework.ParseDotPathParts(parts[0]), false, value) + if err != nil { + return nil, fmt.Errorf("--%s '%s' could not be applied: %w", flagName, entry, err) + } + return after, nil + } +} + +// injectWaitService injects a service into the compose project which waits for all other services to be started, +// healthy, or complete depending on their definition. The workload services may then wait for this. +// This will return an empty string and false if there are no applicable services. +func injectWaitService(p *types.Project) (string, bool) { + if len(p.Services) == 0 { + return "", false + } + newService := types.ServiceConfig{ + Name: "wait-for-resources", + Image: "alpine", + Command: types.ShellCommand{"echo"}, + DependsOn: make(types.DependsOnConfig), + } + for otherServiceName, otherService := range p.Services { + condition := "service_started" + if otherService.HealthCheck != nil { + condition = "service_healthy" + } else if v := otherService.Labels["dev.score.compose.labels.is-init-container"]; v == "true" { + // annoyingly we can't tell based on the definition whether a service is designed to stop or not, + // so we'll use this label as a best effort indicator. + condition = "service_completed_successfully" + } + newService.DependsOn[otherServiceName] = types.ServiceDependency{ + Condition: condition, + Required: true, + } + } + if p.Services == nil { + p.Services = make(types.Services) + } + p.Services[newService.Name] = newService + return newService.Name, true +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go b/gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go new file mode 100644 index 00000000..e402686a --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go @@ -0,0 +1,300 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "crypto/rand" + rand2 "math/rand" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type exampleTestCase struct { + subDir string + adds []string + patchFiles []string + expected string + expectedContains string +} + +func TestExample(t *testing.T) { + for _, tc := range []exampleTestCase{ + { + subDir: "01-hello", + adds: []string{"score.yaml"}, + expected: `name: 01-hello +services: + hello-world-hello: + annotations: + compose.score.dev/workload-name: hello-world + your.custom/annotation: value + command: + - -c + - while true; do echo Hello World!; sleep 5; done + entrypoint: + - /bin/sh + hostname: hello-world + image: busybox +`, + }, + { + subDir: "02-environment", + adds: []string{"score.yaml"}, + expected: `name: 02-environment +services: + hello-world-hello: + annotations: + compose.score.dev/workload-name: hello-world + command: + - -c + - while true; do echo $${GREETING} $${NAME}!; sleep 5; done + entrypoint: + - /bin/sh + environment: + ESCAPED: $$_$${fizzbuzz} + GREETING: Hello + NAME: ${NAME} + WORKLOAD_NAME: hello-world + hostname: hello-world + image: busybox +`, + }, + { + subDir: "03-files", + adds: []string{"score.yaml"}, + expected: `name: 03-files +services: + hello-world-hello: + annotations: + compose.score.dev/workload-name: hello-world + command: + - -c + - while true; do cat /fileA.txt; cat /fileB.txt; cat /fileC.bin; sleep 5; done + entrypoint: + - /bin/sh + hostname: hello-world + image: busybox + volumes: + - type: bind + source: .score-compose/mounts/files/hello-world-files-fileA.txt + target: /fileA.txt + - type: bind + source: .score-compose/mounts/files/hello-world-files-fileB.txt + target: /fileB.txt + - type: bind + source: .score-compose/mounts/files/hello-world-files-fileC.bin + target: /fileC.bin +`, + }, + { + subDir: "04-multiple-workloads", + adds: []string{ + "score.yaml", + "score2.yaml", + }, + expected: `name: 04-multiple-workloads +services: + hello-world-2-first: + annotations: + compose.score.dev/workload-name: hello-world-2 + environment: + NGINX_PORT: "8080" + hostname: hello-world-2 + image: nginx:latest + hello-world-first: + annotations: + compose.score.dev/workload-name: hello-world + environment: + NGINX_PORT: "8080" + hostname: hello-world + image: nginx:latest + hello-world-second: + annotations: + compose.score.dev/workload-name: hello-world + environment: + NGINX_PORT: "8081" + image: nginx:latest + network_mode: service:hello-world-first +`, + }, + { + subDir: "05-volume-mounts", + adds: []string{"score.yaml"}, + expected: `name: 05-volume-mounts +services: + hello-world-first: + annotations: + compose.score.dev/workload-name: hello-world + hostname: hello-world + image: nginx:latest + volumes: + - type: volume + source: hello-world-data-8MjJEo + target: /data +volumes: + hello-world-data-8MjJEo: + name: hello-world-data-8MjJEo + driver: local + labels: + dev.score.compose.res.uid: volume.default#hello-world.data +`, + }, + { + subDir: "06-resource-provisioning", + adds: []string{"score.yaml", "score2.yaml", "--publish 6379:redis#main-cache.host:6379"}, + expectedContains: ` + ports: + - target: 6379 + published: "6379" +`, + }, + { + subDir: "07-overrides", + adds: []string{"score.yaml --override-property containers.web.variables.DEBUG=\"true\""}, + expected: `name: 07-overrides +services: + hello-world-web: + annotations: + compose.score.dev/workload-name: hello-world + environment: + DEBUG: "true" + hostname: hello-world + image: nginx +`, + }, + { + subDir: "08-service-port-resource", + adds: []string{"scoreA.yaml", "scoreB.yaml", "--publish 8080:workload-a:80"}, + expected: `name: 08-service-port-resource +services: + workload-a-example: + annotations: + compose.score.dev/workload-name: workload-a + hostname: workload-a + healthcheck: + test: + - CMD + - /usr/bin/curl + - -f + - -m + - "5" + - http://localhost + timeout: 5s + interval: 5s + image: nginx + ports: + - target: 80 + published: "8080" + workload-b-example: + annotations: + compose.score.dev/workload-name: workload-b + command: + - -c + - while true; do wget $${DEPENDENCY_URL} || true; sleep 5; done + entrypoint: + - /bin/sh + environment: + DEPENDENCY_URL: http://workload-a:80 + hostname: workload-b + image: busybox +`, + }, + { + subDir: "09-dns-and-route", + adds: []string{"score.yaml"}, + }, + { + subDir: "10-amqp-rabbitmq", + adds: []string{"score.yaml"}, + }, + { + subDir: "11-mongodb-document-database", + adds: []string{"score.yaml"}, + }, + { + subDir: "12-mysql-database", + adds: []string{"score.yaml"}, + }, + { + subDir: "13-kafka-topic", + adds: []string{"score.yaml"}, + }, + { + subDir: "14-elasticsearch", + adds: []string{"score.yaml"}, + }, + { + subDir: "15-mssql-database", + adds: []string{"score.yaml"}, + }, + { + subDir: "16-patching-templates", + adds: []string{"score.yaml"}, + patchFiles: []string{"patch-1.tpl", "patch-2.tpl", "patch-3.tpl"}, + }, + } { + t.Run(tc.subDir, func(t *testing.T) { + oldReader := rand.Reader + t.Cleanup(func() { + rand.Reader = oldReader + }) + rand.Reader = rand2.New(rand2.NewSource(0)) + + changeToDir(t, "../../examples/"+tc.subDir) + require.NoError(t, os.RemoveAll(".score-compose")) + require.NoError(t, os.RemoveAll("compose.yaml")) + + args := []string{"init", "--no-sample"} + for _, f := range tc.patchFiles { + args = append(args, "--patch-templates", f) + } + + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, args) + require.NoError(t, err) + assert.Equal(t, "", stdout) + + for _, add := range tc.adds { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, strings.Split("generate "+add, " ")) + require.NoError(t, err) + assert.Equal(t, "", stdout) + } + + if tc.expected != "" || tc.expectedContains != "" { + raw, err := os.ReadFile("compose.yaml") + require.NoError(t, err) + if tc.expected != "" { + assert.Equal(t, tc.expected, string(raw)) + } + assert.Contains(t, string(raw), tc.expectedContains) + } + + if os.Getenv("NO_DOCKER") == "" { + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + } + + }) + } +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/generate_test.go b/gen/external-content/resource-provisioners/default/score-compose/generate_test.go new file mode 100644 index 00000000..3f2ace14 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/generate_test.go @@ -0,0 +1,1633 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/score-spec/score-compose/internal/project" +) + +func TestGenerateHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `The generate command will convert Score files in the current Score compose project into a combined Docker compose +manifest. All resources and links between Workloads will be resolved and provisioned as required. + +By default this command looks for score.yaml in the current directory, but can take explicit file names as positional +arguments. + +"score-compose init" MUST be run first. An error will be thrown if the project directory is not present. + +Usage: + score-compose generate [flags] + +Examples: + + # Specify Score files + score-compose generate score.yaml *.score.yaml + + # Regenerate without adding new score files + score-compose generate + + # Provide overrides when one score file is provided + score-compose generate score.yaml --override-file=./overrides.score.yaml --override-property=metadata.key=value + + # Publish a port exposed by a workload for local testing + score-compose generate score.yaml --publish 8080:my-workload:80 + + # Publish a port from a resource host and port for local testing, the middle expression is RESOURCE_ID.OUTPUT_KEY + score-compose generate score.yaml --publish 5432:postgres#my-workload.db.host:5432 + +Flags: + --build stringArray An optional build context to use for the given container --build=container=./dir or --build=container={"context":"./dir"} + --env-file string Location to store a skeleton .env file for compose - this will override existing content + -h, --help help for generate + --image string An optional container image to use for any container with image == '.' + -o, --output string The output file to write the composed compose file to (default "compose.yaml") + --override-property stringArray An optional set of path=key overrides to set or remove + --overrides-file string An optional file of Score overrides to merge in + --publish stringArray An optional set of HOST_PORT::CONTAINER_PORT to publish on the host system. + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) + + stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "generate"}) + assert.NoError(t, err) + assert.Equal(t, stdout, stdout2) + assert.Equal(t, "", stderr) +} + +func changeToDir(t *testing.T, dir string) string { + t.Helper() + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(dir)) + t.Cleanup(func() { + require.NoError(t, os.Chdir(wd)) + }) + return dir +} + +func changeToTempDir(t *testing.T) string { + return changeToDir(t, t.TempDir()) +} + +func TestGenerateWithoutInit(t *testing.T) { + _ = changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"generate"}) + assert.EqualError(t, err, "state directory does not exist, please run \"score-compose init\" first") + assert.Equal(t, "", stdout) +} + +func TestGenerateWithoutScoreFiles(t *testing.T) { + _ = changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate"}) + assert.EqualError(t, err, "the project is empty, please provide a score file to generate from") + assert.Equal(t, "", stdout) +} + +func TestInitAndGenerateWithBadFile(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + assert.NoError(t, os.WriteFile(filepath.Join(td, "thing"), []byte(`"blah"`), 0644)) + + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "thing"}) + assert.EqualError(t, err, "failed to decode 'thing' as yaml: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `blah` into map[string]interface {}") + assert.Equal(t, "", stdout) +} + +func TestInitAndGenerateWithBadScore(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + assert.NoError(t, os.WriteFile(filepath.Join(td, "thing"), []byte(`{}`), 0644)) + + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "thing"}) + assert.EqualError(t, err, "validation errors in workload '': jsonschema: '' does not validate with https://score.dev/schemas/score#/required: missing properties: 'apiVersion', 'metadata', 'containers'") + assert.Equal(t, "", stdout) +} + +func TestInitAndGenerate_with_sample(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // write overrides file + assert.NoError(t, os.WriteFile(filepath.Join(td, "overrides.yaml"), []byte(`{"resources": {"foo": {"type": "environment"}}}`), 0644)) + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "-o", "compose-output.yaml", + "--overrides-file", "overrides.yaml", + "--override-property", "containers.hello-world.variables.THING=${resources.foo.THING}", + "--", "score.yaml", + }) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + expectedOutput := `name: "001" +services: + example-hello-world: + annotations: + compose.score.dev/workload-name: example + environment: + EXAMPLE_VARIABLE: example-value + THING: ${THING} + hostname: example + image: nginx:latest +` + assert.Equal(t, expectedOutput, string(raw)) + // generate again just for luck + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "-o", "compose-output.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err = os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + assert.Equal(t, expectedOutput, string(raw)) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Equal(t, "score.yaml", *sd.State.Workloads["example"].File) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 1) +} + +func TestInitAndGenerate_with_image_override(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // write new score file + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: . +`), 0644)) + + t.Run("generate but fail due to missing override", func(t *testing.T) { + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "-o", "compose-output.yaml", "--", "score.yaml", + }) + assert.EqualError(t, err, "failed to convert 'example' because container 'example' has no image and neither --image nor --build was provided") + }) + + t.Run("generate with image", func(t *testing.T) { + // generate with image + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "-o", "compose-output.yaml", "--image", "busybox:latest", "--", "score.yaml", + }) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + expectedOutput := `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + hostname: example + image: busybox:latest +` + assert.Equal(t, expectedOutput, string(raw)) + // generate again just for luck + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "-o", "compose-output.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err = os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + assert.Equal(t, expectedOutput, string(raw)) + }) + + t.Run("generate with raw build context", func(t *testing.T) { + // generate with build context + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "-o", "compose-output.yaml", "--build", "example=./dir", "--", "score.yaml", + }) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + expectedOutput := `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + build: + context: ./dir + hostname: example +` + assert.Equal(t, expectedOutput, string(raw)) + // generate again just for luck + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "-o", "compose-output.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err = os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + assert.Equal(t, expectedOutput, string(raw)) + }) + + t.Run("generate with json build context", func(t *testing.T) { + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "-o", "compose-output.yaml", "--build", `example={"context":"./dir","args":{"DEBUG":"true"}}`, "--", "score.yaml", + }) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + expectedOutput := `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + build: + context: ./dir + args: + DEBUG: "true" + hostname: example +` + assert.Equal(t, expectedOutput, string(raw)) + }) + + t.Run("generate with json build context and array args", func(t *testing.T) { + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "-o", "compose-output.yaml", "--build", `example={"context":"./dir","args":["DEBUG"]}`, "--", "score.yaml", + }) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + expectedOutput := `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + build: + context: ./dir + args: + DEBUG: null + hostname: example +` + assert.Equal(t, expectedOutput, string(raw)) + }) + + t.Run("generate with yaml build context", func(t *testing.T) { + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "-o", "compose-output.yaml", "--build", `example={context: "./dir"}`, "--", "score.yaml", + }) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) + assert.NoError(t, err) + expectedOutput := `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + build: + context: ./dir + hostname: example +` + assert.Equal(t, expectedOutput, string(raw)) + }) + +} + +func TestInitAndGenerate_with_files_old_files_spec(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + files: + - target: /blah.txt + source: ./original.txt +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "original.txt"), []byte(`first ${metadata.name} second`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + hostname: example + image: foo + volumes: + - type: bind + source: .score-compose/mounts/files/example-files-blah.txt + target: /blah.txt +`, string(raw)) +} + +func TestInitAndGenerate_with_files_new_files_spec(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + files: + "/blah.txt": + source: ./original.txt +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "original.txt"), []byte(`first ${metadata.name} second`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + hostname: example + image: foo + volumes: + - type: bind + source: .score-compose/mounts/files/example-files-blah.txt + target: /blah.txt +`, string(raw)) +} + +func TestGenerateRedisResource(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + variables: + CONN_STR_1: "redis://${resources.cache1.username}:${resources.cache1.password}@${resources.cache1.host}:${resources.cache1.port}" + CONN_STR_2: "redis://${resources.cache2.username}:${resources.cache2.password}@${resources.cache2.host}:${resources.cache2.port}" +resources: + cache1: + type: redis + cache2: + type: redis +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 2) + assert.Contains(t, sd.State.Resources["redis.default#example.cache1"].State, "serviceName") + assert.Contains(t, sd.State.Resources["redis.default#example.cache1"].State, "password") + assert.Contains(t, sd.State.Resources["redis.default#example.cache2"].State, "serviceName") + assert.NotEqual(t, sd.State.Resources["redis.default#example.cache1"].State, sd.State.Resources["redis.default#example.cache2"].State) + assert.Len(t, sd.State.SharedState, 0) + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestGeneratePostgresResource(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + variables: + CONN_STR_1: "postgres://${resources.db1.username}:${resources.db1.password}@${resources.db1.host}:${resources.db1.port}/${resources.db1.name}" + CONN_STR_2: "postgres://${resources.db2.username}:${resources.db2.password}@${resources.db2.host}:${resources.db2.port}/${resources.db2.name}" +resources: + db1: + type: postgres + db2: + type: postgres +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 2) + assert.Contains(t, sd.State.Resources["postgres.default#example.db1"].State, "database") + assert.Contains(t, sd.State.Resources["postgres.default#example.db1"].State, "username") + assert.Contains(t, sd.State.Resources["postgres.default#example.db1"].State, "password") + assert.Contains(t, sd.State.Resources["postgres.default#example.db2"].State, "database") + assert.NotEqual(t, sd.State.Resources["postgres.default#example.db1"].State, sd.State.Resources["postgres.default#example.db2"].State) + assert.Contains(t, sd.State.SharedState, "default-provisioners-postgres-instance") + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestGenerateS3Resource(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + variables: + output: ${resources.bucket1.endpoint} ${resources.bucket1.region} ${resources.bucket1.bucket} ${resources.bucket1.access_key_id} ${resources.bucket1.secret_key} +resources: + bucket1: + metadata: + annotations: + compose.score.dev/publish-port: "9001" + type: s3 + bucket2: + type: s3 +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 2) + assert.Contains(t, sd.State.Resources["s3.default#example.bucket1"].State, "bucket") + assert.Contains(t, sd.State.Resources["s3.default#example.bucket2"].State, "bucket") + assert.NotEqual(t, sd.State.Resources["s3.default#example.bucket1"].State, sd.State.Resources["postgres.default#example.bucket2"].State) + assert.Contains(t, sd.State.SharedState, "default-provisioners-minio-instance") + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestInitAndGenerate_with_depends_on(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + assert.NoError(t, os.WriteFile("score.yaml", []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo +resources: + thing: + type: thing +`), 0644)) + + assert.NoError(t, os.WriteFile(".score-compose/00-custom.provisioners.yaml", []byte(` +- uri: template://blah + type: thing + services: | + init_service: + image: thing + labels: + dev.score.compose.labels.is-init-container: "true" + generic_service: + image: other + service_with_healthcheck: + image: something + healthcheck: + test: ["CMD", "boo"] +`), 0644)) + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + depends_on: + wait-for-resources: + condition: service_completed_successfully + required: true + hostname: example + image: foo + generic_service: + image: other + init_service: + image: thing + labels: + dev.score.compose.labels.is-init-container: "true" + service_with_healthcheck: + healthcheck: + test: + - CMD + - boo + image: something + wait-for-resources: + command: + - echo + depends_on: + generic_service: + condition: service_started + required: true + init_service: + condition: service_completed_successfully + required: true + service_with_healthcheck: + condition: service_healthy + required: true + image: alpine +`, string(raw)) + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestInitAndGenerate_with_dependent_resources(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // write custom providers + assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` +- uri: template://foo + type: foo + outputs: | + blah: value + services: | + foo-service: + image: foo-image +- uri: template://bar + type: bar + services: | + bar-service: + image: {{ .Params.x }} +`), 0644)) + + // write custom score file + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: busybox +resources: + first: + type: foo + second: + type: bar + params: + x: ${resources.first.blah} +`), 0644)) + + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + bar-service: + image: value + example-example: + annotations: + compose.score.dev/workload-name: example + depends_on: + wait-for-resources: + condition: service_completed_successfully + required: true + hostname: example + image: busybox + foo-service: + image: foo-image + wait-for-resources: + command: + - echo + depends_on: + bar-service: + condition: service_started + required: true + foo-service: + condition: service_started + required: true + image: alpine +`, string(raw)) +} + +func TestInitAndGenerateWithNetworkServicesAcrossWorkloads(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // write custom providers + assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` +- uri: template://default-provisioners/workload-port + type: workload-port + init: | + {{ if not .Params.workload }}{{ fail "'workload' param required" }}{{ end }} + {{ if not .Params.port }}{{ fail "'port' param required - the name of the remote port" }}{{ end }} + {{ $x := index .WorkloadServices .Params.workload }} + {{ if not $x.ServiceName }}{{ fail "unknown workload" }}{{ end }} + {{ $y := index $x.Ports .Params.port }} + {{ if not $y.Name }}{{ fail "unknown port" }}{{ end }} + state: | + {{ $x := index .WorkloadServices .Params.workload }} + hostname: {{ $x.ServiceName | quote }} + {{ $y := index $x.Ports .Params.port }} + port: {{ $y.TargetPort }} +`), + 0644, + )) + + t.Run("fail unknown workload", func(t *testing.T) { + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: busybox +resources: + first: + type: workload-port + params: + workload: example-2 + port: web +`), 0644)) + + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.EqualError(t, err, "failed to provision: resource 'workload-port.default#example.first': failed to provision: init template failed: failed to execute template: template: :4:30: executing \"\" at : error calling fail: unknown workload") + }) + + t.Run("fail unknown port", func(t *testing.T) { + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: busybox +resources: + first: + type: workload-port + params: + workload: example + port: web +`), 0644)) + + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.EqualError(t, err, "failed to provision: resource 'workload-port.default#example.first': failed to provision: init template failed: failed to execute template: template: :6:23: executing \"\" at : error calling fail: unknown port") + }) + + t.Run("succeed", func(t *testing.T) { + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: busybox +service: + ports: + web: + port: 8080 + targetPort: 80 +resources: + first: + type: workload-port + params: + workload: example + port: web +`), 0644)) + + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 1) + assert.Equal(t, map[string]interface{}{ + "hostname": "example", + "port": 80, + }, sd.State.Resources["workload-port.default#example.first"].State) + }) + +} + +func TestInitAndGenerate_with_annotation_ref(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + assert.NoError(t, os.WriteFile("score.yaml", []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example + annotations: + key.com/foo-bar: thing +containers: + example: + image: foo + variables: + REF: ${metadata.annotations.key\.com/foo-bar} +`), 0644)) + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + key.com/foo-bar: thing + environment: + REF: thing + hostname: example + image: foo +`, string(raw)) + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestGenerateRouteResource(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo +service: + ports: + foo: + port: 80 + targetPort: 8080 +resources: + r1: + type: route + params: + host: localhost1 + path: /first + port: foo + r2: + type: route + params: + host: localhost1 + path: /second + port: foo + r3: + type: route + metadata: + annotations: + compose.score.dev/route-provisioner-path-type: Exact + params: + host: localhost2 + path: /third + port: 80 +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 3) + x := sd.State.SharedState["default-provisioners-routing-instance"].(map[string]interface{}) + instanceServiceName := x["instanceServiceName"].(string) + assert.Contains(t, instanceServiceName, "routing-") + delete(x, "instanceServiceName") + assert.Equal(t, map[string]interface{}{ + "default-provisioners-routing-instance": map[string]interface{}{ + "hosts": map[string]interface{}{ + "localhost1": map[string]interface{}{ + "route.default#example.r1": map[string]interface{}{"path": "/first", "port": 8080, "target": "example:8080", "path_type": "Prefix"}, + "route.default#example.r2": map[string]interface{}{"path": "/second", "port": 8080, "target": "example:8080", "path_type": "Prefix"}, + }, + "localhost2": map[string]interface{}{ + "route.default#example.r3": map[string]interface{}{"path": "/third", "port": 8080, "target": "example:8080", "path_type": "Exact"}, + }, + }, + "instancePort": 8080, + }, + }, sd.State.SharedState) + + // validate that the wildcard routes don't exist for /third + raw, err := os.ReadFile(filepath.Join(td, ".score-compose", "mounts", instanceServiceName, "nginx.conf")) + assert.NoError(t, err) + assert.Contains(t, string(raw), `location ~ ^/first$`) + assert.Contains(t, string(raw), `location ~ ^/first/.*`) + assert.Contains(t, string(raw), `location ~ ^/second$`) + assert.Contains(t, string(raw), `location ~ ^/second/.*`) + assert.Contains(t, string(raw), `location ~ ^/third$`) + assert.NotContains(t, string(raw), `location ~ ^/third/.*`) + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestEnvVarsArentRequiredInVariables(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + variables: + ONE: ${resources.env.UNKNOWN_SCORE_VARIABLE} +resources: + env: + type: environment +`), 0644)) + _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + environment: + ONE: ${UNKNOWN_SCORE_VARIABLE} + hostname: example + image: foo +`, string(raw)) +} + +func TestEnvVarsMustResolveInsideFiles_old_files_spec(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + files: + - target: /some/file + content: ${resources.env.UNKNOWN_SCORE_VARIABLE} +resources: + env: + type: environment +`), 0644)) + _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.EqualError(t, err, "failed to convert workload 'example' to Docker compose: containers.example.files[/some/file]: "+ + "failed to substitute in content: invalid ref 'resources.env.UNKNOWN_SCORE_VARIABLE': "+ + "environment variable 'UNKNOWN_SCORE_VARIABLE' must be resolved", + ) +} + +func TestEnvVarsMustResolveInsideFiles_new_files_spec(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + files: + "/some/file": + content: ${resources.env.UNKNOWN_SCORE_VARIABLE} +resources: + env: + type: environment +`), 0644)) + _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.EqualError(t, err, "failed to convert workload 'example' to Docker compose: containers.example.files[/some/file]: "+ + "failed to substitute in content: invalid ref 'resources.env.UNKNOWN_SCORE_VARIABLE': "+ + "environment variable 'UNKNOWN_SCORE_VARIABLE' must be resolved", + ) +} + +func TestEnvVarsMustResolveInsideParams(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo +resources: + env: + type: environment + data: + type: volume + params: + x: ${resources.env.UNKNOWN_SCORE_VARIABLE} +`), 0644)) + _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.EqualError(t, err, "failed to provision: failed to substitute params for resource 'volume.default#example.data': "+ + "x: invalid ref 'resources.env.UNKNOWN_SCORE_VARIABLE': "+ + "environment variable 'UNKNOWN_SCORE_VARIABLE' must be resolved", + ) +} + +func TestInitAndGenerate_with_volume_types_with_old_volumes_spec(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // write custom providers + assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` +- uri: template://docker-volume + type: volume + outputs: | + type: volume + source: named-volume +- uri: template://tmpfs-volume + type: tmp-volume + outputs: | + type: tmpfs + tmpfs: + size: 10000000 +- uri: template://bind-volume + type: bind-volume + outputs: | + type: bind + source: /dev/something + bind: + create_host_path: true +`), 0644)) + + // write custom score file + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: busybox + volumes: + - target: /mnt/v1 + source: ${resources.v1} + - target: /mnt/v2 + source: ${resources.v2} + path: thing + - target: /mnt/v3 + source: ${resources.v3} + path: other/thing +resources: + v1: + type: tmp-volume + v2: + type: bind-volume + v3: + type: volume +`), 0644)) + + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + hostname: example + image: busybox + volumes: + - type: bind + source: /dev/something/thing + target: /mnt/v2 + bind: + create_host_path: true + - type: volume + source: named-volume + target: /mnt/v3 + volume: + subpath: other/thing + - type: tmpfs + source: tmp-volume.default#example.v1 + target: /mnt/v1 + tmpfs: + size: "10000000" +`, string(raw)) +} + +func TestInitAndGenerate_with_volume_types_with_new_volumes_spec(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // write custom providers + assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` +- uri: template://docker-volume + type: volume + outputs: | + type: volume + source: named-volume +- uri: template://tmpfs-volume + type: tmp-volume + outputs: | + type: tmpfs + tmpfs: + size: 10000000 +- uri: template://bind-volume + type: bind-volume + outputs: | + type: bind + source: /dev/something + bind: + create_host_path: true +`), 0644)) + + // write custom score file + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: busybox + volumes: + "/mnt/v1": + source: ${resources.v1} + "/mnt/v2": + source: ${resources.v2} + path: thing + "/mnt/v3": + source: ${resources.v3} + path: other/thing +resources: + v1: + type: tmp-volume + v2: + type: bind-volume + v3: + type: volume +`), 0644)) + + // generate + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, `name: "001" +services: + example-example: + annotations: + compose.score.dev/workload-name: example + hostname: example + image: busybox + volumes: + - type: bind + source: /dev/something/thing + target: /mnt/v2 + bind: + create_host_path: true + - type: volume + source: named-volume + target: /mnt/v3 + volume: + subpath: other/thing + - type: tmpfs + source: tmp-volume.default#example.v1 + target: /mnt/v1 + tmpfs: + size: "10000000" +`, string(raw)) +} + +func TestGenerateMongodbResource(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + variables: + CONN_STR_1: "mongodb://${resources.db.username}:${resources.db.password}@${resources.db.host}:${resources.db.port}/" + CONN_STR_2: "${resources.db.connection}" +resources: + db: + type: mongodb +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 1) + assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "connection") + assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "username") + assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "password") + assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "host") + assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "port") + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestGenerateMySQLResource(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + example: + image: foo + variables: + CONN_STR_1: "mysql://${resources.db1.username}:${resources.db1.password}@${resources.db1.host}:${resources.db1.port}/${resources.db1.name}" + CONN_STR_2: "mysql://${resources.db2.username}:${resources.db2.password}@${resources.db2.host}:${resources.db2.port}/${resources.db2.name}" +resources: + db1: + type: mysql + db2: + type: mysql +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 2) + assert.Contains(t, sd.State.Resources["mysql.default#example.db1"].State, "database") + assert.Contains(t, sd.State.Resources["mysql.default#example.db1"].State, "username") + assert.Contains(t, sd.State.Resources["mysql.default#example.db1"].State, "password") + assert.Contains(t, sd.State.Resources["mysql.default#example.db2"].State, "database") + assert.NotEqual(t, sd.State.Resources["mysql.default#example.db1"].State, sd.State.Resources["mysql.default#example.db2"].State) + assert.Contains(t, sd.State.SharedState, "default-provisioners-mysql-instance") + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestGenerateKeepAnnotations(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example + annotations: + example.com/fizz: buzz +containers: + example: + image: foo +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Contains(t, string(raw), `example.com/fizz: buzz`) + + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + raw, err = os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Contains(t, string(raw), `example.com/fizz: buzz`) +} + +func TestGenerateElasticsearchResource(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + hello: + image: foo +resources: + ecs: + type: elasticsearch +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + // check that state was persisted + sd, ok, err := project.LoadStateDirectory(td) + assert.NoError(t, err) + assert.True(t, ok) + assert.Len(t, sd.State.Workloads, 1) + assert.Len(t, sd.State.Resources, 1) + assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "host") + assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "port") + assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "username") + assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "password") + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestGeneratePublishPorts(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + hello: + image: foo +resources: + db1: + type: postgres + db2: + type: postgres + id: thing +`), 0644)) + + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "score.yaml", + "--publish", "8080:example:80", + "--publish", "42:postgres#example.db1.host:13", + "--publish", "43:postgres.default#example.db1.host:14", + "--publish", "44:postgres.default#thing.host:15", + }) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + + t.Run("validate compose spec", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) + + for _, tc := range [][2]string{ + {"something", "--publish[0] expected 3 :-separated parts"}, + {"x:foo:y", "--publish[0] could not parse host port 'x' as integer"}, + {"8080:foo:y", "--publish[0] could not parse container port 'y' as integer"}, + {"42:thing:13", "--publish[0] failed to find a workload named 'thing'"}, + {"42:x#y:13", "--publish[0] must match RES_UID.OUTPUT"}, + {"42:x#y.z:13", "--publish[0] failed to find a resource with uid 'x#y'"}, + {"42:postgres#thing.foo:13", "--publish[0] resource 'postgres.default#thing' has no output 'foo'"}, + } { + t.Run("invalid publish "+tc[0], func(t *testing.T) { + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "score.yaml", + "--publish", tc[0], + }) + assert.EqualError(t, err, tc[1]) + assert.Equal(t, "", stdout) + }) + } +} + +func TestGenerateMultipleSpecsWithImage(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreA.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example-a +containers: + hello: + image: foo +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreB.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example-b +containers: + hello: + image: foo +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "--image", "nginx:latest", "scoreA.yaml", "scoreB.yaml", + }) + assert.EqualError(t, err, "--image cannot be used when multiple score files are provided") + assert.Equal(t, "", stdout) +} + +func TestGenerateMultipleSpecsWithBuild(t *testing.T) { + td := changeToTempDir(t) + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreA.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example-a +containers: + hello: + image: foo +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreB.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example-b +containers: + hello: + image: foo +`), 0644)) + stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ + "generate", "--build", "foo=.", "scoreA.yaml", "scoreB.yaml", + }) + assert.EqualError(t, err, "--build cannot be used when multiple score files are provided") + assert.Equal(t, "", stdout) +} + +func TestGenerateWithPatching(t *testing.T) { + td := changeToTempDir(t) + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example + custom: + privileged: true +containers: + hello: + image: foo +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "patch1.template"), []byte(` +{{ range $name, $spec := .Workloads }} + {{ if (dig "metadata" "custom" "privileged" false $spec) }} + {{ range $cname, $_ := $spec.containers }} +- op: set + path: services.{{ $name }}-{{ $cname }}.privileged + value: true + description: Enable privileged mode on service containers + {{ end }} + {{ end }} +{{ end }} +--- +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "patch2.template"), []byte(` +{{ range $name, $cfg := .Compose.services }} +- op: set + path: services.{{ $name }}-future + value: {{ toRawJson $cfg }} + description: Rename service {{ $name }} +- op: delete + path: services.{{ $name }} + description: Delete service {{ $name }} +{{ end }} +--- +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "patch3.template"), []byte(` +{{ range $name, $cfg := .Compose.services }} +- op: set + path: services.{{ $name }}.read_only + value: true + description: Set services to read only root fs +{{ end }} +--- +`), 0644)) + _, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", "patch1.template", "--patch-templates", "patch2.template", "--patch-templates", "patch3.template"}) + assert.NoError(t, err) + t.Log(stderr) + + _, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + t.Log(stderr) + + raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + assert.NoError(t, err) + assert.Equal(t, string(raw), `name: "001" +services: + example-hello-future: + annotations: + compose.score.dev/workload-name: example + hostname: example + image: foo + privileged: true + read_only: true +`) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/init.go b/gen/external-content/resource-provisioners/default/score-compose/init.go new file mode 100644 index 00000000..d1f15a06 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/init.go @@ -0,0 +1,316 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + _ "embed" + "errors" + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" + + "github.com/score-spec/score-go/framework" + "github.com/score-spec/score-go/uriget" + "github.com/spf13/cobra" + + "github.com/score-spec/score-compose/internal/patching" + "github.com/score-spec/score-compose/internal/project" + "github.com/score-spec/score-compose/internal/provisioners/loader" +) + +const ( + DefaultScoreFileContent = `# Score provides a developer-centric and platform-agnostic +# Workload specification to improve developer productivity and experience. +# Score eliminates configuration management between local and remote environments. +# +# Specification reference: https://docs.score.dev/docs/reference/score-spec-reference/ +--- + +# Score specification version +apiVersion: score.dev/v1b1 + +metadata: + name: example + +containers: + hello-world: + image: nginx:latest + + # Uncomment the following for a custom entrypoint command + # command: [] + + # Uncomment the following for custom arguments + # args: [] + + # Environment variables to inject into the container + variables: + EXAMPLE_VARIABLE: "example-value" + +service: + ports: + # Expose the http port from nginx on port 8080 + www: + port: 8080 + targetPort: 80 + +resources: {} +` + initCmdFileFlag = "file" + initCmdFileProjectFlag = "project" + initCmdFileNoSampleFlag = "no-sample" + initCmdProvisionerFlag = "provisioners" + initCmdPatchTemplateFlag = "patch-templates" + initCmdNoDefaultProvisionersFlag = "no-default-provisioners" +) + +//go:embed default.provisioners.yaml +var defaultProvisionersContent string + +var initCmd = &cobra.Command{ + Use: "init", + Args: cobra.NoArgs, + Short: "Initialise a new score-compose project with local state directory and score file", + Long: `The init subcommand will prepare the current directory for working with score-compose and prepare any local +files or configuration needed to be successful. + +A directory named .score-compose will be created if it doesn't exist. This file stores local state and generally should +not be checked into source control. Add it to your .gitignore file if you use Git as version control. + +The project name will be used as a Docker compose project name when the final compose files are written. This name +acts as a namespace when multiple score files and containers are used. + +Custom provisioners can be installed by uri using the --provisioners flag. The provisioners will be installed and take +precedence in the order they are defined over the default provisioners. If init has already been called with provisioners +the new provisioners will take precedence. + +To adjust the way the compose project is generated, or perform post processing actions, you can use the --patch-templates +flag to provide one or more template files by uri. Each template file is stored in the project and then evaluated as a +Golang text/template and should output a yaml/json encoded array of patches. Each patch is an object with required 'op' +(set or delete), 'patch' (a dot-separated json path), a 'value' if the 'op' == 'set', and an optional 'description' for +showing in the logs. The template has access to '.Compose' and '.Workloads'. +`, + Example: ` + # Define a score file to generate + score-compose init --file score2.yaml + + # Or override the docker compose project name + score-compose init --project score-compose2 + + # Or disable the default score file generation if you already have a score file + score-compose init --no-sample + + # Optionally loading in provisoners from a remote url + score-compose init --provisioners https://raw.githubusercontent.com/user/repo/main/example.yaml + + # Optionally adding a couple of patching templates + score-compose init --patch-templates ./patching.tpl --patch-templates https://raw.githubusercontent.com/user/repo/main/example.tpl + +URI Retrieval: + The --provisioners and --patch-templates arguments support URI retrieval for pulling the contents from a URI on disk + or over the network. These support: + - HTTP : http://host/file + - HTTPS : https://host/file + - Git (SSH) : git-ssh://git@host/repo.git/file + - Git (HTTPS) : git-https://host/repo.git/file + - OCI : oci://[registry/][namespace/]repository[:tag|@digest][#file] + - Local File : /path/to/local/file + - Stdin : - (read from standard input)`, + + // don't print the errors - we print these ourselves in main() + SilenceErrors: true, + + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + + // load flag values + initCmdScoreFile, _ := cmd.Flags().GetString(initCmdFileFlag) + initCmdComposeProject, _ := cmd.Flags().GetString(initCmdFileProjectFlag) + initCmdPatchingFiles, _ := cmd.Flags().GetStringArray(initCmdPatchTemplateFlag) + + // validate project + if initCmdComposeProject != "" { + cleanedInitCmdComposeProject := cleanComposeProjectName(initCmdComposeProject) + if cleanedInitCmdComposeProject != initCmdComposeProject { + return fmt.Errorf("invalid value for --project, it must match ^[a-z0-9][a-z0-9_-]*$") + } + } + + var templates []string + for _, u := range initCmdPatchingFiles { + slog.Info(fmt.Sprintf("Fetching patch template from %s", u)) + content, err := uriget.GetFile(cmd.Context(), u) + if err != nil { + return fmt.Errorf("error fetching patch template from %s: %w", u, err) + } else if err = patching.ValidatePatchTemplate(string(content)); err != nil { + return fmt.Errorf("error parsing patch template from %s: %w", u, err) + } + templates = append(templates, string(content)) + } + + sd, ok, err := project.LoadStateDirectory(".") + if err != nil { + return fmt.Errorf("failed to load existing state directory: %w", err) + } else if ok { + slog.Info(fmt.Sprintf("Found existing state directory '%s'", sd.Path)) + var hasChanges bool + if initCmdComposeProject != "" && sd.State.Extras.ComposeProjectName != initCmdComposeProject { + sd.State.Extras.ComposeProjectName = initCmdComposeProject + hasChanges = true + } + if len(templates) > 0 { + sd.State.Extras.PatchingTemplates = templates + hasChanges = true + } + if hasChanges { + if err := sd.Persist(); err != nil { + return fmt.Errorf("failed to persist new state file: %w", err) + } + } + + } else { + + slog.Info(fmt.Sprintf("Writing new state directory '%s'", project.DefaultRelativeStateDirectory)) + wd, _ := os.Getwd() + sd = &project.StateDirectory{ + Path: project.DefaultRelativeStateDirectory, + State: project.State{ + Workloads: map[string]framework.ScoreWorkloadState[project.WorkloadExtras]{}, + Resources: map[framework.ResourceUid]framework.ScoreResourceState[framework.NoExtras]{}, + SharedState: map[string]interface{}{}, + Extras: project.StateExtras{ + ComposeProjectName: cleanComposeProjectName(filepath.Base(wd)), + MountsDirectory: filepath.Join(project.DefaultRelativeStateDirectory, project.MountsDirectoryName), + }, + }, + } + if initCmdComposeProject != "" { + sd.State.Extras.ComposeProjectName = initCmdComposeProject + } + if len(templates) > 0 { + sd.State.Extras.PatchingTemplates = templates + } + slog.Info(fmt.Sprintf("Writing new state directory '%s' with project name '%s'", sd.Path, sd.State.Extras.ComposeProjectName)) + if err := sd.Persist(); err != nil { + return fmt.Errorf("failed to persist new compose project name: %w", err) + } + + // create and write the default provisioners file if it doesn't already exist + disableDefaultProvisioners, err := cmd.Flags().GetBool(initCmdNoDefaultProvisionersFlag) + if err != nil { + return fmt.Errorf("failed to parse --%s flag: %w", initCmdNoDefaultProvisionersFlag, err) + } + + if !disableDefaultProvisioners { + defaultProvisioners := filepath.Join(sd.Path, "zz-default.provisioners.yaml") + + if _, err := os.Stat(defaultProvisioners); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to check for existing default provisioners file: %w", err) + } + + f, err := os.OpenFile(defaultProvisioners, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + return fmt.Errorf("failed to create default provisioners file: %w", err) + } + defer f.Close() + + slog.Info("Writing default provisioners file", "path", defaultProvisioners) + if _, err := f.WriteString(defaultProvisionersContent); err != nil { + return fmt.Errorf("failed to write default provisioners content: %w", err) + } + } else { + slog.Info("Default provisioners file already exists, skipping", "path", defaultProvisioners) + } + } else { + slog.Info("Skipping default provisioners due to --no-default-provisioners flag") + } + } + + if _, err := os.ReadFile(initCmdScoreFile); err != nil { + if v, _ := cmd.Flags().GetBool(initCmdFileNoSampleFlag); v { + slog.Info(fmt.Sprintf("Initial Score file '%s' does not exist - and sample generation is disabled", initCmdScoreFile)) + } else { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to check existing Score file: %w", err) + } + slog.Info(fmt.Sprintf("Initial Score file '%s' does not exist - creating it", initCmdScoreFile)) + if err := os.WriteFile(initCmdScoreFile+".temp", []byte(DefaultScoreFileContent), 0755); err != nil { + return fmt.Errorf("failed to write initial score file: %w", err) + } else if err := os.Rename(initCmdScoreFile+".temp", initCmdScoreFile); err != nil { + return fmt.Errorf("failed to complete writing initial Score file: %w", err) + } + } + } else { + slog.Info(fmt.Sprintf("Found existing Score file '%s'", initCmdScoreFile)) + } + + if v, _ := cmd.Flags().GetStringArray(initCmdProvisionerFlag); len(v) > 0 { + for i, vi := range v { + data, err := uriget.GetFile(cmd.Context(), vi) + if err != nil { + return fmt.Errorf("failed to load provisioner %d: %w", i+1, err) + } + + var saveFilename string + if vi == "-" { + saveFilename = "from-stdin.provisioners.yaml" + } else { + saveFilename = vi + } + + if err := loader.SaveProvisionerToDirectory(sd.Path, saveFilename, data); err != nil { + return fmt.Errorf("failed to save provisioner %d: %w", i+1, err) + } + } + } + + if provs, err := loader.LoadProvisionersFromDirectory(sd.Path, loader.DefaultSuffix); err != nil { + return fmt.Errorf("failed to load existing provisioners: %w", err) + } else { + slog.Debug(fmt.Sprintf("Successfully loaded %d resource provisioners", len(provs))) + } + + slog.Info("Read more about the Score specification at https://docs.score.dev/docs/") + + return nil + }, +} + +func init() { + initCmd.Flags().StringP(initCmdFileFlag, "f", scoreFileDefault, "The score file to initialize") + initCmd.Flags().StringP(initCmdFileProjectFlag, "p", "", "Set the name of the docker compose project (defaults to the current directory name)") + initCmd.Flags().Bool(initCmdFileNoSampleFlag, false, "Disable generation of the sample score file") + initCmd.Flags().StringArray(initCmdProvisionerFlag, nil, "Provisioner files to install. May be specified multiple times. Supports URI retrieval.") + initCmd.Flags().StringArray(initCmdPatchTemplateFlag, nil, "Patching template files to include. May be specified multiple times. Supports URI retrieval.") + initCmd.Flags().Bool(initCmdNoDefaultProvisionersFlag, false, "Disable generation of the default provisioners file") + + rootCmd.AddCommand(initCmd) +} + +func cleanComposeProjectName(input string) string { + input = strings.ToLower(input) + isFirst := true + input = strings.Map(func(r rune) rune { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || (!isFirst && ((r == '_') || (r == '-'))) { + isFirst = false + return r + } + isFirst = false + return -1 + }, input) + return input +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/init_test.go b/gen/external-content/resource-provisioners/default/score-compose/init_test.go new file mode 100644 index 00000000..0986b095 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/init_test.go @@ -0,0 +1,331 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/score-spec/score-go/framework" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/score-spec/score-compose/internal/project" + "github.com/score-spec/score-compose/internal/provisioners" + "github.com/score-spec/score-compose/internal/provisioners/loader" +) + +func TestInitHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `The init subcommand will prepare the current directory for working with score-compose and prepare any local +files or configuration needed to be successful. + +A directory named .score-compose will be created if it doesn't exist. This file stores local state and generally should +not be checked into source control. Add it to your .gitignore file if you use Git as version control. + +The project name will be used as a Docker compose project name when the final compose files are written. This name +acts as a namespace when multiple score files and containers are used. + +Custom provisioners can be installed by uri using the --provisioners flag. The provisioners will be installed and take +precedence in the order they are defined over the default provisioners. If init has already been called with provisioners +the new provisioners will take precedence. + +To adjust the way the compose project is generated, or perform post processing actions, you can use the --patch-templates +flag to provide one or more template files by uri. Each template file is stored in the project and then evaluated as a +Golang text/template and should output a yaml/json encoded array of patches. Each patch is an object with required 'op' +(set or delete), 'patch' (a dot-separated json path), a 'value' if the 'op' == 'set', and an optional 'description' for +showing in the logs. The template has access to '.Compose' and '.Workloads'. + +Usage: + score-compose init [flags] + +Examples: + + # Define a score file to generate + score-compose init --file score2.yaml + + # Or override the docker compose project name + score-compose init --project score-compose2 + + # Or disable the default score file generation if you already have a score file + score-compose init --no-sample + + # Optionally loading in provisoners from a remote url + score-compose init --provisioners https://raw.githubusercontent.com/user/repo/main/example.yaml + + # Optionally adding a couple of patching templates + score-compose init --patch-templates ./patching.tpl --patch-templates https://raw.githubusercontent.com/user/repo/main/example.tpl + +URI Retrieval: + The --provisioners and --patch-templates arguments support URI retrieval for pulling the contents from a URI on disk + or over the network. These support: + - HTTP : http://host/file + - HTTPS : https://host/file + - Git (SSH) : git-ssh://git@host/repo.git/file + - Git (HTTPS) : git-https://host/repo.git/file + - OCI : oci://[registry/][namespace/]repository[:tag|@digest][#file] + - Local File : /path/to/local/file + - Stdin : - (read from standard input) + +Flags: + -f, --file string The score file to initialize (default "./score.yaml") + -h, --help help for init + --no-default-provisioners Disable generation of the default provisioners file + --no-sample Disable generation of the sample score file + --patch-templates stringArray Patching template files to include. May be specified multiple times. Supports URI retrieval. + -p, --project string Set the name of the docker compose project (defaults to the current directory name) + --provisioners stringArray Provisioner files to install. May be specified multiple times. Supports URI retrieval. + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) + + stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "init"}) + assert.NoError(t, err) + assert.Equal(t, stdout, stdout2) + assert.Equal(t, "", stderr) +} + +func TestInitNominal(t *testing.T) { + td := t.TempDir() + + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(td)) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + + stdout, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"run"}) + assert.NoError(t, err) + assert.Equal(t, `services: + example-hello-world: + annotations: + compose.score.dev/workload-name: example + environment: + EXAMPLE_VARIABLE: example-value + hostname: example + image: nginx:latest + ports: + - target: 80 + published: "8080" +`, stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + + sd, ok, err := project.LoadStateDirectory(".") + assert.NoError(t, err) + if assert.True(t, ok) { + assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path) + assert.Equal(t, filepath.Base(td), sd.State.Extras.ComposeProjectName) + assert.Equal(t, filepath.Join(project.DefaultRelativeStateDirectory, "mounts"), sd.State.Extras.MountsDirectory) + assert.Equal(t, map[string]framework.ScoreWorkloadState[project.WorkloadExtras]{}, sd.State.Workloads) + assert.Equal(t, map[framework.ResourceUid]framework.ScoreResourceState[framework.NoExtras]{}, sd.State.Resources) + assert.Equal(t, map[string]interface{}{}, sd.State.SharedState) + } +} + +func TestInitNoSample(t *testing.T) { + td := t.TempDir() + + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(td)) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--no-sample"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + + _, err = os.Stat("score.yaml") + assert.ErrorIs(t, err, os.ErrNotExist) +} + +func TestInitNominal_custom_file_and_project(t *testing.T) { + td := t.TempDir() + + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(td)) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--file", "score2.yaml", "--project", "bananas"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + + _, err = os.Stat("score.yaml") + assert.ErrorIs(t, err, os.ErrNotExist) + _, err = os.Stat("score2.yaml") + assert.NoError(t, err) + + sd, ok, err := project.LoadStateDirectory(".") + assert.NoError(t, err) + if assert.True(t, ok) { + assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path) + assert.Equal(t, "bananas", sd.State.Extras.ComposeProjectName) + } +} + +func TestInitNominal_bad_project(t *testing.T) { + td := t.TempDir() + + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(td)) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--project", "-this-is-invalid-"}) + assert.EqualError(t, err, "invalid value for --project, it must match ^[a-z0-9][a-z0-9_-]*$") + assert.Equal(t, "", stdout) + assert.Equal(t, "", stderr) +} + +func TestInitNominal_run_twice(t *testing.T) { + td := t.TempDir() + + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(td)) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + + // first init + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--file", "score2.yaml", "--project", "bananas"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + + // check default provisioners exists and overwrite it with an empty array + dpf, err := os.Stat(filepath.Join(td, ".score-compose", "zz-default.provisioners.yaml")) + assert.NoError(t, err) + assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", dpf.Name()), []byte("[]"), 0644)) + + // init again + stdout, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + + // verify that default provisioners was not overwritten again + dpf, err = os.Stat(filepath.Join(td, ".score-compose", dpf.Name())) + assert.NoError(t, err) + assert.Equal(t, 2, int(dpf.Size())) + + _, err = os.Stat("score.yaml") + assert.NoError(t, err) + _, err = os.Stat("score2.yaml") + assert.NoError(t, err) + + sd, ok, err := project.LoadStateDirectory(".") + assert.NoError(t, err) + if assert.True(t, ok) { + assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path) + assert.Equal(t, "bananas", sd.State.Extras.ComposeProjectName) + assert.Equal(t, filepath.Join(project.DefaultRelativeStateDirectory, "mounts"), sd.State.Extras.MountsDirectory) + assert.Equal(t, map[string]framework.ScoreWorkloadState[project.WorkloadExtras]{}, sd.State.Workloads) + assert.Equal(t, map[framework.ResourceUid]framework.ScoreResourceState[framework.NoExtras]{}, sd.State.Resources) + assert.Equal(t, map[string]interface{}{}, sd.State.SharedState) + } +} + +func TestInitWithProvisioners(t *testing.T) { + td := t.TempDir() + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(td)) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + + td2 := t.TempDir() + assert.NoError(t, os.WriteFile(filepath.Join(td2, "one.provisioners.yaml"), []byte(` +- uri: template://one + type: thing + outputs: "{}" +`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td2, "two.provisioners.yaml"), []byte(` +- uri: template://two + type: thing + outputs: "{}" +`), 0644)) + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--provisioners", filepath.Join(td2, "one.provisioners.yaml"), "--provisioners", "file://" + filepath.Join(td2, "two.provisioners.yaml")}) + assert.NoError(t, err) + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + + provs, err := loader.LoadProvisionersFromDirectory(filepath.Join(td, ".score-compose"), loader.DefaultSuffix) + assert.NoError(t, err) + expectedProvisionerUris := []string{"template://one", "template://two"} + for _, expectedUri := range expectedProvisionerUris { + assert.True(t, slices.ContainsFunc(provs, func(p provisioners.Provisioner) bool { + return p.Uri() == expectedUri + }), fmt.Sprintf("Expected provisioner '%s' not found", expectedUri)) + } +} + +func TestInitWithPatchingFiles(t *testing.T) { + td := t.TempDir() + wd, _ := os.Getwd() + require.NoError(t, os.Chdir(td)) + defer func() { + require.NoError(t, os.Chdir(wd)) + }() + assert.NoError(t, os.WriteFile(filepath.Join(td, "patch-templates-1"), []byte(`[]`), 0644)) + assert.NoError(t, os.WriteFile(filepath.Join(td, "patch-templates-2"), []byte(`[]`), 0644)) + + t.Run("new", func(t *testing.T) { + _, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", filepath.Join(td, "patch-templates-1"), "--patch-templates", filepath.Join(td, "patch-templates-2")}) + assert.NoError(t, err) + t.Log(stderr) + sd, ok, err := project.LoadStateDirectory(".") + assert.NoError(t, err) + if assert.True(t, ok) { + assert.Len(t, sd.State.Extras.PatchingTemplates, 2) + } + }) + + t.Run("update", func(t *testing.T) { + _, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", filepath.Join(td, "patch-templates-2")}) + assert.NoError(t, err) + t.Log(stderr) + sd, ok, err := project.LoadStateDirectory(".") + assert.NoError(t, err) + if assert.True(t, ok) { + assert.Len(t, sd.State.Extras.PatchingTemplates, 1) + } + }) + + t.Run("bad patch", func(t *testing.T) { + assert.NoError(t, os.WriteFile(filepath.Join(td, "patch-templates-3"), []byte(`{{ what is this }}`), 0644)) + _, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", filepath.Join(td, "patch-templates-3")}) + assert.Error(t, err, "failed to parse template: template: :1: function \"what\" not defined") + }) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/provisioners.go b/gen/external-content/resource-provisioners/default/score-compose/provisioners.go new file mode 100644 index 00000000..7ea5ca11 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/provisioners.go @@ -0,0 +1,124 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "fmt" + "log/slog" + "os" + "sort" + "strings" + + "github.com/spf13/cobra" + + "github.com/score-spec/score-compose/internal/project" + "github.com/score-spec/score-compose/internal/provisioners" + "github.com/score-spec/score-compose/internal/provisioners/loader" + "github.com/score-spec/score-go/formatter" +) + +var ( + provisionersGroup = &cobra.Command{ + Use: "provisioners", + Short: "Subcommands related to provisioners", + } + provisionersList = &cobra.Command{ + Use: "list [--format table|json]", + Short: "List the provisioners", + Long: `The list command will list out the provisioners. This requires an active score compose state +after 'init' or 'generate' has been run. The list of provisioners will be empty if no provisioners are defined. +`, + Args: cobra.ArbitraryArgs, + SilenceErrors: true, + RunE: listProvisioners, + } +) + +func listProvisioners(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + wd, _ := os.Getwd() + sd, ok, err := project.LoadStateDirectory(wd) + if err != nil { + return fmt.Errorf("failed to load existing state directory: %w", err) + } else if !ok { + return fmt.Errorf("no state directory found, run 'score-compose init' first") + } + slog.Debug(fmt.Sprintf("Listing provisioners in project '%s'", sd.State.Extras.ComposeProjectName)) + + provisioners, err := loader.LoadProvisionersFromDirectory(sd.Path, loader.DefaultSuffix) + if err != nil { + return fmt.Errorf("failed to load provisioners in %s: %w", sd.Path, err) + } + + if len(provisioners) == 0 { + slog.Info("No provisioners found") + return nil + } + + outputFormat := cmd.Flag("format").Value.String() + return displayProvisioners(provisioners, outputFormat) +} + +func displayProvisioners(loadedProvisioners []provisioners.Provisioner, outputFormat string) error { + var outputFormatter formatter.OutputFormatter + sortedProvisioners := sortProvisionersByType(loadedProvisioners) + + switch outputFormat { + case "json": + type jsonData struct { + Type string + Class string + Params []string + Outputs []string + Description string + } + var outputs []jsonData + for _, provisioner := range sortedProvisioners { + outputs = append(outputs, jsonData{ + Type: provisioner.Type(), + Class: provisioner.Class(), + Params: provisioner.Params(), + Outputs: provisioner.Outputs(), + Description: provisioner.Description(), + }) + } + outputFormatter = &formatter.JSONOutputFormatter[[]jsonData]{Data: outputs} + default: + rows := [][]string{} + + for _, provisioner := range sortedProvisioners { + rows = append(rows, []string{provisioner.Type(), provisioner.Class(), strings.Join(provisioner.Params(), ", "), strings.Join(provisioner.Outputs(), ", "), provisioner.Description()}) + } + headers := []string{"Type", "Class", "Params", "Outputs", "Description"} + outputFormatter = &formatter.TableOutputFormatter{ + Headers: headers, + Rows: rows, + } + } + return outputFormatter.Display() +} + +func sortProvisionersByType(provisioners []provisioners.Provisioner) []provisioners.Provisioner { + sort.Slice(provisioners, func(i, j int) bool { + return provisioners[i].Type() < provisioners[j].Type() + }) + return provisioners +} + +func init() { + provisionersList.Flags().StringP("format", "f", "table", "Format of the output: table (default), json") + provisionersGroup.AddCommand(provisionersList) + rootCmd.AddCommand(provisionersGroup) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go b/gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go new file mode 100644 index 00000000..d9227d32 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go @@ -0,0 +1,117 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "flag" + "fmt" + "io" + "os" + "path/filepath" + "testing" + + "github.com/score-spec/score-compose/internal/provisioners/loader" + "github.com/stretchr/testify/assert" +) + +var ( + update = flag.Bool("update", false, "update the golden files of this test") +) + +func TestDisplayProvisioners(t *testing.T) { + tests := []struct { + name string + fixture string + format string + expectedResponse string + expectedError string + }{ + { + name: "display provisioners in table format", + fixture: "provisioners.custom.golden", + format: "table", + expectedResponse: "provisioners.list.valid.table.golden", + expectedError: "", + }, + { + name: "display provisioners in json format", + fixture: "provisioners.custom.golden", + format: "json", + expectedResponse: "provisioners.list.valid.json.golden", + expectedError: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + provisioners, err := loader.LoadProvisionersFromDirectory("fixtures/", test.fixture) + if err != nil { + t.Fatal(err) + } + + // Capture the output + old := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + displayProvisioners(provisioners, test.format) + + w.Close() + os.Stdout = old + out, _ := io.ReadAll(r) + + got := string(out) + + expected, err := goldenValue(t, "testdata", test.expectedResponse, got, *update) + if err != nil { + t.Fatal(err) + } + + if test.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), test.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, expected, got) + } + }) + } +} + +func goldenValue(t *testing.T, path string, goldenFile string, actual string, update bool) (string, error) { + t.Helper() + goldenPath := filepath.Join(path, goldenFile) + + f, err := os.OpenFile(goldenPath, os.O_RDWR, 0644) + if err != nil { + return "", fmt.Errorf("failed to open file: %w", err) + } + defer f.Close() + + if update { + _, err := f.WriteString(actual) + if err != nil { + return "", fmt.Errorf("failed to update golden file %s: %w", goldenPath, err) + } + + return actual, nil + } + + content, err := io.ReadAll(f) + if err != nil { + return "", fmt.Errorf("failed to read golden file %s: %w", goldenPath, err) + } + + return string(content), nil +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/resources.go b/gen/external-content/resource-provisioners/default/score-compose/resources.go new file mode 100644 index 00000000..2e927375 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/resources.go @@ -0,0 +1,198 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "fmt" + "log/slog" + "slices" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/score-spec/score-go/formatter" + "github.com/score-spec/score-go/framework" + "github.com/spf13/cobra" + + "github.com/score-spec/score-compose/internal/project" +) + +const ( + getOutputsCmdFormatFlag = "format" +) + +var ( + resourcesGroup = &cobra.Command{ + Use: "resources", + Short: "Subcommands related to provisioned resources", + } + listResources = &cobra.Command{ + Use: "list", + Short: "List the resource uids", + Long: `The list command will list out the provisioned resource uids. This requires an active score compose state +after 'init' or 'generate' has been run. The list of uids will be empty if no resources are provisioned. +`, + Args: cobra.ExactArgs(0), + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + sd, ok, err := project.LoadStateDirectory(".") + if err != nil { + return fmt.Errorf("failed to load existing state directory: %w", err) + } else if !ok { + return fmt.Errorf("state directory does not exist, please run \"score-compose init\" first") + } + slog.Debug(fmt.Sprintf("Loaded state directory with docker compose project '%s'", sd.State.Extras.ComposeProjectName)) + currentState := &sd.State + resIds, err := currentState.GetSortedResourceUids() + if err != nil { + return fmt.Errorf("failed to sort resources: %w", err) + } + return displayResourcesList(resIds, *currentState, cmd) + }, + } + getResourceOutputs = &cobra.Command{ + Use: "get-outputs TYPE.CLASS#ID", + Short: "Return the resource outputs", + Long: `The get-outputs command will print the outputs of the resource from the last provisioning. The data will +be returned as json. +`, + Args: cobra.ExactArgs(1), + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + sd, ok, err := project.LoadStateDirectory(".") + if err != nil { + return fmt.Errorf("failed to load existing state directory: %w", err) + } else if !ok { + return fmt.Errorf("state directory does not exist, please run \"score-compose init\" first") + } + slog.Debug(fmt.Sprintf("Loaded state directory with docker compose project '%s'", sd.State.Extras.ComposeProjectName)) + if res, ok := sd.State.Resources[framework.ResourceUid(args[0])]; ok { + outputs := res.Outputs + if outputs == nil { + outputs = make(map[string]interface{}) + } + return displayResourcesOutputs(outputs, cmd) + } + resourceOuptuts, err := getResourceOutputsByUid(framework.ResourceUid(args[0]), &sd.State) + if err != nil { + return fmt.Errorf("no such resource '%s'", args[0]) + } + return displayResourcesOutputs(resourceOuptuts, cmd) + }, + } +) + +func getResourceOutputsByUid(uid framework.ResourceUid, state *project.State) (map[string]interface{}, error) { + if res, ok := state.Resources[uid]; ok { + outputs := res.Outputs + if outputs == nil { + outputs = make(map[string]interface{}) + } + return outputs, nil + } + return nil, fmt.Errorf("no such resource '%s'", uid) +} + +func getResourceOutputsKeys(uid framework.ResourceUid, state *project.State) ([]string, error) { + outputs, err := getResourceOutputsByUid(uid, state) + if err != nil { + return nil, err + } + keys := make([]string, 0, len(outputs)) + for key, _ := range outputs { + keys = append(keys, key) + } + slices.Sort(keys) + return keys, nil +} + +func displayResourcesOutputs(outputs map[string]interface{}, cmd *cobra.Command) error { + outputFormat := cmd.Flags().Lookup(getOutputsCmdFormatFlag).Value.String() + var outputFormatter formatter.OutputFormatter + switch outputFormat { + case "json": + outputFormatter = &formatter.JSONOutputFormatter[map[string]interface{}]{Data: outputs, Out: cmd.OutOrStdout()} + case "yaml": + outputFormatter = &formatter.YAMLOutputFormatter[map[string]interface{}]{Data: outputs, Out: cmd.OutOrStdout()} + default: + // ensure there is a new line at the end if one is not already present + if !strings.HasSuffix(outputFormat, "\n") { + outputFormat += "\n" + } + prepared, err := template.New("").Funcs(sprig.FuncMap()).Parse(outputFormat) + if err != nil { + return fmt.Errorf("failed to parse format template: %w", err) + } + if err := prepared.Execute(cmd.OutOrStdout(), outputs); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + return nil + } + + return outputFormatter.Display() +} + +func displayResourcesList(resources []framework.ResourceUid, state project.State, cmd *cobra.Command) error { + outputFormat := cmd.Flag("format").Value.String() + var outputFormatter formatter.OutputFormatter + + switch outputFormat { + case "json": + type jsonData struct { + UID string + Outputs []string + } + var outputs []jsonData + for _, resource := range resources { + + keys, err := getResourceOutputsKeys(resource, &state) + if err != nil { + return fmt.Errorf("failed to get outputs for resource '%s': %w", resource, err) + } + outputs = append(outputs, jsonData{ + UID: string(resource), + Outputs: keys, + }) + } + outputFormatter = &formatter.JSONOutputFormatter[[]jsonData]{Data: outputs, Out: cmd.OutOrStdout()} + default: + var rows [][]string + for _, resource := range resources { + keys, err := getResourceOutputsKeys(resource, &state) + if err != nil { + return fmt.Errorf("failed to get outputs for resource '%s': %w", resource, err) + } + row := []string{string(resource), strings.Join(keys, ", ")} + rows = append(rows, row) + } + outputFormatter = &formatter.TableOutputFormatter{ + Headers: []string{"UID", "Outputs"}, + Rows: rows, + Out: cmd.OutOrStdout(), + } + } + + return outputFormatter.Display() +} + +func init() { + getResourceOutputs.Flags().StringP(getOutputsCmdFormatFlag, "f", "json", "Format of the output: json, yaml, or a Go template with sprig functions") + resourcesGroup.AddCommand(listResources) + listResources.Flags().StringP("format", "f", "table", "Format of the output: table (default), json") + resourcesGroup.AddCommand(getResourceOutputs) + rootCmd.AddCommand(resourcesGroup) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/resources_test.go b/gen/external-content/resource-provisioners/default/score-compose/resources_test.go new file mode 100644 index 00000000..6ae55210 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/resources_test.go @@ -0,0 +1,163 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestResourcesHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `Subcommands related to provisioned resources + +Usage: + score-compose resources [command] + +Available Commands: + get-outputs Return the resource outputs + list List the resource uids + +Flags: + -h, --help help for resources + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times + +Use "score-compose resources [command] --help" for more information about a command. +`, stdout) + assert.Equal(t, "", stderr) + + stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "resources"}) + assert.NoError(t, err) + assert.Equal(t, stdout, stdout2) + assert.Equal(t, "", stderr) +} + +func TestResourcesExample(t *testing.T) { + td := changeToTempDir(t) + _, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--no-sample"}) + assert.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example +containers: + container: + image: thing +resources: + vol: + type: volume + dns: + type: dns +`), 0600)) + _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) + assert.NoError(t, err) + + t.Run("list table", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "list"}) + assert.NoError(t, err) + assert.Equal(t, `+----------------------------+--------------+ +| UID | OUTPUTS | ++----------------------------+--------------+ +| dns.default#example.dns | host | ++----------------------------+--------------+ +| volume.default#example.vol | source, type | ++----------------------------+--------------+ +`, stdout) + }) + + t.Run("list json", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "list", "-f", "json"}) + assert.NoError(t, err) + assert.Equal(t, `[ + { + "UID": "dns.default#example.dns", + "Outputs": [ + "host" + ] + }, + { + "UID": "volume.default#example.vol", + "Outputs": [ + "source", + "type" + ] + } +] +`, stdout) + }) + + t.Run("get not found", func(t *testing.T) { + _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "foo"}) + assert.EqualError(t, err, "no such resource 'foo'") + }) + + t.Run("get dns", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "dns.default#example.dns"}) + assert.NoError(t, err) + var out map[string]interface{} + if assert.NoError(t, json.Unmarshal([]byte(stdout), &out)) { + assert.NotEmpty(t, out["host"].(string)) + } + }) + + t.Run("get vol", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol"}) + assert.NoError(t, err) + var out map[string]interface{} + if assert.NoError(t, json.Unmarshal([]byte(stdout), &out)) { + assert.NotEmpty(t, out["source"].(string)) + } + }) + + t.Run("format json", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", "json"}) + assert.NoError(t, err) + var out map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(stdout), &out)) + assert.True(t, strings.HasSuffix(stdout, "\n")) + }) + + t.Run("format yaml", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", "yaml"}) + assert.NoError(t, err) + var out map[string]interface{} + assert.NoError(t, yaml.Unmarshal([]byte(stdout), &out)) + assert.True(t, strings.HasSuffix(stdout, "\n")) + }) + + t.Run("format template", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", `{{ . | len }}`}) + assert.NoError(t, err) + assert.Equal(t, "2\n", stdout) + }) + + t.Run("format template with newline", func(t *testing.T) { + stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", "{{ . | len }}\n"}) + assert.NoError(t, err) + assert.Equal(t, "2\n", stdout) + }) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/root.go b/gen/external-content/resource-provisioners/default/score-compose/root.go new file mode 100644 index 00000000..94f57109 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/root.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "io" + "log/slog" + + "github.com/spf13/cobra" + + "github.com/score-spec/score-compose/internal/logging" + "github.com/score-spec/score-compose/internal/version" +) + +var ( + rootCmd = &cobra.Command{ + Use: "score-compose", + Short: "SCORE to docker-compose translator", + Long: `SCORE is a specification for defining environment agnostic configuration for cloud based workloads. +This tool produces a docker-compose configuration file from the SCORE specification. +Complete documentation is available at https://score.dev`, + // don't print the errors - we print these ourselves in main() + SilenceErrors: true, + + // This function always runs for all subcommands + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if q, _ := cmd.Flags().GetBool("quiet"); q { + slog.SetDefault(slog.New(&logging.SimpleHandler{Level: slog.LevelError, Writer: io.Discard})) + } else if v, _ := cmd.Flags().GetCount("verbose"); v == 0 { + slog.SetDefault(slog.New(&logging.SimpleHandler{Level: slog.LevelInfo, Writer: cmd.ErrOrStderr()})) + } else if v == 1 { + slog.SetDefault(slog.New(&logging.SimpleHandler{Level: slog.LevelDebug, Writer: cmd.ErrOrStderr()})) + } else if v == 2 { + slog.SetDefault(slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{ + Level: slog.LevelDebug, AddSource: true, + }))) + } + return nil + }, + } +) + +func init() { + rootCmd.Version = version.BuildVersionString() + rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s" .Version}} +`) + rootCmd.PersistentFlags().Bool("quiet", false, "Mute any logging output") + rootCmd.PersistentFlags().CountP("verbose", "v", "Increase log verbosity and detail by specifying this flag one or more times") +} + +func Execute() error { + return rootCmd.Execute() +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/root_test.go b/gen/external-content/resource-provisioners/default/score-compose/root_test.go new file mode 100644 index 00000000..ad9b6923 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/root_test.go @@ -0,0 +1,261 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRootHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"--help"}) + assert.NoError(t, err) + assert.Equal(t, `SCORE is a specification for defining environment agnostic configuration for cloud based workloads. +This tool produces a docker-compose configuration file from the SCORE specification. +Complete documentation is available at https://score.dev + +Usage: + score-compose [command] + +Available Commands: + check-version Assert that the version of score-compose matches the required constraint + completion Generate the autocompletion script for the specified shell + generate Convert one or more Score files into a Docker compose manifest + help Help about any command + init Initialise a new score-compose project with local state directory and score file + provisioners Subcommands related to provisioners + resources Subcommands related to provisioned resources + +Flags: + -h, --help help for score-compose + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times + --version version for score-compose + +Use "score-compose [command] --help" for more information about a command. +`, stdout) + assert.Equal(t, "", stderr) +} + +func TestRootVersion(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"--version"}) + assert.NoError(t, err) + pattern := regexp.MustCompile(`^score-compose 0.0.0 \(build: \S+, sha: \S+\)\n$`) + assert.Truef(t, pattern.MatchString(stdout), "%s does not match: '%s'", pattern.String(), stdout) + assert.Equal(t, "", stderr) +} + +func TestRootCompletion(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion"}) + assert.NoError(t, err) + assert.Equal(t, `Generate the autocompletion script for score-compose for the specified shell. +See each sub-command's help for details on how to use the generated script. + +Usage: + score-compose completion [command] + +Available Commands: + bash Generate the autocompletion script for bash + fish Generate the autocompletion script for fish + powershell Generate the autocompletion script for powershell + zsh Generate the autocompletion script for zsh + +Flags: + -h, --help help for completion + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times + +Use "score-compose completion [command] --help" for more information about a command. +`, stdout) + assert.Equal(t, "", stderr) + + stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "completion"}) + assert.NoError(t, err) + assert.Equal(t, stdout, stdout2) + assert.Equal(t, "", stderr) +} + +func TestRootCompletionBashHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "bash", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `Generate the autocompletion script for the bash shell. + +This script depends on the 'bash-completion' package. +If it is not installed already, you can install it via your OS's package manager. + +To load completions in your current shell session: + + source <(score-compose completion bash) + +To load completions for every new session, execute once: + +#### Linux: + + score-compose completion bash > /etc/bash_completion.d/score-compose + +#### macOS: + + score-compose completion bash > $(brew --prefix)/etc/bash_completion.d/score-compose + +You will need to start a new shell for this setup to take effect. + +Usage: + score-compose completion bash + +Flags: + -h, --help help for bash + --no-descriptions disable completion descriptions + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) +} + +func TestRootCompletionBash(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "bash"}) + assert.NoError(t, err) + assert.Contains(t, stdout, "# bash completion V2 for score-compose") + assert.Equal(t, "", stderr) +} + +func TestRootCompletionFishHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "fish", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `Generate the autocompletion script for the fish shell. + +To load completions in your current shell session: + + score-compose completion fish | source + +To load completions for every new session, execute once: + + score-compose completion fish > ~/.config/fish/completions/score-compose.fish + +You will need to start a new shell for this setup to take effect. + +Usage: + score-compose completion fish [flags] + +Flags: + -h, --help help for fish + --no-descriptions disable completion descriptions + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) +} + +func TestRootCompletionFish(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "fish"}) + assert.NoError(t, err) + assert.Contains(t, stdout, "# fish completion for score-compose") + assert.Equal(t, "", stderr) +} + +func TestRootCompletionZshHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "zsh", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(score-compose completion zsh) + +To load completions for every new session, execute once: + +#### Linux: + + score-compose completion zsh > "${fpath[1]}/_score-compose" + +#### macOS: + + score-compose completion zsh > $(brew --prefix)/share/zsh/site-functions/_score-compose + +You will need to start a new shell for this setup to take effect. + +Usage: + score-compose completion zsh [flags] + +Flags: + -h, --help help for zsh + --no-descriptions disable completion descriptions + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) +} + +func TestRootCompletionZsh(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "zsh"}) + assert.NoError(t, err) + assert.Contains(t, stdout, "# zsh completion for score-compose") + assert.Equal(t, "", stderr) +} + +func TestRootCompletionPowershellHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "powershell", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `Generate the autocompletion script for powershell. + +To load completions in your current shell session: + + score-compose completion powershell | Out-String | Invoke-Expression + +To load completions for every new session, add the output of the above command +to your powershell profile. + +Usage: + score-compose completion powershell [flags] + +Flags: + -h, --help help for powershell + --no-descriptions disable completion descriptions + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) +} + +func TestRootCompletionPowershell(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "powershell"}) + assert.NoError(t, err) + assert.Contains(t, stdout, "# powershell completion for score-compose") + assert.Equal(t, "", stderr) +} + +func TestRootUnknown(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"unknown"}) + assert.EqualError(t, err, "unknown command \"unknown\" for \"score-compose\"") + assert.Equal(t, "", stdout) + assert.Equal(t, "", stderr) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/run.go b/gen/external-content/resource-provisioners/default/score-compose/run.go new file mode 100644 index 00000000..5cf496dc --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/run.go @@ -0,0 +1,364 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "sort" + "strings" + + "dario.cat/mergo" + "github.com/compose-spec/compose-go/v2/types" + "github.com/score-spec/score-go/framework" + "github.com/spf13/cobra" + "github.com/tidwall/sjson" + "gopkg.in/yaml.v3" + + loader "github.com/score-spec/score-go/loader" + schema "github.com/score-spec/score-go/schema" + score "github.com/score-spec/score-go/types" + + "github.com/score-spec/score-compose/internal/compose" + "github.com/score-spec/score-compose/internal/project" + "github.com/score-spec/score-compose/internal/provisioners" + "github.com/score-spec/score-compose/internal/provisioners/envprov" + "github.com/score-spec/score-compose/internal/util" +) + +const ( + scoreFileDefault = "./score.yaml" + overridesFileDefault = "./overrides.score.yaml" +) + +var ( + scoreFile string + overridesFile string + outFile string + envFile string + buildCtx string + + overrideParams []string + + skipValidation bool +) + +func init() { + runCmd.Flags().StringVarP(&scoreFile, "file", "f", scoreFileDefault, "Source SCORE file") + runCmd.Flags().StringVar(&overridesFile, "overrides", overridesFileDefault, "Overrides SCORE file") + runCmd.Flags().StringVarP(&outFile, "output", "o", "", "Output file") + runCmd.Flags().StringVar(&envFile, "env-file", "", "Location to store sample .env file") + runCmd.Flags().StringVar(&buildCtx, "build", "", "Replaces 'image' name with compose 'build' instruction") + + runCmd.Flags().StringArrayVarP(&overrideParams, "property", "p", nil, "Overrides selected property value") + + runCmd.Flags().BoolVar(&skipValidation, "skip-validation", false, "DEPRECATED: Disables Score file schema validation") + + rootCmd.AddCommand(runCmd) +} + +var runCmd = &cobra.Command{ + Use: "run [--file=score.yaml] [--output=compose.yaml]", + Args: cobra.NoArgs, + Short: "(Deprecated) Translate the SCORE file to docker-compose configuration", + Hidden: true, + RunE: run, + // don't print the errors - we print these ourselves in main() + SilenceErrors: true, +} + +func run(cmd *cobra.Command, args []string) error { + // Silence usage message if args are parsed correctly + cmd.SilenceUsage = true + + // Log a deprecation warning + slog.Warn("\n-----\nDeprecation notice: 'score-compose run' will be deprecated in the future - we recommend that you migrate to 'score-compose init' and 'score-compose generate'\n-----") + + // Open source file + // + slog.Info(fmt.Sprintf("Reading Score file '%s'", scoreFile)) + var err error + var src *os.File + if src, err = os.Open(scoreFile); err != nil { + return err + } + defer src.Close() + + // Parse SCORE spec + // + slog.Info("Parsing Score specification") + var srcMap map[string]interface{} + if err = loader.ParseYAML(&srcMap, src); err != nil { + return err + } + + // Apply overrides from file (optional) + // + if overridesFile != "" { + if ovr, err := os.Open(overridesFile); err == nil { + defer ovr.Close() + + slog.Info(fmt.Sprintf("Loading Score overrides file '%s'", overridesFile)) + var ovrMap map[string]interface{} + if err = loader.ParseYAML(&ovrMap, ovr); err != nil { + return err + } + slog.Info("Applying Score overrides") + if err := mergo.MergeWithOverwrite(&srcMap, ovrMap); err != nil { + return fmt.Errorf("applying overrides fom '%s': %w", overridesFile, err) + } + } else if !os.IsNotExist(err) || overridesFile != overridesFileDefault { + return err + } + } + + // Apply overrides from command line (optional) + // + for _, pstr := range overrideParams { + jsonBytes, err := json.Marshal(srcMap) + if err != nil { + return fmt.Errorf("marshalling score spec: %w", err) + } + + pmap := strings.SplitN(pstr, "=", 2) + if len(pmap) <= 1 { + var path = pmap[0] + slog.Info(fmt.Sprintf("Applying Score properties override: removing '%s'", path)) + if jsonBytes, err = sjson.DeleteBytes(jsonBytes, path); err != nil { + return fmt.Errorf("removing '%s': %w", path, err) + } + } else { + var path = pmap[0] + var val interface{} + if err := yaml.Unmarshal([]byte(pmap[1]), &val); err != nil { + val = pmap[1] + } + + slog.Info(fmt.Sprintf("Applying Score properties override: overriding '%s' = '%s' (%T)", path, val, val)) + if jsonBytes, err = sjson.SetBytes(jsonBytes, path, val); err != nil { + return fmt.Errorf("overriding '%s': %w", path, err) + } + } + + if err = json.Unmarshal(jsonBytes, &srcMap); err != nil { + return fmt.Errorf("unmarshalling score spec: %w", err) + } + } + + // Apply upgrades to fix backports or backward incompatible things + if changes, err := schema.ApplyCommonUpgradeTransforms(srcMap); err != nil { + return fmt.Errorf("failed to upgrade spec: %w", err) + } else if len(changes) > 0 { + for _, change := range changes { + slog.Info(fmt.Sprintf("Applying upgrade to specification: %s", change)) + } + } + + // Validate SCORE spec + // + if !skipValidation { + slog.Info("Validating final Score specification") + if err := schema.Validate(srcMap); err != nil { + return fmt.Errorf("validating workload spec: %w", err) + } + } + + // Convert SCORE spec + // + var spec score.Workload + if err = loader.MapSpec(&spec, srcMap); err != nil { + return fmt.Errorf("validating workload spec: %w", err) + } + + // Build a fake score-compose init state. We don't actually need to store or persist this because we're not doing + // anything iterative or stateful. + state := &project.State{Extras: project.StateExtras{MountsDirectory: "/dev/null"}} + state, err = state.WithWorkload(&spec, &scoreFile, project.WorkloadExtras{}) + if err != nil { + return fmt.Errorf("failed to add score file to state: %w", err) + } + + // Prime the resources with initial state and validate any issues + state, err = state.WithPrimedResources() + if err != nil { + return fmt.Errorf("failed to prime resources: %w", err) + } + + // Instead of actually calling the resource provisioning system, we skip it and fill in the supported resources + // ourselves. + provisionerList, envProvisioner, err := buildLegacyProvisioners(spec.Metadata["name"].(string), state) + if err != nil { + return err + } + + state, err = provisioners.ProvisionResources(context.Background(), state, provisionerList, nil) + if err != nil { + return fmt.Errorf("failed to provision resources: %w", err) + } + + // Build docker-compose configuration + // + slog.Info("Building docker-compose configuration") + proj, err := compose.ConvertSpec(state, &spec) + if err != nil { + return fmt.Errorf("building docker-compose configuration: %w", err) + } + + // Deprecated behavior of the run command which used to publish ports + // Todo: remove this once score-compose run is removed + if spec.Service != nil && len(spec.Service.Ports) > 0 { + ports := make([]types.ServicePortConfig, 0) + for _, pSpec := range spec.Service.Ports { + var pubPort = fmt.Sprintf("%v", pSpec.Port) + var protocol string + if pSpec.Protocol != nil { + protocol = strings.ToLower(string(*pSpec.Protocol)) + } + ports = append(ports, types.ServicePortConfig{ + Published: pubPort, + Target: uint32(util.DerefOr(pSpec.TargetPort, pSpec.Port)), + Protocol: protocol, + }) + } + for serviceName, service := range proj.Services { + if service.NetworkMode == "" { + service.Ports = ports + proj.Services[serviceName] = service + } + } + sort.Slice(ports, func(i, j int) bool { + return ports[i].Published < ports[j].Published + }) + } + + // Override 'image' reference with 'build' instructions + // + if buildCtx != "" { + slog.Info(fmt.Sprintf("Applying build context '%s' for service images", buildCtx)) + // We add the build context to all services and containers here and make a big assumption that all are + // using the image we are building here and now. If this is unexpected, users should use a more complex + // overrides file. + for serviceName, service := range proj.Services { + service.Build = &types.BuildConfig{Context: buildCtx} + service.Image = "" + proj.Services[serviceName] = service + } + } + + // Open output file (optional) + // + var dest = cmd.OutOrStdout() + if outFile != "" { + slog.Info(fmt.Sprintf("Writing output compose file '%s'", outFile)) + destFile, err := os.Create(outFile) + if err != nil { + return err + } + defer destFile.Close() + + dest = io.MultiWriter(dest, destFile) + } + + // Write docker-compose spec + // + if err = compose.WriteYAML(dest, proj); err != nil { + return err + } + + if envFile != "" { + // Open .env file + // + slog.Info(fmt.Sprintf("Writing output .env file '%s'", envFile)) + dest, err := os.Create(envFile) + if err != nil { + return err + } + defer dest.Close() + + // Write .env file + // + envVars := make([]string, 0) + for key, val := range envProvisioner.Accessed() { + var envVar = fmt.Sprintf("%s=%v\n", key, val) + envVars = append(envVars, envVar) + } + sort.Strings(envVars) + + for _, envVar := range envVars { + if _, err := dest.WriteString(envVar); err != nil { + return err + } + } + } + + return nil +} + +func buildLegacyProvisioners(workloadName string, state *project.State) ([]provisioners.Provisioner, *envprov.Provisioner, error) { + envProv := new(envprov.Provisioner) + out := []provisioners.Provisioner{envProv} + for resName, res := range state.Workloads[workloadName].Spec.Resources { + resUid := framework.NewResourceUid(workloadName, resName, res.Type, res.Class, res.Id) + if resUid.Type() == "environment" { + // handled by env prov which is already added above + } else if resUid.Type() == "volume" && resUid.Class() == "default" { + out = append(out, &legacyVolumeProvisioner{MatchResourceUid: resUid}) + } else { + slog.Warn(fmt.Sprintf("resources.%s: '%s.%s' is not directly supported in score-compose, references will be converted to environment variables", resName, resUid.Type(), resUid.Class())) + out = append(out, envProv.GenerateSubProvisioner(resName, resUid)) + } + } + return out, envProv, nil +} + +type legacyVolumeProvisioner struct { + MatchResourceUid framework.ResourceUid +} + +func (l *legacyVolumeProvisioner) Params() []string { + return []string{} +} + +func (l *legacyVolumeProvisioner) Outputs() []string { + return []string{} +} + +func (l *legacyVolumeProvisioner) Uri() string { + return "builtin://legacy-volume" +} + +func (l *legacyVolumeProvisioner) Match(resUid framework.ResourceUid) bool { + return l.MatchResourceUid == resUid +} + +func (l *legacyVolumeProvisioner) Provision(ctx context.Context, input *provisioners.Input) (*provisioners.ProvisionOutput, error) { + return &provisioners.ProvisionOutput{ResourceOutputs: map[string]interface{}{}}, nil +} + +func (l *legacyVolumeProvisioner) Class() string { + return l.MatchResourceUid.Class() +} + +func (l *legacyVolumeProvisioner) Type() string { + return l.MatchResourceUid.Type() +} + +func (l *legacyVolumeProvisioner) Description() string { + return "" +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/run_test.go b/gen/external-content/resource-provisioners/default/score-compose/run_test.go new file mode 100644 index 00000000..74c2b531 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/run_test.go @@ -0,0 +1,430 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package command + +import ( + "bytes" + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// executeAndResetCommand is a test helper that runs and then resets a command for executing in another test. +func executeAndResetCommand(ctx context.Context, cmd *cobra.Command, args []string) (string, string, error) { + beforeOut, beforeErr := cmd.OutOrStdout(), cmd.ErrOrStderr() + defer func() { + cmd.SetOut(beforeOut) + cmd.SetErr(beforeErr) + // also have to remove completion commands which get auto added and bound to an output buffer + for _, command := range cmd.Commands() { + if command.Name() == "completion" { + cmd.RemoveCommand(command) + break + } + } + }() + + nowOut, nowErr := new(bytes.Buffer), new(bytes.Buffer) + cmd.SetOut(nowOut) + cmd.SetErr(nowErr) + cmd.SetArgs(args) + subCmd, err := cmd.ExecuteContextC(ctx) + if subCmd != nil { + subCmd.SetOut(nil) + subCmd.SetErr(nil) + subCmd.SetContext(context.TODO()) + subCmd.SilenceUsage = false + subCmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Value.Type() == "stringArray" { + _ = f.Value.(pflag.SliceValue).Replace(nil) + } else { + _ = f.Value.Set(f.DefValue) + } + }) + } + return nowOut.String(), nowErr.String(), err +} + +func TestRunHelp(t *testing.T) { + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--help"}) + assert.NoError(t, err) + assert.Equal(t, `(Deprecated) Translate the SCORE file to docker-compose configuration + +Usage: + score-compose run [--file=score.yaml] [--output=compose.yaml] [flags] + +Flags: + --build string Replaces 'image' name with compose 'build' instruction + --env-file string Location to store sample .env file + -f, --file string Source SCORE file (default "./score.yaml") + -h, --help help for run + -o, --output string Output file + --overrides string Overrides SCORE file (default "./overrides.score.yaml") + -p, --property stringArray Overrides selected property value + --skip-validation DEPRECATED: Disables Score file schema validation + +Global Flags: + --quiet Mute any logging output + -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times +`, stdout) + assert.Equal(t, "", stderr) + + stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "run"}) + assert.NoError(t, err) + assert.Equal(t, stdout, stdout2) + assert.Equal(t, "", stderr) +} + +func TestRunExample(t *testing.T) { + td := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example-workload-name123 + extra-key: extra-value +service: + ports: + port-one: + port: 1000 + protocol: TCP + targetPort: 10000 + port-two2: + port: 8000 +containers: + container-one1: + image: localhost:4000/repo/my-image:tag + command: ["/bin/sh", "-c"] + args: ["hello", "world"] + resources: + requests: + cpu: 1000m + memory: 10Gi + limits: + cpu: "0.24" + memory: 128M + variables: + SOME_VAR: some content here + volumes: + - source: volume-name + target: /mnt/something + readOnly: false + - source: volume-two + target: /mnt/something-else + livenessProbe: + httpGet: + port: 8080 + path: /livez + readinessProbe: + httpGet: + host: 127.0.0.1 + port: 80 + scheme: HTTP + path: /readyz + httpHeaders: + - name: SOME_HEADER + value: some-value-here + container-two2: + image: localhost:4000/repo/my-image:tag +resources: + resource-one1: + metadata: + annotations: + Default-Annotation: this is my annotation + prefix.com/Another-Key_Annotation.2: something else + extra-key: extra-value + type: Resource-One + class: default + params: + extra: + data: here + resource-two2: + type: Resource-Two + volume-name: + type: volume + volume-two: + type: volume +`), 0600)) + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) + assert.NoError(t, err) + assert.NotEqual(t, "", stdout) + for _, l := range []string{ + "WARN: resources.resource-one1: 'Resource-One.default' is not directly supported in score-compose, references will be converted to environment variables\n", + "WARN: resources.resource-two2: 'Resource-Two.default' is not directly supported in score-compose, references will be converted to environment variables\n", + "WARN: containers.container-one1.resources.requests: not supported - ignoring\n", + "WARN: containers.container-one1.resources.limits: not supported - ignoring\n", + } { + assert.Contains(t, stderr, l) + } + + rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + require.NoError(t, err) + var actualComposeContent map[string]interface{} + assert.NoError(t, yaml.Unmarshal(rawComposeContent, &actualComposeContent)) + assert.Equal(t, map[string]interface{}{ + "services": map[string]interface{}{ + "example-workload-name123-container-one1": map[string]interface{}{ + "annotations": map[string]interface{}{ + "compose.score.dev/workload-name": "example-workload-name123", + }, + "hostname": "example-workload-name123", + "image": "localhost:4000/repo/my-image:tag", + "entrypoint": []interface{}{"/bin/sh", "-c"}, + "command": []interface{}{"hello", "world"}, + "environment": map[string]interface{}{ + "SOME_VAR": "some content here", + }, + "ports": []interface{}{ + map[string]interface{}{"target": 10000, "published": "1000", "protocol": "tcp"}, + map[string]interface{}{"target": 8000, "published": "8000"}, + }, + "volumes": []interface{}{ + map[string]interface{}{"type": "volume", "source": "volume-name", "target": "/mnt/something"}, + map[string]interface{}{"type": "volume", "source": "volume-two", "target": "/mnt/something-else"}, + }, + }, + "example-workload-name123-container-two2": map[string]interface{}{ + "annotations": map[string]interface{}{ + "compose.score.dev/workload-name": "example-workload-name123", + }, + "image": "localhost:4000/repo/my-image:tag", + "network_mode": "service:example-workload-name123-container-one1", + }, + }, + }, actualComposeContent) + + t.Run("validate", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + + assert.NoError(t, os.WriteFile(filepath.Join(td, "volume.yaml"), []byte(` +volumes: + volume-name: + driver: local + volume-two: + driver: local + +`), 0644)) + + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "-f", "volume.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestExample_invalid_spec(t *testing.T) { + td := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +{}`), 0600)) + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) + assert.EqualError(t, err, "validating workload spec: jsonschema: '' does not validate with https://score.dev/schemas/score#/required: missing properties: 'apiVersion', 'metadata', 'containers'") + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) +} + +func TestFilesNotSupported(t *testing.T) { + td := t.TempDir() + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: example-workload-name123 +containers: + container-one1: + image: localhost:4000/repo/my-image:tag + files: + - target: /mnt/something + content: bananas +`), 0600)) + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) + assert.EqualError(t, err, "building docker-compose configuration: files are not supported") + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) +} + +func TestInvalidWorkloadName(t *testing.T) { + td := t.TempDir() + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: Invalid Name +containers: + container-one1: + image: localhost:4000/repo/my-image:tag +`), 0600)) + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml")}) + assert.EqualError(t, err, "validating workload spec: jsonschema: '/metadata/name' does not validate with https://score.dev/schemas/score#/properties/metadata/properties/name/pattern: does not match pattern '^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$'") + assert.Equal(t, "", stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) +} + +func TestRunExample01(t *testing.T) { + td := t.TempDir() + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", "../../examples/01-hello/score.yaml", "--output", filepath.Join(td, "compose.yaml")}) + assert.NoError(t, err) + + expectedOutput := `services: + hello-world-hello: + annotations: + compose.score.dev/workload-name: hello-world + your.custom/annotation: value + command: + - -c + - while true; do echo Hello World!; sleep 5; done + entrypoint: + - /bin/sh + hostname: hello-world + image: busybox +` + + assert.Equal(t, expectedOutput, stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedOutput, string(rawComposeContent)) + + t.Run("validate", func(t *testing.T) { + if os.Getenv("NO_DOCKER") != "" { + t.Skip("NO_DOCKER is set") + return + } + + dockerCmd, err := exec.LookPath("docker") + require.NoError(t, err) + + cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") + cmd.Dir = td + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + assert.NoError(t, cmd.Run()) + }) +} + +func TestRunWithBuild(t *testing.T) { + td := t.TempDir() + + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: hello-world +containers: + hello: + image: busybox +`), 0600)) + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{ + "run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml"), "--build", "./test", + }) + assert.NoError(t, err) + + expectedOutput := `services: + hello-world-hello: + annotations: + compose.score.dev/workload-name: hello-world + build: + context: ./test + hostname: hello-world +` + + assert.Equal(t, expectedOutput, stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedOutput, string(rawComposeContent)) +} + +func TestRunWithOverrides(t *testing.T) { + td := t.TempDir() + + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: hello-world +containers: + hello: + image: busybox +`), 0600)) + + assert.NoError(t, os.WriteFile(filepath.Join(td, "score-overrides.yaml"), []byte(` +containers: + hello: + image: nginx +`), 0600)) + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{ + "run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml"), "--overrides", filepath.Join(td, "score-overrides.yaml"), + }) + assert.NoError(t, err) + + expectedOutput := `services: + hello-world-hello: + annotations: + compose.score.dev/workload-name: hello-world + hostname: hello-world + image: nginx +` + + assert.Equal(t, expectedOutput, stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedOutput, string(rawComposeContent)) +} + +func TestRunWithPropertyOverrides(t *testing.T) { + td := t.TempDir() + + assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` +apiVersion: score.dev/v1b1 +metadata: + name: hello-world +containers: + hello: + image: busybox +`), 0600)) + + stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{ + "run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml"), "--property", "containers.hello.image=bananas:latest", + }) + assert.NoError(t, err) + + expectedOutput := `services: + hello-world-hello: + annotations: + compose.score.dev/workload-name: hello-world + hostname: hello-world + image: bananas:latest +` + + assert.Equal(t, expectedOutput, stdout) + assert.NotEqual(t, "", strings.TrimSpace(stderr)) + rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) + require.NoError(t, err) + assert.Equal(t, expectedOutput, string(rawComposeContent)) +} diff --git a/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden b/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden new file mode 100644 index 00000000..85f4b92c --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden @@ -0,0 +1,377 @@ +[ + { + "Type": "cmd-with-class-with-params-in-outputs", + "Class": "c1", + "Params": [ + "p1", + "po1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-with-class-with-params-in-shared", + "Class": "c1", + "Params": [ + "p1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-with-class-with-params-in-shared-outputs", + "Class": "c1", + "Params": [ + "p1", + "po1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-with-class-with-params-in-state-outputs", + "Class": "c1", + "Params": [ + "p1", + "po1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-with-class-with-params-in-state-outputs-shared", + "Class": "c1", + "Params": [ + "p1", + "po1", + "psh1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-with-class-without-params", + "Class": "c1", + "Params": [], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-with-params-in-outputs", + "Class": "(any)", + "Params": [ + "p1", + "po1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-with-params-in-shared", + "Class": "(any)", + "Params": [ + "p1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-with-params-in-shared-outputs", + "Class": "(any)", + "Params": [ + "p1", + "po1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-with-params-in-state", + "Class": "(any)", + "Params": [ + "p1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-with-params-in-state-outputs", + "Class": "(any)", + "Params": [ + "p1", + "po1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-with-params-in-state-outputs-shared", + "Class": "(any)", + "Params": [ + "p1", + "po1", + "psh1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-without-params", + "Class": "(any)", + "Params": [], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "cmd-without-class-without-params-with-description", + "Class": "(any)", + "Params": [], + "Outputs": [ + "o1", + "o2" + ], + "Description": "cmd-without-class-without-params-with-description" + }, + { + "Type": "template-with-class-with-params-in-outputs", + "Class": "c1", + "Params": [ + "p1", + "po1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-with-class-with-params-in-shared", + "Class": "c1", + "Params": [ + "p1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-with-class-with-params-in-shared-outputs", + "Class": "c1", + "Params": [ + "p1", + "po1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-with-class-with-params-in-state", + "Class": "c1", + "Params": [ + "p1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-with-class-with-params-in-state-outputs", + "Class": "c1", + "Params": [ + "p1", + "po1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-with-class-with-params-in-state-outputs-shared", + "Class": "c1", + "Params": [ + "p1", + "po1", + "psh1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-with-class-without-params", + "Class": "c1", + "Params": [], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-with-params-in-outputs", + "Class": "(any)", + "Params": [ + "p1", + "po1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-with-params-in-shared", + "Class": "(any)", + "Params": [ + "p1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-with-params-in-shared-outputs", + "Class": "(any)", + "Params": [ + "p1", + "po1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-with-params-in-state", + "Class": "(any)", + "Params": [ + "p1", + "psh1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-with-params-in-state-outputs", + "Class": "(any)", + "Params": [ + "p1", + "po1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-with-params-in-state-outputs-shared", + "Class": "(any)", + "Params": [ + "p1", + "po1", + "psh1", + "pst1" + ], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-without-params", + "Class": "(any)", + "Params": [], + "Outputs": [ + "o1", + "o2" + ], + "Description": "" + }, + { + "Type": "template-without-class-without-params-with-description", + "Class": "(any)", + "Params": [], + "Outputs": [ + "o1", + "o2" + ], + "Description": "without-class-without-params-with-description" + } +] diff --git a/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden b/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden new file mode 100644 index 00000000..261ef47f --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden @@ -0,0 +1,61 @@ ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| TYPE | CLASS | PARAMS | OUTPUTS | DESCRIPTION | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-with-class-with-params-in-outputs | c1 | p1, po1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-with-class-with-params-in-shared | c1 | p1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-with-class-with-params-in-shared-outputs | c1 | p1, po1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-with-class-with-params-in-state-outputs | c1 | p1, po1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-with-class-with-params-in-state-outputs-shared | c1 | p1, po1, psh1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-with-class-without-params | c1 | | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-with-params-in-outputs | (any) | p1, po1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-with-params-in-shared | (any) | p1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-with-params-in-shared-outputs | (any) | p1, po1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-with-params-in-state | (any) | p1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-with-params-in-state-outputs | (any) | p1, po1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-with-params-in-state-outputs-shared | (any) | p1, po1, psh1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-without-params | (any) | | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| cmd-without-class-without-params-with-description | (any) | | o1, o2 | cmd-without-class-without-params-with-description | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-with-class-with-params-in-outputs | c1 | p1, po1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-with-class-with-params-in-shared | c1 | p1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-with-class-with-params-in-shared-outputs | c1 | p1, po1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-with-class-with-params-in-state | c1 | p1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-with-class-with-params-in-state-outputs | c1 | p1, po1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-with-class-with-params-in-state-outputs-shared | c1 | p1, po1, psh1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-with-class-without-params | c1 | | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-with-params-in-outputs | (any) | p1, po1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-with-params-in-shared | (any) | p1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-with-params-in-shared-outputs | (any) | p1, po1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-with-params-in-state | (any) | p1, psh1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-with-params-in-state-outputs | (any) | p1, po1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-with-params-in-state-outputs-shared | (any) | p1, po1, psh1, pst1 | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-without-params | (any) | | o1, o2 | | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ +| template-without-class-without-params-with-description | (any) | | o1, o2 | without-class-without-params-with-description | ++------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ diff --git a/gen/external-content/resource-provisioners/default/score-k8s/default.go b/gen/external-content/resource-provisioners/default/score-k8s/default.go new file mode 100644 index 00000000..832edcea --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-k8s/default.go @@ -0,0 +1,22 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package _default + +import ( + _ "embed" +) + +//go:embed zz-default.provisioners.yaml +var DefaultProvisioners string diff --git a/gen/external-content/resource-provisioners/default/score-k8s/default_test.go b/gen/external-content/resource-provisioners/default/score-k8s/default_test.go new file mode 100644 index 00000000..6424e65c --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-k8s/default_test.go @@ -0,0 +1,30 @@ +// Copyright 2024 The Score Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package _default + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/score-spec/score-k8s/internal/provisioners/loader" +) + +func TestDefaultProvisioners(t *testing.T) { + p, err := loader.LoadProvisioners([]byte(DefaultProvisioners)) + assert.NoError(t, err) + assert.NotNil(t, p) + assert.Greater(t, len(p), 0) +} diff --git a/gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml b/gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml new file mode 100644 index 00000000..180f6a73 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml @@ -0,0 +1,1390 @@ +# This example provisioner is a fake resource type used to demonstrate the template provisioner mechanism. The URI should +# be a unique indicator of the provisioner. The scheme indicates how it is executed. +- uri: template://example-provisioners/example-provisioner + # (Required) Which resource type to match + type: example-provisioner-resource + description: Example provisioner that runs a template. + # (Optional) Which 'class' of the resource. Null will match any class, a non-empty value like 'default' will match + # only resources of that class. + class: null + # (Optional) The exact resource id to match. Null will match any resource, a non-empty value will only match + # the resource with exact same id. + id: null + # (Optional) The init template sets the initial context values on each provision request. This is a text template + # that must evaluate to a YAML/JSON key-value map. + init: | + key: value + # sprig functions are also supported + key2: {{ print "value" | upper }} + # other attributes are available such as Type, Class, Id, Uid, Guid. + my-uid: "{{ .Uid }}#{{ .Guid }}" + # (Optional) The state template gets evaluated next and sets the internal state of this resource based on the previous + # state and the init context. Like init, this evaluates to a YAML/JSON object. This is the template that allows + # state to be stored between each generate call. + state: | + stateKey: {{ .Init.key }} # will copy the value from init + stateKey2: {{ default 0 .State.stateKey2 | add 1 }} # will increment on each provision attempt + # (Optional) The shared state template is like state, but is a key-value structure shared between all resources. + # This can be used to coordinate shared resources and state between resources of the same or related types. + shared: | + section: + key: {{ .Shared.foo }} + # (Optional) The outputs template gets evaluated last and translates into the outputs available as placeholder + # references like ${resources.my-resource.key}. + outputs: | + plaintext: my-value + nested: + example: thing + # Instead of returning secret outputs as plaintext. They can be embedded as reference to Kubernetes Secrets. When + # these are detected, they can be used in environment variables or file contents securely. The format is + # 🔐💬_💬🔐. For example, the next line refers to a secret created in the manifests. + secret-reference: 🔐💬secret-{{ .Guid }}_password💬🔐 + # A template function also exists for generating these in the template provisioner. + secret-reference-alt: {{ encodeSecretRef (printf "secret-%s" .Guid) "password" }} + expected_outputs: + - plaintext + - nested + - secret-reference + - secret-reference-alt + # (Optional) The manifests template gets evaluated as a list of Kubernetes object manifests to be added to the output. + manifests: | + - apiVersion: v1 + kind: ConfigMap + metadata: + name: cfg-{{ .Guid }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + data: + key: {{ .Init.key }} + - apiVersion: v1 + kind: Secret + metadata: + name: secret-{{ .Guid }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + password: {{ b64enc "my-secret-password" }} + +# The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts +# with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory. +- uri: cmd://bash#example-provisioner + type: example-provisioner-resource + description: Example provisioner that runs a bash script. + class: default + id: specific + # (Optional) additional args that the binary gets run with + # If any of the args are '' it will be replaced with "provision" + args: ["-c", "echo '{\"resource_outputs\":{\"key\":\"value\",\"secret\":\"🔐💬mysecret_mykey💬🔐\"},\"manifests\":[]}'"] + expected_outputs: + - key + - secret + +# The default provisioner for service resources, this expects a workload and port name and will return the hostname and +# port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency +# relationship yet. +- uri: template://default-provisioners/service-port + type: service-port + description: Outputs a hostname and port for connecting to another workload. + outputs: | + {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} + {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + {{ if not $w }}{{ fail "unknown workload" }}{{ end }} + {{ $p := (index $w.Ports .Params.port) }} + {{ if not $p }}{{ fail "unknown service port" }}{{ end }} + hostname: {{ $w.ServiceName | quote }} + port: {{ $p.TargetPort }} + expected_outputs: + - hostname + - port + supported_params: + - workload + - port + +# As an example we have a 'volume' type which returns an emptyDir volume. +# In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or +# persistent volume and claims. +- uri: template://default-provisioners/volume + type: volume + description: Creates a persistent volume that can be mounted on a workload. + outputs: | + source: + emptyDir: {} + expected_outputs: + - source + +# The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is +# available. You should replace this with your own dns name generation that matches your external-dns controller. +- uri: template://default-provisioners/dns + type: dns + description: Outputs a *.localhost domain as the hostname. + init: | + randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost + state: | + instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} + outputs: | + host: {{ .State.instanceHostname }} + expected_outputs: + - host + +# Routes could be implemented as either traditional ingress resources or using the newer gateway API. +# In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this. +- uri: template://default-provisioners/route + type: route + description: Provisions an HTTPRoute on a shared Nginx instance. + init: | + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + state: | + routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + manifests: | + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: {{ .State.routeName }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.routeName }} + app.kubernetes.io/instance: {{ .State.routeName }} + spec: + parentRefs: + - name: default + hostnames: + - {{ .Params.host | quote }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ .Params.path | quote }} + backendRefs: + - name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} + port: {{ .Params.port }} + supported_params: + - host + - port + - path + +- uri: template://default-provisioners/postgres + type: postgres + description: Provisions a dedicated database on a shared PostgreSQL instance. + init: | + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 5432 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: postgres-db + image: mirror.gcr.io/postgres:17-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + - name: POSTGRES_USER + value: {{ .State.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: password + - name: POSTGRES_DB + value: {{ .State.database | quote }} + volumeMounts: + - name: pv-data + mountPath: /var/lib/postgresql/data + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .State.username | quote }} + - -d + - {{ .State.database | quote }} + periodSeconds: 3 + securityContext: + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: pv-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + expected_outputs: + - host + - port + - name + - database + - username + - password + +- uri: template://default-provisioners/postgres-instance + type: postgres-instance + description: Provisions a dedicated PostgreSQL instance. + init: | + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 5432 + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: postgres-db + image: mirror.gcr.io/postgres:17-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + - name: POSTGRES_USER + value: {{ .State.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: password + volumeMounts: + - name: pv-data + mountPath: /var/lib/postgresql/data + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .State.username | quote }} + periodSeconds: 3 + securityContext: + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: pv-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + expected_outputs: + - host + - port + - username + - password + +- uri: template://default-provisioners/redis + type: redis + description: Provisions a dedicated Redis instance. + init: | + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: redis-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: default + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 6379 + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + redis.conf: {{ printf "requirepass %s\nport 6379\nsave 60 1\nloglevel warning\n" .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: redis + image: mirror.gcr.io/redis:7-alpine + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + ports: + - name: redis + containerPort: 6379 + volumeMounts: + - name: redis-data + mountPath: /data + - name: config + mountPath: /usr/local/etc/redis + readinessProbe: + exec: + command: + - redis-cli + - ping + periodSeconds: 3 + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumes: + - name: config + secret: + secretName: {{ .State.service }} + items: + - key: redis.conf + path: redis.conf + volumeClaimTemplates: + - metadata: + name: redis-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 6379 + targetPort: 6379 + expected_outputs: + - host + - port + - username + - password + +- uri: template://default-provisioners/mysql + type: mysql + description: Provisions a dedicated MySQL database on a shared instance. + init: | + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: mysql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 3306 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MYSQL_PASSWORD" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + MYSQL_PASSWORD: {{ .State.password | b64enc }} + MYSQL_ROOT_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + containers: + - name: mysql-db + image: mirror.gcr.io/mysql:8 + ports: + - name: mysql + containerPort: 3306 + env: + - name: MYSQL_USER + value: {{ .State.username | quote }} + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MYSQL_PASSWORD + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + value: {{ .State.database | quote }} + volumeMounts: + - name: data + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + periodSeconds: 3 + volumeClaimTemplates: + - metadata: + name: data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 3306 + targetPort: 3306 + expected_outputs: + - host + - port + - name + - database + - username + - password + +- uri: template://default-provisioners/mongo + type: mongodb + description: Provisions a dedicated MongoDB database. + init: | + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: mongo-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 27017 + connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.service }}:27017/" + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MONGO_INITDB_ROOT_PASSWORD" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: mongo-db + image: mirror.gcr.io/mongo:8 + ports: + - name: mongo + containerPort: 27017 + env: + - name: MONGO_INITDB_ROOT_USERNAME + value: {{ .State.username | quote }} + - name: MONGO_INITDB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MONGO_INITDB_ROOT_PASSWORD + livenessProbe: + exec: + command: + - /bin/sh + - -c + - echo 'db.runCommand("ping").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 20 + securityContext: + runAsUser: 1001 + runAsGroup: 1001 + allowPrivilegeEscalation: false + privileged: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data/db + - name: tmp + mountPath: /tmp + securityContext: + runAsNonRoot: true + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault + volumes: + - name: tmp + emptyDir: {} + volumeClaimTemplates: + - metadata: + name: data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 27017 + targetPort: 27017 + expected_outputs: + - host + - port + - username + - password + - connection + +- uri: template://default-provisioners/rabbitmq + type: amqp + description: Provisions a dedicated RabbitMQ vhost on a shared instance. + init: | + randomVHost: vhost-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: rabbitmq-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 5672 + vhost: {{ .State.vhost }} + username: {{ .State.username }} + password: {{ .State.password }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }}-secret + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }}-secret + app.kubernetes.io/instance: {{ .State.service }} + data: + RABBITMQ_DEFAULT_VHOST: {{ .State.vhost | b64enc }} + RABBITMQ_DEFAULT_USER: {{ .State.username | b64enc }} + RABBITMQ_DEFAULT_PASS: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + serviceName: {{ .State.service }} + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + containers: + - name: rabbitmq + image: mirror.gcr.io/rabbitmq:3-management-alpine + ports: + - name: amqp + containerPort: 5672 + - name: management + containerPort: 15672 + envFrom: + - secretRef: + name: {{ .State.service }}-secret + volumeMounts: + - name: data + mountPath: /var/lib/rabbitmq + readinessProbe: + exec: + command: + - rabbitmq-diagnostics + - -q + - check_port_connectivity + periodSeconds: 3 + initialDelaySeconds: 30 + timeoutSeconds: 5 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 3Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + ports: + - port: 5672 + targetPort: 5672 + name: amqp + - port: 15672 + targetPort: 15672 + name: management + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + expected_outputs: + - host + - port + - vhost + - username + - password + +- uri: template://default-provisioners/mssql + type: mssql + description: Provisions a dedicated database on a shared MS SQL server instance. + init: | + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: mssql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: master + username: sa + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + server: {{ .State.service }} + port: 1433 + connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }}" + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + MSSQL_SA_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + containers: + - name: mssql-db + image: mcr.microsoft.com/mssql/server:latest + ports: + - name: mssql + containerPort: 1433 + env: + - name: ACCEPT_EULA + value: "Y" + - name: MSSQL_ENABLE_HADR + value: "1" + - name: MSSQL_AGENT_ENABLED + value: "1" + - name: MSSQL_SA_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MSSQL_SA_PASSWORD + volumeMounts: + - name: mssql + mountPath: "/var/opt/mssql" + volumeClaimTemplates: + - metadata: + name: mssql + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 1433 + targetPort: 1433 + expected_outputs: + - server + - port + - connection + - database + - username + - password + +# This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. +# This provides some common and well known outputs that can be used with any generic AWS s3 client. +# The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket. +- uri: template://default-provisioners/s3 + type: s3 + description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. + # The init template contains some initial seed data that can be used t needed. + init: | + sk: default-provisioners-minio-instance + state: | + bucket: {{ dig "bucket" (printf "bucket-%s" .Guid) .State | quote }} + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" (randAlpha 7 | lower | printf "minio-%s") .Shared | quote }} + instanceUsername: {{ dig .Init.sk "instanceUsername" (randAlpha 7 | printf "user-%s") .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" (randAlphaNum 16) .Shared | quote }} + instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" (randAlphaNum 20) .Shared | quote }} + instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" (randAlphaNum 40) .Shared | quote }} + outputs: | + {{ $shared := dig .Init.sk (dict) .Shared }} + {{ $service := $shared.instanceServiceName }} + bucket: {{ .State.bucket }} + access_key_id: {{ $shared.instanceAccessKeyId | quote }} + secret_key: {{ encodeSecretRef $service "secret_key" }} + endpoint: http://{{ $service }}:9000 + region: "us-east-1" + # for compatibility with Humanitec's existing s3 resource + aws_access_key_id: {{ $shared.instanceAccessKeyId | quote }} + aws_secret_key: {{ encodeSecretRef $service "secret_key" }} + manifests: | + {{ $shared := dig .Init.sk (dict) .Shared }} + {{ $service := $shared.instanceServiceName }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ $service | quote }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + replicas: 1 + serviceName: {{ $service | quote }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ $service | quote }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + automountServiceAccountToken: false + containers: + - name: minio + image: quay.io/minio/minio + args: ["server", "/data", "--console-address", ":9001"] + ports: + - name: service + containerPort: 9000 + - name: console + containerPort: 9001 + env: + - name: MINIO_ROOT_USER + value: {{ $shared.instanceUsername | quote }} + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: password + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: data + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Secret + metadata: + name: {{ $service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service }} + app.kubernetes.io/instance: {{ $service }} + data: + password: {{ $shared.instancePassword | b64enc }} + secret_key: {{ $shared.instanceSecretKey | b64enc }} + - apiVersion: v1 + kind: Service + metadata: + name: {{ $service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service }} + app.kubernetes.io/instance: {{ $service }} + spec: + selector: + app.kubernetes.io/instance: {{ $service }} + type: ClusterIP + ports: + - name: service + port: 9000 + targetPort: 9000 + - name: console + port: 9001 + targetPort: 9001 + - apiVersion: batch/v1 + kind: Job + metadata: + name: {{ printf "%s-bucket-%s" $service .Guid }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: main + image: quay.io/minio/minio + command: + - /bin/bash + - -c + - | + set -eu + mc alias set myminio http://{{ $service }}:9000 {{ $shared.instanceUsername | quote }} $MINIO_ROOT_PASSWORD + mc admin user svcacct info myminio {{ $shared.instanceAccessKeyId | quote }} || mc admin user svcacct add myminio {{ $shared.instanceUsername | quote }} --access-key {{ $shared.instanceAccessKeyId | quote }} --secret-key $MINIO_SECRET_KEY + mc mb -p myminio/{{ .State.bucket }} + env: + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: password + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: secret_key + expected_outputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key diff --git a/package.json b/package.json index 820a1d02..9fe9d006 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,14 @@ "fmt": "dprint fmt", "lint": "vale content/en/docs/**/* && dprint check", "gen-all": "yarn gen-get-external-content && yarn gen-example-pages", - "gen-get-external-content": "yarn gen-score-specification && yarn gen-score-resources-default-provisioners && yarn gen-score-resources-community-provisioners", + "gen-get-external-content": "yarn gen-score-specification && yarn gen-score-resources-default-provisioners && yarn gen-score-resources-community-provisioners && yarn gen-resource-provisioners-community && yarn gen-resource-provisioners-default-score-compose && yarn gen-resource-provisioners-default-score-k8s && yarn gen-transform-default-resource-provisioners", "gen-score-specification": "git rm -rfq gen/external-content/score/specification && git read-tree --prefix=gen/external-content/score/specification -u examples/main:specification", "gen-score-resources-default-provisioners": "git rm -rfq gen/external-content/score/resources/default-provisioners && git read-tree --prefix=gen/external-content/score/resources/default-provisioners -u examples/main:resources", "gen-score-resources-community-provisioners": "git rm -rfq gen/external-content/score/resources/community-provisioners && git read-tree --prefix=gen/external-content/score/resources/community-provisioners -u community-provisioners/main", + "gen-resource-provisioners-community": "git rm -rfq gen/external-content/resource-provisioners/community && git read-tree --prefix=gen/external-content/resource-provisioners/community -u community-provisioners/main", + "gen-resource-provisioners-default-score-compose": "git rm -rfq gen/external-content/resource-provisioners/default/score-compose && git read-tree --prefix=gen/external-content/resource-provisioners/default/score-compose -u score-compose/main:internal/command", + "gen-resource-provisioners-default-score-k8s": "git rm -rfq gen/external-content/resource-provisioners/default/score-k8s && git read-tree --prefix=gen/external-content/resource-provisioners/default/score-k8s -u score-k8s/main:internal/provisioners/default", + "gen-transform-default-resource-provisioners": "node ./gen/examples-site/transform-default-resource-provisioners.js", "gen-example-pages": "node ./gen/examples-site/gen-example-pages.js ./gen/external-content && yarn fmt" }, "version": "0.0.1" From 748f706021f57ca45720b70c305493c0ee790d07 Mon Sep 17 00:00:00 2001 From: Santiago Beroch Date: Thu, 16 Oct 2025 14:44:51 -0300 Subject: [PATCH 2/7] default provisioner generated content transformed Signed-off-by: Santiago Beroch --- ...transform-default-resource-provisioners.js | 193 +- .../default/dns/score-compose/README.md | 1 + .../dns/score-compose/provisioners.yaml | 11 + .../default/dns/score-k8s/README.md | 1 + .../default/dns/score-k8s/provisioners.yaml | 11 + .../elasticsearch/score-compose/README.md | 1 + .../score-compose/provisioners.yaml | 136 ++ .../example-provisioner/score-k8s/README.md | 1 + .../score-k8s/provisioners.yaml | 11 + .../score-compose/provisioners.yaml | 61 + .../default/mongo/score-k8s/provisioners.yaml | 156 ++ .../default/mongodb/score-compose/README.md | 1 + .../mongodb/score-compose/provisioners.yaml | 53 + .../mssql/score-compose/provisioners.yaml | 54 + .../default/mssql/score-k8s/provisioners.yaml | 133 ++ .../default/mysql/score-compose/README.md | 1 + .../mysql/score-compose/provisioners.yaml | 85 + .../default/mysql/score-k8s/provisioners.yaml | 147 ++ .../score-compose/provisioners.yaml | 58 + .../score-k8s/provisioners.yaml | 148 ++ .../default/postgres/score-compose/README.md | 1 + .../postgres/score-compose/provisioners.yaml | 97 + .../postgres/score-k8s/provisioners.yaml | 158 ++ .../default/rabbitmq/score-compose/README.md | 1 + .../rabbitmq/score-compose/provisioners.yaml | 92 + .../rabbitmq/score-k8s/provisioners.yaml | 126 ++ .../default/redis/score-compose/README.md | 1 + .../redis/score-compose/provisioners.yaml | 60 + .../default/redis/score-k8s/provisioners.yaml | 147 ++ .../default/route/score-compose/README.md | 1 + .../route/score-compose/provisioners.yaml | 104 ++ .../default/route/score-k8s/README.md | 1 + .../default/route/score-k8s/provisioners.yaml | 45 + .../default/s3/score-compose/README.md | 1 + .../s3/score-compose/provisioners.yaml | 100 + .../default/s3/score-k8s/README.md | 1 + .../default/s3/score-k8s/provisioners.yaml | 180 ++ .../default/score-compose/check_version.go | 50 - .../score-compose/check_version_test.go | 72 - .../fixtures/provisioners.custom.golden | 284 --- .../default/score-compose/generate.go | 526 ------ .../score-compose/generate_examples_test.go | 300 --- .../default/score-compose/generate_test.go | 1633 ----------------- .../default/score-compose/init.go | 316 ---- .../default/score-compose/init_test.go | 331 ---- .../default/score-compose/provisioners.go | 124 -- .../score-compose/provisioners_test.go | 117 -- .../default/score-compose/resources.go | 198 -- .../default/score-compose/resources_test.go | 163 -- .../default/score-compose/root.go | 65 - .../default/score-compose/root_test.go | 261 --- .../default/score-compose/run.go | 364 ---- .../default/score-compose/run_test.go | 430 ----- .../provisioners.list.valid.json.golden | 377 ---- .../provisioners.list.valid.table.golden | 61 - .../default/score-k8s/default.go | 22 - .../default/score-k8s/default_test.go | 30 - .../service-port/score-compose/README.md | 1 + .../score-compose/provisioners.yaml | 18 + .../default/service-port/score-k8s/README.md | 1 + .../service-port/score-k8s/provisioners.yaml | 18 + .../default/volume/score-compose/README.md | 1 + .../volume/score-compose/provisioners.yaml | 25 + .../default/volume/score-k8s/README.md | 1 + .../volume/score-k8s/provisioners.yaml | 8 + 65 files changed, 2451 insertions(+), 5725 deletions(-) create mode 100644 gen/external-content/resource-provisioners/default/dns/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/dns/score-k8s/README.md create mode 100644 gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/elasticsearch/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/README.md create mode 100644 gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/mongodb/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/mysql/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/postgres/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/rabbitmq/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/redis/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/route/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/route/score-k8s/README.md create mode 100644 gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/s3/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/s3/score-k8s/README.md create mode 100644 gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/check_version.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/check_version_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/generate.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/generate_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/init.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/init_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/provisioners.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/resources.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/resources_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/root.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/root_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/run.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/run_test.go delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden delete mode 100644 gen/external-content/resource-provisioners/default/score-k8s/default.go delete mode 100644 gen/external-content/resource-provisioners/default/score-k8s/default_test.go create mode 100644 gen/external-content/resource-provisioners/default/service-port/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/service-port/score-k8s/README.md create mode 100644 gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/volume/score-compose/README.md create mode 100644 gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml create mode 100644 gen/external-content/resource-provisioners/default/volume/score-k8s/README.md create mode 100644 gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml diff --git a/gen/examples-site/transform-default-resource-provisioners.js b/gen/examples-site/transform-default-resource-provisioners.js index 39f0aedf..6b01b2a2 100644 --- a/gen/examples-site/transform-default-resource-provisioners.js +++ b/gen/examples-site/transform-default-resource-provisioners.js @@ -1 +1,192 @@ -console.log("TODO"); +const fs = require("fs"); +const path = require("path"); +const matter = require("gray-matter"); + +const scoreComposeDir = path.join( + __dirname, + "../external-content/resource-provisioners/default/score-compose" +); +const scoreK8sDir = path.join( + __dirname, + "../external-content/resource-provisioners/default/score-k8s" +); + +const directories = [scoreComposeDir, scoreK8sDir]; + +function cleanDirectory(dirPath) { + if (!fs.existsSync(dirPath)) { + return; + } + + const items = fs.readdirSync(dirPath); + items.forEach((item) => { + const fullPath = path.join(dirPath, item); + const stats = fs.lstatSync(fullPath); + + if (stats.isDirectory()) { + fs.rmSync(fullPath, { recursive: true, force: true }); + } else if (stats.isFile()) { + const ext = path.extname(item).toLowerCase(); + if (ext !== ".yaml" && ext !== ".yml") { + fs.unlinkSync(fullPath); + } + } + }); +} + +function extractProvisionerSection(content, uri) { + const lines = content.split("\n"); + let startIdx = -1; + let endIdx = -1; + let commentStartIdx = -1; + + // Find the line with the URI + for (let i = 0; i < lines.length; i++) { + if (lines[i].includes(`- uri: ${uri}`)) { + startIdx = i; + + // Go backwards to find the start of comments + commentStartIdx = i - 1; + while ( + commentStartIdx >= 0 && + lines[commentStartIdx].trim().startsWith("#") + ) { + commentStartIdx--; + } + commentStartIdx++; // Move to the first comment line + + // Find the end of this provisioner (next "- uri:" or end of file) + for (let j = i + 1; j < lines.length; j++) { + if (lines[j].match(/^- uri:/)) { + // Found the next provisioner, now go backwards to find where its comments start + let commentStart = j - 1; + while ( + commentStart > i && + lines[commentStart].trim().startsWith("#") + ) { + commentStart--; + } + endIdx = commentStart + 1; + break; + } + } + if (endIdx === -1) { + endIdx = lines.length; + } + break; + } + } + + if (startIdx === -1) { + return { yamlContent: null, comments: null }; + } + + // Extract the provisioner YAML (without leading comments) + const yamlLines = lines.slice(startIdx, endIdx); + // Remove the leading "- " from the first line to make it an object instead of an array + // and remove 2 spaces of indentation from all subsequent lines + if (yamlLines.length > 0 && yamlLines[0].startsWith("- ")) { + yamlLines[0] = yamlLines[0].substring(2); + // Remove 2 spaces from the beginning of all other lines + for (let i = 1; i < yamlLines.length; i++) { + if (yamlLines[i].startsWith(" ")) { + yamlLines[i] = yamlLines[i].substring(2); + } + } + } + const yamlContent = yamlLines.join("\n").trim(); + + // Extract just the comments for README + const commentLines = lines.slice(commentStartIdx, startIdx); + const comments = commentLines + .map((line) => line.trim().replace(/^#\s?/, "")) + .join(" ") + .trim(); + + return { yamlContent, comments }; +} + +function splitProvisionersFile(sourceFile, baseDir, implementation) { + const content = fs.readFileSync(sourceFile, "utf8"); + + // Parse YAML to get the list of provisioners and their URIs + const parsed = matter(`---\n${content}\n---`); + const provisioners = parsed.data; + + if (!Array.isArray(provisioners)) { + console.error(`Expected array in ${sourceFile}`); + return; + } + + provisioners.forEach((provisioner, index) => { + if (!provisioner.uri) { + console.warn( + `Provisioner at index ${index} in ${sourceFile} has no uri, skipping` + ); + return; + } + + // Extract folder name from URI + const uriParts = provisioner.uri.split("/"); + let folderName = uriParts[uriParts.length - 1]; + if (!folderName) { + console.warn( + `Provisioner at index ${index} in ${sourceFile} has invalid uri, skipping` + ); + return; + } + // Handle URIs with # (e.g., cmd://bash#example-provisioner) + if (folderName.includes("#")) { + folderName = folderName.split("#")[1]; + } + + const targetDir = path.join(baseDir, "..", folderName, implementation); + + // Create directory if it doesn't exist + fs.mkdirSync(targetDir, { recursive: true }); + + const targetFile = path.join(targetDir, "provisioners.yaml"); + + // Extract the raw YAML section from the original file + const { yamlContent, comments } = extractProvisionerSection( + content, + provisioner.uri + ); + + if (yamlContent) { + fs.writeFileSync(targetFile, yamlContent, "utf8"); + console.log(`Created: ${targetFile}`); + + // Create README.md with comments + if (comments) { + const readmeFile = path.join(targetDir, "README.md"); + fs.writeFileSync(readmeFile, comments, "utf8"); + console.log(`Created: ${readmeFile}`); + } + } else { + console.warn(`Could not extract YAML for ${provisioner.uri}`); + } + }); +} + +// Clean directories first +directories.forEach((dir) => { + cleanDirectory(dir); +}); + +// Split the provisioners files +const scoreComposeYaml = path.join( + scoreComposeDir, + "default.provisioners.yaml" +); +const scoreK8sYaml = path.join(scoreK8sDir, "zz-default.provisioners.yaml"); + +if (fs.existsSync(scoreComposeYaml)) { + console.log("\nProcessing score-compose provisioners..."); + splitProvisionersFile(scoreComposeYaml, scoreComposeDir, "score-compose"); +} + +if (fs.existsSync(scoreK8sYaml)) { + console.log("\nProcessing score-k8s provisioners..."); + splitProvisionersFile(scoreK8sYaml, scoreK8sDir, "score-k8s"); +} diff --git a/gen/external-content/resource-provisioners/default/dns/score-compose/README.md b/gen/external-content/resource-provisioners/default/dns/score-compose/README.md new file mode 100644 index 00000000..69db207b --- /dev/null +++ b/gen/external-content/resource-provisioners/default/dns/score-compose/README.md @@ -0,0 +1 @@ +The default dns provisioner just outputs localhost as the hostname every time. This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This can be replaced by a new provisioner in the future. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml new file mode 100644 index 00000000..4d870dbb --- /dev/null +++ b/gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml @@ -0,0 +1,11 @@ +uri: template://default-provisioners/dns +type: dns +description: Outputs a *.localhost domain as the hostname. +init: | + randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost +state: | + instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} +outputs: | + host: {{ .State.instanceHostname }} +expected_outputs: + - host \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/dns/score-k8s/README.md b/gen/external-content/resource-provisioners/default/dns/score-k8s/README.md new file mode 100644 index 00000000..3f5de71d --- /dev/null +++ b/gen/external-content/resource-provisioners/default/dns/score-k8s/README.md @@ -0,0 +1 @@ +The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is available. You should replace this with your own dns name generation that matches your external-dns controller. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml new file mode 100644 index 00000000..4d870dbb --- /dev/null +++ b/gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml @@ -0,0 +1,11 @@ +uri: template://default-provisioners/dns +type: dns +description: Outputs a *.localhost domain as the hostname. +init: | + randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost +state: | + instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} +outputs: | + host: {{ .State.instanceHostname }} +expected_outputs: + - host \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/README.md b/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/README.md new file mode 100644 index 00000000..c68b9aa9 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/README.md @@ -0,0 +1 @@ +The default elasticsearch provisioner adds a elasticsearch instance. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml new file mode 100644 index 00000000..378b6007 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml @@ -0,0 +1,136 @@ +uri: template://default-provisioners/elasticsearch +# By default, match all elasticsearch types regardless of class and id. +# If you want to override this, create another provisioner definition with a higher priority. +type: elasticsearch +description: Provisions a dedicated Elastic Search instance. +# Init template has the random service name and password if needed later +init: | + serviceName: elasticsearch + randomPassword: {{ randAlphaNum 16 | quote }} + clusterName: cluster-ecs-{{ randAlphaNum 6 }} + username: elastic + sk: default-provisioners-elasticsearch-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "9200" .Metadata | quote }} + license: {{ dig "annotations" "compose.score.dev/license" "basic" .Metadata | quote }} + stackVersion: {{ dig "annotations" "compose.score.dev/stack-version" "8.14.0" .Metadata | quote }} + esMemLimit: {{ dig "annotations" "compose.score.dev/es-mem-limit" "1073741824" .Metadata | quote }} +# The state for each elasticsearch resource is a unique host, port, and credentials +state: | + clusterName: {{ dig "clusterName" .Init.clusterName .State | quote }} + username: {{ dig "username" .Init.username .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + host: {{ dig "host" .Init.serviceName .State | quote }} +outputs: | + host: {{ .State.host }} + port: {{ .Init.publishPort }} + username: {{ .State.username | quote }} + password: {{ .State.password | quote }} +# Ensure the data volume exists +volumes: | + ecscerts: + driver: local + ecsdata: + driver: local +# Create 2 services, the first is the setup container which creates the certificates, the second is the elasticsearch itself +services: | + setup: + image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} + volumes: + - type: volume + source: ecscerts + target: /usr/share/elasticsearch/config/certs + user: "0" + command: + - "bash" + - "-c" + - | + if [ ! -f config/certs/ca.zip ]; then + echo "Creating CA"; + bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; + unzip config/certs/ca.zip -d config/certs; + fi; + if [ ! -f config/certs/certs.zip ]; then + echo "Creating certs"; + echo -ne \ + "instances:\n"\ + " - name: {{ .State.host }}\n"\ + " dns:\n"\ + " - {{ .State.host }}\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + > config/certs/instances.yml; + bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; + unzip config/certs/certs.zip -d config/certs; + fi; + echo "Setting file permissions" + chown -R root:root config/certs; + find . -type d -exec chmod 750 \{\} \;; + find . -type f -exec chmod 640 \{\} \;; + echo "Waiting for Elasticsearch availability"; + until curl -s --cacert config/certs/ca/ca.crt https://{{ .State.host }}:9200 | grep -q "missing authentication credentials"; do sleep 10; done; + echo "All done!"; + healthcheck: + test: ["CMD-SHELL", "[ -f config/certs/{{ .State.host }}/{{ .State.host }}.crt ]"] + interval: 1s + timeout: 5s + retries: 120 + {{ .State.host }}: + depends_on: + setup: + condition: service_healthy + image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} + labels: + co.elastic.logs/module: elasticsearch + volumes: + - type: volume + source: ecscerts + target: /usr/share/elasticsearch/config/certs + - type: volume + source: ecsdata + target: /usr/share/elasticsearch/data + ports: + - target: 9200 + published: {{ .Init.publishPort }} + environment: + - node.name={{ .State.host }} + - cluster.name={{ .State.clusterName }} + - discovery.type=single-node + - bootstrap.memory_lock=true + - ELASTIC_PASSWORD={{ .State.password }} + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key + - xpack.security.http.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt + - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.enabled=true + - xpack.security.transport.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key + - xpack.security.transport.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt + - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.verification_mode=certificate + - xpack.license.self_generated.type={{ .Init.license }} + mem_limit: {{ .Init.esMemLimit }} + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", + ] + interval: 10s + timeout: 10s + retries: 120 +info_logs: | + - "{{.Uid}}: To connect to elasticsearch:\n + download certificate from container per command like: \n + \tdocker cp [CONTAINER-NAME]:/usr/share/elasticsearch/config/certs/ca/ca.crt /tmp/ \n + and than check connection per culr like: \n + \tcurl --cacert /tmp/ca.crt -u {{ .State.username }}:{{ .State.password }} https://localhost:{{ .Init.publishPort }}" +expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/README.md b/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/README.md new file mode 100644 index 00000000..ee0f8a90 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/README.md @@ -0,0 +1 @@ +The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml new file mode 100644 index 00000000..1fd3b1de --- /dev/null +++ b/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml @@ -0,0 +1,11 @@ +uri: cmd://bash#example-provisioner +type: example-provisioner-resource +description: Example provisioner that runs a bash script. +class: default +id: specific +# (Optional) additional args that the binary gets run with +# If any of the args are '' it will be replaced with "provision" +args: ["-c", "echo '{\"resource_outputs\":{\"key\":\"value\",\"secret\":\"🔐💬mysecret_mykey💬🔐\"},\"manifests\":[]}'"] +expected_outputs: + - key + - secret \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml new file mode 100644 index 00000000..4fa9a699 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml @@ -0,0 +1,61 @@ +uri: template://default-provisioners/kafka-topic +type: kafka-topic +description: Provisions a dedicated Kafka topic on a shared Kafka broker. +init: | + brokerPort: 9092 + ctrlPort: 9093 +state: | + topic: {{ dig "topic" (print "topic-" (randAlphaNum 6)) .State | quote }} +shared: | + shared_kafka_instance_name: {{ dig "shared_kafka_instance_name" (print "kafka-" (randAlphaNum 6)) .Shared | quote }} +services: | + {{ .Shared.shared_kafka_instance_name }}: + image: bitnami/kafka:latest + restart: always + environment: + KAFKA_CFG_NODE_ID: "0" + KAFKA_CFG_PROCESS_ROLES: controller,broker + KAFKA_CFG_LISTENERS: "PLAINTEXT://:{{ .Init.brokerPort }},CONTROLLER://:{{ .Init.ctrlPort }}" + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "0@{{ .Shared.shared_kafka_instance_name }}:{{ .Init.ctrlPort }}" + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "false" + healthcheck: + test: ["CMD", "kafka-topics.sh", "--list", "--bootstrap-server=localhost:{{ .Init.brokerPort }}"] + interval: 2s + timeout: 2s + retries: 10 + {{ $publishPort := (dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi) }} + {{ if ne $publishPort 0 }} + ports: + - target: {{ .Init.brokerPort }} + published: {{ $publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .Shared.shared_kafka_instance_name }}-data + target: /bitnami/kafka + {{ .State.topic }}-init: + image: bitnami/kafka:latest + entrypoint: ["/bin/sh"] + command: ["-c", "kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --describe || kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --create --partitions=3"] + network_mode: "service:{{ .Shared.shared_kafka_instance_name }}" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ .Shared.shared_kafka_instance_name }}: + condition: service_healthy + restart: true +volumes: | + {{ .Shared.shared_kafka_instance_name }}-data: + driver: local +outputs: | + host: {{ .Shared.shared_kafka_instance_name }} + port: "{{ .Init.brokerPort }}" + name: {{ .State.topic }} + num_partitions: 3 +expected_outputs: + - host + - port + - name + - num_partitions \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml new file mode 100644 index 00000000..0e2c3d8b --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml @@ -0,0 +1,156 @@ +uri: template://default-provisioners/mongo +type: mongodb +description: Provisions a dedicated MongoDB database. +init: | + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + service: mongo-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ .State.service }} + port: 27017 + connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.service }}:27017/" + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MONGO_INITDB_ROOT_PASSWORD" }} +manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: mongo-db + image: mirror.gcr.io/mongo:8 + ports: + - name: mongo + containerPort: 27017 + env: + - name: MONGO_INITDB_ROOT_USERNAME + value: {{ .State.username | quote }} + - name: MONGO_INITDB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MONGO_INITDB_ROOT_PASSWORD + livenessProbe: + exec: + command: + - /bin/sh + - -c + - echo 'db.runCommand("ping").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 20 + securityContext: + runAsUser: 1001 + runAsGroup: 1001 + allowPrivilegeEscalation: false + privileged: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data/db + - name: tmp + mountPath: /tmp + securityContext: + runAsNonRoot: true + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault + volumes: + - name: tmp + emptyDir: {} + volumeClaimTemplates: + - metadata: + name: data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 27017 + targetPort: 27017 +expected_outputs: + - host + - port + - username + - password + - connection \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mongodb/score-compose/README.md b/gen/external-content/resource-provisioners/default/mongodb/score-compose/README.md new file mode 100644 index 00000000..acd2d512 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mongodb/score-compose/README.md @@ -0,0 +1 @@ +The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml new file mode 100644 index 00000000..38eec046 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml @@ -0,0 +1,53 @@ +uri: template://default-provisioners/mongodb +type: mongodb +description: Provisions a dedicated MongoDB database. +init: | + port: 27017 + randomServiceName: mongo-{{ randAlphaNum 6 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ .State.serviceName }} + port: {{ .Init.port }} + username: {{ .State.username | quote }} + password: {{ .State.password | quote }} + connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.serviceName }}:{{ .Init.port }}/" +volumes: | + {{ .State.serviceName }}-data: + name: {{ .State.serviceName }}-data + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} +services: | + {{ .State.serviceName }}: + labels: + dev.score.compose.res.uid: {{ .Uid }} + image: mirror.gcr.io/mongo:8 + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: {{ .State.username | quote }} + MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | quote }} + healthcheck: + test: ["CMD-SHELL", "echo 'db.runCommand(\"ping\").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD"] + interval: 2s + timeout: 5s + retries: 15 + start_period: 10s + volumes: + - type: volume + source: {{ .State.serviceName }}-data + target: /data/db + volume: + nocopy: true +info_logs: | + - "{{.Uid}}: To connect to mongo: \"docker exec -ti {{ .ComposeProjectName }}-{{ .State.serviceName }}-1 mongosh -u {{ .State.username }} -p {{ .State.password }}\"" +expected_outputs: + - host + - port + - username + - password + - connection \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml new file mode 100644 index 00000000..bfbdafec --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml @@ -0,0 +1,54 @@ +uri: template://default-provisioners/mssql +type: mssql +description: Provisions a dedicated database on a shared MS SQL server instance. +init: | + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-mssql + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} +state: | + service: {{ .Init.sk }} + database: master + username: sa + password: {{ dig "password" .Init.randomPassword .State | quote }} + publishPort: {{ .Init.publishPort }} +outputs: | + server: {{ .State.service }} + port: {{ .State.publishPort }} + connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};" + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} +volumes: | + {{ .Init.sk }}-data: + driver: local +services: | + {{ .Init.sk }}: + image: mcr.microsoft.com/mssql/server:latest + restart: always + environment: + ACCEPT_EULA: "Y" + MSSQL_ENABLE_HADR: "1" + MSSQL_AGENT_ENABLED: "1" + MSSQL_SA_PASSWORD: {{ .State.password }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 1433 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .Init.sk }}-data + target: /var/opt/mssql +info_logs: | + - "{{.Uid}}: To connect to mssql: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mcr.microsoft.com/mssql/server:latest mysql /opt/mssql-tools/bin/sqlcmd -S localhost -U {{ .State.username }} -p {{ .State.password | squote }}\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your mssql client to \" + Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};\"" + {{ end }} +expected_outputs: + - server + - port + - connection + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml new file mode 100644 index 00000000..1d816f7f --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml @@ -0,0 +1,133 @@ +uri: template://default-provisioners/mssql +type: mssql +description: Provisions a dedicated database on a shared MS SQL server instance. +init: | + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + service: mssql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: master + username: sa + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + server: {{ .State.service }} + port: 1433 + connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }}" + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }} +manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + MSSQL_SA_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + containers: + - name: mssql-db + image: mcr.microsoft.com/mssql/server:latest + ports: + - name: mssql + containerPort: 1433 + env: + - name: ACCEPT_EULA + value: "Y" + - name: MSSQL_ENABLE_HADR + value: "1" + - name: MSSQL_AGENT_ENABLED + value: "1" + - name: MSSQL_SA_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MSSQL_SA_PASSWORD + volumeMounts: + - name: mssql + mountPath: "/var/opt/mssql" + volumeClaimTemplates: + - metadata: + name: mssql + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 1433 + targetPort: 1433 +expected_outputs: + - server + - port + - connection + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mysql/score-compose/README.md b/gen/external-content/resource-provisioners/default/mysql/score-compose/README.md new file mode 100644 index 00000000..4490c5d0 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mysql/score-compose/README.md @@ -0,0 +1 @@ +The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on startup. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml new file mode 100644 index 00000000..a65f3c6b --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml @@ -0,0 +1,85 @@ +uri: template://default-provisioners/mysql +# By default, match all mysql types regardless of class and id. If you want to override this, create another +# provisioner definition with a higher priority. +type: mysql +description: Provisions a dedicated MySQL database on a shared instance. +# Init template has the random service name and password if needed later +init: | + randomServiceName: mysql-{{ randAlphaNum 6 }} + randomDatabase: db{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + randomRootPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-mysql-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} +# The state for each database resource is a unique db name and credentials +state: | + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +# All instances agree on the shared state since there is no concurrency here +shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + instanceRootPassword: {{ dig .Init.sk "instanceRootPassword" .Init.randomRootPassword .Shared | quote }} +# The outputs are the core database outputs. We output both name and database for broader compatibility. +outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 3306 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} +# Write out an idempotent create script per database +files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | + CREATE DATABASE IF NOT EXISTS {{ .State.database }}; + USE {{ .State.database }}; + CREATE USER IF NOT EXISTS '{{ .State.username }}'@'localhost' IDENTIFIED BY '{{ .State.password }}'; + GRANT ALL PRIVILEGES ON {{ .State.database }} TO '{{ .State.username }}'@'localhost'; + FLUSH PRIVILEGES; +# Ensure the data volume exists +volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local +# Create an services with the database itself +services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/mysql:8 + restart: always + environment: + MYSQL_DATABASE: {{ .State.database }} + MYSQL_USER: {{ .State.username }} + MYSQL_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + MYSQL_ROOT_PASSWORD: {{ dig .Init.sk "instanceRootPassword" "" .Shared | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 3306 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /docker-entrypoint-initdb.d/ + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p$${MYSQL_ROOT_PASSWORD}"] + interval: 5s + timeout: 3s + retries: 10 +info_logs: | + - "{{.Uid}}: To connect to mysql, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mysql:8 mysql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -u {{ .State.username }} -p\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your mysql client to \" + mysql://{{ .State.username }}:{{ .State.password }}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:{{ .Init.publishPort }}/{{ .State.database }}\"" + {{ end }} +expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml new file mode 100644 index 00000000..129685b2 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml @@ -0,0 +1,147 @@ +uri: template://default-provisioners/mysql +type: mysql +description: Provisions a dedicated MySQL database on a shared instance. +init: | + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + service: mysql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ .State.service }} + port: 3306 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MYSQL_PASSWORD" }} +manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + MYSQL_PASSWORD: {{ .State.password | b64enc }} + MYSQL_ROOT_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + containers: + - name: mysql-db + image: mirror.gcr.io/mysql:8 + ports: + - name: mysql + containerPort: 3306 + env: + - name: MYSQL_USER + value: {{ .State.username | quote }} + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MYSQL_PASSWORD + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + value: {{ .State.database | quote }} + volumeMounts: + - name: data + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + periodSeconds: 3 + volumeClaimTemplates: + - metadata: + name: data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 3306 + targetPort: 3306 +expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml new file mode 100644 index 00000000..8dd91204 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml @@ -0,0 +1,58 @@ +uri: template://default-provisioners/postgres-instance + +type: postgres-instance +description: Provisions a dedicated PostgreSQL instance. +# Init template has the random service name and password if needed later +init: | + randomServiceName: pg-{{ randAlphaNum 6 }} + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-postgres-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} +# The state for each database resource is a unique db name and credentials +state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + database: "postgres" + username: "postgres" + password: {{ dig "password" .Init.randomPassword .State | quote }} +# The outputs are the core database outputs. We output both name and database for broader compatibility. +outputs: | + host: {{ .State.serviceName }} + port: 5432 + username: postgres + password: {{ .State.password }} +# Ensure the data volume exists +volumes: | + {{ .State.serviceName }}-data: + driver: local +# Create 2 services, the first is the database itself, the second is the init container which runs the scripts +services: | + {{ .State.serviceName }}: + image: mirror.gcr.io/postgres:17-alpine + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: {{ .State.password | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 5432 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .State.serviceName }}-data + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + +info_logs: | + - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ .State.serviceName }} -U {{ .State.username }} --dbname {{ .State.database }}\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" + {{ end }} +expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml new file mode 100644 index 00000000..52302923 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml @@ -0,0 +1,148 @@ +uri: template://default-provisioners/postgres-instance +type: postgres-instance +description: Provisions a dedicated PostgreSQL instance. +init: | + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ .State.service }} + port: 5432 + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} +manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: postgres-db + image: mirror.gcr.io/postgres:17-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + - name: POSTGRES_USER + value: {{ .State.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: password + volumeMounts: + - name: pv-data + mountPath: /var/lib/postgresql/data + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .State.username | quote }} + periodSeconds: 3 + securityContext: + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: pv-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 +expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres/score-compose/README.md b/gen/external-content/resource-provisioners/default/postgres/score-compose/README.md new file mode 100644 index 00000000..dbfd4852 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/postgres/score-compose/README.md @@ -0,0 +1 @@ +The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on startup. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml new file mode 100644 index 00000000..48935a6c --- /dev/null +++ b/gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml @@ -0,0 +1,97 @@ +uri: template://default-provisioners/postgres +# By default, match all redis types regardless of class and id. If you want to override this, create another +# provisioner definition with a higher priority. +type: postgres +description: Provisions a dedicated database on a shared PostgreSQL instance. +# Init template has the random service name and password if needed later +init: | + randomServiceName: pg-{{ randAlphaNum 6 }} + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-postgres-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} +# The state for each database resource is a unique db name and credentials +state: | + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +# All instances agree on the shared state since there is no concurrency here +shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} +# The outputs are the core database outputs. We output both name and database for broader compatibility. +outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 5432 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} +# Write out an idempotent create script per database +files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | + SELECT 'CREATE DATABASE "{{ .State.database }}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{{ .State.database }}')\gexec + SELECT $$CREATE USER "{{ .State.username }}" WITH PASSWORD '{{ .State.password }}'$$ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .State.username }}')\gexec + GRANT ALL PRIVILEGES ON DATABASE "{{ .State.database }}" TO "{{ .State.username }}"; + \connect "{{ .State.database }}"; + GRANT ALL ON SCHEMA public TO "{{ .State.username }}"; +# Ensure the data volume exists +volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local +# Create 2 services, the first is the database itself, the second is the init container which runs the scripts +services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/postgres:17-alpine + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 5432 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + interval: 2s + timeout: 2s + retries: 15 + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: mirror.gcr.io/postgres:17-alpine + entrypoint: ["/bin/sh"] + environment: + POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + command: + - "-c" + - | + cd /db-scripts + ls db-*.sql | xargs cat | psql "postgresql://postgres:$${POSTGRES_PASSWORD}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:5432/postgres" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /db-scripts +info_logs: | + - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -U {{ .State.username }} --dbname {{ .State.database }}\"" + {{ if ne .Init.publishPort "0" }} + - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" + {{ end }} +expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml new file mode 100644 index 00000000..75103f05 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml @@ -0,0 +1,158 @@ +uri: template://default-provisioners/postgres +type: postgres +description: Provisions a dedicated database on a shared PostgreSQL instance. +init: | + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ .State.service }} + port: 5432 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} +manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: postgres-db + image: mirror.gcr.io/postgres:17-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + - name: POSTGRES_USER + value: {{ .State.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: password + - name: POSTGRES_DB + value: {{ .State.database | quote }} + volumeMounts: + - name: pv-data + mountPath: /var/lib/postgresql/data + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .State.username | quote }} + - -d + - {{ .State.database | quote }} + periodSeconds: 3 + securityContext: + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: pv-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 +expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/README.md b/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/README.md new file mode 100644 index 00000000..e6996d36 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/README.md @@ -0,0 +1 @@ +The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml new file mode 100644 index 00000000..cccf3255 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml @@ -0,0 +1,92 @@ +uri: template://default-provisioners/rabbitmq +type: amqp +description: Provisions a dedicated RabbitMQ vhost on a shared instance. +init: | + randomServiceName: rabbitmq-{{ randAlphaNum 6 }} + randomVHost: vhost-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-rabbitmq + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} + publishManagementPort: {{ dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi }} +state: | + vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 5672 + vhost: {{ .State.vhost }} + username: {{ .State.username }} + password: {{ .State.password }} +shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instanceErlangCookie: {{ dig .Init.sk "instanceErlangCookie" (randAlpha 20) .Shared }} + {{ $publishPorts := (list) }} + {{ if ne .Init.publishPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 5672 "published" .Init.publishPort)) }}{{ end }} + {{ $x := (dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi) }} + {{ if ne .Init.publishManagementPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 15672 "published" .Init.publishManagementPort)) }}{{ end }} + publishPorts: {{ $publishPorts | toJson }} +volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local +files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.vhost }}.sh: | + while ! rabbitmqctl list_vhosts > /dev/null 2>&1; do + sleep 1 + done + rabbitmqctl list_vhosts | grep {{ .State.vhost }} || rabbitmqctl add_vhost {{ .State.vhost }} + rabbitmqctl list_users | grep {{ .State.username }} || rabbitmqctl add_user {{ .State.username }} {{ .State.password }} + rabbitmqctl set_user_tags {{ .State.username }} administrator + rabbitmqctl set_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" + rabbitmqctl set_topic_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" +services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/rabbitmq:3-management-alpine + restart: always + environment: + RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + ports: {{ dig .Init.sk "publishPorts" "" .Shared | toJson}} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/rabbitmq + healthcheck: + test: ["CMD-SHELL", "rabbitmq-diagnostics -q check_port_connectivity"] + interval: 2s + timeout: 5s + retries: 15 + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: mirror.gcr.io/rabbitmq:3-management-alpine + entrypoint: ["/bin/sh"] + environment: + RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} + command: + - "-c" + - | + set -exu + for s in /db-scripts/*.sh; do source $$s; done + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + labels: + dev.score.compose.labels.is-init-container: "true" + network_mode: service:{{ dig .Init.sk "instanceServiceName" "" .Shared }} + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /db-scripts +info_logs: | + {{ if ne .Init.publishManagementPort 0 }} + - "{{.Uid}}: Browse the rabbitmq UI at \"http://localhost:{{ .Init.publishManagementPort }}\"" + {{ end }} +expected_outputs: + - host + - port + - vhost + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml new file mode 100644 index 00000000..97d574fd --- /dev/null +++ b/gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml @@ -0,0 +1,126 @@ +uri: template://default-provisioners/rabbitmq +type: amqp +description: Provisions a dedicated RabbitMQ vhost on a shared instance. +init: | + randomVHost: vhost-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + service: rabbitmq-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ .State.service }} + port: 5672 + vhost: {{ .State.vhost }} + username: {{ .State.username }} + password: {{ .State.password }} +manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }}-secret + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }}-secret + app.kubernetes.io/instance: {{ .State.service }} + data: + RABBITMQ_DEFAULT_VHOST: {{ .State.vhost | b64enc }} + RABBITMQ_DEFAULT_USER: {{ .State.username | b64enc }} + RABBITMQ_DEFAULT_PASS: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + serviceName: {{ .State.service }} + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + containers: + - name: rabbitmq + image: mirror.gcr.io/rabbitmq:3-management-alpine + ports: + - name: amqp + containerPort: 5672 + - name: management + containerPort: 15672 + envFrom: + - secretRef: + name: {{ .State.service }}-secret + volumeMounts: + - name: data + mountPath: /var/lib/rabbitmq + readinessProbe: + exec: + command: + - rabbitmq-diagnostics + - -q + - check_port_connectivity + periodSeconds: 3 + initialDelaySeconds: 30 + timeoutSeconds: 5 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 3Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + ports: + - port: 5672 + targetPort: 5672 + name: amqp + - port: 15672 + targetPort: 15672 + name: management + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP +expected_outputs: + - host + - port + - vhost + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/redis/score-compose/README.md b/gen/external-content/resource-provisioners/default/redis/score-compose/README.md new file mode 100644 index 00000000..3b9f84c7 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/redis/score-compose/README.md @@ -0,0 +1 @@ +The default redis provisioner adds a redis service to the project which returns a host, port, username, and password. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml new file mode 100644 index 00000000..6c6d9760 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml @@ -0,0 +1,60 @@ +uri: template://default-provisioners/redis +# By default, match all redis types regardless of class and id. If you want to override this, create another +# provisioner definition with a higher priority. +type: redis +description: Provisions a dedicated Redis instance. +# Init template has the default port and a random service name and password if needed later +init: | + port: 6379 + randomServiceName: redis-{{ randAlphaNum 6 }} + randomPassword: {{ randAlphaNum 16 | quote }} +# The only state we need to persist is the chosen random service name and password +state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} +# Return the outputs schema that consumers expect +outputs: | + host: {{ .State.serviceName }} + port: {{ .Init.port }} + username: default + password: {{ .State.password | quote }} +# write the config file to the mounts directory +files: | + {{ .State.serviceName }}/redis.conf: | + requirepass {{ .State.password }} + port {{ .Init.port }} + save 60 1 + loglevel warning +# add a volume for persistence of the redis data +volumes: | + {{ .State.serviceName }}-data: + name: {{ .State.serviceName }}-data + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} +# And the redis service itself with volumes bound in +services: | + {{ .State.serviceName }}: + labels: + dev.score.compose.res.uid: {{ .Uid }} + image: mirror.gcr.io/redis:7-alpine + restart: always + entrypoint: ["redis-server"] + command: ["/usr/local/etc/redis/redis.conf"] + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ .State.serviceName }}/redis.conf + target: /usr/local/etc/redis/redis.conf + read_only: true + - type: volume + source: {{ .State.serviceName }}-data + target: /data + volume: + nocopy: true +info_logs: | + - "{{.Uid}}: To connect to redis: \"docker run -it --network {{ .ComposeProjectName }}_default --rm redis redis-cli -h {{ .State.serviceName | squote }} -a {{ .State.password | squote }}\"" +expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml new file mode 100644 index 00000000..da0920f3 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml @@ -0,0 +1,147 @@ +uri: template://default-provisioners/redis +type: redis +description: Provisions a dedicated Redis instance. +init: | + randomPassword: {{ randAlphaNum 16 | quote }} +state: | + service: redis-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: default + password: {{ dig "password" .Init.randomPassword .State | quote }} +outputs: | + host: {{ .State.service }} + port: 6379 + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} +manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + redis.conf: {{ printf "requirepass %s\nport 6379\nsave 60 1\nloglevel warning\n" .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ .State.service }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: redis + image: mirror.gcr.io/redis:7-alpine + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + ports: + - name: redis + containerPort: 6379 + volumeMounts: + - name: redis-data + mountPath: /data + - name: config + mountPath: /usr/local/etc/redis + readinessProbe: + exec: + command: + - redis-cli + - ping + periodSeconds: 3 + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumes: + - name: config + secret: + secretName: {{ .State.service }} + items: + - key: redis.conf + path: redis.conf + volumeClaimTemplates: + - metadata: + name: redis-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 6379 + targetPort: 6379 +expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/route/score-compose/README.md b/gen/external-content/resource-provisioners/default/route/score-compose/README.md new file mode 100644 index 00000000..027715e4 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/route/score-compose/README.md @@ -0,0 +1 @@ +The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml new file mode 100644 index 00000000..5b7d48c2 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml @@ -0,0 +1,104 @@ +uri: template://default-provisioners/route +type: route +description: Provisions a ingress route on a shared Nginx instance. +init: | + randomServiceName: routing-{{ randAlphaNum 6 }} + sk: default-provisioners-routing-instance + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + +shared: | + {{ .Init.sk }}: + instancePort: {{ dig .Init.sk "instancePort" 8080 .Shared }} + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + {{ $targetHost := (index .WorkloadServices .SourceWorkload).ServiceName }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ $port := index $ports (print .Params.port) }} + {{ $targetPort := $port.TargetPort }} + {{ $target := (printf "%s:%d" $targetHost $targetPort) }} + {{ $hBefore := dig .Init.sk "hosts" (dict) .Shared }} + {{ $rBefore := dig .Params.host (dict) $hBefore }} + {{ $pathType := dig "compose.score.dev/route-provisioner-path-type" "Prefix" (dig "annotations" (dict) (.Metadata | default (dict))) }} + {{ $inner := dict "path" .Params.path "target" $target "port" $targetPort "path_type" $pathType }} + {{ $rAfter := (merge $rBefore (dict .Uid $inner)) }} + {{ $hAfter := (merge $hBefore (dict .Params.host $rAfter)) }} + hosts: {{ $hAfter | toRawJson }} +files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf: | + worker_processes 1; + worker_rlimit_nofile 8192; + events { + worker_connections 4096; + } + http { + resolver 127.0.0.11; + + {{ range $h, $r := (dig .Init.sk "hosts" "" .Shared) }} + server { + listen 80; + listen [::]:80; + server_name {{ $h }}; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; + proxy_set_header Proxy ""; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + proxy_buffers 16 4k; + proxy_buffer_size 2k; + client_max_body_size 10m; + {{ dig "compose.score.dev/route-provisioner-server-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 4 }} + + location = /favicon.ico { + return 204; + access_log off; + log_not_found off; + } + + {{ range $k, $v := $r }} + # the basic path variant, "/" or "/one/two" + location ~ ^{{ index $v "path" }}$ { + set $backend {{ index $v "target" }}; + proxy_pass http://$backend; + {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} + } + + # The prefix match variants are included by default but can be excluded via 'compose.score.dev/route-provisioner-path-type' annotation + {{ if eq (index $v "path_type") "Prefix" }} + location ~ ^{{ index $v "path" | trimSuffix "/" }}/.* { + set $backend {{ index $v "target" }}; + proxy_pass http://$backend; + {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} + } + {{ end }} + {{ end }} + } + {{ end }} + } + +services: | + {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/nginx:1-alpine + restart: always + ports: + - published: {{ $p }} + target: 80 + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf + target: /etc/nginx/nginx.conf + readOnly: true +info_logs: | + {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} + - "{{.Uid}}: To connect to this route, http://{{ .Params.host }}:{{ $p }}{{ .Params.path }} (make sure {{ .Params.host }} resolves to localhost)" +supported_params: + - host + - port + - path \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/route/score-k8s/README.md b/gen/external-content/resource-provisioners/default/route/score-k8s/README.md new file mode 100644 index 00000000..d9fb30ce --- /dev/null +++ b/gen/external-content/resource-provisioners/default/route/score-k8s/README.md @@ -0,0 +1 @@ +Routes could be implemented as either traditional ingress resources or using the newer gateway API. In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml new file mode 100644 index 00000000..cae025b6 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml @@ -0,0 +1,45 @@ +uri: template://default-provisioners/route +type: route +description: Provisions an HTTPRoute on a shared Nginx instance. +init: | + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} +state: | + routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} +manifests: | + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: {{ .State.routeName }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.routeName }} + app.kubernetes.io/instance: {{ .State.routeName }} + spec: + parentRefs: + - name: default + hostnames: + - {{ .Params.host | quote }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ .Params.path | quote }} + backendRefs: + - name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} + port: {{ .Params.port }} +supported_params: + - host + - port + - path \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/s3/score-compose/README.md b/gen/external-content/resource-provisioners/default/s3/score-compose/README.md new file mode 100644 index 00000000..4845e478 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/s3/score-compose/README.md @@ -0,0 +1 @@ +This resource provides a minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. If the provider has a publish port annotation, it can expose a management port on the local network for debugging and connectivity. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml new file mode 100644 index 00000000..b19ee83d --- /dev/null +++ b/gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml @@ -0,0 +1,100 @@ +uri: template://default-provisioners/s3 +type: s3 +description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. +# The init template contains some initial seed data that can be used it needed. +init: | + randomServiceName: minio-{{ randAlphaNum 6 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + randomBucket: bucket-{{ randAlpha 8 | lower }} + randomAccessKeyId: {{ randAlphaNum 20 | quote }} + randomSecretKey: {{ randAlphaNum 40 | quote }} + sk: default-provisioners-minio-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} +# The only instance state is the bucket name, for now we provision a single aws key across s3 resources. +state: | + bucket: {{ dig "bucket" .Init.randomBucket .State | quote }} +# The shared state contains the chosen service name and credentials +shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instanceUsername: {{ dig .Init.sk "instanceUsername" .Init.randomUsername .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" .Init.randomAccessKeyId .Shared | quote }} + instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" .Init.randomSecretKey .Shared | quote }} + publishPort: {{ with (dig .Init.sk "publishPort" 0 .Shared) }}{{ if ne . 0 }}{{ . }}{{ else }}{{ $.Init.publishPort }}{{ end }}{{ end }} +# the outputs that we can expose +outputs: | + bucket: {{ .State.bucket }} + access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} + secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + endpoint: http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 + # for compatibility with Humanitec's existing s3 resource + region: "us-east-1" + aws_access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} + aws_secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} +# we store 2 files, 1 is always the same and overridden, the other is per bucket +files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/00-svcacct.sh: | + set -eu + mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} + mc admin user svcacct info myminio {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} || mc admin user svcacct add myminio {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} --access-key {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} --secret-key {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/10-bucket-{{ .State.bucket }}.sh: | + set -eu + mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} + mc mb -p myminio/{{ .State.bucket }} +volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local +# 2 services, the minio one, and the init container which ensures the service account and buckets exist +services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: quay.io/minio/minio + command: ["server", "/data", "--console-address", ":9001"] + restart: always + {{ if ne .Init.publishPort 0 }} + ports: + - target: 9001 + published: {{ .Init.publishPort }} + {{ end }} + healthcheck: + test: ["CMD-SHELL", "mc alias set myminio http://localhost:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }}"] + interval: 2s + timeout: 2s + retries: 15 + environment: + MINIO_ROOT_USER: {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} + MINIO_ROOT_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /data + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: quay.io/minio/minio + entrypoint: ["/bin/bash"] + command: + - "-c" + - "for s in $$(ls /setup-scripts -1); do sh /setup-scripts/$$s; done" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts + target: /setup-scripts +info_logs: | + - "{{.Uid}}: To connect with a minio client: use the myminio alias at \"docker run -it --network {{ .ComposeProjectName }}_default --rm --entrypoint /bin/bash quay.io/minio/minio -c 'mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceAccessKeyId" "" .Shared }} {{ dig .Init.sk "instanceSecretKey" "" .Shared }}; bash'\"" + {{ if ne .Init.publishPort 0 }} + - "{{.Uid}}: Or enter {{ dig .Init.sk "instanceUsername" "" .Shared }} / {{ dig .Init.sk "instancePassword" "" .Shared }} at https://localhost:{{ .Init.publishPort }}" + {{ end }} +expected_outputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/s3/score-k8s/README.md b/gen/external-content/resource-provisioners/default/s3/score-k8s/README.md new file mode 100644 index 00000000..e81cfcc6 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/s3/score-k8s/README.md @@ -0,0 +1 @@ +This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml new file mode 100644 index 00000000..afc42b05 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml @@ -0,0 +1,180 @@ +uri: template://default-provisioners/s3 +type: s3 +description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. +# The init template contains some initial seed data that can be used t needed. +init: | + sk: default-provisioners-minio-instance +state: | + bucket: {{ dig "bucket" (printf "bucket-%s" .Guid) .State | quote }} +shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" (randAlpha 7 | lower | printf "minio-%s") .Shared | quote }} + instanceUsername: {{ dig .Init.sk "instanceUsername" (randAlpha 7 | printf "user-%s") .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" (randAlphaNum 16) .Shared | quote }} + instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" (randAlphaNum 20) .Shared | quote }} + instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" (randAlphaNum 40) .Shared | quote }} +outputs: | + {{ $shared := dig .Init.sk (dict) .Shared }} + {{ $service := $shared.instanceServiceName }} + bucket: {{ .State.bucket }} + access_key_id: {{ $shared.instanceAccessKeyId | quote }} + secret_key: {{ encodeSecretRef $service "secret_key" }} + endpoint: http://{{ $service }}:9000 + region: "us-east-1" + # for compatibility with Humanitec's existing s3 resource + aws_access_key_id: {{ $shared.instanceAccessKeyId | quote }} + aws_secret_key: {{ encodeSecretRef $service "secret_key" }} +manifests: | + {{ $shared := dig .Init.sk (dict) .Shared }} + {{ $service := $shared.instanceServiceName }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ $service | quote }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + replicas: 1 + serviceName: {{ $service | quote }} + selector: + matchLabels: + app.kubernetes.io/instance: {{ $service | quote }} + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + automountServiceAccountToken: false + containers: + - name: minio + image: quay.io/minio/minio + args: ["server", "/data", "--console-address", ":9001"] + ports: + - name: service + containerPort: 9000 + - name: console + containerPort: 9001 + env: + - name: MINIO_ROOT_USER + value: {{ $shared.instanceUsername | quote }} + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: password + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: data + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Secret + metadata: + name: {{ $service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service }} + app.kubernetes.io/instance: {{ $service }} + data: + password: {{ $shared.instancePassword | b64enc }} + secret_key: {{ $shared.instanceSecretKey | b64enc }} + - apiVersion: v1 + kind: Service + metadata: + name: {{ $service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service }} + app.kubernetes.io/instance: {{ $service }} + spec: + selector: + app.kubernetes.io/instance: {{ $service }} + type: ClusterIP + ports: + - name: service + port: 9000 + targetPort: 9000 + - name: console + port: 9001 + targetPort: 9001 + - apiVersion: batch/v1 + kind: Job + metadata: + name: {{ printf "%s-bucket-%s" $service .Guid }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: main + image: quay.io/minio/minio + command: + - /bin/bash + - -c + - | + set -eu + mc alias set myminio http://{{ $service }}:9000 {{ $shared.instanceUsername | quote }} $MINIO_ROOT_PASSWORD + mc admin user svcacct info myminio {{ $shared.instanceAccessKeyId | quote }} || mc admin user svcacct add myminio {{ $shared.instanceUsername | quote }} --access-key {{ $shared.instanceAccessKeyId | quote }} --secret-key $MINIO_SECRET_KEY + mc mb -p myminio/{{ .State.bucket }} + env: + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: password + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: secret_key +expected_outputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/score-compose/check_version.go b/gen/external-content/resource-provisioners/default/score-compose/check_version.go deleted file mode 100644 index 48b9cff4..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/check_version.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "github.com/spf13/cobra" - - "github.com/score-spec/score-compose/internal/version" -) - -var checkVersionCmd = &cobra.Command{ - Use: "check-version [constraint]", - Short: "Assert that the version of score-compose matches the required constraint", - Long: `score-compose is commonly used in Makefiles and CI pipelines which may depend on a particular functionality -or a particular default provisioner provided by score-compose init. This command provides a common way to check that -the version of score-compose matches a required version. -`, - Example: ` - # check that the version is exactly 1.2.3 - score-compose check-version =v1.2.3 - - # check that the version is 1.3.0 or greater - score-compose check-version >v1.2 - - # check that the version is equal or greater to 1.2.3 - score-compose check-version >=1.2.3`, - Args: cobra.ExactArgs(1), - SilenceErrors: true, - CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - return version.AssertVersion(args[0], version.Version) - }, -} - -func init() { - rootCmd.AddCommand(checkVersionCmd) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/check_version_test.go b/gen/external-content/resource-provisioners/default/score-compose/check_version_test.go deleted file mode 100644 index 07f7d909..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/check_version_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCheckVersionHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `score-compose is commonly used in Makefiles and CI pipelines which may depend on a particular functionality -or a particular default provisioner provided by score-compose init. This command provides a common way to check that -the version of score-compose matches a required version. - -Usage: - score-compose check-version [constraint] [flags] - -Examples: - - # check that the version is exactly 1.2.3 - score-compose check-version =v1.2.3 - - # check that the version is 1.3.0 or greater - score-compose check-version >v1.2 - - # check that the version is equal or greater to 1.2.3 - score-compose check-version >=1.2.3 - -Flags: - -h, --help help for check-version - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) - - stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "check-version"}) - assert.NoError(t, err) - assert.Equal(t, stdout, stdout2) - assert.Equal(t, "", stderr) -} - -func TestCheckVersionPass(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">=0.0.0"}) - assert.NoError(t, err) - assert.Equal(t, stdout, "") - assert.Equal(t, "", stderr) -} - -func TestCheckVersionFail(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"check-version", ">99"}) - assert.EqualError(t, err, "current version 0.0.0 does not match requested constraint >99") - assert.Equal(t, stdout, "") - assert.Equal(t, "", stderr) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden b/gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden deleted file mode 100644 index 8cdb8f94..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/fixtures/provisioners.custom.golden +++ /dev/null @@ -1,284 +0,0 @@ -## TEMPLATE PROVISIONERS ## - -- uri: template://test-provisioners/without-class-without-params - type: template-without-class-without-params - expected_outputs: - - o1 - - o2 - -- uri: template://test-provisioners/with-class-without-params - type: template-with-class-without-params - class: c1 - expected_outputs: - - o1 - - o2 - -- uri: template://test-provisioners/without-class-with-params-in-outputs - type: template-without-class-with-params-in-outputs - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - -- uri: template://test-provisioners/with-class-with-params-in-outputs - type: template-with-class-with-params-in-outputs - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - -- uri: template://test-provisioners/without-class-with-params-in-shared - type: template-without-class-with-params-in-shared - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - psh1 - -- uri: template://test-provisioners/with-class-with-params-in-shared - type: template-with-class-with-params-in-shared - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - psh1 - -- uri: template://test-provisioners/without-class-with-params-in-state - type: template-without-class-with-params-in-state - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - psh1 - -- uri: template://test-provisioners/with-class-with-params-in-state - type: template-with-class-with-params-in-state - class: c1 - outputs: | - o1: o1 - o2: o2 - state: | - {{ if not .Params.p1 }}{{ fail "expected 'p1' param for the target workload name" }}{{ end }} - {{ if not .Params.pst1 }}{{ fail "expected 'pst1' param for the target workload name" }}{{ end }} - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - pst1 - -- uri: template://test-provisioners/without-class-with-params-in-shared-outputs - type: template-without-class-with-params-in-shared-outputs - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - psh1 - -- uri: template://test-provisioners/with-class-with-params-in-shared-outputs - type: template-with-class-with-params-in-shared-outputs - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - psh1 - -- uri: template://test-provisioners/without-class-with-params-in-state-outputs - type: template-without-class-with-params-in-state-outputs - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - -- uri: template://test-provisioners/with-class-with-params-in-state-outputs - type: template-with-class-with-params-in-state-outputs - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - -- uri: template://test-provisioners/without-class-with-params-in-state-outputs-state-shared - type: template-without-class-with-params-in-state-outputs-shared - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - - psh1 - -- uri: template://test-provisioners/with-class-with-params-in-state-outputs-shared - type: template-with-class-with-params-in-state-outputs-shared - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - - psh1 - -- uri: template://test-provisioners/without-class-without-params-with-description - type: template-without-class-without-params-with-description - expected_outputs: - - o1 - - o2 - description: without-class-without-params-with-description - - -## CMD PROVISIONERS ## - -- uri: cmd://test-provisioners/without-class-without-params - type: cmd-without-class-without-params - expected_outputs: - - o1 - - o2 - -- uri: cmd://test-provisioners/with-class-without-params - type: cmd-with-class-without-params - class: c1 - expected_outputs: - - o1 - - o2 - -- uri: cmd://test-provisioners/without-class-with-params-in-outputs - type: cmd-without-class-with-params-in-outputs - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - -- uri: cmd://test-provisioners/with-class-with-params-in-outputs - type: cmd-with-class-with-params-in-outputs - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - -- uri: cmd://test-provisioners/without-class-with-params-in-shared - type: cmd-without-class-with-params-in-shared - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - psh1 - -- uri: cmd://test-provisioners/with-class-with-params-in-shared - type: cmd-with-class-with-params-in-shared - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - psh1 - -- uri: cmd://test-provisioners/without-class-with-params-in-state - type: cmd-without-class-with-params-in-state - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - psh1 - -- uri: cmd://test-provisioners/without-class-with-params-in-shared-outputs - type: cmd-without-class-with-params-in-shared-outputs - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - psh1 - -- uri: cmd://test-provisioners/with-class-with-params-in-shared-outputs - type: cmd-with-class-with-params-in-shared-outputs - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - psh1 - -- uri: cmd://test-provisioners/without-class-with-params-in-state-outputs - type: cmd-without-class-with-params-in-state-outputs - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - -- uri: cmd://test-provisioners/with-class-with-params-in-state-outputs - type: cmd-with-class-with-params-in-state-outputs - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - -- uri: cmd://test-provisioners/without-class-with-params-in-state-outputs-state-shared - type: cmd-without-class-with-params-in-state-outputs-shared - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - - psh1 - -- uri: cmd://test-provisioners/with-class-with-params-in-state-outputs-shared - type: cmd-with-class-with-params-in-state-outputs-shared - class: c1 - expected_outputs: - - o1 - - o2 - supported_params: - - p1 - - po1 - - pst1 - - psh1 - -- uri: cmd://test-provisioners/without-class-without-params-with-description - type: cmd-without-class-without-params-with-description - expected_outputs: - - o1 - - o2 - description: cmd-without-class-without-params-with-description \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/score-compose/generate.go b/gen/external-content/resource-provisioners/default/score-compose/generate.go deleted file mode 100644 index 58ebfac7..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/generate.go +++ /dev/null @@ -1,526 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "fmt" - "log/slog" - "os" - "slices" - "strconv" - "strings" - - "dario.cat/mergo" - composeloader "github.com/compose-spec/compose-go/v2/loader" - "github.com/compose-spec/compose-go/v2/types" - "github.com/score-spec/score-go/framework" - "github.com/score-spec/score-go/loader" - "github.com/score-spec/score-go/schema" - score "github.com/score-spec/score-go/types" - "github.com/spf13/cobra" - "gopkg.in/yaml.v3" - - "github.com/score-spec/score-compose/internal/compose" - "github.com/score-spec/score-compose/internal/patching" - "github.com/score-spec/score-compose/internal/project" - "github.com/score-spec/score-compose/internal/provisioners" - "github.com/score-spec/score-compose/internal/provisioners/envprov" - provloader "github.com/score-spec/score-compose/internal/provisioners/loader" -) - -const ( - generateCmdOverridesFileFlag = "overrides-file" - generateCmdOverridePropertyFlag = "override-property" - generateCmdImageFlag = "image" - generateCmdBuildFlag = "build" - generateCmdOutputFlag = "output" - generateCmdEnvFileFlag = "env-file" - generateCmdPublishFlag = "publish" -) - -var generateCommand = &cobra.Command{ - Use: "generate", - Args: cobra.ArbitraryArgs, - Short: "Convert one or more Score files into a Docker compose manifest", - Long: `The generate command will convert Score files in the current Score compose project into a combined Docker compose -manifest. All resources and links between Workloads will be resolved and provisioned as required. - -By default this command looks for score.yaml in the current directory, but can take explicit file names as positional -arguments. - -"score-compose init" MUST be run first. An error will be thrown if the project directory is not present. - -`, - Example: ` - # Specify Score files - score-compose generate score.yaml *.score.yaml - - # Regenerate without adding new score files - score-compose generate - - # Provide overrides when one score file is provided - score-compose generate score.yaml --override-file=./overrides.score.yaml --override-property=metadata.key=value - - # Publish a port exposed by a workload for local testing - score-compose generate score.yaml --publish 8080:my-workload:80 - - # Publish a port from a resource host and port for local testing, the middle expression is RESOURCE_ID.OUTPUT_KEY - score-compose generate score.yaml --publish 5432:postgres#my-workload.db.host:5432`, - - // don't print the errors - we print these ourselves in main() - SilenceErrors: true, - - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - - inputFiles := args - slices.Sort(inputFiles) - slog.Debug("Input Score files", "files", inputFiles) - - // first load all the score files, parse them with a dummy yaml decoder to find the workload name, reject any - // with invalid or duplicate names. - workloadNames, workloadSpecs, err := loadRawScoreFiles(inputFiles) - if err != nil { - return err - } - slog.Debug("Input Workload names", "names", workloadNames) - - // Forbid --image and --build when multiple score files are provided - if v, _ := cmd.Flags().GetString(generateCmdImageFlag); v != "" && len(workloadNames) > 1 { - return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdImageFlag) - } - if v, _ := cmd.Flags().GetStringArray(generateCmdBuildFlag); len(v) > 0 && len(workloadNames) > 1 { - return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdBuildFlag) - } - - // Now read and apply any overrides files to the score files - if v, _ := cmd.Flags().GetString(generateCmdOverridesFileFlag); v != "" { - if len(workloadNames) == 0 { - return fmt.Errorf("--%s cannot be used without providing a score file", generateCmdOverridesFileFlag) - } else if len(workloadNames) > 1 { - return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdOverridesFileFlag) - } - if err := parseAndApplyOverrideFile(v, generateCmdOverridesFileFlag, workloadSpecs[workloadNames[0]]); err != nil { - return err - } - } - - // Now read, parse, and apply any override properties to the score files - if v, _ := cmd.Flags().GetStringArray(generateCmdOverridePropertyFlag); len(v) > 0 { - if len(workloadNames) == 0 { - return fmt.Errorf("--%s cannot be used without providing a score file", generateCmdOverridesFileFlag) - } else if len(workloadNames) > 1 { - return fmt.Errorf("--%s cannot be used when multiple score files are provided", generateCmdOverridesFileFlag) - } - for _, overridePropertyEntry := range v { - if workloadSpecs[workloadNames[0]], err = parseAndApplyOverrideProperty(overridePropertyEntry, generateCmdOverridePropertyFlag, workloadSpecs[workloadNames[0]]); err != nil { - return err - } - } - } - - sd, ok, err := project.LoadStateDirectory(".") - if err != nil { - return fmt.Errorf("failed to load existing state directory: %w", err) - } else if !ok { - return fmt.Errorf("state directory does not exist, please run \"score-compose init\" first") - } - slog.Info(fmt.Sprintf("Loaded state directory with docker compose project '%s'", sd.State.Extras.ComposeProjectName)) - - currentState := &sd.State - - // Now validate with score spec - for i, workloadName := range workloadNames { - spec := workloadSpecs[workloadName] - inputFile := inputFiles[i] - - // Ensure transforms are applied (be a good citizen) - if changes, err := schema.ApplyCommonUpgradeTransforms(spec); err != nil { - return fmt.Errorf("failed to upgrade spec: %w", err) - } else if len(changes) > 0 { - for _, change := range changes { - slog.Info(fmt.Sprintf("Applying backwards compatible upgrade to '%s': %s", workloadName, change)) - } - } - if err := schema.Validate(spec); err != nil { - return fmt.Errorf("validation errors in workload '%s': %w", workloadName, err) - } - slog.Info(fmt.Sprintf("Validated workload '%s'", workloadName)) - - var out score.Workload - if err := loader.MapSpec(&out, spec); err != nil { - return fmt.Errorf("failed to convert '%s' to structure: %w", workloadName, err) - } - - // Gather container build contexts, these will be stored and added to the generated compose output later - containerBuildContexts := make(map[string]types.BuildConfig) - if v, _ := cmd.Flags().GetStringArray(generateCmdBuildFlag); len(v) > 0 { - for _, buildFlag := range v { - parts := strings.SplitN(buildFlag, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid --%s '%s': expected 2 =-separated parts", generateCmdBuildFlag, buildFlag) - } else if _, ok := out.Containers[parts[0]]; !ok { - return fmt.Errorf("invalid --%s '%s': unknown container '%s'", generateCmdBuildFlag, buildFlag, parts[0]) - } - if strings.HasPrefix(parts[1], "{") { - var intermediate interface{} - if err := yaml.Unmarshal([]byte(parts[1]), &intermediate); err != nil { - return fmt.Errorf("invalid --%s '%s': %w", generateCmdBuildFlag, buildFlag, err) - } - var out types.BuildConfig - if err := composeloader.Transform(intermediate, &out); err != nil { - return fmt.Errorf("invalid --%s '%s': %w", generateCmdBuildFlag, buildFlag, err) - } - containerBuildContexts[parts[0]] = out - } else { - containerBuildContexts[parts[0]] = types.BuildConfig{Context: parts[1]} - } - } - } - - // Apply image if container image is . - for containerName, container := range out.Containers { - if container.Image == "." { - if v, _ := cmd.Flags().GetString(generateCmdImageFlag); v != "" { - container.Image = v - slog.Info(fmt.Sprintf("Set container image for workload '%s' container '%s' to %s from --%s", workloadName, containerName, v, generateCmdImageFlag)) - out.Containers[containerName] = container - } else if _, ok := containerBuildContexts[containerName]; !ok { - return fmt.Errorf("failed to convert '%s' because container '%s' has no image and neither --%s nor --%s was provided", workloadName, containerName, generateCmdImageFlag, generateCmdBuildFlag) - } - } - } - - currentState, err = currentState.WithWorkload(&out, &inputFile, project.WorkloadExtras{BuildConfigs: containerBuildContexts}) - if err != nil { - return fmt.Errorf("failed to add workload '%s': %w", workloadName, err) - } - } - - if len(currentState.Workloads) == 0 { - return fmt.Errorf("the project is empty, please provide a score file to generate from") - } - - loadedProvisioners, err := provloader.LoadProvisionersFromDirectory(sd.Path, provloader.DefaultSuffix) - if err != nil { - return fmt.Errorf("failed to load provisioners: %w", err) - } else if len(loadedProvisioners) > 0 { - slog.Info(fmt.Sprintf("Successfully loaded %d resource provisioners", len(loadedProvisioners))) - } - - // append the env var provisioner - environmentProvisioner := new(envprov.Provisioner) - loadedProvisioners = append(loadedProvisioners, environmentProvisioner) - - currentState, err = currentState.WithPrimedResources() - if err != nil { - return fmt.Errorf("failed to prime resources: %w", err) - } - - superProject := &types.Project{ - Name: sd.State.Extras.ComposeProjectName, - Services: make(types.Services, 0), - Volumes: map[string]types.VolumeConfig{}, - Networks: map[string]types.NetworkConfig{}, - } - - currentState, err = provisioners.ProvisionResources(context.Background(), currentState, loadedProvisioners, superProject) - if err != nil { - return fmt.Errorf("failed to provision: %w", err) - } else if len(currentState.Resources) > 0 { - slog.Info(fmt.Sprintf("Provisioned %d resources", len(currentState.Resources))) - } - - waitServiceName, hasWaitService := injectWaitService(superProject) - - for workloadName, workloadState := range currentState.Workloads { - - slog.Info(fmt.Sprintf("Converting workload '%s' to Docker compose", workloadName)) - converted, err := compose.ConvertSpec(currentState, &workloadState.Spec) - if err != nil { - return fmt.Errorf("failed to convert workload '%s' to Docker compose: %w", workloadName, err) - } - - for serviceName, service := range converted.Services { - if _, ok := superProject.Services[serviceName]; ok { - return fmt.Errorf("failed to add converted workload '%s': duplicate service name '%s'", workloadName, serviceName) - } - if hasWaitService { - if service.DependsOn == nil { - service.DependsOn = make(types.DependsOnConfig) - } - service.DependsOn[waitServiceName] = types.ServiceDependency{Condition: "service_completed_successfully", Required: true} - } - superProject.Services[serviceName] = service - } - for volumeName, volume := range converted.Volumes { - if _, ok := superProject.Volumes[volumeName]; ok { - return fmt.Errorf("failed to add converted workload '%s': duplicate volume name '%s'", workloadName, volumeName) - } - superProject.Volumes[volumeName] = volume - } - for networkName, network := range converted.Networks { - if _, ok := superProject.Networks[networkName]; ok { - return fmt.Errorf("failed to add converted workload '%s': duplicated network name '%s'", workloadName, networkName) - } - superProject.Networks[networkName] = network - } - } - - for i, content := range currentState.Extras.PatchingTemplates { - slog.Info(fmt.Sprintf("Applying patching template %d", i+1)) - superProject, err = patching.PatchServices(currentState, superProject, content) - if err != nil { - return fmt.Errorf("failed to patch template %d: %w", i+1, err) - } - } - - if v, err := cmd.Flags().GetStringArray(generateCmdPublishFlag); err != nil { - return fmt.Errorf("failed to read publish property: %w", err) - } else { - for i, k := range v { - parts := strings.Split(k, ":") - if len(parts) <= 2 { - return fmt.Errorf("--%s[%d] expected 3 :-separated parts", generateCmdPublishFlag, i) - } - // raw host port - rhp := parts[0] - // raw ref - rr := strings.Join(parts[1:len(parts)-1], ":") - // raw container port - rcp := parts[len(parts)-1] - - hp, err := strconv.Atoi(rhp) - if err != nil { - return fmt.Errorf("--%s[%d] could not parse host port '%s' as integer", generateCmdPublishFlag, i, rhp) - } else if hp <= 1 { - return fmt.Errorf("--%s[%d] host port must be > 1", generateCmdPublishFlag, i) - } - - cp, err := strconv.Atoi(rcp) - if err != nil { - return fmt.Errorf("--%s[%d] could not parse container port '%s' as integer", generateCmdPublishFlag, i, rcp) - } else if cp <= 1 { - return fmt.Errorf("--%s[%d] container port must be > 1", generateCmdPublishFlag, i) - } - - if strings.Contains(rr, "#") { - parts := strings.Split(rr, ".") - if len(parts) < 2 { - return fmt.Errorf("--%s[%d] must match RES_UID.OUTPUT", generateCmdPublishFlag, i) - } - rr = strings.Join(parts[0:len(parts)-1], ".") - outputKey := parts[len(parts)-1] - - resUid := parseResourceUid(rr) - res, ok := currentState.Resources[resUid] - if !ok { - return fmt.Errorf("--%s[%d] failed to find a resource with uid '%s'", generateCmdPublishFlag, i, rr) - } - - if v, ok := res.Outputs[outputKey]; !ok { - return fmt.Errorf("--%s[%d] resource '%s' has no output '%s'", generateCmdPublishFlag, i, resUid, outputKey) - } else if sv, ok := v.(string); !ok { - return fmt.Errorf("--%s[%d] resource '%s' output '%s' is not a string", generateCmdPublishFlag, i, resUid, outputKey) - } else if config, ok := superProject.Services[sv]; !ok { - return fmt.Errorf("--%s[%d] host '%s' does not exist", generateCmdPublishFlag, i, sv) - } else { - config.Ports = append(config.Ports, types.ServicePortConfig{ - Published: strconv.Itoa(hp), - Target: uint32(cp), - }) - superProject.Services[sv] = config - slog.Info(fmt.Sprintf("Published port %d of service '%s' to host port %d", cp, sv, hp)) - } - } else if _, ok := currentState.Workloads[rr]; !ok { - return fmt.Errorf("--%s[%d] failed to find a workload named '%s'", generateCmdPublishFlag, i, rr) - } else { - for sv, config := range superProject.Services { - if config.Hostname == rr { - config.Ports = append(config.Ports, types.ServicePortConfig{ - Published: strconv.Itoa(hp), - Target: uint32(cp), - }) - superProject.Services[sv] = config - slog.Info(fmt.Sprintf("Published port %d of service '%s' to host port %d", cp, sv, hp)) - } - } - } - } - } - - sd.State = *currentState - if err := sd.Persist(); err != nil { - return fmt.Errorf("failed to persist updated state directory: %w", err) - } - - raw, _ := yaml.Marshal(superProject) - - v, _ := cmd.Flags().GetString(generateCmdOutputFlag) - if v == "" { - return fmt.Errorf("no output file specified") - } else if v == "-" { - _, _ = fmt.Fprint(cmd.OutOrStdout(), string(raw)) - } else if err := os.WriteFile(v+".temp", raw, 0644); err != nil { - return fmt.Errorf("failed to write output file: %w", err) - } else if err := os.Rename(v+".temp", v); err != nil { - return fmt.Errorf("failed to complete writing output file: %w", err) - } - - if v, _ := cmd.Flags().GetString(generateCmdEnvFileFlag); v != "" { - content := new(strings.Builder) - for k := range environmentProvisioner.Accessed() { - _, _ = content.WriteString(k) - _, _ = content.WriteRune('=') - _, _ = content.WriteRune('\n') - } - slog.Info(fmt.Sprintf("Writing env var file to '%s'", v)) - if err := os.WriteFile(v, []byte(content.String()), 0644); err != nil { - return fmt.Errorf("failed to write env var file: %w", err) - } - } - return nil - }, -} - -func parseResourceUid(raw string) framework.ResourceUid { - parts := strings.SplitN(raw, "#", 2) - firstParts := strings.SplitN(parts[0], ".", 2) - secondParts := strings.SplitN(parts[1], ".", 2) - resType := firstParts[0] - var resClass *string - if len(firstParts) > 1 { - resClass = &firstParts[1] - } - if len(secondParts) == 1 { - return framework.NewResourceUid("", "", resType, resClass, &parts[1]) - } - return framework.NewResourceUid(secondParts[0], secondParts[1], resType, resClass, nil) -} - -// loadRawScoreFiles loads raw score specs as yaml from the given files and finds all the workload names. It throws -// errors if it failed to read, load, or if names are duplicated. -func loadRawScoreFiles(fileNames []string) ([]string, map[string]map[string]interface{}, error) { - workloadNames := make([]string, 0, len(fileNames)) - workloadToRawScore := make(map[string]map[string]interface{}, len(fileNames)) - - for _, fileName := range fileNames { - var out map[string]interface{} - raw, err := os.ReadFile(fileName) - if err != nil { - return nil, nil, fmt.Errorf("failed to read '%s': %w", fileName, err) - } else if err := yaml.Unmarshal(raw, &out); err != nil { - return nil, nil, fmt.Errorf("failed to decode '%s' as yaml: %w", fileName, err) - } - - var workloadName string - if meta, ok := out["metadata"].(map[string]interface{}); ok { - workloadName, _ = meta["name"].(string) - if _, ok := workloadToRawScore[workloadName]; ok { - return nil, nil, fmt.Errorf("workload name '%s' in file '%s' is used more than once", workloadName, fileName) - } - } - workloadNames = append(workloadNames, workloadName) - workloadToRawScore[workloadName] = out - } - return workloadNames, workloadToRawScore, nil -} - -func init() { - generateCommand.Flags().StringP(generateCmdOutputFlag, "o", "compose.yaml", "The output file to write the composed compose file to") - generateCommand.Flags().String(generateCmdOverridesFileFlag, "", "An optional file of Score overrides to merge in") - generateCommand.Flags().StringArray(generateCmdOverridePropertyFlag, []string{}, "An optional set of path=key overrides to set or remove") - generateCommand.Flags().String(generateCmdImageFlag, "", "An optional container image to use for any container with image == '.'") - generateCommand.Flags().StringArray(generateCmdBuildFlag, []string{}, "An optional build context to use for the given container --build=container=./dir or --build=container={\"context\":\"./dir\"}") - generateCommand.Flags().String(generateCmdEnvFileFlag, "", "Location to store a skeleton .env file for compose - this will override existing content") - generateCommand.Flags().StringArray(generateCmdPublishFlag, []string{}, "An optional set of HOST_PORT::CONTAINER_PORT to publish on the host system.") - rootCmd.AddCommand(generateCommand) -} - -func parseAndApplyOverrideFile(entry string, flagName string, spec map[string]interface{}) error { - if raw, err := os.ReadFile(entry); err != nil { - return fmt.Errorf("--%s '%s' is invalid, failed to read file: %w", flagName, entry, err) - } else { - slog.Info(fmt.Sprintf("Applying overrides from %s to workload", entry)) - var out map[string]interface{} - if err := yaml.Unmarshal(raw, &out); err != nil { - return fmt.Errorf("--%s '%s' is invalid: failed to decode yaml: %w", flagName, entry, err) - } else if err := mergo.Merge(&spec, out, mergo.WithOverride); err != nil { - return fmt.Errorf("--%s '%s' failed to apply: %w", flagName, entry, err) - } - } - return nil -} - -func parseAndApplyOverrideProperty(entry string, flagName string, spec map[string]interface{}) (map[string]interface{}, error) { - parts := strings.SplitN(entry, "=", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("--%s '%s' is invalid, expected a =-separated path and value", flagName, entry) - } - if parts[1] == "" { - slog.Info(fmt.Sprintf("Overriding '%s' in workload", parts[0])) - after, err := framework.OverridePathInMap(spec, framework.ParseDotPathParts(parts[0]), true, nil) - if err != nil { - return nil, fmt.Errorf("--%s '%s' could not be applied: %w", flagName, entry, err) - } - return after, nil - } else { - var value interface{} - if err := yaml.Unmarshal([]byte(parts[1]), &value); err != nil { - return nil, fmt.Errorf("--%s '%s' is invalid, failed to unmarshal value as json: %w", flagName, entry, err) - } - slog.Info(fmt.Sprintf("Overriding '%s' in workload", parts[0])) - after, err := framework.OverridePathInMap(spec, framework.ParseDotPathParts(parts[0]), false, value) - if err != nil { - return nil, fmt.Errorf("--%s '%s' could not be applied: %w", flagName, entry, err) - } - return after, nil - } -} - -// injectWaitService injects a service into the compose project which waits for all other services to be started, -// healthy, or complete depending on their definition. The workload services may then wait for this. -// This will return an empty string and false if there are no applicable services. -func injectWaitService(p *types.Project) (string, bool) { - if len(p.Services) == 0 { - return "", false - } - newService := types.ServiceConfig{ - Name: "wait-for-resources", - Image: "alpine", - Command: types.ShellCommand{"echo"}, - DependsOn: make(types.DependsOnConfig), - } - for otherServiceName, otherService := range p.Services { - condition := "service_started" - if otherService.HealthCheck != nil { - condition = "service_healthy" - } else if v := otherService.Labels["dev.score.compose.labels.is-init-container"]; v == "true" { - // annoyingly we can't tell based on the definition whether a service is designed to stop or not, - // so we'll use this label as a best effort indicator. - condition = "service_completed_successfully" - } - newService.DependsOn[otherServiceName] = types.ServiceDependency{ - Condition: condition, - Required: true, - } - } - if p.Services == nil { - p.Services = make(types.Services) - } - p.Services[newService.Name] = newService - return newService.Name, true -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go b/gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go deleted file mode 100644 index e402686a..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/generate_examples_test.go +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "crypto/rand" - rand2 "math/rand" - "os" - "os/exec" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type exampleTestCase struct { - subDir string - adds []string - patchFiles []string - expected string - expectedContains string -} - -func TestExample(t *testing.T) { - for _, tc := range []exampleTestCase{ - { - subDir: "01-hello", - adds: []string{"score.yaml"}, - expected: `name: 01-hello -services: - hello-world-hello: - annotations: - compose.score.dev/workload-name: hello-world - your.custom/annotation: value - command: - - -c - - while true; do echo Hello World!; sleep 5; done - entrypoint: - - /bin/sh - hostname: hello-world - image: busybox -`, - }, - { - subDir: "02-environment", - adds: []string{"score.yaml"}, - expected: `name: 02-environment -services: - hello-world-hello: - annotations: - compose.score.dev/workload-name: hello-world - command: - - -c - - while true; do echo $${GREETING} $${NAME}!; sleep 5; done - entrypoint: - - /bin/sh - environment: - ESCAPED: $$_$${fizzbuzz} - GREETING: Hello - NAME: ${NAME} - WORKLOAD_NAME: hello-world - hostname: hello-world - image: busybox -`, - }, - { - subDir: "03-files", - adds: []string{"score.yaml"}, - expected: `name: 03-files -services: - hello-world-hello: - annotations: - compose.score.dev/workload-name: hello-world - command: - - -c - - while true; do cat /fileA.txt; cat /fileB.txt; cat /fileC.bin; sleep 5; done - entrypoint: - - /bin/sh - hostname: hello-world - image: busybox - volumes: - - type: bind - source: .score-compose/mounts/files/hello-world-files-fileA.txt - target: /fileA.txt - - type: bind - source: .score-compose/mounts/files/hello-world-files-fileB.txt - target: /fileB.txt - - type: bind - source: .score-compose/mounts/files/hello-world-files-fileC.bin - target: /fileC.bin -`, - }, - { - subDir: "04-multiple-workloads", - adds: []string{ - "score.yaml", - "score2.yaml", - }, - expected: `name: 04-multiple-workloads -services: - hello-world-2-first: - annotations: - compose.score.dev/workload-name: hello-world-2 - environment: - NGINX_PORT: "8080" - hostname: hello-world-2 - image: nginx:latest - hello-world-first: - annotations: - compose.score.dev/workload-name: hello-world - environment: - NGINX_PORT: "8080" - hostname: hello-world - image: nginx:latest - hello-world-second: - annotations: - compose.score.dev/workload-name: hello-world - environment: - NGINX_PORT: "8081" - image: nginx:latest - network_mode: service:hello-world-first -`, - }, - { - subDir: "05-volume-mounts", - adds: []string{"score.yaml"}, - expected: `name: 05-volume-mounts -services: - hello-world-first: - annotations: - compose.score.dev/workload-name: hello-world - hostname: hello-world - image: nginx:latest - volumes: - - type: volume - source: hello-world-data-8MjJEo - target: /data -volumes: - hello-world-data-8MjJEo: - name: hello-world-data-8MjJEo - driver: local - labels: - dev.score.compose.res.uid: volume.default#hello-world.data -`, - }, - { - subDir: "06-resource-provisioning", - adds: []string{"score.yaml", "score2.yaml", "--publish 6379:redis#main-cache.host:6379"}, - expectedContains: ` - ports: - - target: 6379 - published: "6379" -`, - }, - { - subDir: "07-overrides", - adds: []string{"score.yaml --override-property containers.web.variables.DEBUG=\"true\""}, - expected: `name: 07-overrides -services: - hello-world-web: - annotations: - compose.score.dev/workload-name: hello-world - environment: - DEBUG: "true" - hostname: hello-world - image: nginx -`, - }, - { - subDir: "08-service-port-resource", - adds: []string{"scoreA.yaml", "scoreB.yaml", "--publish 8080:workload-a:80"}, - expected: `name: 08-service-port-resource -services: - workload-a-example: - annotations: - compose.score.dev/workload-name: workload-a - hostname: workload-a - healthcheck: - test: - - CMD - - /usr/bin/curl - - -f - - -m - - "5" - - http://localhost - timeout: 5s - interval: 5s - image: nginx - ports: - - target: 80 - published: "8080" - workload-b-example: - annotations: - compose.score.dev/workload-name: workload-b - command: - - -c - - while true; do wget $${DEPENDENCY_URL} || true; sleep 5; done - entrypoint: - - /bin/sh - environment: - DEPENDENCY_URL: http://workload-a:80 - hostname: workload-b - image: busybox -`, - }, - { - subDir: "09-dns-and-route", - adds: []string{"score.yaml"}, - }, - { - subDir: "10-amqp-rabbitmq", - adds: []string{"score.yaml"}, - }, - { - subDir: "11-mongodb-document-database", - adds: []string{"score.yaml"}, - }, - { - subDir: "12-mysql-database", - adds: []string{"score.yaml"}, - }, - { - subDir: "13-kafka-topic", - adds: []string{"score.yaml"}, - }, - { - subDir: "14-elasticsearch", - adds: []string{"score.yaml"}, - }, - { - subDir: "15-mssql-database", - adds: []string{"score.yaml"}, - }, - { - subDir: "16-patching-templates", - adds: []string{"score.yaml"}, - patchFiles: []string{"patch-1.tpl", "patch-2.tpl", "patch-3.tpl"}, - }, - } { - t.Run(tc.subDir, func(t *testing.T) { - oldReader := rand.Reader - t.Cleanup(func() { - rand.Reader = oldReader - }) - rand.Reader = rand2.New(rand2.NewSource(0)) - - changeToDir(t, "../../examples/"+tc.subDir) - require.NoError(t, os.RemoveAll(".score-compose")) - require.NoError(t, os.RemoveAll("compose.yaml")) - - args := []string{"init", "--no-sample"} - for _, f := range tc.patchFiles { - args = append(args, "--patch-templates", f) - } - - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, args) - require.NoError(t, err) - assert.Equal(t, "", stdout) - - for _, add := range tc.adds { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, strings.Split("generate "+add, " ")) - require.NoError(t, err) - assert.Equal(t, "", stdout) - } - - if tc.expected != "" || tc.expectedContains != "" { - raw, err := os.ReadFile("compose.yaml") - require.NoError(t, err) - if tc.expected != "" { - assert.Equal(t, tc.expected, string(raw)) - } - assert.Contains(t, string(raw), tc.expectedContains) - } - - if os.Getenv("NO_DOCKER") == "" { - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - } - - }) - } -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/generate_test.go b/gen/external-content/resource-provisioners/default/score-compose/generate_test.go deleted file mode 100644 index 3f2ace14..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/generate_test.go +++ /dev/null @@ -1,1633 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "os" - "os/exec" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/score-spec/score-compose/internal/project" -) - -func TestGenerateHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `The generate command will convert Score files in the current Score compose project into a combined Docker compose -manifest. All resources and links between Workloads will be resolved and provisioned as required. - -By default this command looks for score.yaml in the current directory, but can take explicit file names as positional -arguments. - -"score-compose init" MUST be run first. An error will be thrown if the project directory is not present. - -Usage: - score-compose generate [flags] - -Examples: - - # Specify Score files - score-compose generate score.yaml *.score.yaml - - # Regenerate without adding new score files - score-compose generate - - # Provide overrides when one score file is provided - score-compose generate score.yaml --override-file=./overrides.score.yaml --override-property=metadata.key=value - - # Publish a port exposed by a workload for local testing - score-compose generate score.yaml --publish 8080:my-workload:80 - - # Publish a port from a resource host and port for local testing, the middle expression is RESOURCE_ID.OUTPUT_KEY - score-compose generate score.yaml --publish 5432:postgres#my-workload.db.host:5432 - -Flags: - --build stringArray An optional build context to use for the given container --build=container=./dir or --build=container={"context":"./dir"} - --env-file string Location to store a skeleton .env file for compose - this will override existing content - -h, --help help for generate - --image string An optional container image to use for any container with image == '.' - -o, --output string The output file to write the composed compose file to (default "compose.yaml") - --override-property stringArray An optional set of path=key overrides to set or remove - --overrides-file string An optional file of Score overrides to merge in - --publish stringArray An optional set of HOST_PORT::CONTAINER_PORT to publish on the host system. - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) - - stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "generate"}) - assert.NoError(t, err) - assert.Equal(t, stdout, stdout2) - assert.Equal(t, "", stderr) -} - -func changeToDir(t *testing.T, dir string) string { - t.Helper() - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(dir)) - t.Cleanup(func() { - require.NoError(t, os.Chdir(wd)) - }) - return dir -} - -func changeToTempDir(t *testing.T) string { - return changeToDir(t, t.TempDir()) -} - -func TestGenerateWithoutInit(t *testing.T) { - _ = changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"generate"}) - assert.EqualError(t, err, "state directory does not exist, please run \"score-compose init\" first") - assert.Equal(t, "", stdout) -} - -func TestGenerateWithoutScoreFiles(t *testing.T) { - _ = changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate"}) - assert.EqualError(t, err, "the project is empty, please provide a score file to generate from") - assert.Equal(t, "", stdout) -} - -func TestInitAndGenerateWithBadFile(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - assert.NoError(t, os.WriteFile(filepath.Join(td, "thing"), []byte(`"blah"`), 0644)) - - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "thing"}) - assert.EqualError(t, err, "failed to decode 'thing' as yaml: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `blah` into map[string]interface {}") - assert.Equal(t, "", stdout) -} - -func TestInitAndGenerateWithBadScore(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - assert.NoError(t, os.WriteFile(filepath.Join(td, "thing"), []byte(`{}`), 0644)) - - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "thing"}) - assert.EqualError(t, err, "validation errors in workload '': jsonschema: '' does not validate with https://score.dev/schemas/score#/required: missing properties: 'apiVersion', 'metadata', 'containers'") - assert.Equal(t, "", stdout) -} - -func TestInitAndGenerate_with_sample(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // write overrides file - assert.NoError(t, os.WriteFile(filepath.Join(td, "overrides.yaml"), []byte(`{"resources": {"foo": {"type": "environment"}}}`), 0644)) - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "-o", "compose-output.yaml", - "--overrides-file", "overrides.yaml", - "--override-property", "containers.hello-world.variables.THING=${resources.foo.THING}", - "--", "score.yaml", - }) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - expectedOutput := `name: "001" -services: - example-hello-world: - annotations: - compose.score.dev/workload-name: example - environment: - EXAMPLE_VARIABLE: example-value - THING: ${THING} - hostname: example - image: nginx:latest -` - assert.Equal(t, expectedOutput, string(raw)) - // generate again just for luck - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "-o", "compose-output.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err = os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - assert.Equal(t, expectedOutput, string(raw)) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Equal(t, "score.yaml", *sd.State.Workloads["example"].File) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 1) -} - -func TestInitAndGenerate_with_image_override(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // write new score file - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: . -`), 0644)) - - t.Run("generate but fail due to missing override", func(t *testing.T) { - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "-o", "compose-output.yaml", "--", "score.yaml", - }) - assert.EqualError(t, err, "failed to convert 'example' because container 'example' has no image and neither --image nor --build was provided") - }) - - t.Run("generate with image", func(t *testing.T) { - // generate with image - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "-o", "compose-output.yaml", "--image", "busybox:latest", "--", "score.yaml", - }) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - expectedOutput := `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - hostname: example - image: busybox:latest -` - assert.Equal(t, expectedOutput, string(raw)) - // generate again just for luck - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "-o", "compose-output.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err = os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - assert.Equal(t, expectedOutput, string(raw)) - }) - - t.Run("generate with raw build context", func(t *testing.T) { - // generate with build context - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "-o", "compose-output.yaml", "--build", "example=./dir", "--", "score.yaml", - }) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - expectedOutput := `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - build: - context: ./dir - hostname: example -` - assert.Equal(t, expectedOutput, string(raw)) - // generate again just for luck - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "-o", "compose-output.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err = os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - assert.Equal(t, expectedOutput, string(raw)) - }) - - t.Run("generate with json build context", func(t *testing.T) { - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "-o", "compose-output.yaml", "--build", `example={"context":"./dir","args":{"DEBUG":"true"}}`, "--", "score.yaml", - }) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - expectedOutput := `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - build: - context: ./dir - args: - DEBUG: "true" - hostname: example -` - assert.Equal(t, expectedOutput, string(raw)) - }) - - t.Run("generate with json build context and array args", func(t *testing.T) { - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "-o", "compose-output.yaml", "--build", `example={"context":"./dir","args":["DEBUG"]}`, "--", "score.yaml", - }) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - expectedOutput := `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - build: - context: ./dir - args: - DEBUG: null - hostname: example -` - assert.Equal(t, expectedOutput, string(raw)) - }) - - t.Run("generate with yaml build context", func(t *testing.T) { - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "-o", "compose-output.yaml", "--build", `example={context: "./dir"}`, "--", "score.yaml", - }) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose-output.yaml")) - assert.NoError(t, err) - expectedOutput := `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - build: - context: ./dir - hostname: example -` - assert.Equal(t, expectedOutput, string(raw)) - }) - -} - -func TestInitAndGenerate_with_files_old_files_spec(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - files: - - target: /blah.txt - source: ./original.txt -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "original.txt"), []byte(`first ${metadata.name} second`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - hostname: example - image: foo - volumes: - - type: bind - source: .score-compose/mounts/files/example-files-blah.txt - target: /blah.txt -`, string(raw)) -} - -func TestInitAndGenerate_with_files_new_files_spec(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - files: - "/blah.txt": - source: ./original.txt -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "original.txt"), []byte(`first ${metadata.name} second`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - hostname: example - image: foo - volumes: - - type: bind - source: .score-compose/mounts/files/example-files-blah.txt - target: /blah.txt -`, string(raw)) -} - -func TestGenerateRedisResource(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - variables: - CONN_STR_1: "redis://${resources.cache1.username}:${resources.cache1.password}@${resources.cache1.host}:${resources.cache1.port}" - CONN_STR_2: "redis://${resources.cache2.username}:${resources.cache2.password}@${resources.cache2.host}:${resources.cache2.port}" -resources: - cache1: - type: redis - cache2: - type: redis -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 2) - assert.Contains(t, sd.State.Resources["redis.default#example.cache1"].State, "serviceName") - assert.Contains(t, sd.State.Resources["redis.default#example.cache1"].State, "password") - assert.Contains(t, sd.State.Resources["redis.default#example.cache2"].State, "serviceName") - assert.NotEqual(t, sd.State.Resources["redis.default#example.cache1"].State, sd.State.Resources["redis.default#example.cache2"].State) - assert.Len(t, sd.State.SharedState, 0) - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestGeneratePostgresResource(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - variables: - CONN_STR_1: "postgres://${resources.db1.username}:${resources.db1.password}@${resources.db1.host}:${resources.db1.port}/${resources.db1.name}" - CONN_STR_2: "postgres://${resources.db2.username}:${resources.db2.password}@${resources.db2.host}:${resources.db2.port}/${resources.db2.name}" -resources: - db1: - type: postgres - db2: - type: postgres -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 2) - assert.Contains(t, sd.State.Resources["postgres.default#example.db1"].State, "database") - assert.Contains(t, sd.State.Resources["postgres.default#example.db1"].State, "username") - assert.Contains(t, sd.State.Resources["postgres.default#example.db1"].State, "password") - assert.Contains(t, sd.State.Resources["postgres.default#example.db2"].State, "database") - assert.NotEqual(t, sd.State.Resources["postgres.default#example.db1"].State, sd.State.Resources["postgres.default#example.db2"].State) - assert.Contains(t, sd.State.SharedState, "default-provisioners-postgres-instance") - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestGenerateS3Resource(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - variables: - output: ${resources.bucket1.endpoint} ${resources.bucket1.region} ${resources.bucket1.bucket} ${resources.bucket1.access_key_id} ${resources.bucket1.secret_key} -resources: - bucket1: - metadata: - annotations: - compose.score.dev/publish-port: "9001" - type: s3 - bucket2: - type: s3 -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 2) - assert.Contains(t, sd.State.Resources["s3.default#example.bucket1"].State, "bucket") - assert.Contains(t, sd.State.Resources["s3.default#example.bucket2"].State, "bucket") - assert.NotEqual(t, sd.State.Resources["s3.default#example.bucket1"].State, sd.State.Resources["postgres.default#example.bucket2"].State) - assert.Contains(t, sd.State.SharedState, "default-provisioners-minio-instance") - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestInitAndGenerate_with_depends_on(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - assert.NoError(t, os.WriteFile("score.yaml", []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo -resources: - thing: - type: thing -`), 0644)) - - assert.NoError(t, os.WriteFile(".score-compose/00-custom.provisioners.yaml", []byte(` -- uri: template://blah - type: thing - services: | - init_service: - image: thing - labels: - dev.score.compose.labels.is-init-container: "true" - generic_service: - image: other - service_with_healthcheck: - image: something - healthcheck: - test: ["CMD", "boo"] -`), 0644)) - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - depends_on: - wait-for-resources: - condition: service_completed_successfully - required: true - hostname: example - image: foo - generic_service: - image: other - init_service: - image: thing - labels: - dev.score.compose.labels.is-init-container: "true" - service_with_healthcheck: - healthcheck: - test: - - CMD - - boo - image: something - wait-for-resources: - command: - - echo - depends_on: - generic_service: - condition: service_started - required: true - init_service: - condition: service_completed_successfully - required: true - service_with_healthcheck: - condition: service_healthy - required: true - image: alpine -`, string(raw)) - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestInitAndGenerate_with_dependent_resources(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // write custom providers - assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` -- uri: template://foo - type: foo - outputs: | - blah: value - services: | - foo-service: - image: foo-image -- uri: template://bar - type: bar - services: | - bar-service: - image: {{ .Params.x }} -`), 0644)) - - // write custom score file - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: busybox -resources: - first: - type: foo - second: - type: bar - params: - x: ${resources.first.blah} -`), 0644)) - - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - bar-service: - image: value - example-example: - annotations: - compose.score.dev/workload-name: example - depends_on: - wait-for-resources: - condition: service_completed_successfully - required: true - hostname: example - image: busybox - foo-service: - image: foo-image - wait-for-resources: - command: - - echo - depends_on: - bar-service: - condition: service_started - required: true - foo-service: - condition: service_started - required: true - image: alpine -`, string(raw)) -} - -func TestInitAndGenerateWithNetworkServicesAcrossWorkloads(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // write custom providers - assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` -- uri: template://default-provisioners/workload-port - type: workload-port - init: | - {{ if not .Params.workload }}{{ fail "'workload' param required" }}{{ end }} - {{ if not .Params.port }}{{ fail "'port' param required - the name of the remote port" }}{{ end }} - {{ $x := index .WorkloadServices .Params.workload }} - {{ if not $x.ServiceName }}{{ fail "unknown workload" }}{{ end }} - {{ $y := index $x.Ports .Params.port }} - {{ if not $y.Name }}{{ fail "unknown port" }}{{ end }} - state: | - {{ $x := index .WorkloadServices .Params.workload }} - hostname: {{ $x.ServiceName | quote }} - {{ $y := index $x.Ports .Params.port }} - port: {{ $y.TargetPort }} -`), - 0644, - )) - - t.Run("fail unknown workload", func(t *testing.T) { - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: busybox -resources: - first: - type: workload-port - params: - workload: example-2 - port: web -`), 0644)) - - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.EqualError(t, err, "failed to provision: resource 'workload-port.default#example.first': failed to provision: init template failed: failed to execute template: template: :4:30: executing \"\" at : error calling fail: unknown workload") - }) - - t.Run("fail unknown port", func(t *testing.T) { - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: busybox -resources: - first: - type: workload-port - params: - workload: example - port: web -`), 0644)) - - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.EqualError(t, err, "failed to provision: resource 'workload-port.default#example.first': failed to provision: init template failed: failed to execute template: template: :6:23: executing \"\" at : error calling fail: unknown port") - }) - - t.Run("succeed", func(t *testing.T) { - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: busybox -service: - ports: - web: - port: 8080 - targetPort: 80 -resources: - first: - type: workload-port - params: - workload: example - port: web -`), 0644)) - - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 1) - assert.Equal(t, map[string]interface{}{ - "hostname": "example", - "port": 80, - }, sd.State.Resources["workload-port.default#example.first"].State) - }) - -} - -func TestInitAndGenerate_with_annotation_ref(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - assert.NoError(t, os.WriteFile("score.yaml", []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example - annotations: - key.com/foo-bar: thing -containers: - example: - image: foo - variables: - REF: ${metadata.annotations.key\.com/foo-bar} -`), 0644)) - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - key.com/foo-bar: thing - environment: - REF: thing - hostname: example - image: foo -`, string(raw)) - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestGenerateRouteResource(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo -service: - ports: - foo: - port: 80 - targetPort: 8080 -resources: - r1: - type: route - params: - host: localhost1 - path: /first - port: foo - r2: - type: route - params: - host: localhost1 - path: /second - port: foo - r3: - type: route - metadata: - annotations: - compose.score.dev/route-provisioner-path-type: Exact - params: - host: localhost2 - path: /third - port: 80 -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 3) - x := sd.State.SharedState["default-provisioners-routing-instance"].(map[string]interface{}) - instanceServiceName := x["instanceServiceName"].(string) - assert.Contains(t, instanceServiceName, "routing-") - delete(x, "instanceServiceName") - assert.Equal(t, map[string]interface{}{ - "default-provisioners-routing-instance": map[string]interface{}{ - "hosts": map[string]interface{}{ - "localhost1": map[string]interface{}{ - "route.default#example.r1": map[string]interface{}{"path": "/first", "port": 8080, "target": "example:8080", "path_type": "Prefix"}, - "route.default#example.r2": map[string]interface{}{"path": "/second", "port": 8080, "target": "example:8080", "path_type": "Prefix"}, - }, - "localhost2": map[string]interface{}{ - "route.default#example.r3": map[string]interface{}{"path": "/third", "port": 8080, "target": "example:8080", "path_type": "Exact"}, - }, - }, - "instancePort": 8080, - }, - }, sd.State.SharedState) - - // validate that the wildcard routes don't exist for /third - raw, err := os.ReadFile(filepath.Join(td, ".score-compose", "mounts", instanceServiceName, "nginx.conf")) - assert.NoError(t, err) - assert.Contains(t, string(raw), `location ~ ^/first$`) - assert.Contains(t, string(raw), `location ~ ^/first/.*`) - assert.Contains(t, string(raw), `location ~ ^/second$`) - assert.Contains(t, string(raw), `location ~ ^/second/.*`) - assert.Contains(t, string(raw), `location ~ ^/third$`) - assert.NotContains(t, string(raw), `location ~ ^/third/.*`) - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestEnvVarsArentRequiredInVariables(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - variables: - ONE: ${resources.env.UNKNOWN_SCORE_VARIABLE} -resources: - env: - type: environment -`), 0644)) - _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - environment: - ONE: ${UNKNOWN_SCORE_VARIABLE} - hostname: example - image: foo -`, string(raw)) -} - -func TestEnvVarsMustResolveInsideFiles_old_files_spec(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - files: - - target: /some/file - content: ${resources.env.UNKNOWN_SCORE_VARIABLE} -resources: - env: - type: environment -`), 0644)) - _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.EqualError(t, err, "failed to convert workload 'example' to Docker compose: containers.example.files[/some/file]: "+ - "failed to substitute in content: invalid ref 'resources.env.UNKNOWN_SCORE_VARIABLE': "+ - "environment variable 'UNKNOWN_SCORE_VARIABLE' must be resolved", - ) -} - -func TestEnvVarsMustResolveInsideFiles_new_files_spec(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - files: - "/some/file": - content: ${resources.env.UNKNOWN_SCORE_VARIABLE} -resources: - env: - type: environment -`), 0644)) - _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.EqualError(t, err, "failed to convert workload 'example' to Docker compose: containers.example.files[/some/file]: "+ - "failed to substitute in content: invalid ref 'resources.env.UNKNOWN_SCORE_VARIABLE': "+ - "environment variable 'UNKNOWN_SCORE_VARIABLE' must be resolved", - ) -} - -func TestEnvVarsMustResolveInsideParams(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo -resources: - env: - type: environment - data: - type: volume - params: - x: ${resources.env.UNKNOWN_SCORE_VARIABLE} -`), 0644)) - _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.EqualError(t, err, "failed to provision: failed to substitute params for resource 'volume.default#example.data': "+ - "x: invalid ref 'resources.env.UNKNOWN_SCORE_VARIABLE': "+ - "environment variable 'UNKNOWN_SCORE_VARIABLE' must be resolved", - ) -} - -func TestInitAndGenerate_with_volume_types_with_old_volumes_spec(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // write custom providers - assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` -- uri: template://docker-volume - type: volume - outputs: | - type: volume - source: named-volume -- uri: template://tmpfs-volume - type: tmp-volume - outputs: | - type: tmpfs - tmpfs: - size: 10000000 -- uri: template://bind-volume - type: bind-volume - outputs: | - type: bind - source: /dev/something - bind: - create_host_path: true -`), 0644)) - - // write custom score file - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: busybox - volumes: - - target: /mnt/v1 - source: ${resources.v1} - - target: /mnt/v2 - source: ${resources.v2} - path: thing - - target: /mnt/v3 - source: ${resources.v3} - path: other/thing -resources: - v1: - type: tmp-volume - v2: - type: bind-volume - v3: - type: volume -`), 0644)) - - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - hostname: example - image: busybox - volumes: - - type: bind - source: /dev/something/thing - target: /mnt/v2 - bind: - create_host_path: true - - type: volume - source: named-volume - target: /mnt/v3 - volume: - subpath: other/thing - - type: tmpfs - source: tmp-volume.default#example.v1 - target: /mnt/v1 - tmpfs: - size: "10000000" -`, string(raw)) -} - -func TestInitAndGenerate_with_volume_types_with_new_volumes_spec(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // write custom providers - assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", "00-custom.provisioners.yaml"), []byte(` -- uri: template://docker-volume - type: volume - outputs: | - type: volume - source: named-volume -- uri: template://tmpfs-volume - type: tmp-volume - outputs: | - type: tmpfs - tmpfs: - size: 10000000 -- uri: template://bind-volume - type: bind-volume - outputs: | - type: bind - source: /dev/something - bind: - create_host_path: true -`), 0644)) - - // write custom score file - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: busybox - volumes: - "/mnt/v1": - source: ${resources.v1} - "/mnt/v2": - source: ${resources.v2} - path: thing - "/mnt/v3": - source: ${resources.v3} - path: other/thing -resources: - v1: - type: tmp-volume - v2: - type: bind-volume - v3: - type: volume -`), 0644)) - - // generate - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, `name: "001" -services: - example-example: - annotations: - compose.score.dev/workload-name: example - hostname: example - image: busybox - volumes: - - type: bind - source: /dev/something/thing - target: /mnt/v2 - bind: - create_host_path: true - - type: volume - source: named-volume - target: /mnt/v3 - volume: - subpath: other/thing - - type: tmpfs - source: tmp-volume.default#example.v1 - target: /mnt/v1 - tmpfs: - size: "10000000" -`, string(raw)) -} - -func TestGenerateMongodbResource(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - variables: - CONN_STR_1: "mongodb://${resources.db.username}:${resources.db.password}@${resources.db.host}:${resources.db.port}/" - CONN_STR_2: "${resources.db.connection}" -resources: - db: - type: mongodb -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 1) - assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "connection") - assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "username") - assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "password") - assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "host") - assert.Contains(t, sd.State.Resources["mongodb.default#example.db"].Outputs, "port") - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestGenerateMySQLResource(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - example: - image: foo - variables: - CONN_STR_1: "mysql://${resources.db1.username}:${resources.db1.password}@${resources.db1.host}:${resources.db1.port}/${resources.db1.name}" - CONN_STR_2: "mysql://${resources.db2.username}:${resources.db2.password}@${resources.db2.host}:${resources.db2.port}/${resources.db2.name}" -resources: - db1: - type: mysql - db2: - type: mysql -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 2) - assert.Contains(t, sd.State.Resources["mysql.default#example.db1"].State, "database") - assert.Contains(t, sd.State.Resources["mysql.default#example.db1"].State, "username") - assert.Contains(t, sd.State.Resources["mysql.default#example.db1"].State, "password") - assert.Contains(t, sd.State.Resources["mysql.default#example.db2"].State, "database") - assert.NotEqual(t, sd.State.Resources["mysql.default#example.db1"].State, sd.State.Resources["mysql.default#example.db2"].State) - assert.Contains(t, sd.State.SharedState, "default-provisioners-mysql-instance") - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestGenerateKeepAnnotations(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example - annotations: - example.com/fizz: buzz -containers: - example: - image: foo -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Contains(t, string(raw), `example.com/fizz: buzz`) - - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - raw, err = os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Contains(t, string(raw), `example.com/fizz: buzz`) -} - -func TestGenerateElasticsearchResource(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - hello: - image: foo -resources: - ecs: - type: elasticsearch -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - // check that state was persisted - sd, ok, err := project.LoadStateDirectory(td) - assert.NoError(t, err) - assert.True(t, ok) - assert.Len(t, sd.State.Workloads, 1) - assert.Len(t, sd.State.Resources, 1) - assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "host") - assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "port") - assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "username") - assert.Contains(t, sd.State.Resources["elasticsearch.default#example.ecs"].Outputs, "password") - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestGeneratePublishPorts(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - hello: - image: foo -resources: - db1: - type: postgres - db2: - type: postgres - id: thing -`), 0644)) - - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "score.yaml", - "--publish", "8080:example:80", - "--publish", "42:postgres#example.db1.host:13", - "--publish", "43:postgres.default#example.db1.host:14", - "--publish", "44:postgres.default#thing.host:15", - }) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - - t.Run("validate compose spec", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) - - for _, tc := range [][2]string{ - {"something", "--publish[0] expected 3 :-separated parts"}, - {"x:foo:y", "--publish[0] could not parse host port 'x' as integer"}, - {"8080:foo:y", "--publish[0] could not parse container port 'y' as integer"}, - {"42:thing:13", "--publish[0] failed to find a workload named 'thing'"}, - {"42:x#y:13", "--publish[0] must match RES_UID.OUTPUT"}, - {"42:x#y.z:13", "--publish[0] failed to find a resource with uid 'x#y'"}, - {"42:postgres#thing.foo:13", "--publish[0] resource 'postgres.default#thing' has no output 'foo'"}, - } { - t.Run("invalid publish "+tc[0], func(t *testing.T) { - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "score.yaml", - "--publish", tc[0], - }) - assert.EqualError(t, err, tc[1]) - assert.Equal(t, "", stdout) - }) - } -} - -func TestGenerateMultipleSpecsWithImage(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreA.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example-a -containers: - hello: - image: foo -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreB.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example-b -containers: - hello: - image: foo -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "--image", "nginx:latest", "scoreA.yaml", "scoreB.yaml", - }) - assert.EqualError(t, err, "--image cannot be used when multiple score files are provided") - assert.Equal(t, "", stdout) -} - -func TestGenerateMultipleSpecsWithBuild(t *testing.T) { - td := changeToTempDir(t) - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreA.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example-a -containers: - hello: - image: foo -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "scoreB.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example-b -containers: - hello: - image: foo -`), 0644)) - stdout, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{ - "generate", "--build", "foo=.", "scoreA.yaml", "scoreB.yaml", - }) - assert.EqualError(t, err, "--build cannot be used when multiple score files are provided") - assert.Equal(t, "", stdout) -} - -func TestGenerateWithPatching(t *testing.T) { - td := changeToTempDir(t) - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example - custom: - privileged: true -containers: - hello: - image: foo -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "patch1.template"), []byte(` -{{ range $name, $spec := .Workloads }} - {{ if (dig "metadata" "custom" "privileged" false $spec) }} - {{ range $cname, $_ := $spec.containers }} -- op: set - path: services.{{ $name }}-{{ $cname }}.privileged - value: true - description: Enable privileged mode on service containers - {{ end }} - {{ end }} -{{ end }} ---- -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "patch2.template"), []byte(` -{{ range $name, $cfg := .Compose.services }} -- op: set - path: services.{{ $name }}-future - value: {{ toRawJson $cfg }} - description: Rename service {{ $name }} -- op: delete - path: services.{{ $name }} - description: Delete service {{ $name }} -{{ end }} ---- -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "patch3.template"), []byte(` -{{ range $name, $cfg := .Compose.services }} -- op: set - path: services.{{ $name }}.read_only - value: true - description: Set services to read only root fs -{{ end }} ---- -`), 0644)) - _, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", "patch1.template", "--patch-templates", "patch2.template", "--patch-templates", "patch3.template"}) - assert.NoError(t, err) - t.Log(stderr) - - _, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - t.Log(stderr) - - raw, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - assert.NoError(t, err) - assert.Equal(t, string(raw), `name: "001" -services: - example-hello-future: - annotations: - compose.score.dev/workload-name: example - hostname: example - image: foo - privileged: true - read_only: true -`) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/init.go b/gen/external-content/resource-provisioners/default/score-compose/init.go deleted file mode 100644 index d1f15a06..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/init.go +++ /dev/null @@ -1,316 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - _ "embed" - "errors" - "fmt" - "log/slog" - "os" - "path/filepath" - "strings" - - "github.com/score-spec/score-go/framework" - "github.com/score-spec/score-go/uriget" - "github.com/spf13/cobra" - - "github.com/score-spec/score-compose/internal/patching" - "github.com/score-spec/score-compose/internal/project" - "github.com/score-spec/score-compose/internal/provisioners/loader" -) - -const ( - DefaultScoreFileContent = `# Score provides a developer-centric and platform-agnostic -# Workload specification to improve developer productivity and experience. -# Score eliminates configuration management between local and remote environments. -# -# Specification reference: https://docs.score.dev/docs/reference/score-spec-reference/ ---- - -# Score specification version -apiVersion: score.dev/v1b1 - -metadata: - name: example - -containers: - hello-world: - image: nginx:latest - - # Uncomment the following for a custom entrypoint command - # command: [] - - # Uncomment the following for custom arguments - # args: [] - - # Environment variables to inject into the container - variables: - EXAMPLE_VARIABLE: "example-value" - -service: - ports: - # Expose the http port from nginx on port 8080 - www: - port: 8080 - targetPort: 80 - -resources: {} -` - initCmdFileFlag = "file" - initCmdFileProjectFlag = "project" - initCmdFileNoSampleFlag = "no-sample" - initCmdProvisionerFlag = "provisioners" - initCmdPatchTemplateFlag = "patch-templates" - initCmdNoDefaultProvisionersFlag = "no-default-provisioners" -) - -//go:embed default.provisioners.yaml -var defaultProvisionersContent string - -var initCmd = &cobra.Command{ - Use: "init", - Args: cobra.NoArgs, - Short: "Initialise a new score-compose project with local state directory and score file", - Long: `The init subcommand will prepare the current directory for working with score-compose and prepare any local -files or configuration needed to be successful. - -A directory named .score-compose will be created if it doesn't exist. This file stores local state and generally should -not be checked into source control. Add it to your .gitignore file if you use Git as version control. - -The project name will be used as a Docker compose project name when the final compose files are written. This name -acts as a namespace when multiple score files and containers are used. - -Custom provisioners can be installed by uri using the --provisioners flag. The provisioners will be installed and take -precedence in the order they are defined over the default provisioners. If init has already been called with provisioners -the new provisioners will take precedence. - -To adjust the way the compose project is generated, or perform post processing actions, you can use the --patch-templates -flag to provide one or more template files by uri. Each template file is stored in the project and then evaluated as a -Golang text/template and should output a yaml/json encoded array of patches. Each patch is an object with required 'op' -(set or delete), 'patch' (a dot-separated json path), a 'value' if the 'op' == 'set', and an optional 'description' for -showing in the logs. The template has access to '.Compose' and '.Workloads'. -`, - Example: ` - # Define a score file to generate - score-compose init --file score2.yaml - - # Or override the docker compose project name - score-compose init --project score-compose2 - - # Or disable the default score file generation if you already have a score file - score-compose init --no-sample - - # Optionally loading in provisoners from a remote url - score-compose init --provisioners https://raw.githubusercontent.com/user/repo/main/example.yaml - - # Optionally adding a couple of patching templates - score-compose init --patch-templates ./patching.tpl --patch-templates https://raw.githubusercontent.com/user/repo/main/example.tpl - -URI Retrieval: - The --provisioners and --patch-templates arguments support URI retrieval for pulling the contents from a URI on disk - or over the network. These support: - - HTTP : http://host/file - - HTTPS : https://host/file - - Git (SSH) : git-ssh://git@host/repo.git/file - - Git (HTTPS) : git-https://host/repo.git/file - - OCI : oci://[registry/][namespace/]repository[:tag|@digest][#file] - - Local File : /path/to/local/file - - Stdin : - (read from standard input)`, - - // don't print the errors - we print these ourselves in main() - SilenceErrors: true, - - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - - // load flag values - initCmdScoreFile, _ := cmd.Flags().GetString(initCmdFileFlag) - initCmdComposeProject, _ := cmd.Flags().GetString(initCmdFileProjectFlag) - initCmdPatchingFiles, _ := cmd.Flags().GetStringArray(initCmdPatchTemplateFlag) - - // validate project - if initCmdComposeProject != "" { - cleanedInitCmdComposeProject := cleanComposeProjectName(initCmdComposeProject) - if cleanedInitCmdComposeProject != initCmdComposeProject { - return fmt.Errorf("invalid value for --project, it must match ^[a-z0-9][a-z0-9_-]*$") - } - } - - var templates []string - for _, u := range initCmdPatchingFiles { - slog.Info(fmt.Sprintf("Fetching patch template from %s", u)) - content, err := uriget.GetFile(cmd.Context(), u) - if err != nil { - return fmt.Errorf("error fetching patch template from %s: %w", u, err) - } else if err = patching.ValidatePatchTemplate(string(content)); err != nil { - return fmt.Errorf("error parsing patch template from %s: %w", u, err) - } - templates = append(templates, string(content)) - } - - sd, ok, err := project.LoadStateDirectory(".") - if err != nil { - return fmt.Errorf("failed to load existing state directory: %w", err) - } else if ok { - slog.Info(fmt.Sprintf("Found existing state directory '%s'", sd.Path)) - var hasChanges bool - if initCmdComposeProject != "" && sd.State.Extras.ComposeProjectName != initCmdComposeProject { - sd.State.Extras.ComposeProjectName = initCmdComposeProject - hasChanges = true - } - if len(templates) > 0 { - sd.State.Extras.PatchingTemplates = templates - hasChanges = true - } - if hasChanges { - if err := sd.Persist(); err != nil { - return fmt.Errorf("failed to persist new state file: %w", err) - } - } - - } else { - - slog.Info(fmt.Sprintf("Writing new state directory '%s'", project.DefaultRelativeStateDirectory)) - wd, _ := os.Getwd() - sd = &project.StateDirectory{ - Path: project.DefaultRelativeStateDirectory, - State: project.State{ - Workloads: map[string]framework.ScoreWorkloadState[project.WorkloadExtras]{}, - Resources: map[framework.ResourceUid]framework.ScoreResourceState[framework.NoExtras]{}, - SharedState: map[string]interface{}{}, - Extras: project.StateExtras{ - ComposeProjectName: cleanComposeProjectName(filepath.Base(wd)), - MountsDirectory: filepath.Join(project.DefaultRelativeStateDirectory, project.MountsDirectoryName), - }, - }, - } - if initCmdComposeProject != "" { - sd.State.Extras.ComposeProjectName = initCmdComposeProject - } - if len(templates) > 0 { - sd.State.Extras.PatchingTemplates = templates - } - slog.Info(fmt.Sprintf("Writing new state directory '%s' with project name '%s'", sd.Path, sd.State.Extras.ComposeProjectName)) - if err := sd.Persist(); err != nil { - return fmt.Errorf("failed to persist new compose project name: %w", err) - } - - // create and write the default provisioners file if it doesn't already exist - disableDefaultProvisioners, err := cmd.Flags().GetBool(initCmdNoDefaultProvisionersFlag) - if err != nil { - return fmt.Errorf("failed to parse --%s flag: %w", initCmdNoDefaultProvisionersFlag, err) - } - - if !disableDefaultProvisioners { - defaultProvisioners := filepath.Join(sd.Path, "zz-default.provisioners.yaml") - - if _, err := os.Stat(defaultProvisioners); err != nil { - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to check for existing default provisioners file: %w", err) - } - - f, err := os.OpenFile(defaultProvisioners, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) - if err != nil { - return fmt.Errorf("failed to create default provisioners file: %w", err) - } - defer f.Close() - - slog.Info("Writing default provisioners file", "path", defaultProvisioners) - if _, err := f.WriteString(defaultProvisionersContent); err != nil { - return fmt.Errorf("failed to write default provisioners content: %w", err) - } - } else { - slog.Info("Default provisioners file already exists, skipping", "path", defaultProvisioners) - } - } else { - slog.Info("Skipping default provisioners due to --no-default-provisioners flag") - } - } - - if _, err := os.ReadFile(initCmdScoreFile); err != nil { - if v, _ := cmd.Flags().GetBool(initCmdFileNoSampleFlag); v { - slog.Info(fmt.Sprintf("Initial Score file '%s' does not exist - and sample generation is disabled", initCmdScoreFile)) - } else { - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to check existing Score file: %w", err) - } - slog.Info(fmt.Sprintf("Initial Score file '%s' does not exist - creating it", initCmdScoreFile)) - if err := os.WriteFile(initCmdScoreFile+".temp", []byte(DefaultScoreFileContent), 0755); err != nil { - return fmt.Errorf("failed to write initial score file: %w", err) - } else if err := os.Rename(initCmdScoreFile+".temp", initCmdScoreFile); err != nil { - return fmt.Errorf("failed to complete writing initial Score file: %w", err) - } - } - } else { - slog.Info(fmt.Sprintf("Found existing Score file '%s'", initCmdScoreFile)) - } - - if v, _ := cmd.Flags().GetStringArray(initCmdProvisionerFlag); len(v) > 0 { - for i, vi := range v { - data, err := uriget.GetFile(cmd.Context(), vi) - if err != nil { - return fmt.Errorf("failed to load provisioner %d: %w", i+1, err) - } - - var saveFilename string - if vi == "-" { - saveFilename = "from-stdin.provisioners.yaml" - } else { - saveFilename = vi - } - - if err := loader.SaveProvisionerToDirectory(sd.Path, saveFilename, data); err != nil { - return fmt.Errorf("failed to save provisioner %d: %w", i+1, err) - } - } - } - - if provs, err := loader.LoadProvisionersFromDirectory(sd.Path, loader.DefaultSuffix); err != nil { - return fmt.Errorf("failed to load existing provisioners: %w", err) - } else { - slog.Debug(fmt.Sprintf("Successfully loaded %d resource provisioners", len(provs))) - } - - slog.Info("Read more about the Score specification at https://docs.score.dev/docs/") - - return nil - }, -} - -func init() { - initCmd.Flags().StringP(initCmdFileFlag, "f", scoreFileDefault, "The score file to initialize") - initCmd.Flags().StringP(initCmdFileProjectFlag, "p", "", "Set the name of the docker compose project (defaults to the current directory name)") - initCmd.Flags().Bool(initCmdFileNoSampleFlag, false, "Disable generation of the sample score file") - initCmd.Flags().StringArray(initCmdProvisionerFlag, nil, "Provisioner files to install. May be specified multiple times. Supports URI retrieval.") - initCmd.Flags().StringArray(initCmdPatchTemplateFlag, nil, "Patching template files to include. May be specified multiple times. Supports URI retrieval.") - initCmd.Flags().Bool(initCmdNoDefaultProvisionersFlag, false, "Disable generation of the default provisioners file") - - rootCmd.AddCommand(initCmd) -} - -func cleanComposeProjectName(input string) string { - input = strings.ToLower(input) - isFirst := true - input = strings.Map(func(r rune) rune { - if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || (!isFirst && ((r == '_') || (r == '-'))) { - isFirst = false - return r - } - isFirst = false - return -1 - }, input) - return input -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/init_test.go b/gen/external-content/resource-provisioners/default/score-compose/init_test.go deleted file mode 100644 index 0986b095..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/init_test.go +++ /dev/null @@ -1,331 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "fmt" - "os" - "path/filepath" - "slices" - "strings" - "testing" - - "github.com/score-spec/score-go/framework" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/score-spec/score-compose/internal/project" - "github.com/score-spec/score-compose/internal/provisioners" - "github.com/score-spec/score-compose/internal/provisioners/loader" -) - -func TestInitHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `The init subcommand will prepare the current directory for working with score-compose and prepare any local -files or configuration needed to be successful. - -A directory named .score-compose will be created if it doesn't exist. This file stores local state and generally should -not be checked into source control. Add it to your .gitignore file if you use Git as version control. - -The project name will be used as a Docker compose project name when the final compose files are written. This name -acts as a namespace when multiple score files and containers are used. - -Custom provisioners can be installed by uri using the --provisioners flag. The provisioners will be installed and take -precedence in the order they are defined over the default provisioners. If init has already been called with provisioners -the new provisioners will take precedence. - -To adjust the way the compose project is generated, or perform post processing actions, you can use the --patch-templates -flag to provide one or more template files by uri. Each template file is stored in the project and then evaluated as a -Golang text/template and should output a yaml/json encoded array of patches. Each patch is an object with required 'op' -(set or delete), 'patch' (a dot-separated json path), a 'value' if the 'op' == 'set', and an optional 'description' for -showing in the logs. The template has access to '.Compose' and '.Workloads'. - -Usage: - score-compose init [flags] - -Examples: - - # Define a score file to generate - score-compose init --file score2.yaml - - # Or override the docker compose project name - score-compose init --project score-compose2 - - # Or disable the default score file generation if you already have a score file - score-compose init --no-sample - - # Optionally loading in provisoners from a remote url - score-compose init --provisioners https://raw.githubusercontent.com/user/repo/main/example.yaml - - # Optionally adding a couple of patching templates - score-compose init --patch-templates ./patching.tpl --patch-templates https://raw.githubusercontent.com/user/repo/main/example.tpl - -URI Retrieval: - The --provisioners and --patch-templates arguments support URI retrieval for pulling the contents from a URI on disk - or over the network. These support: - - HTTP : http://host/file - - HTTPS : https://host/file - - Git (SSH) : git-ssh://git@host/repo.git/file - - Git (HTTPS) : git-https://host/repo.git/file - - OCI : oci://[registry/][namespace/]repository[:tag|@digest][#file] - - Local File : /path/to/local/file - - Stdin : - (read from standard input) - -Flags: - -f, --file string The score file to initialize (default "./score.yaml") - -h, --help help for init - --no-default-provisioners Disable generation of the default provisioners file - --no-sample Disable generation of the sample score file - --patch-templates stringArray Patching template files to include. May be specified multiple times. Supports URI retrieval. - -p, --project string Set the name of the docker compose project (defaults to the current directory name) - --provisioners stringArray Provisioner files to install. May be specified multiple times. Supports URI retrieval. - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) - - stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "init"}) - assert.NoError(t, err) - assert.Equal(t, stdout, stdout2) - assert.Equal(t, "", stderr) -} - -func TestInitNominal(t *testing.T) { - td := t.TempDir() - - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(td)) - defer func() { - require.NoError(t, os.Chdir(wd)) - }() - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - - stdout, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"run"}) - assert.NoError(t, err) - assert.Equal(t, `services: - example-hello-world: - annotations: - compose.score.dev/workload-name: example - environment: - EXAMPLE_VARIABLE: example-value - hostname: example - image: nginx:latest - ports: - - target: 80 - published: "8080" -`, stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - - sd, ok, err := project.LoadStateDirectory(".") - assert.NoError(t, err) - if assert.True(t, ok) { - assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path) - assert.Equal(t, filepath.Base(td), sd.State.Extras.ComposeProjectName) - assert.Equal(t, filepath.Join(project.DefaultRelativeStateDirectory, "mounts"), sd.State.Extras.MountsDirectory) - assert.Equal(t, map[string]framework.ScoreWorkloadState[project.WorkloadExtras]{}, sd.State.Workloads) - assert.Equal(t, map[framework.ResourceUid]framework.ScoreResourceState[framework.NoExtras]{}, sd.State.Resources) - assert.Equal(t, map[string]interface{}{}, sd.State.SharedState) - } -} - -func TestInitNoSample(t *testing.T) { - td := t.TempDir() - - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(td)) - defer func() { - require.NoError(t, os.Chdir(wd)) - }() - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--no-sample"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - - _, err = os.Stat("score.yaml") - assert.ErrorIs(t, err, os.ErrNotExist) -} - -func TestInitNominal_custom_file_and_project(t *testing.T) { - td := t.TempDir() - - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(td)) - defer func() { - require.NoError(t, os.Chdir(wd)) - }() - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--file", "score2.yaml", "--project", "bananas"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - - _, err = os.Stat("score.yaml") - assert.ErrorIs(t, err, os.ErrNotExist) - _, err = os.Stat("score2.yaml") - assert.NoError(t, err) - - sd, ok, err := project.LoadStateDirectory(".") - assert.NoError(t, err) - if assert.True(t, ok) { - assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path) - assert.Equal(t, "bananas", sd.State.Extras.ComposeProjectName) - } -} - -func TestInitNominal_bad_project(t *testing.T) { - td := t.TempDir() - - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(td)) - defer func() { - require.NoError(t, os.Chdir(wd)) - }() - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--project", "-this-is-invalid-"}) - assert.EqualError(t, err, "invalid value for --project, it must match ^[a-z0-9][a-z0-9_-]*$") - assert.Equal(t, "", stdout) - assert.Equal(t, "", stderr) -} - -func TestInitNominal_run_twice(t *testing.T) { - td := t.TempDir() - - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(td)) - defer func() { - require.NoError(t, os.Chdir(wd)) - }() - - // first init - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--file", "score2.yaml", "--project", "bananas"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - - // check default provisioners exists and overwrite it with an empty array - dpf, err := os.Stat(filepath.Join(td, ".score-compose", "zz-default.provisioners.yaml")) - assert.NoError(t, err) - assert.NoError(t, os.WriteFile(filepath.Join(td, ".score-compose", dpf.Name()), []byte("[]"), 0644)) - - // init again - stdout, stderr, err = executeAndResetCommand(context.Background(), rootCmd, []string{"init"}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - - // verify that default provisioners was not overwritten again - dpf, err = os.Stat(filepath.Join(td, ".score-compose", dpf.Name())) - assert.NoError(t, err) - assert.Equal(t, 2, int(dpf.Size())) - - _, err = os.Stat("score.yaml") - assert.NoError(t, err) - _, err = os.Stat("score2.yaml") - assert.NoError(t, err) - - sd, ok, err := project.LoadStateDirectory(".") - assert.NoError(t, err) - if assert.True(t, ok) { - assert.Equal(t, project.DefaultRelativeStateDirectory, sd.Path) - assert.Equal(t, "bananas", sd.State.Extras.ComposeProjectName) - assert.Equal(t, filepath.Join(project.DefaultRelativeStateDirectory, "mounts"), sd.State.Extras.MountsDirectory) - assert.Equal(t, map[string]framework.ScoreWorkloadState[project.WorkloadExtras]{}, sd.State.Workloads) - assert.Equal(t, map[framework.ResourceUid]framework.ScoreResourceState[framework.NoExtras]{}, sd.State.Resources) - assert.Equal(t, map[string]interface{}{}, sd.State.SharedState) - } -} - -func TestInitWithProvisioners(t *testing.T) { - td := t.TempDir() - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(td)) - defer func() { - require.NoError(t, os.Chdir(wd)) - }() - - td2 := t.TempDir() - assert.NoError(t, os.WriteFile(filepath.Join(td2, "one.provisioners.yaml"), []byte(` -- uri: template://one - type: thing - outputs: "{}" -`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td2, "two.provisioners.yaml"), []byte(` -- uri: template://two - type: thing - outputs: "{}" -`), 0644)) - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--provisioners", filepath.Join(td2, "one.provisioners.yaml"), "--provisioners", "file://" + filepath.Join(td2, "two.provisioners.yaml")}) - assert.NoError(t, err) - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - - provs, err := loader.LoadProvisionersFromDirectory(filepath.Join(td, ".score-compose"), loader.DefaultSuffix) - assert.NoError(t, err) - expectedProvisionerUris := []string{"template://one", "template://two"} - for _, expectedUri := range expectedProvisionerUris { - assert.True(t, slices.ContainsFunc(provs, func(p provisioners.Provisioner) bool { - return p.Uri() == expectedUri - }), fmt.Sprintf("Expected provisioner '%s' not found", expectedUri)) - } -} - -func TestInitWithPatchingFiles(t *testing.T) { - td := t.TempDir() - wd, _ := os.Getwd() - require.NoError(t, os.Chdir(td)) - defer func() { - require.NoError(t, os.Chdir(wd)) - }() - assert.NoError(t, os.WriteFile(filepath.Join(td, "patch-templates-1"), []byte(`[]`), 0644)) - assert.NoError(t, os.WriteFile(filepath.Join(td, "patch-templates-2"), []byte(`[]`), 0644)) - - t.Run("new", func(t *testing.T) { - _, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", filepath.Join(td, "patch-templates-1"), "--patch-templates", filepath.Join(td, "patch-templates-2")}) - assert.NoError(t, err) - t.Log(stderr) - sd, ok, err := project.LoadStateDirectory(".") - assert.NoError(t, err) - if assert.True(t, ok) { - assert.Len(t, sd.State.Extras.PatchingTemplates, 2) - } - }) - - t.Run("update", func(t *testing.T) { - _, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", filepath.Join(td, "patch-templates-2")}) - assert.NoError(t, err) - t.Log(stderr) - sd, ok, err := project.LoadStateDirectory(".") - assert.NoError(t, err) - if assert.True(t, ok) { - assert.Len(t, sd.State.Extras.PatchingTemplates, 1) - } - }) - - t.Run("bad patch", func(t *testing.T) { - assert.NoError(t, os.WriteFile(filepath.Join(td, "patch-templates-3"), []byte(`{{ what is this }}`), 0644)) - _, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--patch-templates", filepath.Join(td, "patch-templates-3")}) - assert.Error(t, err, "failed to parse template: template: :1: function \"what\" not defined") - }) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/provisioners.go b/gen/external-content/resource-provisioners/default/score-compose/provisioners.go deleted file mode 100644 index 7ea5ca11..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/provisioners.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "fmt" - "log/slog" - "os" - "sort" - "strings" - - "github.com/spf13/cobra" - - "github.com/score-spec/score-compose/internal/project" - "github.com/score-spec/score-compose/internal/provisioners" - "github.com/score-spec/score-compose/internal/provisioners/loader" - "github.com/score-spec/score-go/formatter" -) - -var ( - provisionersGroup = &cobra.Command{ - Use: "provisioners", - Short: "Subcommands related to provisioners", - } - provisionersList = &cobra.Command{ - Use: "list [--format table|json]", - Short: "List the provisioners", - Long: `The list command will list out the provisioners. This requires an active score compose state -after 'init' or 'generate' has been run. The list of provisioners will be empty if no provisioners are defined. -`, - Args: cobra.ArbitraryArgs, - SilenceErrors: true, - RunE: listProvisioners, - } -) - -func listProvisioners(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - wd, _ := os.Getwd() - sd, ok, err := project.LoadStateDirectory(wd) - if err != nil { - return fmt.Errorf("failed to load existing state directory: %w", err) - } else if !ok { - return fmt.Errorf("no state directory found, run 'score-compose init' first") - } - slog.Debug(fmt.Sprintf("Listing provisioners in project '%s'", sd.State.Extras.ComposeProjectName)) - - provisioners, err := loader.LoadProvisionersFromDirectory(sd.Path, loader.DefaultSuffix) - if err != nil { - return fmt.Errorf("failed to load provisioners in %s: %w", sd.Path, err) - } - - if len(provisioners) == 0 { - slog.Info("No provisioners found") - return nil - } - - outputFormat := cmd.Flag("format").Value.String() - return displayProvisioners(provisioners, outputFormat) -} - -func displayProvisioners(loadedProvisioners []provisioners.Provisioner, outputFormat string) error { - var outputFormatter formatter.OutputFormatter - sortedProvisioners := sortProvisionersByType(loadedProvisioners) - - switch outputFormat { - case "json": - type jsonData struct { - Type string - Class string - Params []string - Outputs []string - Description string - } - var outputs []jsonData - for _, provisioner := range sortedProvisioners { - outputs = append(outputs, jsonData{ - Type: provisioner.Type(), - Class: provisioner.Class(), - Params: provisioner.Params(), - Outputs: provisioner.Outputs(), - Description: provisioner.Description(), - }) - } - outputFormatter = &formatter.JSONOutputFormatter[[]jsonData]{Data: outputs} - default: - rows := [][]string{} - - for _, provisioner := range sortedProvisioners { - rows = append(rows, []string{provisioner.Type(), provisioner.Class(), strings.Join(provisioner.Params(), ", "), strings.Join(provisioner.Outputs(), ", "), provisioner.Description()}) - } - headers := []string{"Type", "Class", "Params", "Outputs", "Description"} - outputFormatter = &formatter.TableOutputFormatter{ - Headers: headers, - Rows: rows, - } - } - return outputFormatter.Display() -} - -func sortProvisionersByType(provisioners []provisioners.Provisioner) []provisioners.Provisioner { - sort.Slice(provisioners, func(i, j int) bool { - return provisioners[i].Type() < provisioners[j].Type() - }) - return provisioners -} - -func init() { - provisionersList.Flags().StringP("format", "f", "table", "Format of the output: table (default), json") - provisionersGroup.AddCommand(provisionersList) - rootCmd.AddCommand(provisionersGroup) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go b/gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go deleted file mode 100644 index d9227d32..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/provisioners_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "flag" - "fmt" - "io" - "os" - "path/filepath" - "testing" - - "github.com/score-spec/score-compose/internal/provisioners/loader" - "github.com/stretchr/testify/assert" -) - -var ( - update = flag.Bool("update", false, "update the golden files of this test") -) - -func TestDisplayProvisioners(t *testing.T) { - tests := []struct { - name string - fixture string - format string - expectedResponse string - expectedError string - }{ - { - name: "display provisioners in table format", - fixture: "provisioners.custom.golden", - format: "table", - expectedResponse: "provisioners.list.valid.table.golden", - expectedError: "", - }, - { - name: "display provisioners in json format", - fixture: "provisioners.custom.golden", - format: "json", - expectedResponse: "provisioners.list.valid.json.golden", - expectedError: "", - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - provisioners, err := loader.LoadProvisionersFromDirectory("fixtures/", test.fixture) - if err != nil { - t.Fatal(err) - } - - // Capture the output - old := os.Stdout - r, w, _ := os.Pipe() - os.Stdout = w - - displayProvisioners(provisioners, test.format) - - w.Close() - os.Stdout = old - out, _ := io.ReadAll(r) - - got := string(out) - - expected, err := goldenValue(t, "testdata", test.expectedResponse, got, *update) - if err != nil { - t.Fatal(err) - } - - if test.expectedError != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), test.expectedError) - } else { - assert.NoError(t, err) - assert.Equal(t, expected, got) - } - }) - } -} - -func goldenValue(t *testing.T, path string, goldenFile string, actual string, update bool) (string, error) { - t.Helper() - goldenPath := filepath.Join(path, goldenFile) - - f, err := os.OpenFile(goldenPath, os.O_RDWR, 0644) - if err != nil { - return "", fmt.Errorf("failed to open file: %w", err) - } - defer f.Close() - - if update { - _, err := f.WriteString(actual) - if err != nil { - return "", fmt.Errorf("failed to update golden file %s: %w", goldenPath, err) - } - - return actual, nil - } - - content, err := io.ReadAll(f) - if err != nil { - return "", fmt.Errorf("failed to read golden file %s: %w", goldenPath, err) - } - - return string(content), nil -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/resources.go b/gen/external-content/resource-provisioners/default/score-compose/resources.go deleted file mode 100644 index 2e927375..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/resources.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "fmt" - "log/slog" - "slices" - "strings" - "text/template" - - "github.com/Masterminds/sprig/v3" - "github.com/score-spec/score-go/formatter" - "github.com/score-spec/score-go/framework" - "github.com/spf13/cobra" - - "github.com/score-spec/score-compose/internal/project" -) - -const ( - getOutputsCmdFormatFlag = "format" -) - -var ( - resourcesGroup = &cobra.Command{ - Use: "resources", - Short: "Subcommands related to provisioned resources", - } - listResources = &cobra.Command{ - Use: "list", - Short: "List the resource uids", - Long: `The list command will list out the provisioned resource uids. This requires an active score compose state -after 'init' or 'generate' has been run. The list of uids will be empty if no resources are provisioned. -`, - Args: cobra.ExactArgs(0), - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - sd, ok, err := project.LoadStateDirectory(".") - if err != nil { - return fmt.Errorf("failed to load existing state directory: %w", err) - } else if !ok { - return fmt.Errorf("state directory does not exist, please run \"score-compose init\" first") - } - slog.Debug(fmt.Sprintf("Loaded state directory with docker compose project '%s'", sd.State.Extras.ComposeProjectName)) - currentState := &sd.State - resIds, err := currentState.GetSortedResourceUids() - if err != nil { - return fmt.Errorf("failed to sort resources: %w", err) - } - return displayResourcesList(resIds, *currentState, cmd) - }, - } - getResourceOutputs = &cobra.Command{ - Use: "get-outputs TYPE.CLASS#ID", - Short: "Return the resource outputs", - Long: `The get-outputs command will print the outputs of the resource from the last provisioning. The data will -be returned as json. -`, - Args: cobra.ExactArgs(1), - SilenceErrors: true, - RunE: func(cmd *cobra.Command, args []string) error { - cmd.SilenceUsage = true - sd, ok, err := project.LoadStateDirectory(".") - if err != nil { - return fmt.Errorf("failed to load existing state directory: %w", err) - } else if !ok { - return fmt.Errorf("state directory does not exist, please run \"score-compose init\" first") - } - slog.Debug(fmt.Sprintf("Loaded state directory with docker compose project '%s'", sd.State.Extras.ComposeProjectName)) - if res, ok := sd.State.Resources[framework.ResourceUid(args[0])]; ok { - outputs := res.Outputs - if outputs == nil { - outputs = make(map[string]interface{}) - } - return displayResourcesOutputs(outputs, cmd) - } - resourceOuptuts, err := getResourceOutputsByUid(framework.ResourceUid(args[0]), &sd.State) - if err != nil { - return fmt.Errorf("no such resource '%s'", args[0]) - } - return displayResourcesOutputs(resourceOuptuts, cmd) - }, - } -) - -func getResourceOutputsByUid(uid framework.ResourceUid, state *project.State) (map[string]interface{}, error) { - if res, ok := state.Resources[uid]; ok { - outputs := res.Outputs - if outputs == nil { - outputs = make(map[string]interface{}) - } - return outputs, nil - } - return nil, fmt.Errorf("no such resource '%s'", uid) -} - -func getResourceOutputsKeys(uid framework.ResourceUid, state *project.State) ([]string, error) { - outputs, err := getResourceOutputsByUid(uid, state) - if err != nil { - return nil, err - } - keys := make([]string, 0, len(outputs)) - for key, _ := range outputs { - keys = append(keys, key) - } - slices.Sort(keys) - return keys, nil -} - -func displayResourcesOutputs(outputs map[string]interface{}, cmd *cobra.Command) error { - outputFormat := cmd.Flags().Lookup(getOutputsCmdFormatFlag).Value.String() - var outputFormatter formatter.OutputFormatter - switch outputFormat { - case "json": - outputFormatter = &formatter.JSONOutputFormatter[map[string]interface{}]{Data: outputs, Out: cmd.OutOrStdout()} - case "yaml": - outputFormatter = &formatter.YAMLOutputFormatter[map[string]interface{}]{Data: outputs, Out: cmd.OutOrStdout()} - default: - // ensure there is a new line at the end if one is not already present - if !strings.HasSuffix(outputFormat, "\n") { - outputFormat += "\n" - } - prepared, err := template.New("").Funcs(sprig.FuncMap()).Parse(outputFormat) - if err != nil { - return fmt.Errorf("failed to parse format template: %w", err) - } - if err := prepared.Execute(cmd.OutOrStdout(), outputs); err != nil { - return fmt.Errorf("failed to execute template: %w", err) - } - return nil - } - - return outputFormatter.Display() -} - -func displayResourcesList(resources []framework.ResourceUid, state project.State, cmd *cobra.Command) error { - outputFormat := cmd.Flag("format").Value.String() - var outputFormatter formatter.OutputFormatter - - switch outputFormat { - case "json": - type jsonData struct { - UID string - Outputs []string - } - var outputs []jsonData - for _, resource := range resources { - - keys, err := getResourceOutputsKeys(resource, &state) - if err != nil { - return fmt.Errorf("failed to get outputs for resource '%s': %w", resource, err) - } - outputs = append(outputs, jsonData{ - UID: string(resource), - Outputs: keys, - }) - } - outputFormatter = &formatter.JSONOutputFormatter[[]jsonData]{Data: outputs, Out: cmd.OutOrStdout()} - default: - var rows [][]string - for _, resource := range resources { - keys, err := getResourceOutputsKeys(resource, &state) - if err != nil { - return fmt.Errorf("failed to get outputs for resource '%s': %w", resource, err) - } - row := []string{string(resource), strings.Join(keys, ", ")} - rows = append(rows, row) - } - outputFormatter = &formatter.TableOutputFormatter{ - Headers: []string{"UID", "Outputs"}, - Rows: rows, - Out: cmd.OutOrStdout(), - } - } - - return outputFormatter.Display() -} - -func init() { - getResourceOutputs.Flags().StringP(getOutputsCmdFormatFlag, "f", "json", "Format of the output: json, yaml, or a Go template with sprig functions") - resourcesGroup.AddCommand(listResources) - listResources.Flags().StringP("format", "f", "table", "Format of the output: table (default), json") - resourcesGroup.AddCommand(getResourceOutputs) - rootCmd.AddCommand(resourcesGroup) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/resources_test.go b/gen/external-content/resource-provisioners/default/score-compose/resources_test.go deleted file mode 100644 index 6ae55210..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/resources_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" -) - -func TestResourcesHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `Subcommands related to provisioned resources - -Usage: - score-compose resources [command] - -Available Commands: - get-outputs Return the resource outputs - list List the resource uids - -Flags: - -h, --help help for resources - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times - -Use "score-compose resources [command] --help" for more information about a command. -`, stdout) - assert.Equal(t, "", stderr) - - stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "resources"}) - assert.NoError(t, err) - assert.Equal(t, stdout, stdout2) - assert.Equal(t, "", stderr) -} - -func TestResourcesExample(t *testing.T) { - td := changeToTempDir(t) - _, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--no-sample"}) - assert.NoError(t, err) - require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example -containers: - container: - image: thing -resources: - vol: - type: volume - dns: - type: dns -`), 0600)) - _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"generate", "score.yaml"}) - assert.NoError(t, err) - - t.Run("list table", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "list"}) - assert.NoError(t, err) - assert.Equal(t, `+----------------------------+--------------+ -| UID | OUTPUTS | -+----------------------------+--------------+ -| dns.default#example.dns | host | -+----------------------------+--------------+ -| volume.default#example.vol | source, type | -+----------------------------+--------------+ -`, stdout) - }) - - t.Run("list json", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "list", "-f", "json"}) - assert.NoError(t, err) - assert.Equal(t, `[ - { - "UID": "dns.default#example.dns", - "Outputs": [ - "host" - ] - }, - { - "UID": "volume.default#example.vol", - "Outputs": [ - "source", - "type" - ] - } -] -`, stdout) - }) - - t.Run("get not found", func(t *testing.T) { - _, _, err = executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "foo"}) - assert.EqualError(t, err, "no such resource 'foo'") - }) - - t.Run("get dns", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "dns.default#example.dns"}) - assert.NoError(t, err) - var out map[string]interface{} - if assert.NoError(t, json.Unmarshal([]byte(stdout), &out)) { - assert.NotEmpty(t, out["host"].(string)) - } - }) - - t.Run("get vol", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol"}) - assert.NoError(t, err) - var out map[string]interface{} - if assert.NoError(t, json.Unmarshal([]byte(stdout), &out)) { - assert.NotEmpty(t, out["source"].(string)) - } - }) - - t.Run("format json", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", "json"}) - assert.NoError(t, err) - var out map[string]interface{} - assert.NoError(t, json.Unmarshal([]byte(stdout), &out)) - assert.True(t, strings.HasSuffix(stdout, "\n")) - }) - - t.Run("format yaml", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", "yaml"}) - assert.NoError(t, err) - var out map[string]interface{} - assert.NoError(t, yaml.Unmarshal([]byte(stdout), &out)) - assert.True(t, strings.HasSuffix(stdout, "\n")) - }) - - t.Run("format template", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", `{{ . | len }}`}) - assert.NoError(t, err) - assert.Equal(t, "2\n", stdout) - }) - - t.Run("format template with newline", func(t *testing.T) { - stdout, _, err := executeAndResetCommand(context.Background(), rootCmd, []string{"resources", "get-outputs", "volume.default#example.vol", "--format", "{{ . | len }}\n"}) - assert.NoError(t, err) - assert.Equal(t, "2\n", stdout) - }) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/root.go b/gen/external-content/resource-provisioners/default/score-compose/root.go deleted file mode 100644 index 94f57109..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/root.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "io" - "log/slog" - - "github.com/spf13/cobra" - - "github.com/score-spec/score-compose/internal/logging" - "github.com/score-spec/score-compose/internal/version" -) - -var ( - rootCmd = &cobra.Command{ - Use: "score-compose", - Short: "SCORE to docker-compose translator", - Long: `SCORE is a specification for defining environment agnostic configuration for cloud based workloads. -This tool produces a docker-compose configuration file from the SCORE specification. -Complete documentation is available at https://score.dev`, - // don't print the errors - we print these ourselves in main() - SilenceErrors: true, - - // This function always runs for all subcommands - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if q, _ := cmd.Flags().GetBool("quiet"); q { - slog.SetDefault(slog.New(&logging.SimpleHandler{Level: slog.LevelError, Writer: io.Discard})) - } else if v, _ := cmd.Flags().GetCount("verbose"); v == 0 { - slog.SetDefault(slog.New(&logging.SimpleHandler{Level: slog.LevelInfo, Writer: cmd.ErrOrStderr()})) - } else if v == 1 { - slog.SetDefault(slog.New(&logging.SimpleHandler{Level: slog.LevelDebug, Writer: cmd.ErrOrStderr()})) - } else if v == 2 { - slog.SetDefault(slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{ - Level: slog.LevelDebug, AddSource: true, - }))) - } - return nil - }, - } -) - -func init() { - rootCmd.Version = version.BuildVersionString() - rootCmd.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s" .Version}} -`) - rootCmd.PersistentFlags().Bool("quiet", false, "Mute any logging output") - rootCmd.PersistentFlags().CountP("verbose", "v", "Increase log verbosity and detail by specifying this flag one or more times") -} - -func Execute() error { - return rootCmd.Execute() -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/root_test.go b/gen/external-content/resource-provisioners/default/score-compose/root_test.go deleted file mode 100644 index ad9b6923..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/root_test.go +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "regexp" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRootHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"--help"}) - assert.NoError(t, err) - assert.Equal(t, `SCORE is a specification for defining environment agnostic configuration for cloud based workloads. -This tool produces a docker-compose configuration file from the SCORE specification. -Complete documentation is available at https://score.dev - -Usage: - score-compose [command] - -Available Commands: - check-version Assert that the version of score-compose matches the required constraint - completion Generate the autocompletion script for the specified shell - generate Convert one or more Score files into a Docker compose manifest - help Help about any command - init Initialise a new score-compose project with local state directory and score file - provisioners Subcommands related to provisioners - resources Subcommands related to provisioned resources - -Flags: - -h, --help help for score-compose - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times - --version version for score-compose - -Use "score-compose [command] --help" for more information about a command. -`, stdout) - assert.Equal(t, "", stderr) -} - -func TestRootVersion(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"--version"}) - assert.NoError(t, err) - pattern := regexp.MustCompile(`^score-compose 0.0.0 \(build: \S+, sha: \S+\)\n$`) - assert.Truef(t, pattern.MatchString(stdout), "%s does not match: '%s'", pattern.String(), stdout) - assert.Equal(t, "", stderr) -} - -func TestRootCompletion(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion"}) - assert.NoError(t, err) - assert.Equal(t, `Generate the autocompletion script for score-compose for the specified shell. -See each sub-command's help for details on how to use the generated script. - -Usage: - score-compose completion [command] - -Available Commands: - bash Generate the autocompletion script for bash - fish Generate the autocompletion script for fish - powershell Generate the autocompletion script for powershell - zsh Generate the autocompletion script for zsh - -Flags: - -h, --help help for completion - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times - -Use "score-compose completion [command] --help" for more information about a command. -`, stdout) - assert.Equal(t, "", stderr) - - stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "completion"}) - assert.NoError(t, err) - assert.Equal(t, stdout, stdout2) - assert.Equal(t, "", stderr) -} - -func TestRootCompletionBashHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "bash", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `Generate the autocompletion script for the bash shell. - -This script depends on the 'bash-completion' package. -If it is not installed already, you can install it via your OS's package manager. - -To load completions in your current shell session: - - source <(score-compose completion bash) - -To load completions for every new session, execute once: - -#### Linux: - - score-compose completion bash > /etc/bash_completion.d/score-compose - -#### macOS: - - score-compose completion bash > $(brew --prefix)/etc/bash_completion.d/score-compose - -You will need to start a new shell for this setup to take effect. - -Usage: - score-compose completion bash - -Flags: - -h, --help help for bash - --no-descriptions disable completion descriptions - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) -} - -func TestRootCompletionBash(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "bash"}) - assert.NoError(t, err) - assert.Contains(t, stdout, "# bash completion V2 for score-compose") - assert.Equal(t, "", stderr) -} - -func TestRootCompletionFishHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "fish", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `Generate the autocompletion script for the fish shell. - -To load completions in your current shell session: - - score-compose completion fish | source - -To load completions for every new session, execute once: - - score-compose completion fish > ~/.config/fish/completions/score-compose.fish - -You will need to start a new shell for this setup to take effect. - -Usage: - score-compose completion fish [flags] - -Flags: - -h, --help help for fish - --no-descriptions disable completion descriptions - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) -} - -func TestRootCompletionFish(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "fish"}) - assert.NoError(t, err) - assert.Contains(t, stdout, "# fish completion for score-compose") - assert.Equal(t, "", stderr) -} - -func TestRootCompletionZshHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "zsh", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `Generate the autocompletion script for the zsh shell. - -If shell completion is not already enabled in your environment you will need -to enable it. You can execute the following once: - - echo "autoload -U compinit; compinit" >> ~/.zshrc - -To load completions in your current shell session: - - source <(score-compose completion zsh) - -To load completions for every new session, execute once: - -#### Linux: - - score-compose completion zsh > "${fpath[1]}/_score-compose" - -#### macOS: - - score-compose completion zsh > $(brew --prefix)/share/zsh/site-functions/_score-compose - -You will need to start a new shell for this setup to take effect. - -Usage: - score-compose completion zsh [flags] - -Flags: - -h, --help help for zsh - --no-descriptions disable completion descriptions - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) -} - -func TestRootCompletionZsh(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "zsh"}) - assert.NoError(t, err) - assert.Contains(t, stdout, "# zsh completion for score-compose") - assert.Equal(t, "", stderr) -} - -func TestRootCompletionPowershellHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "powershell", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `Generate the autocompletion script for powershell. - -To load completions in your current shell session: - - score-compose completion powershell | Out-String | Invoke-Expression - -To load completions for every new session, add the output of the above command -to your powershell profile. - -Usage: - score-compose completion powershell [flags] - -Flags: - -h, --help help for powershell - --no-descriptions disable completion descriptions - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) -} - -func TestRootCompletionPowershell(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"completion", "powershell"}) - assert.NoError(t, err) - assert.Contains(t, stdout, "# powershell completion for score-compose") - assert.Equal(t, "", stderr) -} - -func TestRootUnknown(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"unknown"}) - assert.EqualError(t, err, "unknown command \"unknown\" for \"score-compose\"") - assert.Equal(t, "", stdout) - assert.Equal(t, "", stderr) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/run.go b/gen/external-content/resource-provisioners/default/score-compose/run.go deleted file mode 100644 index 5cf496dc..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/run.go +++ /dev/null @@ -1,364 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "context" - "encoding/json" - "fmt" - "io" - "log/slog" - "os" - "sort" - "strings" - - "dario.cat/mergo" - "github.com/compose-spec/compose-go/v2/types" - "github.com/score-spec/score-go/framework" - "github.com/spf13/cobra" - "github.com/tidwall/sjson" - "gopkg.in/yaml.v3" - - loader "github.com/score-spec/score-go/loader" - schema "github.com/score-spec/score-go/schema" - score "github.com/score-spec/score-go/types" - - "github.com/score-spec/score-compose/internal/compose" - "github.com/score-spec/score-compose/internal/project" - "github.com/score-spec/score-compose/internal/provisioners" - "github.com/score-spec/score-compose/internal/provisioners/envprov" - "github.com/score-spec/score-compose/internal/util" -) - -const ( - scoreFileDefault = "./score.yaml" - overridesFileDefault = "./overrides.score.yaml" -) - -var ( - scoreFile string - overridesFile string - outFile string - envFile string - buildCtx string - - overrideParams []string - - skipValidation bool -) - -func init() { - runCmd.Flags().StringVarP(&scoreFile, "file", "f", scoreFileDefault, "Source SCORE file") - runCmd.Flags().StringVar(&overridesFile, "overrides", overridesFileDefault, "Overrides SCORE file") - runCmd.Flags().StringVarP(&outFile, "output", "o", "", "Output file") - runCmd.Flags().StringVar(&envFile, "env-file", "", "Location to store sample .env file") - runCmd.Flags().StringVar(&buildCtx, "build", "", "Replaces 'image' name with compose 'build' instruction") - - runCmd.Flags().StringArrayVarP(&overrideParams, "property", "p", nil, "Overrides selected property value") - - runCmd.Flags().BoolVar(&skipValidation, "skip-validation", false, "DEPRECATED: Disables Score file schema validation") - - rootCmd.AddCommand(runCmd) -} - -var runCmd = &cobra.Command{ - Use: "run [--file=score.yaml] [--output=compose.yaml]", - Args: cobra.NoArgs, - Short: "(Deprecated) Translate the SCORE file to docker-compose configuration", - Hidden: true, - RunE: run, - // don't print the errors - we print these ourselves in main() - SilenceErrors: true, -} - -func run(cmd *cobra.Command, args []string) error { - // Silence usage message if args are parsed correctly - cmd.SilenceUsage = true - - // Log a deprecation warning - slog.Warn("\n-----\nDeprecation notice: 'score-compose run' will be deprecated in the future - we recommend that you migrate to 'score-compose init' and 'score-compose generate'\n-----") - - // Open source file - // - slog.Info(fmt.Sprintf("Reading Score file '%s'", scoreFile)) - var err error - var src *os.File - if src, err = os.Open(scoreFile); err != nil { - return err - } - defer src.Close() - - // Parse SCORE spec - // - slog.Info("Parsing Score specification") - var srcMap map[string]interface{} - if err = loader.ParseYAML(&srcMap, src); err != nil { - return err - } - - // Apply overrides from file (optional) - // - if overridesFile != "" { - if ovr, err := os.Open(overridesFile); err == nil { - defer ovr.Close() - - slog.Info(fmt.Sprintf("Loading Score overrides file '%s'", overridesFile)) - var ovrMap map[string]interface{} - if err = loader.ParseYAML(&ovrMap, ovr); err != nil { - return err - } - slog.Info("Applying Score overrides") - if err := mergo.MergeWithOverwrite(&srcMap, ovrMap); err != nil { - return fmt.Errorf("applying overrides fom '%s': %w", overridesFile, err) - } - } else if !os.IsNotExist(err) || overridesFile != overridesFileDefault { - return err - } - } - - // Apply overrides from command line (optional) - // - for _, pstr := range overrideParams { - jsonBytes, err := json.Marshal(srcMap) - if err != nil { - return fmt.Errorf("marshalling score spec: %w", err) - } - - pmap := strings.SplitN(pstr, "=", 2) - if len(pmap) <= 1 { - var path = pmap[0] - slog.Info(fmt.Sprintf("Applying Score properties override: removing '%s'", path)) - if jsonBytes, err = sjson.DeleteBytes(jsonBytes, path); err != nil { - return fmt.Errorf("removing '%s': %w", path, err) - } - } else { - var path = pmap[0] - var val interface{} - if err := yaml.Unmarshal([]byte(pmap[1]), &val); err != nil { - val = pmap[1] - } - - slog.Info(fmt.Sprintf("Applying Score properties override: overriding '%s' = '%s' (%T)", path, val, val)) - if jsonBytes, err = sjson.SetBytes(jsonBytes, path, val); err != nil { - return fmt.Errorf("overriding '%s': %w", path, err) - } - } - - if err = json.Unmarshal(jsonBytes, &srcMap); err != nil { - return fmt.Errorf("unmarshalling score spec: %w", err) - } - } - - // Apply upgrades to fix backports or backward incompatible things - if changes, err := schema.ApplyCommonUpgradeTransforms(srcMap); err != nil { - return fmt.Errorf("failed to upgrade spec: %w", err) - } else if len(changes) > 0 { - for _, change := range changes { - slog.Info(fmt.Sprintf("Applying upgrade to specification: %s", change)) - } - } - - // Validate SCORE spec - // - if !skipValidation { - slog.Info("Validating final Score specification") - if err := schema.Validate(srcMap); err != nil { - return fmt.Errorf("validating workload spec: %w", err) - } - } - - // Convert SCORE spec - // - var spec score.Workload - if err = loader.MapSpec(&spec, srcMap); err != nil { - return fmt.Errorf("validating workload spec: %w", err) - } - - // Build a fake score-compose init state. We don't actually need to store or persist this because we're not doing - // anything iterative or stateful. - state := &project.State{Extras: project.StateExtras{MountsDirectory: "/dev/null"}} - state, err = state.WithWorkload(&spec, &scoreFile, project.WorkloadExtras{}) - if err != nil { - return fmt.Errorf("failed to add score file to state: %w", err) - } - - // Prime the resources with initial state and validate any issues - state, err = state.WithPrimedResources() - if err != nil { - return fmt.Errorf("failed to prime resources: %w", err) - } - - // Instead of actually calling the resource provisioning system, we skip it and fill in the supported resources - // ourselves. - provisionerList, envProvisioner, err := buildLegacyProvisioners(spec.Metadata["name"].(string), state) - if err != nil { - return err - } - - state, err = provisioners.ProvisionResources(context.Background(), state, provisionerList, nil) - if err != nil { - return fmt.Errorf("failed to provision resources: %w", err) - } - - // Build docker-compose configuration - // - slog.Info("Building docker-compose configuration") - proj, err := compose.ConvertSpec(state, &spec) - if err != nil { - return fmt.Errorf("building docker-compose configuration: %w", err) - } - - // Deprecated behavior of the run command which used to publish ports - // Todo: remove this once score-compose run is removed - if spec.Service != nil && len(spec.Service.Ports) > 0 { - ports := make([]types.ServicePortConfig, 0) - for _, pSpec := range spec.Service.Ports { - var pubPort = fmt.Sprintf("%v", pSpec.Port) - var protocol string - if pSpec.Protocol != nil { - protocol = strings.ToLower(string(*pSpec.Protocol)) - } - ports = append(ports, types.ServicePortConfig{ - Published: pubPort, - Target: uint32(util.DerefOr(pSpec.TargetPort, pSpec.Port)), - Protocol: protocol, - }) - } - for serviceName, service := range proj.Services { - if service.NetworkMode == "" { - service.Ports = ports - proj.Services[serviceName] = service - } - } - sort.Slice(ports, func(i, j int) bool { - return ports[i].Published < ports[j].Published - }) - } - - // Override 'image' reference with 'build' instructions - // - if buildCtx != "" { - slog.Info(fmt.Sprintf("Applying build context '%s' for service images", buildCtx)) - // We add the build context to all services and containers here and make a big assumption that all are - // using the image we are building here and now. If this is unexpected, users should use a more complex - // overrides file. - for serviceName, service := range proj.Services { - service.Build = &types.BuildConfig{Context: buildCtx} - service.Image = "" - proj.Services[serviceName] = service - } - } - - // Open output file (optional) - // - var dest = cmd.OutOrStdout() - if outFile != "" { - slog.Info(fmt.Sprintf("Writing output compose file '%s'", outFile)) - destFile, err := os.Create(outFile) - if err != nil { - return err - } - defer destFile.Close() - - dest = io.MultiWriter(dest, destFile) - } - - // Write docker-compose spec - // - if err = compose.WriteYAML(dest, proj); err != nil { - return err - } - - if envFile != "" { - // Open .env file - // - slog.Info(fmt.Sprintf("Writing output .env file '%s'", envFile)) - dest, err := os.Create(envFile) - if err != nil { - return err - } - defer dest.Close() - - // Write .env file - // - envVars := make([]string, 0) - for key, val := range envProvisioner.Accessed() { - var envVar = fmt.Sprintf("%s=%v\n", key, val) - envVars = append(envVars, envVar) - } - sort.Strings(envVars) - - for _, envVar := range envVars { - if _, err := dest.WriteString(envVar); err != nil { - return err - } - } - } - - return nil -} - -func buildLegacyProvisioners(workloadName string, state *project.State) ([]provisioners.Provisioner, *envprov.Provisioner, error) { - envProv := new(envprov.Provisioner) - out := []provisioners.Provisioner{envProv} - for resName, res := range state.Workloads[workloadName].Spec.Resources { - resUid := framework.NewResourceUid(workloadName, resName, res.Type, res.Class, res.Id) - if resUid.Type() == "environment" { - // handled by env prov which is already added above - } else if resUid.Type() == "volume" && resUid.Class() == "default" { - out = append(out, &legacyVolumeProvisioner{MatchResourceUid: resUid}) - } else { - slog.Warn(fmt.Sprintf("resources.%s: '%s.%s' is not directly supported in score-compose, references will be converted to environment variables", resName, resUid.Type(), resUid.Class())) - out = append(out, envProv.GenerateSubProvisioner(resName, resUid)) - } - } - return out, envProv, nil -} - -type legacyVolumeProvisioner struct { - MatchResourceUid framework.ResourceUid -} - -func (l *legacyVolumeProvisioner) Params() []string { - return []string{} -} - -func (l *legacyVolumeProvisioner) Outputs() []string { - return []string{} -} - -func (l *legacyVolumeProvisioner) Uri() string { - return "builtin://legacy-volume" -} - -func (l *legacyVolumeProvisioner) Match(resUid framework.ResourceUid) bool { - return l.MatchResourceUid == resUid -} - -func (l *legacyVolumeProvisioner) Provision(ctx context.Context, input *provisioners.Input) (*provisioners.ProvisionOutput, error) { - return &provisioners.ProvisionOutput{ResourceOutputs: map[string]interface{}{}}, nil -} - -func (l *legacyVolumeProvisioner) Class() string { - return l.MatchResourceUid.Class() -} - -func (l *legacyVolumeProvisioner) Type() string { - return l.MatchResourceUid.Type() -} - -func (l *legacyVolumeProvisioner) Description() string { - return "" -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/run_test.go b/gen/external-content/resource-provisioners/default/score-compose/run_test.go deleted file mode 100644 index 74c2b531..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/run_test.go +++ /dev/null @@ -1,430 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package command - -import ( - "bytes" - "context" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" -) - -// executeAndResetCommand is a test helper that runs and then resets a command for executing in another test. -func executeAndResetCommand(ctx context.Context, cmd *cobra.Command, args []string) (string, string, error) { - beforeOut, beforeErr := cmd.OutOrStdout(), cmd.ErrOrStderr() - defer func() { - cmd.SetOut(beforeOut) - cmd.SetErr(beforeErr) - // also have to remove completion commands which get auto added and bound to an output buffer - for _, command := range cmd.Commands() { - if command.Name() == "completion" { - cmd.RemoveCommand(command) - break - } - } - }() - - nowOut, nowErr := new(bytes.Buffer), new(bytes.Buffer) - cmd.SetOut(nowOut) - cmd.SetErr(nowErr) - cmd.SetArgs(args) - subCmd, err := cmd.ExecuteContextC(ctx) - if subCmd != nil { - subCmd.SetOut(nil) - subCmd.SetErr(nil) - subCmd.SetContext(context.TODO()) - subCmd.SilenceUsage = false - subCmd.Flags().VisitAll(func(f *pflag.Flag) { - if f.Value.Type() == "stringArray" { - _ = f.Value.(pflag.SliceValue).Replace(nil) - } else { - _ = f.Value.Set(f.DefValue) - } - }) - } - return nowOut.String(), nowErr.String(), err -} - -func TestRunHelp(t *testing.T) { - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--help"}) - assert.NoError(t, err) - assert.Equal(t, `(Deprecated) Translate the SCORE file to docker-compose configuration - -Usage: - score-compose run [--file=score.yaml] [--output=compose.yaml] [flags] - -Flags: - --build string Replaces 'image' name with compose 'build' instruction - --env-file string Location to store sample .env file - -f, --file string Source SCORE file (default "./score.yaml") - -h, --help help for run - -o, --output string Output file - --overrides string Overrides SCORE file (default "./overrides.score.yaml") - -p, --property stringArray Overrides selected property value - --skip-validation DEPRECATED: Disables Score file schema validation - -Global Flags: - --quiet Mute any logging output - -v, --verbose count Increase log verbosity and detail by specifying this flag one or more times -`, stdout) - assert.Equal(t, "", stderr) - - stdout2, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"help", "run"}) - assert.NoError(t, err) - assert.Equal(t, stdout, stdout2) - assert.Equal(t, "", stderr) -} - -func TestRunExample(t *testing.T) { - td := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example-workload-name123 - extra-key: extra-value -service: - ports: - port-one: - port: 1000 - protocol: TCP - targetPort: 10000 - port-two2: - port: 8000 -containers: - container-one1: - image: localhost:4000/repo/my-image:tag - command: ["/bin/sh", "-c"] - args: ["hello", "world"] - resources: - requests: - cpu: 1000m - memory: 10Gi - limits: - cpu: "0.24" - memory: 128M - variables: - SOME_VAR: some content here - volumes: - - source: volume-name - target: /mnt/something - readOnly: false - - source: volume-two - target: /mnt/something-else - livenessProbe: - httpGet: - port: 8080 - path: /livez - readinessProbe: - httpGet: - host: 127.0.0.1 - port: 80 - scheme: HTTP - path: /readyz - httpHeaders: - - name: SOME_HEADER - value: some-value-here - container-two2: - image: localhost:4000/repo/my-image:tag -resources: - resource-one1: - metadata: - annotations: - Default-Annotation: this is my annotation - prefix.com/Another-Key_Annotation.2: something else - extra-key: extra-value - type: Resource-One - class: default - params: - extra: - data: here - resource-two2: - type: Resource-Two - volume-name: - type: volume - volume-two: - type: volume -`), 0600)) - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) - assert.NoError(t, err) - assert.NotEqual(t, "", stdout) - for _, l := range []string{ - "WARN: resources.resource-one1: 'Resource-One.default' is not directly supported in score-compose, references will be converted to environment variables\n", - "WARN: resources.resource-two2: 'Resource-Two.default' is not directly supported in score-compose, references will be converted to environment variables\n", - "WARN: containers.container-one1.resources.requests: not supported - ignoring\n", - "WARN: containers.container-one1.resources.limits: not supported - ignoring\n", - } { - assert.Contains(t, stderr, l) - } - - rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - require.NoError(t, err) - var actualComposeContent map[string]interface{} - assert.NoError(t, yaml.Unmarshal(rawComposeContent, &actualComposeContent)) - assert.Equal(t, map[string]interface{}{ - "services": map[string]interface{}{ - "example-workload-name123-container-one1": map[string]interface{}{ - "annotations": map[string]interface{}{ - "compose.score.dev/workload-name": "example-workload-name123", - }, - "hostname": "example-workload-name123", - "image": "localhost:4000/repo/my-image:tag", - "entrypoint": []interface{}{"/bin/sh", "-c"}, - "command": []interface{}{"hello", "world"}, - "environment": map[string]interface{}{ - "SOME_VAR": "some content here", - }, - "ports": []interface{}{ - map[string]interface{}{"target": 10000, "published": "1000", "protocol": "tcp"}, - map[string]interface{}{"target": 8000, "published": "8000"}, - }, - "volumes": []interface{}{ - map[string]interface{}{"type": "volume", "source": "volume-name", "target": "/mnt/something"}, - map[string]interface{}{"type": "volume", "source": "volume-two", "target": "/mnt/something-else"}, - }, - }, - "example-workload-name123-container-two2": map[string]interface{}{ - "annotations": map[string]interface{}{ - "compose.score.dev/workload-name": "example-workload-name123", - }, - "image": "localhost:4000/repo/my-image:tag", - "network_mode": "service:example-workload-name123-container-one1", - }, - }, - }, actualComposeContent) - - t.Run("validate", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - - assert.NoError(t, os.WriteFile(filepath.Join(td, "volume.yaml"), []byte(` -volumes: - volume-name: - driver: local - volume-two: - driver: local - -`), 0644)) - - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "-f", "volume.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestExample_invalid_spec(t *testing.T) { - td := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -{}`), 0600)) - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) - assert.EqualError(t, err, "validating workload spec: jsonschema: '' does not validate with https://score.dev/schemas/score#/required: missing properties: 'apiVersion', 'metadata', 'containers'") - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) -} - -func TestFilesNotSupported(t *testing.T) { - td := t.TempDir() - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: example-workload-name123 -containers: - container-one1: - image: localhost:4000/repo/my-image:tag - files: - - target: /mnt/something - content: bananas -`), 0600)) - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml")}) - assert.EqualError(t, err, "building docker-compose configuration: files are not supported") - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) -} - -func TestInvalidWorkloadName(t *testing.T) { - td := t.TempDir() - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: Invalid Name -containers: - container-one1: - image: localhost:4000/repo/my-image:tag -`), 0600)) - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", filepath.Join(td, "score.yaml")}) - assert.EqualError(t, err, "validating workload spec: jsonschema: '/metadata/name' does not validate with https://score.dev/schemas/score#/properties/metadata/properties/name/pattern: does not match pattern '^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$'") - assert.Equal(t, "", stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) -} - -func TestRunExample01(t *testing.T) { - td := t.TempDir() - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"run", "--file", "../../examples/01-hello/score.yaml", "--output", filepath.Join(td, "compose.yaml")}) - assert.NoError(t, err) - - expectedOutput := `services: - hello-world-hello: - annotations: - compose.score.dev/workload-name: hello-world - your.custom/annotation: value - command: - - -c - - while true; do echo Hello World!; sleep 5; done - entrypoint: - - /bin/sh - hostname: hello-world - image: busybox -` - - assert.Equal(t, expectedOutput, stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - require.NoError(t, err) - assert.Equal(t, expectedOutput, string(rawComposeContent)) - - t.Run("validate", func(t *testing.T) { - if os.Getenv("NO_DOCKER") != "" { - t.Skip("NO_DOCKER is set") - return - } - - dockerCmd, err := exec.LookPath("docker") - require.NoError(t, err) - - cmd := exec.Command(dockerCmd, "compose", "-f", "compose.yaml", "config", "--quiet", "--dry-run") - cmd.Dir = td - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - assert.NoError(t, cmd.Run()) - }) -} - -func TestRunWithBuild(t *testing.T) { - td := t.TempDir() - - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: hello-world -containers: - hello: - image: busybox -`), 0600)) - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{ - "run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml"), "--build", "./test", - }) - assert.NoError(t, err) - - expectedOutput := `services: - hello-world-hello: - annotations: - compose.score.dev/workload-name: hello-world - build: - context: ./test - hostname: hello-world -` - - assert.Equal(t, expectedOutput, stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - require.NoError(t, err) - assert.Equal(t, expectedOutput, string(rawComposeContent)) -} - -func TestRunWithOverrides(t *testing.T) { - td := t.TempDir() - - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: hello-world -containers: - hello: - image: busybox -`), 0600)) - - assert.NoError(t, os.WriteFile(filepath.Join(td, "score-overrides.yaml"), []byte(` -containers: - hello: - image: nginx -`), 0600)) - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{ - "run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml"), "--overrides", filepath.Join(td, "score-overrides.yaml"), - }) - assert.NoError(t, err) - - expectedOutput := `services: - hello-world-hello: - annotations: - compose.score.dev/workload-name: hello-world - hostname: hello-world - image: nginx -` - - assert.Equal(t, expectedOutput, stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - require.NoError(t, err) - assert.Equal(t, expectedOutput, string(rawComposeContent)) -} - -func TestRunWithPropertyOverrides(t *testing.T) { - td := t.TempDir() - - assert.NoError(t, os.WriteFile(filepath.Join(td, "score.yaml"), []byte(` -apiVersion: score.dev/v1b1 -metadata: - name: hello-world -containers: - hello: - image: busybox -`), 0600)) - - stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{ - "run", "--file", filepath.Join(td, "score.yaml"), "--output", filepath.Join(td, "compose.yaml"), "--property", "containers.hello.image=bananas:latest", - }) - assert.NoError(t, err) - - expectedOutput := `services: - hello-world-hello: - annotations: - compose.score.dev/workload-name: hello-world - hostname: hello-world - image: bananas:latest -` - - assert.Equal(t, expectedOutput, stdout) - assert.NotEqual(t, "", strings.TrimSpace(stderr)) - rawComposeContent, err := os.ReadFile(filepath.Join(td, "compose.yaml")) - require.NoError(t, err) - assert.Equal(t, expectedOutput, string(rawComposeContent)) -} diff --git a/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden b/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden deleted file mode 100644 index 85f4b92c..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.json.golden +++ /dev/null @@ -1,377 +0,0 @@ -[ - { - "Type": "cmd-with-class-with-params-in-outputs", - "Class": "c1", - "Params": [ - "p1", - "po1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-with-class-with-params-in-shared", - "Class": "c1", - "Params": [ - "p1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-with-class-with-params-in-shared-outputs", - "Class": "c1", - "Params": [ - "p1", - "po1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-with-class-with-params-in-state-outputs", - "Class": "c1", - "Params": [ - "p1", - "po1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-with-class-with-params-in-state-outputs-shared", - "Class": "c1", - "Params": [ - "p1", - "po1", - "psh1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-with-class-without-params", - "Class": "c1", - "Params": [], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-with-params-in-outputs", - "Class": "(any)", - "Params": [ - "p1", - "po1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-with-params-in-shared", - "Class": "(any)", - "Params": [ - "p1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-with-params-in-shared-outputs", - "Class": "(any)", - "Params": [ - "p1", - "po1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-with-params-in-state", - "Class": "(any)", - "Params": [ - "p1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-with-params-in-state-outputs", - "Class": "(any)", - "Params": [ - "p1", - "po1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-with-params-in-state-outputs-shared", - "Class": "(any)", - "Params": [ - "p1", - "po1", - "psh1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-without-params", - "Class": "(any)", - "Params": [], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "cmd-without-class-without-params-with-description", - "Class": "(any)", - "Params": [], - "Outputs": [ - "o1", - "o2" - ], - "Description": "cmd-without-class-without-params-with-description" - }, - { - "Type": "template-with-class-with-params-in-outputs", - "Class": "c1", - "Params": [ - "p1", - "po1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-with-class-with-params-in-shared", - "Class": "c1", - "Params": [ - "p1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-with-class-with-params-in-shared-outputs", - "Class": "c1", - "Params": [ - "p1", - "po1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-with-class-with-params-in-state", - "Class": "c1", - "Params": [ - "p1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-with-class-with-params-in-state-outputs", - "Class": "c1", - "Params": [ - "p1", - "po1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-with-class-with-params-in-state-outputs-shared", - "Class": "c1", - "Params": [ - "p1", - "po1", - "psh1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-with-class-without-params", - "Class": "c1", - "Params": [], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-with-params-in-outputs", - "Class": "(any)", - "Params": [ - "p1", - "po1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-with-params-in-shared", - "Class": "(any)", - "Params": [ - "p1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-with-params-in-shared-outputs", - "Class": "(any)", - "Params": [ - "p1", - "po1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-with-params-in-state", - "Class": "(any)", - "Params": [ - "p1", - "psh1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-with-params-in-state-outputs", - "Class": "(any)", - "Params": [ - "p1", - "po1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-with-params-in-state-outputs-shared", - "Class": "(any)", - "Params": [ - "p1", - "po1", - "psh1", - "pst1" - ], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-without-params", - "Class": "(any)", - "Params": [], - "Outputs": [ - "o1", - "o2" - ], - "Description": "" - }, - { - "Type": "template-without-class-without-params-with-description", - "Class": "(any)", - "Params": [], - "Outputs": [ - "o1", - "o2" - ], - "Description": "without-class-without-params-with-description" - } -] diff --git a/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden b/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden deleted file mode 100644 index 261ef47f..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/testdata/provisioners.list.valid.table.golden +++ /dev/null @@ -1,61 +0,0 @@ -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| TYPE | CLASS | PARAMS | OUTPUTS | DESCRIPTION | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-with-class-with-params-in-outputs | c1 | p1, po1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-with-class-with-params-in-shared | c1 | p1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-with-class-with-params-in-shared-outputs | c1 | p1, po1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-with-class-with-params-in-state-outputs | c1 | p1, po1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-with-class-with-params-in-state-outputs-shared | c1 | p1, po1, psh1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-with-class-without-params | c1 | | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-with-params-in-outputs | (any) | p1, po1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-with-params-in-shared | (any) | p1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-with-params-in-shared-outputs | (any) | p1, po1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-with-params-in-state | (any) | p1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-with-params-in-state-outputs | (any) | p1, po1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-with-params-in-state-outputs-shared | (any) | p1, po1, psh1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-without-params | (any) | | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| cmd-without-class-without-params-with-description | (any) | | o1, o2 | cmd-without-class-without-params-with-description | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-with-class-with-params-in-outputs | c1 | p1, po1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-with-class-with-params-in-shared | c1 | p1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-with-class-with-params-in-shared-outputs | c1 | p1, po1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-with-class-with-params-in-state | c1 | p1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-with-class-with-params-in-state-outputs | c1 | p1, po1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-with-class-with-params-in-state-outputs-shared | c1 | p1, po1, psh1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-with-class-without-params | c1 | | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-with-params-in-outputs | (any) | p1, po1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-with-params-in-shared | (any) | p1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-with-params-in-shared-outputs | (any) | p1, po1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-with-params-in-state | (any) | p1, psh1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-with-params-in-state-outputs | (any) | p1, po1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-with-params-in-state-outputs-shared | (any) | p1, po1, psh1, pst1 | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-without-params | (any) | | o1, o2 | | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ -| template-without-class-without-params-with-description | (any) | | o1, o2 | without-class-without-params-with-description | -+------------------------------------------------------------+-------+---------------------+---------+---------------------------------------------------+ diff --git a/gen/external-content/resource-provisioners/default/score-k8s/default.go b/gen/external-content/resource-provisioners/default/score-k8s/default.go deleted file mode 100644 index 832edcea..00000000 --- a/gen/external-content/resource-provisioners/default/score-k8s/default.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package _default - -import ( - _ "embed" -) - -//go:embed zz-default.provisioners.yaml -var DefaultProvisioners string diff --git a/gen/external-content/resource-provisioners/default/score-k8s/default_test.go b/gen/external-content/resource-provisioners/default/score-k8s/default_test.go deleted file mode 100644 index 6424e65c..00000000 --- a/gen/external-content/resource-provisioners/default/score-k8s/default_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2024 The Score Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package _default - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/score-spec/score-k8s/internal/provisioners/loader" -) - -func TestDefaultProvisioners(t *testing.T) { - p, err := loader.LoadProvisioners([]byte(DefaultProvisioners)) - assert.NoError(t, err) - assert.NotNil(t, p) - assert.Greater(t, len(p), 0) -} diff --git a/gen/external-content/resource-provisioners/default/service-port/score-compose/README.md b/gen/external-content/resource-provisioners/default/service-port/score-compose/README.md new file mode 100644 index 00000000..18af681b --- /dev/null +++ b/gen/external-content/resource-provisioners/default/service-port/score-compose/README.md @@ -0,0 +1 @@ +The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml new file mode 100644 index 00000000..4ac4bf32 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml @@ -0,0 +1,18 @@ +uri: template://default-provisioners/service-port +type: service-port +description: Outputs a hostname and port for connecting to another workload. +outputs: | + {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} + {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} + {{ $w := (index .WorkloadServices .Params.workload) }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + {{ $p := (index $w.Ports .Params.port) }} + {{ if not $p }}{{ fail "unknown service port" }}{{ end }} + hostname: {{ $w.ServiceName | quote }} + port: {{ $p.TargetPort }} +expected_outputs: + - hostname + - port +supported_params: + - workload + - port \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/service-port/score-k8s/README.md b/gen/external-content/resource-provisioners/default/service-port/score-k8s/README.md new file mode 100644 index 00000000..18af681b --- /dev/null +++ b/gen/external-content/resource-provisioners/default/service-port/score-k8s/README.md @@ -0,0 +1 @@ +The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml new file mode 100644 index 00000000..71be2eaf --- /dev/null +++ b/gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml @@ -0,0 +1,18 @@ +uri: template://default-provisioners/service-port +type: service-port +description: Outputs a hostname and port for connecting to another workload. +outputs: | + {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} + {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + {{ if not $w }}{{ fail "unknown workload" }}{{ end }} + {{ $p := (index $w.Ports .Params.port) }} + {{ if not $p }}{{ fail "unknown service port" }}{{ end }} + hostname: {{ $w.ServiceName | quote }} + port: {{ $p.TargetPort }} +expected_outputs: + - hostname + - port +supported_params: + - workload + - port \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/volume/score-compose/README.md b/gen/external-content/resource-provisioners/default/volume/score-compose/README.md new file mode 100644 index 00000000..132f96ef --- /dev/null +++ b/gen/external-content/resource-provisioners/default/volume/score-compose/README.md @@ -0,0 +1 @@ +The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source attribute that we can reference. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml new file mode 100644 index 00000000..b4209539 --- /dev/null +++ b/gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml @@ -0,0 +1,25 @@ +uri: template://default-provisioners/volume +# By default, match all classes and ids of volume. If you want to override this, create another provisioner definition +# with a higher priority. +type: volume +description: Creates a persistent volume that can be mounted on a workload. +init: | + randomVolumeName: {{ .Id | replace "." "-" }}-{{ randAlphaNum 6 }} +# Store the random volume name if we haven't chosen one yet, otherwise use the one that exists already +state: | + name: {{ dig "name" .Init.randomVolumeName .State }} +# Return a source value with the volume name. This can be used in volume resource references now. +outputs: | + type: volume + source: {{ .State.name }} +# Add a volume to the docker compose file. We assume our name is unique here. We also apply a label to help ensure +# that we can track the volume back to the workload and resource that created it. +volumes: | + {{ .State.name }}: + name: {{ .State.name }} + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} +expected_outputs: + - source + - type \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/volume/score-k8s/README.md b/gen/external-content/resource-provisioners/default/volume/score-k8s/README.md new file mode 100644 index 00000000..034e136a --- /dev/null +++ b/gen/external-content/resource-provisioners/default/volume/score-k8s/README.md @@ -0,0 +1 @@ +As an example we have a 'volume' type which returns an emptyDir volume. In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or persistent volume and claims. \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml new file mode 100644 index 00000000..a086fbad --- /dev/null +++ b/gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml @@ -0,0 +1,8 @@ +uri: template://default-provisioners/volume +type: volume +description: Creates a persistent volume that can be mounted on a workload. +outputs: | + source: + emptyDir: {} +expected_outputs: + - source \ No newline at end of file From affce948bdec3c56cdb9aa6f47ef47233f3c0f10 Mon Sep 17 00:00:00 2001 From: Santiago Beroch Date: Wed, 22 Oct 2025 09:33:44 -0300 Subject: [PATCH 3/7] refactor gen-example-pages.js Signed-off-by: Santiago Beroch --- content/en/examples/patch-templates/_index.md | 2 +- .../default/dns/score-compose.md | 15 ++ .../default/dns/score-k8s.md | 15 ++ .../default/elasticsearch/score-compose.md | 15 ++ .../default/example-provisioner/score-k8s.md | 15 ++ .../default/kafka-topic/score-compose.md | 13 ++ .../default/mongo/score-k8s.md | 13 ++ .../default/mongodb/score-compose.md | 15 ++ .../default/mssql/score-compose.md | 13 ++ .../default/mssql/score-k8s.md | 13 ++ .../default/mysql/score-compose.md | 15 ++ .../default/mysql/score-k8s.md | 13 ++ .../postgres-instance/score-compose.md | 13 ++ .../default/postgres-instance/score-k8s.md | 13 ++ .../default/postgres/score-compose.md | 15 ++ .../default/postgres/score-k8s.md | 13 ++ .../default/rabbitmq/score-compose.md | 15 ++ .../default/rabbitmq/score-k8s.md | 13 ++ .../default/redis/score-compose.md | 15 ++ .../default/redis/score-k8s.md | 13 ++ .../default/route/score-compose.md | 15 ++ .../default/route/score-k8s.md | 15 ++ .../default/s3/score-compose.md | 15 ++ .../default/s3/score-k8s.md | 15 ++ .../default/score-compose.md | 12 ++ .../default/score-k8s.md | 12 ++ .../default/service-port/score-compose.md | 15 ++ .../default/service-port/score-k8s.md | 15 ++ .../default/volume/score-compose.md | 15 ++ .../default/volume/score-k8s.md | 15 ++ gen/examples-site/file-utils.js | 23 +++ gen/examples-site/gen-example-pages.js | 189 +++++++++--------- 32 files changed, 529 insertions(+), 94 deletions(-) create mode 100644 content/en/examples/resource-provisioners/default/dns/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/dns/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/elasticsearch/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/example-provisioner/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/mongo/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/mongodb/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/mssql/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/mssql/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/mysql/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/mysql/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/postgres/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/postgres/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/rabbitmq/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/redis/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/redis/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/route/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/route/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/s3/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/s3/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/service-port/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/service-port/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/volume/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/volume/score-k8s.md diff --git a/content/en/examples/patch-templates/_index.md b/content/en/examples/patch-templates/_index.md index 21ef0254..7ef208b2 100644 --- a/content/en/examples/patch-templates/_index.md +++ b/content/en/examples/patch-templates/_index.md @@ -4,6 +4,6 @@ draft: false type: examples --- -The examples below illustrate how to use patch templates with either [`score-compose`](https://docs.score.dev/docs/score-implementation/score-compose/patch-templates/) or [`score-k8s`](https://docs.score.dev/docs/score-implementation/score-k8s/patch-templates/). +The examples below illustrate how to use patch templates for each Score implementation. --- diff --git a/content/en/examples/resource-provisioners/default/dns/score-compose.md b/content/en/examples/resource-provisioners/default/dns/score-compose.md new file mode 100644 index 00000000..f0e367d7 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/dns/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default dns provisioner just outputs localhost as the hostname every time. This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This can be replaced by a new provisioner in the future.' +hasMore: true +parent: "Dns" +flavor: "Default" + +--- + +The default dns provisioner just outputs localhost as the hostname every time. This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This can be replaced by a new provisioner in the future. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/dns/score-k8s.md b/content/en/examples/resource-provisioners/default/dns/score-k8s.md new file mode 100644 index 00000000..63c5ef66 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/dns/score-k8s.md @@ -0,0 +1,15 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is available. You should replace this with your own dns name generation that matches your external-dns controller.' +hasMore: true +parent: "Dns" +flavor: "Default" + +--- + +The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is available. You should replace this with your own dns name generation that matches your external-dns controller. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/elasticsearch/score-compose.md b/content/en/examples/resource-provisioners/default/elasticsearch/score-compose.md new file mode 100644 index 00000000..5e6766b3 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/elasticsearch/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default elasticsearch provisioner adds a elasticsearch instance.' +hasMore: false +parent: "Elasticsearch" +flavor: "Default" + +--- + +The default elasticsearch provisioner adds a elasticsearch instance. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/elasticsearch/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s.md b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s.md new file mode 100644 index 00000000..edba93ca --- /dev/null +++ b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s.md @@ -0,0 +1,15 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory.' +hasMore: true +parent: "Example Provisioner" +flavor: "Default" + +--- + +The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/example-provisioner/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md b/content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md new file mode 100644 index 00000000..9bde81e2 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Kafka Topic" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/kafka-topic/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mongo/score-k8s.md b/content/en/examples/resource-provisioners/default/mongo/score-k8s.md new file mode 100644 index 00000000..a44bd1ad --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mongo/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Mongo" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongo/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mongodb/score-compose.md b/content/en/examples/resource-provisioners/default/mongodb/score-compose.md new file mode 100644 index 00000000..2fac8ff4 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mongodb/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string.' +hasMore: false +parent: "Mongodb" +flavor: "Default" + +--- + +The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongodb/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-compose.md b/content/en/examples/resource-provisioners/default/mssql/score-compose.md new file mode 100644 index 00000000..f133bc22 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mssql/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Mssql" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-k8s.md b/content/en/examples/resource-provisioners/default/mssql/score-k8s.md new file mode 100644 index 00000000..6103ce74 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mssql/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Mssql" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mysql/score-compose.md b/content/en/examples/resource-provisioners/default/mysql/score-compose.md new file mode 100644 index 00000000..ee1401b3 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mysql/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on startup.' +hasMore: false +parent: "Mysql" +flavor: "Default" + +--- + +The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on startup. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mysql/score-k8s.md b/content/en/examples/resource-provisioners/default/mysql/score-k8s.md new file mode 100644 index 00000000..94f1bde0 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mysql/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Mysql" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md new file mode 100644 index 00000000..7a1070f0 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md @@ -0,0 +1,13 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Postgres Instance" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md new file mode 100644 index 00000000..2c3e7dca --- /dev/null +++ b/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Postgres Instance" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres/score-compose.md b/content/en/examples/resource-provisioners/default/postgres/score-compose.md new file mode 100644 index 00000000..96fe3805 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/postgres/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on startup.' +hasMore: false +parent: "Postgres" +flavor: "Default" + +--- + +The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on startup. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres/score-k8s.md b/content/en/examples/resource-provisioners/default/postgres/score-k8s.md new file mode 100644 index 00000000..e1319473 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/postgres/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Postgres" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/rabbitmq/score-compose.md b/content/en/examples/resource-provisioners/default/rabbitmq/score-compose.md new file mode 100644 index 00000000..3a86b574 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/rabbitmq/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins.' +hasMore: false +parent: "Rabbitmq" +flavor: "Default" + +--- + +The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md b/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md new file mode 100644 index 00000000..bfb0aec7 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Rabbitmq" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/redis/score-compose.md b/content/en/examples/resource-provisioners/default/redis/score-compose.md new file mode 100644 index 00000000..8cc56c8b --- /dev/null +++ b/content/en/examples/resource-provisioners/default/redis/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default redis provisioner adds a redis service to the project which returns a host, port, username, and password.' +hasMore: false +parent: "Redis" +flavor: "Default" + +--- + +The default redis provisioner adds a redis service to the project which returns a host, port, username, and password. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/redis/score-k8s.md b/content/en/examples/resource-provisioners/default/redis/score-k8s.md new file mode 100644 index 00000000..480b79ec --- /dev/null +++ b/content/en/examples/resource-provisioners/default/redis/score-k8s.md @@ -0,0 +1,13 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Redis" +flavor: "Default" + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/route/score-compose.md b/content/en/examples/resource-provisioners/default/route/score-compose.md new file mode 100644 index 00000000..a614ceb1 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/route/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps.' +hasMore: false +parent: "Route" +flavor: "Default" + +--- + +The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/route/score-k8s.md b/content/en/examples/resource-provisioners/default/route/score-k8s.md new file mode 100644 index 00000000..c4e1ed0a --- /dev/null +++ b/content/en/examples/resource-provisioners/default/route/score-k8s.md @@ -0,0 +1,15 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'Routes could be implemented as either traditional ingress resources or using the newer gateway API. In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this.' +hasMore: false +parent: "Route" +flavor: "Default" + +--- + +Routes could be implemented as either traditional ingress resources or using the newer gateway API. In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/s3/score-compose.md b/content/en/examples/resource-provisioners/default/s3/score-compose.md new file mode 100644 index 00000000..8b172ca5 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/s3/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'This resource provides a minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. If the provider has a publish port annotation, it can expose a management port on the local network for debugging and connectivity.' +hasMore: false +parent: "S3" +flavor: "Default" + +--- + +This resource provides a minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. If the provider has a publish port annotation, it can expose a management port on the local network for debugging and connectivity. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/s3/score-k8s.md b/content/en/examples/resource-provisioners/default/s3/score-k8s.md new file mode 100644 index 00000000..fab2a38f --- /dev/null +++ b/content/en/examples/resource-provisioners/default/s3/score-k8s.md @@ -0,0 +1,15 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket.' +hasMore: false +parent: "S3" +flavor: "Default" + +--- + +This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/score-compose.md b/content/en/examples/resource-provisioners/default/score-compose.md new file mode 100644 index 00000000..895b1649 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/score-compose.md @@ -0,0 +1,12 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Default" + +--- + +{{% example-file filename="default.provisioners.yaml" dir="resource-provisioners/default/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/score-k8s.md b/content/en/examples/resource-provisioners/default/score-k8s.md new file mode 100644 index 00000000..7e3bca08 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/score-k8s.md @@ -0,0 +1,12 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "Default" + +--- + +{{% example-file filename="zz-default.provisioners.yaml" dir="resource-provisioners/default/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/service-port/score-compose.md b/content/en/examples/resource-provisioners/default/service-port/score-compose.md new file mode 100644 index 00000000..fe3ad9bf --- /dev/null +++ b/content/en/examples/resource-provisioners/default/service-port/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet.' +hasMore: true +parent: "Service Port" +flavor: "Default" + +--- + +The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/service-port/score-k8s.md b/content/en/examples/resource-provisioners/default/service-port/score-k8s.md new file mode 100644 index 00000000..310b4846 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/service-port/score-k8s.md @@ -0,0 +1,15 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet.' +hasMore: true +parent: "Service Port" +flavor: "Default" + +--- + +The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/volume/score-compose.md b/content/en/examples/resource-provisioners/default/volume/score-compose.md new file mode 100644 index 00000000..45209a44 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/volume/score-compose.md @@ -0,0 +1,15 @@ +--- +title: "Score Compose" +draft: false +mermaid: true +type: examples +excerpt: 'The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source attribute that we can reference.' +hasMore: false +parent: "Volume" +flavor: "Default" + +--- + +The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source attribute that we can reference. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/volume/score-k8s.md b/content/en/examples/resource-provisioners/default/volume/score-k8s.md new file mode 100644 index 00000000..ed1a4b98 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/volume/score-k8s.md @@ -0,0 +1,15 @@ +--- +title: "Score K8s" +draft: false +mermaid: true +type: examples +excerpt: 'As an example we have a 'volume' type which returns an emptyDir volume. In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or persistent volume and claims.' +hasMore: true +parent: "Volume" +flavor: "Default" + +--- + +As an example we have a 'volume' type which returns an emptyDir volume. In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or persistent volume and claims. + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/gen/examples-site/file-utils.js b/gen/examples-site/file-utils.js index c6ab0f39..ccf41ff0 100644 --- a/gen/examples-site/file-utils.js +++ b/gen/examples-site/file-utils.js @@ -36,9 +36,32 @@ function traverseDirectory(dir, callback) { }); } +/** + * Check if a folder is valid (is a directory and not ignored) + * @param {string} folderPath - Path to the folder + * @returns {boolean} - True if folder is valid + */ +const isValidFolder = (folderPath) => { + const folderName = folderPath.split("/").pop(); + return isDirectory(folderPath) && !shouldIgnoreFolder(folderName); +}; + +/** + * Check if a directory contains subdirectories + * @param {string} dirPath - Path to the directory + * @returns {boolean} - True if directory contains subdirectories + */ +const hasSubdirectories = (dirPath) => { + return fs + .readdirSync(dirPath) + .some((file) => isDirectory(`${dirPath}/${file}`)); +}; + module.exports = { isDirectory, mkdirIfNotExistsSync, traverseDirectory, shouldIgnoreFolder, + isValidFolder, + hasSubdirectories, }; diff --git a/gen/examples-site/gen-example-pages.js b/gen/examples-site/gen-example-pages.js index 45338522..9ba53646 100644 --- a/gen/examples-site/gen-example-pages.js +++ b/gen/examples-site/gen-example-pages.js @@ -4,10 +4,15 @@ const { parseConfig } = require("./config-parser"); const { isDirectory, mkdirIfNotExistsSync, - shouldIgnoreFolder, + isValidFolder, + hasSubdirectories, } = require("./file-utils"); const { beautify } = require("./content-utils"); +// Constants +const CONTENT_OUTPUT_BASE = "./content/en/examples"; +const CATEGORY_CONTENT_PATH = "./gen/examples-site/examples-category-content"; +const README_FILE = "README.md"; const sourceFolder = process.argv[2]; //Get the folders inside sourceFolder: @@ -21,78 +26,43 @@ const config = parseConfig("./config.toml"); const blacklist = config.exampleLibraryBlacklistedFolders; // Remove blacklisted folders from the categoryFolders array: -categoryFolders.forEach((folder, index) => { - if (blacklist.includes(folder)) { - categoryFolders.splice(index, 1); - } -}); +const filteredCategoryFolders = categoryFolders.filter( + (folder) => !blacklist.includes(folder) +); //For each folder, make a folder in ./content/examples: -for (const categoryFolder of categoryFolders) { - mkdirIfNotExistsSync(`./content/en/examples/${categoryFolder}`); +for (const categoryFolder of filteredCategoryFolders) { + mkdirIfNotExistsSync(`${CONTENT_OUTPUT_BASE}/${categoryFolder}`); + createCategoryIndex(categoryFolder); - //Write an _index.md file in each folder. - //Get content from examples-site category content files - const categoryIndexContent = fs.readFileSync( - `./gen/examples-site/examples-category-content/${categoryFolder}.md`, - "utf8" - ); - fs.writeFileSync( - `./content/en/examples/${categoryFolder}/_index.md`, - `--- -title: "${beautify(categoryFolder)}" -draft: false -type: examples ---- -${categoryIndexContent} - ----` - ); - - //Get the folders inside each category: + //Process the folders inside each category: const folders = fs.readdirSync(`${sourceFolder}/${categoryFolder}`); - - //For each folder, check if this is the last nesting level: for (const folder of folders) { - //Discard readme and other files: - if ( - !isDirectory(`${sourceFolder}/${categoryFolder}/${folder}`) || - shouldIgnoreFolder(folder) - ) { + if (!isValidFolder(`${sourceFolder}/${categoryFolder}/${folder}`)) { continue; } - const isLastNestingLevel = !fs - .readdirSync(`${sourceFolder}/${categoryFolder}/${folder}`) - .some((file) => - isDirectory(`${sourceFolder}/${categoryFolder}/${folder}/${file}`) - ); + const isLastNestingLevel = !hasSubdirectories( + `${sourceFolder}/${categoryFolder}/${folder}` + ); if (!isLastNestingLevel) { const subfolders = fs.readdirSync( `${sourceFolder}/${categoryFolder}/${folder}` ); - const folderPath = `./content/en/examples/${categoryFolder}/${folder}`; + const folderPath = `${CONTENT_OUTPUT_BASE}/${categoryFolder}/${folder}`; for (const subfolder of subfolders) { mkdirIfNotExistsSync(folderPath); - //Discard readme and other files, and dot folders: if ( - !isDirectory( + !isValidFolder( `${sourceFolder}/${categoryFolder}/${folder}/${subfolder}` - ) || - shouldIgnoreFolder(subfolder) + ) ) { continue; } - const isLastNestingLevel = !fs - .readdirSync( - `${sourceFolder}/${categoryFolder}/${folder}/${subfolder}` - ) - .some((file) => - isDirectory( - `${sourceFolder}/${categoryFolder}/${folder}/${subfolder}/${file}` - ) - ); + const isLastNestingLevel = !hasSubdirectories( + `${sourceFolder}/${categoryFolder}/${folder}/${subfolder}` + ); if (!isLastNestingLevel) { const subsubfolders = fs.readdirSync( @@ -101,12 +71,10 @@ ${categoryIndexContent} const subfolderPath = `${folderPath}/${subfolder}`; for (const subsubfolder of subsubfolders) { mkdirIfNotExistsSync(subfolderPath); - //Discard readme and other files, and dot folders: if ( - !isDirectory( + !isValidFolder( `${sourceFolder}/${categoryFolder}/${folder}/${subfolder}/${subsubfolder}` - ) || - shouldIgnoreFolder(subsubfolder) + ) ) { continue; } @@ -128,45 +96,80 @@ ${categoryIndexContent} } } else { if (config.exampleLibraryOnePagePerFileFolders.includes(categoryFolder)) { - const path = `${sourceFolder}/${categoryFolder}/${folder}`; - const files = fs.readdirSync(path); - // Create output directory structure - mkdirIfNotExistsSync( - `./content/en/examples/${categoryFolder}/${folder}` - ); - for (const file of files) { - if (file === "README.md") continue; - const fileWithoutExtension = file.replace(/\.[^/.]+$/, ""); - mkdirIfNotExistsSync(`${path}/${fileWithoutExtension}`); - fs.renameSync( - `${path}/${file}`, - `${path}/${fileWithoutExtension}/${file}` - ); - buildFrontmatter( - fileWithoutExtension, - `${categoryFolder}/${folder}/${fileWithoutExtension}`, - folder, - "", - { - fileLocation: `${categoryFolder}/${folder}`, - readmeLocation: `${categoryFolder}/${folder}`, - shouldBeautifyParent: false, - } - ); - } - // Move files back to their original location - for (const file of files) { - if (file === "README.md") continue; - const fileWithoutExtension = file.replace(/\.[^/.]+$/, ""); - fs.renameSync( - `${path}/${fileWithoutExtension}/${file}`, - `${path}/${file}` - ); - fs.rmdirSync(`${path}/${fileWithoutExtension}`); - } + processOnePagePerFileFolder(categoryFolder, folder); } else { buildFrontmatter(folder, `${categoryFolder}/${folder}`); } } } } + +/** + * Process folders that need one page per file + * Temporarily moves files into subdirectories for processing, then moves them back + */ +function processOnePagePerFileFolder(categoryFolder, folder) { + const sourcePath = `${sourceFolder}/${categoryFolder}/${folder}`; + const files = fs.readdirSync(sourcePath); + + // Create output directory structure + mkdirIfNotExistsSync(`${CONTENT_OUTPUT_BASE}/${categoryFolder}/${folder}`); + + // Temporarily move files into subdirectories for processing + for (const file of files) { + if (file === README_FILE) continue; + + const fileWithoutExtension = file.replace(/\.[^/.]+$/, ""); + const tempDir = `${sourcePath}/${fileWithoutExtension}`; + + mkdirIfNotExistsSync(tempDir); + fs.renameSync(`${sourcePath}/${file}`, `${tempDir}/${file}`); + + buildFrontmatter( + fileWithoutExtension, + `${categoryFolder}/${folder}/${fileWithoutExtension}`, + folder, + "", + { + fileLocation: `${categoryFolder}/${folder}`, + readmeLocation: `${categoryFolder}/${folder}`, + shouldBeautifyParent: false, + } + ); + } + + // Move files back to their original location + for (const file of files) { + if (file === README_FILE) continue; + + const fileWithoutExtension = file.replace(/\.[^/.]+$/, ""); + const tempDir = `${sourcePath}/${fileWithoutExtension}`; + + fs.renameSync(`${tempDir}/${file}`, `${sourcePath}/${file}`); + fs.rmdirSync(tempDir); + } +} + +/** + * Create the _index.md file for a category folder + */ +function createCategoryIndex(categoryFolder) { + const categoryIndexContent = fs.readFileSync( + `${CATEGORY_CONTENT_PATH}/${categoryFolder}.md`, + "utf8" + ); + + const indexContent = `--- +title: "${beautify(categoryFolder)}" +draft: false +type: examples +--- +${categoryIndexContent} + +---`; + + fs.writeFileSync( + `${CONTENT_OUTPUT_BASE}/${categoryFolder}/_index.md`, + indexContent + ); +} From 16246fab5696fb9621ba2eac1a3214c19a952b37 Mon Sep 17 00:00:00 2001 From: Santiago Beroch Date: Wed, 22 Oct 2025 11:52:58 -0300 Subject: [PATCH 4/7] generated resource provisioners content files Signed-off-by: Santiago Beroch --- .../template/redis-dapr-pubsub.md} | 10 +- .../template/rabbitmq-dapr-pubsub.md} | 11 +- .../score-k8s/template/redis-dapr-pubsub.md | 17 + .../template/redis-dapr-state-store.md} | 10 +- .../template/redis.md} | 10 +- .../template/dapr-subscription.md} | 14 +- .../template/dapr-subscription.md} | 14 +- .../cmd/dns-in-codespace.md} | 13 +- .../score-compose/template/dns-with-route.md | 23 ++ .../cmd/dns-in-codespace.md} | 13 +- .../dns/score-k8s/template/dns-with-url.md | 23 ++ .../cmd/dotenv.md} | 9 +- .../{score-k8s.md => score-k8s/cmd/dotenv.md} | 9 +- .../template/empty-hpa.md} | 8 +- .../template/default-hpa.md} | 12 +- .../cmd/helm-template-redis.md} | 15 +- .../redis/score-k8s/cmd/helm-upgrade-redis.md | 38 ++ .../community/route/score-k8s.md | 16 - .../route/score-k8s/template/ingress-route.md | 19 + .../template/ingress-with-net-pol-route.md | 19 + .../route-with-shared-gateway-with-netpol.md | 19 + .../template/route-with-shared-gateway.md | 19 + .../template/static-service.md} | 10 +- .../template/static-service-with-netpol.md | 17 + .../template/static-service.md} | 11 +- .../template/dns.md} | 12 +- .../template/dns.md} | 12 +- .../template/elasticsearch.md} | 15 +- .../cmd/example-provisioner.md} | 14 +- .../default/kafka-topic/score-compose.md | 13 - .../score-compose/template/kafka-topic.md | 20 + .../default/mongo/score-k8s.md | 13 - .../default/mongo/score-k8s/template/mongo.md | 21 ++ .../template/mongodb.md} | 16 +- .../default/mssql/score-compose.md | 13 - .../mssql/score-compose/template/mssql.md | 22 ++ .../default/mssql/score-k8s.md | 13 - .../default/mssql/score-k8s/template/mssql.md | 22 ++ .../template/mysql.md} | 17 +- .../default/mysql/score-k8s.md | 13 - .../default/mysql/score-k8s/template/mysql.md | 22 ++ .../postgres-instance/score-compose.md | 13 - .../template/postgres-instance.md | 20 + .../default/postgres-instance/score-k8s.md | 13 - .../score-k8s/template/postgres-instance.md | 20 + .../template/postgres.md} | 17 +- .../default/postgres/score-k8s.md | 13 - .../postgres/score-k8s/template/postgres.md | 22 ++ .../template/rabbitmq.md} | 16 +- .../default/rabbitmq/score-k8s.md | 13 - .../rabbitmq/score-k8s/template/rabbitmq.md | 21 ++ .../template/redis.md} | 15 +- .../default/redis/score-k8s.md | 13 - .../default/redis/score-k8s/template/redis.md | 20 + .../template/route.md} | 14 +- .../template/route.md} | 14 +- .../template/s3.md} | 18 +- .../template/s3.md} | 18 +- .../template/service-port.md} | 16 +- .../template/service-port.md} | 16 +- .../template/volume.md} | 13 +- .../template/volume.md} | 12 +- gen/examples-site/frontmatter-provisioners.js | 173 +++++++++ gen/examples-site/gen-example-pages.js | 21 +- ...transform-default-resource-provisioners.js | 15 - .../dns/score-compose/provisioners.yaml | 22 +- .../default/dns/score-k8s/provisioners.yaml | 22 +- .../score-compose/provisioners.yaml | 272 +++++++------- .../score-k8s/provisioners.yaml | 22 +- .../score-compose/provisioners.yaml | 122 +++--- .../default/mongo/score-k8s/provisioners.yaml | 306 +++++++-------- .../mongodb/score-compose/provisioners.yaml | 106 +++--- .../mssql/score-compose/provisioners.yaml | 104 +++--- .../default/mssql/score-k8s/provisioners.yaml | 262 ++++++------- .../mysql/score-compose/provisioners.yaml | 166 ++++----- .../default/mysql/score-k8s/provisioners.yaml | 290 +++++++-------- .../score-compose/provisioners.yaml | 110 +++--- .../score-k8s/provisioners.yaml | 290 +++++++-------- .../postgres/score-compose/provisioners.yaml | 190 +++++----- .../postgres/score-k8s/provisioners.yaml | 310 ++++++++-------- .../rabbitmq/score-compose/provisioners.yaml | 184 ++++----- .../rabbitmq/score-k8s/provisioners.yaml | 246 ++++++------ .../redis/score-compose/provisioners.yaml | 120 +++--- .../default/redis/score-k8s/provisioners.yaml | 286 +++++++------- .../route/score-compose/provisioners.yaml | 186 +++++----- .../default/route/score-k8s/provisioners.yaml | 90 ++--- .../s3/score-compose/provisioners.yaml | 196 +++++----- .../default/s3/score-k8s/provisioners.yaml | 350 +++++++++--------- .../score-compose/provisioners.yaml | 36 +- .../service-port/score-k8s/provisioners.yaml | 36 +- .../volume/score-compose/provisioners.yaml | 48 +-- .../volume/score-k8s/provisioners.yaml | 16 +- 92 files changed, 3091 insertions(+), 2480 deletions(-) rename content/en/examples/resource-provisioners/community/dapr-pubsub/{score-compose.md => score-compose/template/redis-dapr-pubsub.md} (58%) rename content/en/examples/resource-provisioners/community/dapr-pubsub/{score-k8s.md => score-k8s/template/rabbitmq-dapr-pubsub.md} (51%) create mode 100644 content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md rename content/en/examples/resource-provisioners/community/dapr-state-store/{score-compose.md => score-compose/template/redis-dapr-state-store.md} (57%) rename content/en/examples/resource-provisioners/community/dapr-state-store/{score-k8s.md => score-k8s/template/redis.md} (59%) rename content/en/examples/resource-provisioners/community/dapr-subscription/{score-compose.md => score-compose/template/dapr-subscription.md} (53%) rename content/en/examples/resource-provisioners/community/dapr-subscription/{score-k8s.md => score-k8s/template/dapr-subscription.md} (53%) rename content/en/examples/resource-provisioners/community/dns/{score-compose.md => score-compose/cmd/dns-in-codespace.md} (69%) create mode 100644 content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md rename content/en/examples/resource-provisioners/community/dns/{score-k8s.md => score-k8s/cmd/dns-in-codespace.md} (70%) create mode 100644 content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md rename content/en/examples/resource-provisioners/community/environment/{score-compose.md => score-compose/cmd/dotenv.md} (74%) rename content/en/examples/resource-provisioners/community/environment/{score-k8s.md => score-k8s/cmd/dotenv.md} (74%) rename content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/{score-compose.md => score-compose/template/empty-hpa.md} (59%) rename content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/{score-k8s.md => score-k8s/template/default-hpa.md} (52%) rename content/en/examples/resource-provisioners/community/redis/{score-k8s.md => score-k8s/cmd/helm-template-redis.md} (81%) create mode 100644 content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md delete mode 100644 content/en/examples/resource-provisioners/community/route/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md create mode 100644 content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md create mode 100644 content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md create mode 100644 content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md rename content/en/examples/resource-provisioners/community/service/{score-compose.md => score-compose/template/static-service.md} (55%) create mode 100644 content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md rename content/en/examples/resource-provisioners/community/service/{score-k8s.md => score-k8s/template/static-service.md} (50%) rename content/en/examples/resource-provisioners/default/dns/{score-compose.md => score-compose/template/dns.md} (73%) rename content/en/examples/resource-provisioners/default/dns/{score-k8s.md => score-k8s/template/dns.md} (72%) rename content/en/examples/resource-provisioners/default/elasticsearch/{score-compose.md => score-compose/template/elasticsearch.md} (51%) rename content/en/examples/resource-provisioners/default/example-provisioner/{score-k8s.md => score-k8s/cmd/example-provisioner.md} (66%) delete mode 100644 content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md delete mode 100644 content/en/examples/resource-provisioners/default/mongo/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md rename content/en/examples/resource-provisioners/default/mongodb/{score-compose.md => score-compose/template/mongodb.md} (61%) delete mode 100644 content/en/examples/resource-provisioners/default/mssql/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md delete mode 100644 content/en/examples/resource-provisioners/default/mssql/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md rename content/en/examples/resource-provisioners/default/mysql/{score-compose.md => score-compose/template/mysql.md} (57%) delete mode 100644 content/en/examples/resource-provisioners/default/mysql/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md delete mode 100644 content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md create mode 100644 content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md delete mode 100644 content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md rename content/en/examples/resource-provisioners/default/postgres/{score-compose.md => score-compose/template/postgres.md} (57%) delete mode 100644 content/en/examples/resource-provisioners/default/postgres/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md rename content/en/examples/resource-provisioners/default/rabbitmq/{score-compose.md => score-compose/template/rabbitmq.md} (56%) delete mode 100644 content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md rename content/en/examples/resource-provisioners/default/redis/{score-compose.md => score-compose/template/redis.md} (60%) delete mode 100644 content/en/examples/resource-provisioners/default/redis/score-k8s.md create mode 100644 content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md rename content/en/examples/resource-provisioners/default/route/{score-compose.md => score-compose/template/route.md} (69%) rename content/en/examples/resource-provisioners/default/route/{score-k8s.md => score-k8s/template/route.md} (68%) rename content/en/examples/resource-provisioners/default/s3/{score-compose.md => score-compose/template/s3.md} (67%) rename content/en/examples/resource-provisioners/default/s3/{score-k8s.md => score-k8s/template/s3.md} (66%) rename content/en/examples/resource-provisioners/default/service-port/{score-compose.md => score-compose/template/service-port.md} (66%) rename content/en/examples/resource-provisioners/default/service-port/{score-k8s.md => score-k8s/template/service-port.md} (66%) rename content/en/examples/resource-provisioners/default/volume/{score-compose.md => score-compose/template/volume.md} (71%) rename content/en/examples/resource-provisioners/default/volume/{score-k8s.md => score-k8s/template/volume.md} (69%) create mode 100644 gen/examples-site/frontmatter-provisioners.js diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose/template/redis-dapr-pubsub.md similarity index 58% rename from content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose.md rename to content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose/template/redis-dapr-pubsub.md index feff8df0..9d665849 100644 --- a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose.md +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose/template/redis-dapr-pubsub.md @@ -1,12 +1,16 @@ --- -title: "Score Compose" +title: "redis-dapr-pubsub" draft: false mermaid: true type: examples +resourceType: "dapr-pubsub" +provisionerType: "template" +flavor: "redis" excerpt: '' +description: 'Generates a Dapr PubSub Component pointing to a Redis Service.' +expectedOutputs: + - name hasMore: false -parent: "Dapr Pubsub" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/rabbitmq-dapr-pubsub.md similarity index 51% rename from content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s.md rename to content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/rabbitmq-dapr-pubsub.md index 451c547c..b1487f9b 100644 --- a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/rabbitmq-dapr-pubsub.md @@ -1,14 +1,17 @@ --- -title: "Score K8s" +title: "rabbitmq-dapr-pubsub" draft: false mermaid: true type: examples +resourceType: "dapr-pubsub" +provisionerType: "template" +flavor: "rabbitmq" excerpt: '' +description: 'Generates a Dapr PubSub Component pointing to a RabbitMQ StatefulSet.' +expectedOutputs: + - name hasMore: false -parent: "Dapr Pubsub" -flavor: "Community" --- {{% example-file filename="10-rabbitmq-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} -{{% example-file filename="10-redis-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md new file mode 100644 index 00000000..4158645e --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md @@ -0,0 +1,17 @@ +--- +title: "redis-dapr-pubsub" +draft: false +mermaid: true +type: examples +resourceType: "dapr-pubsub" +provisionerType: "template" +flavor: "redis" +excerpt: '' +description: 'Generates a Dapr PubSub Component pointing to a Redis StatefulSet.' +expectedOutputs: + - name +hasMore: false + +--- + +{{% example-file filename="10-redis-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose.md b/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose/template/redis-dapr-state-store.md similarity index 57% rename from content/en/examples/resource-provisioners/community/dapr-state-store/score-compose.md rename to content/en/examples/resource-provisioners/community/dapr-state-store/score-compose/template/redis-dapr-state-store.md index a5a17ba4..613fb140 100644 --- a/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose.md +++ b/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose/template/redis-dapr-state-store.md @@ -1,12 +1,16 @@ --- -title: "Score Compose" +title: "redis-dapr-state-store" draft: false mermaid: true type: examples +resourceType: "dapr-state-store" +provisionerType: "template" +flavor: "redis" excerpt: '' +description: 'Generates a Dapr StateStore Component pointing to a Redis Service.' +expectedOutputs: + - name hasMore: false -parent: "Dapr State Store" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s.md b/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s/template/redis.md similarity index 59% rename from content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s.md rename to content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s/template/redis.md index eba79a2a..bd734e98 100644 --- a/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s/template/redis.md @@ -1,12 +1,16 @@ --- -title: "Score K8s" +title: "redis" draft: false mermaid: true type: examples +resourceType: "dapr-state-store" +provisionerType: "template" +flavor: "redis" excerpt: '' +description: 'Generates a Dapr StateStore Component pointing to a Redis StatefulSet.' +expectedOutputs: + - name hasMore: false -parent: "Dapr State Store" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose.md b/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose/template/dapr-subscription.md similarity index 53% rename from content/en/examples/resource-provisioners/community/dapr-subscription/score-compose.md rename to content/en/examples/resource-provisioners/community/dapr-subscription/score-compose/template/dapr-subscription.md index a16dffd9..3fff8bbb 100644 --- a/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose.md +++ b/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose/template/dapr-subscription.md @@ -1,12 +1,20 @@ --- -title: "Score Compose" +title: "dapr-subscription" draft: false mermaid: true type: examples +resourceType: "dapr-subscription" +provisionerType: "template" +flavor: "dapr" excerpt: '' +description: 'Generates a Dapr Subscription on a given Topic and PubSub.' +expectedOutputs: + - name + - topic +supportedParams: + - topic + - pubsub hasMore: false -parent: "Dapr Subscription" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s.md b/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s/template/dapr-subscription.md similarity index 53% rename from content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s.md rename to content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s/template/dapr-subscription.md index d999b850..f871df56 100644 --- a/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s/template/dapr-subscription.md @@ -1,12 +1,20 @@ --- -title: "Score K8s" +title: "dapr-subscription" draft: false mermaid: true type: examples +resourceType: "dapr-subscription" +provisionerType: "template" +flavor: "dapr" excerpt: '' +description: 'Generates a Dapr Subscription on a given Topic and PubSub.' +expectedOutputs: + - name + - topic +supportedParams: + - topic + - pubsub hasMore: false -parent: "Dapr Subscription" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/dns/score-compose.md b/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md similarity index 69% rename from content/en/examples/resource-provisioners/community/dns/score-compose.md rename to content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md index 5c6ca03e..df429692 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-compose.md +++ b/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md @@ -1,13 +1,19 @@ --- -title: "Score Compose" +title: "dns-in-codespace" draft: false mermaid: true type: examples +resourceType: "dns" +provisionerType: "cmd" +flavor: "dns" excerpt: 'Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' +description: 'Get the forwarded port URL in current GitHub Codespace on port 8080' +expectedOutputs: + - host + - url +tool: dns hasMore: false -parent: "Dns" -flavor: "Community" --- @@ -16,4 +22,3 @@ Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. {{% example-file filename="10-dns-in-codespace.provisioners.yaml" dir="resource-provisioners/community/dns/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} -{{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md b/content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md new file mode 100644 index 00000000..37f4c697 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md @@ -0,0 +1,23 @@ +--- +title: "dns-with-route" +draft: false +mermaid: true +type: examples +resourceType: "dns" +provisionerType: "template" +flavor: "dns" +excerpt: 'Prerequisites for `dns-in-codespace`: +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' +description: 'Outputs a *.localhost domain as the hostname and associated URL in http on port 8080' +expectedOutputs: + - host + - url +hasMore: false + +--- + +Prerequisites for `dns-in-codespace`: + +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. + +{{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-k8s.md b/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md similarity index 70% rename from content/en/examples/resource-provisioners/community/dns/score-k8s.md rename to content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md index 7a07decd..ba093e49 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md @@ -1,13 +1,19 @@ --- -title: "Score K8s" +title: "dns-in-codespace" draft: false mermaid: true type: examples +resourceType: "dns" +provisionerType: "cmd" +flavor: "dns" excerpt: 'Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' +description: 'Get the forwarded port URL in current GitHub Codespace on port 80' +expectedOutputs: + - host + - url +tool: dns hasMore: false -parent: "Dns" -flavor: "Community" --- @@ -16,4 +22,3 @@ Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. {{% example-file filename="10-dns-in-codespace.provisioners.yaml" dir="resource-provisioners/community/dns/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} -{{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md b/content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md new file mode 100644 index 00000000..ba4ba109 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md @@ -0,0 +1,23 @@ +--- +title: "dns-with-url" +draft: false +mermaid: true +type: examples +resourceType: "dns" +provisionerType: "template" +flavor: "dns" +excerpt: 'Prerequisites for `dns-in-codespace`: +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' +description: 'Outputs a *.localhost domain as the hostname and associated URL in http on port 80' +expectedOutputs: + - host + - url +hasMore: false + +--- + +Prerequisites for `dns-in-codespace`: + +- Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. + +{{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/environment/score-compose.md b/content/en/examples/resource-provisioners/community/environment/score-compose/cmd/dotenv.md similarity index 74% rename from content/en/examples/resource-provisioners/community/environment/score-compose.md rename to content/en/examples/resource-provisioners/community/environment/score-compose/cmd/dotenv.md index b37f760f..aacf84f8 100644 --- a/content/en/examples/resource-provisioners/community/environment/score-compose.md +++ b/content/en/examples/resource-provisioners/community/environment/score-compose/cmd/dotenv.md @@ -1,13 +1,16 @@ --- -title: "Score Compose" +title: "dotenv" draft: false mermaid: true type: examples +resourceType: "environment" +provisionerType: "cmd" +flavor: "dotenv" excerpt: 'Prerequisites: - Have `python` installed, this provisioner is using Python to load the `.env` file.' +description: 'Loads environment variables from a local .env file.' +tool: dotenv hasMore: false -parent: "Environment" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/environment/score-k8s.md b/content/en/examples/resource-provisioners/community/environment/score-k8s/cmd/dotenv.md similarity index 74% rename from content/en/examples/resource-provisioners/community/environment/score-k8s.md rename to content/en/examples/resource-provisioners/community/environment/score-k8s/cmd/dotenv.md index d10b219a..b9f5453a 100644 --- a/content/en/examples/resource-provisioners/community/environment/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/environment/score-k8s/cmd/dotenv.md @@ -1,13 +1,16 @@ --- -title: "Score K8s" +title: "dotenv" draft: false mermaid: true type: examples +resourceType: "environment" +provisionerType: "cmd" +flavor: "dotenv" excerpt: 'Prerequisites: - Have `python` installed, this provisioner is using Python to load the `.env` file.' +description: 'Loads environment variables from a local .env file.' +tool: dotenv hasMore: false -parent: "Environment" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose.md b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/template/empty-hpa.md similarity index 59% rename from content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose.md rename to content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/template/empty-hpa.md index ec1605c8..73ba70bf 100644 --- a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose.md +++ b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/template/empty-hpa.md @@ -1,12 +1,14 @@ --- -title: "Score Compose" +title: "empty-hpa" draft: false mermaid: true type: examples +resourceType: "horizontal-pod-autoscaler" +provisionerType: "template" +flavor: "empty" excerpt: '' +description: 'Generates an empty object because HPA is not supported in Docker Compose.' hasMore: false -parent: "Horizontal Pod Autoscaler" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s.md b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/template/default-hpa.md similarity index 52% rename from content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s.md rename to content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/template/default-hpa.md index b4b795ee..d32cf4cd 100644 --- a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/template/default-hpa.md @@ -1,12 +1,18 @@ --- -title: "Score K8s" +title: "default-hpa" draft: false mermaid: true type: examples +resourceType: "horizontal-pod-autoscaler" +provisionerType: "template" +flavor: "default" excerpt: '' +description: 'Generates an HorizontalPodAutoscaler manifest.' +supportedParams: + - maxReplicas + - minReplicas + - targetCPUUtilizationPercentage hasMore: false -parent: "Horizontal Pod Autoscaler" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/redis/score-k8s.md b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md similarity index 81% rename from content/en/examples/resource-provisioners/community/redis/score-k8s.md rename to content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md index 19271176..c52bbc98 100644 --- a/content/en/examples/resource-provisioners/community/redis/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md @@ -1,14 +1,22 @@ --- -title: "Score K8s" +title: "helm-template-redis" draft: false mermaid: true type: examples +resourceType: "redis" +provisionerType: "cmd" +flavor: "helm" excerpt: 'Prerequisites: - Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). - Have `yq` installed locally.' +description: 'Generates the manifests of the bitnami/redis Helm chart.' +expectedOutputs: + - host + - port + - username + - password +tool: helm hasMore: true -parent: "Redis" -flavor: "Community" --- @@ -28,4 +36,3 @@ Prerequisites: - If you don't have one, you can deploy a `Kind` cluster locally by running this script: `.scripts/setup-kind-cluster.sh`. {{% example-file filename="10-redis-helm-template.provisioners.yaml" dir="resource-provisioners/community/redis/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} -{{% example-file filename="10-redis-helm-upgrade.provisioners.yaml" dir="resource-provisioners/community/redis/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md new file mode 100644 index 00000000..40959a9f --- /dev/null +++ b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md @@ -0,0 +1,38 @@ +--- +title: "helm-upgrade-redis" +draft: false +mermaid: true +type: examples +resourceType: "redis" +provisionerType: "cmd" +flavor: "helm" +excerpt: 'Prerequisites: +- Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have `yq` installed locally.' +description: 'Deploys the bitnami/redis Helm chart in an existing cluster.' +expectedOutputs: + - host + - port + - username + - password +tool: helm +hasMore: true + +--- + +## For `10-redis-helm-template.provisioners.yaml` + +Prerequisites: + +- Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have `yq` installed locally. + +## For `10-redis-helm-upgrade.provisioners.yaml` + +Prerequisites: + +- Have `helm` installed locally, this provisioner installs the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). +- Have access to a cluster where the Helm chart will be installed. + - If you don't have one, you can deploy a `Kind` cluster locally by running this script: `.scripts/setup-kind-cluster.sh`. + +{{% example-file filename="10-redis-helm-upgrade.provisioners.yaml" dir="resource-provisioners/community/redis/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s.md b/content/en/examples/resource-provisioners/community/route/score-k8s.md deleted file mode 100644 index a624a65e..00000000 --- a/content/en/examples/resource-provisioners/community/route/score-k8s.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Route" -flavor: "Community" - ---- - -{{% example-file filename="10-ingress-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} -{{% example-file filename="10-ingress-with-netpol-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} -{{% example-file filename="10-shared-gateway-httproute-with-netpol.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} -{{% example-file filename="10-shared-gateway-httproute.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md new file mode 100644 index 00000000..e4198995 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md @@ -0,0 +1,19 @@ +--- +title: "ingress-route" +draft: false +mermaid: true +type: examples +resourceType: "route" +provisionerType: "template" +flavor: "ingress" +excerpt: '' +description: 'Provisions an Ingress route on a shared nginx instance.' +supportedParams: + - path + - host + - port +hasMore: false + +--- + +{{% example-file filename="10-ingress-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md new file mode 100644 index 00000000..17421d4b --- /dev/null +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md @@ -0,0 +1,19 @@ +--- +title: "ingress-with-net-pol-route" +draft: false +mermaid: true +type: examples +resourceType: "route" +provisionerType: "template" +flavor: "ingress" +excerpt: '' +description: 'Provisions an Ingress route on a shared nginx instance, and a NetworkPolicy between them.' +supportedParams: + - path + - host + - port +hasMore: false + +--- + +{{% example-file filename="10-ingress-with-netpol-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md new file mode 100644 index 00000000..ba9b0df4 --- /dev/null +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md @@ -0,0 +1,19 @@ +--- +title: "route-with-shared-gateway-with-netpol" +draft: false +mermaid: true +type: examples +resourceType: "route" +provisionerType: "template" +flavor: "route" +excerpt: '' +description: 'Generates an HTTPRoute attached to a default Gateway in default Namespace, and a NetworkPolicy between them.' +supportedParams: + - path + - host + - port +hasMore: false + +--- + +{{% example-file filename="10-shared-gateway-httproute-with-netpol.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md new file mode 100644 index 00000000..d96f19ce --- /dev/null +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md @@ -0,0 +1,19 @@ +--- +title: "route-with-shared-gateway" +draft: false +mermaid: true +type: examples +resourceType: "route" +provisionerType: "template" +flavor: "route" +excerpt: '' +description: 'Generates an HTTPRoute attached to a default Gateway in default Namespace.' +supportedParams: + - path + - host + - port +hasMore: false + +--- + +{{% example-file filename="10-shared-gateway-httproute.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/service/score-compose.md b/content/en/examples/resource-provisioners/community/service/score-compose/template/static-service.md similarity index 55% rename from content/en/examples/resource-provisioners/community/service/score-compose.md rename to content/en/examples/resource-provisioners/community/service/score-compose/template/static-service.md index e8f11b6f..6ea339aa 100644 --- a/content/en/examples/resource-provisioners/community/service/score-compose.md +++ b/content/en/examples/resource-provisioners/community/service/score-compose/template/static-service.md @@ -1,12 +1,16 @@ --- -title: "Score Compose" +title: "static-service" draft: false mermaid: true type: examples +resourceType: "service" +provisionerType: "template" +flavor: "static" excerpt: '' +description: 'Outputs the name of the Workload dependency if it exists in the list of Workloads.' +expectedOutputs: + - name hasMore: false -parent: "Service" -flavor: "Community" --- diff --git a/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md new file mode 100644 index 00000000..284aeeeb --- /dev/null +++ b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md @@ -0,0 +1,17 @@ +--- +title: "static-service-with-netpol" +draft: false +mermaid: true +type: examples +resourceType: "service" +provisionerType: "template" +flavor: "static" +excerpt: '' +description: 'Outputs the name of the Workload dependency if it exists in the list of Workloads, and generate NetworkPolicies between them.' +expectedOutputs: + - name +hasMore: false + +--- + +{{% example-file filename="10-service-with-netpol.provisioners.yaml" dir="resource-provisioners/community/service/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/service/score-k8s.md b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service.md similarity index 50% rename from content/en/examples/resource-provisioners/community/service/score-k8s.md rename to content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service.md index 112b44de..3df48f86 100644 --- a/content/en/examples/resource-provisioners/community/service/score-k8s.md +++ b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service.md @@ -1,14 +1,17 @@ --- -title: "Score K8s" +title: "static-service" draft: false mermaid: true type: examples +resourceType: "service" +provisionerType: "template" +flavor: "static" excerpt: '' +description: 'Outputs the name of the Workload dependency if it exists in the list of Workloads.' +expectedOutputs: + - name hasMore: false -parent: "Service" -flavor: "Community" --- -{{% example-file filename="10-service-with-netpol.provisioners.yaml" dir="resource-provisioners/community/service/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} {{% example-file filename="10-service.provisioners.yaml" dir="resource-provisioners/community/service/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/dns/score-compose.md b/content/en/examples/resource-provisioners/default/dns/score-compose/template/dns.md similarity index 73% rename from content/en/examples/resource-provisioners/default/dns/score-compose.md rename to content/en/examples/resource-provisioners/default/dns/score-compose/template/dns.md index f0e367d7..28be1926 100644 --- a/content/en/examples/resource-provisioners/default/dns/score-compose.md +++ b/content/en/examples/resource-provisioners/default/dns/score-compose/template/dns.md @@ -1,15 +1,19 @@ --- -title: "Score Compose" +title: "dns" draft: false mermaid: true type: examples +resourceType: "dns" +provisionerType: "template" +flavor: "dns" excerpt: 'The default dns provisioner just outputs localhost as the hostname every time. This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This can be replaced by a new provisioner in the future.' +description: 'Outputs a *.localhost domain as the hostname.' +expectedOutputs: + - host hasMore: true -parent: "Dns" -flavor: "Default" --- The default dns provisioner just outputs localhost as the hostname every time. This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This can be replaced by a new provisioner in the future. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/dns/score-k8s.md b/content/en/examples/resource-provisioners/default/dns/score-k8s/template/dns.md similarity index 72% rename from content/en/examples/resource-provisioners/default/dns/score-k8s.md rename to content/en/examples/resource-provisioners/default/dns/score-k8s/template/dns.md index 63c5ef66..64629c05 100644 --- a/content/en/examples/resource-provisioners/default/dns/score-k8s.md +++ b/content/en/examples/resource-provisioners/default/dns/score-k8s/template/dns.md @@ -1,15 +1,19 @@ --- -title: "Score K8s" +title: "dns" draft: false mermaid: true type: examples +resourceType: "dns" +provisionerType: "template" +flavor: "dns" excerpt: 'The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is available. You should replace this with your own dns name generation that matches your external-dns controller.' +description: 'Outputs a *.localhost domain as the hostname.' +expectedOutputs: + - host hasMore: true -parent: "Dns" -flavor: "Default" --- The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is available. You should replace this with your own dns name generation that matches your external-dns controller. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/elasticsearch/score-compose.md b/content/en/examples/resource-provisioners/default/elasticsearch/score-compose/template/elasticsearch.md similarity index 51% rename from content/en/examples/resource-provisioners/default/elasticsearch/score-compose.md rename to content/en/examples/resource-provisioners/default/elasticsearch/score-compose/template/elasticsearch.md index 5e6766b3..d14eb0b9 100644 --- a/content/en/examples/resource-provisioners/default/elasticsearch/score-compose.md +++ b/content/en/examples/resource-provisioners/default/elasticsearch/score-compose/template/elasticsearch.md @@ -1,15 +1,22 @@ --- -title: "Score Compose" +title: "elasticsearch" draft: false mermaid: true type: examples +resourceType: "elasticsearch" +provisionerType: "template" +flavor: "elasticsearch" excerpt: 'The default elasticsearch provisioner adds a elasticsearch instance.' +description: 'Provisions a dedicated Elastic Search instance.' +expectedOutputs: + - host + - port + - username + - password hasMore: false -parent: "Elasticsearch" -flavor: "Default" --- The default elasticsearch provisioner adds a elasticsearch instance. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/elasticsearch/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/elasticsearch/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s.md b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md similarity index 66% rename from content/en/examples/resource-provisioners/default/example-provisioner/score-k8s.md rename to content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md index edba93ca..8e006afd 100644 --- a/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s.md +++ b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md @@ -1,15 +1,21 @@ --- -title: "Score K8s" +title: "example-provisioner" draft: false mermaid: true type: examples +resourceType: "example-provisioner-resource" +provisionerType: "cmd" +flavor: "example" excerpt: 'The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory.' +description: 'Example provisioner that runs a bash script.' +expectedOutputs: + - key + - secret +tool: example hasMore: true -parent: "Example Provisioner" -flavor: "Default" --- The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/example-provisioner/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/example-provisioner/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md b/content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md deleted file mode 100644 index 9bde81e2..00000000 --- a/content/en/examples/resource-provisioners/default/kafka-topic/score-compose.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score Compose" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Kafka Topic" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/kafka-topic/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md b/content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md new file mode 100644 index 00000000..e739202b --- /dev/null +++ b/content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md @@ -0,0 +1,20 @@ +--- +title: "kafka-topic" +draft: false +mermaid: true +type: examples +resourceType: "kafka-topic" +provisionerType: "template" +flavor: "kafka" +excerpt: '' +description: 'Provisions a dedicated Kafka topic on a shared Kafka broker.' +expectedOutputs: + - host + - port + - name + - num_partitions +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/kafka-topic/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mongo/score-k8s.md b/content/en/examples/resource-provisioners/default/mongo/score-k8s.md deleted file mode 100644 index a44bd1ad..00000000 --- a/content/en/examples/resource-provisioners/default/mongo/score-k8s.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Mongo" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongo/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md b/content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md new file mode 100644 index 00000000..eff401c2 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md @@ -0,0 +1,21 @@ +--- +title: "mongo" +draft: false +mermaid: true +type: examples +resourceType: "mongodb" +provisionerType: "template" +flavor: "mongo" +excerpt: '' +description: 'Provisions a dedicated MongoDB database.' +expectedOutputs: + - host + - port + - username + - password + - connection +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongo/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mongodb/score-compose.md b/content/en/examples/resource-provisioners/default/mongodb/score-compose/template/mongodb.md similarity index 61% rename from content/en/examples/resource-provisioners/default/mongodb/score-compose.md rename to content/en/examples/resource-provisioners/default/mongodb/score-compose/template/mongodb.md index 2fac8ff4..d4bcb782 100644 --- a/content/en/examples/resource-provisioners/default/mongodb/score-compose.md +++ b/content/en/examples/resource-provisioners/default/mongodb/score-compose/template/mongodb.md @@ -1,15 +1,23 @@ --- -title: "Score Compose" +title: "mongodb" draft: false mermaid: true type: examples +resourceType: "mongodb" +provisionerType: "template" +flavor: "mongodb" excerpt: 'The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string.' +description: 'Provisions a dedicated MongoDB database.' +expectedOutputs: + - host + - port + - username + - password + - connection hasMore: false -parent: "Mongodb" -flavor: "Default" --- The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongodb/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongodb/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-compose.md b/content/en/examples/resource-provisioners/default/mssql/score-compose.md deleted file mode 100644 index f133bc22..00000000 --- a/content/en/examples/resource-provisioners/default/mssql/score-compose.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score Compose" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Mssql" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md b/content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md new file mode 100644 index 00000000..36586bf8 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md @@ -0,0 +1,22 @@ +--- +title: "mssql" +draft: false +mermaid: true +type: examples +resourceType: "mssql" +provisionerType: "template" +flavor: "mssql" +excerpt: '' +description: 'Provisions a dedicated database on a shared MS SQL server instance.' +expectedOutputs: + - server + - port + - connection + - database + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-k8s.md b/content/en/examples/resource-provisioners/default/mssql/score-k8s.md deleted file mode 100644 index 6103ce74..00000000 --- a/content/en/examples/resource-provisioners/default/mssql/score-k8s.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Mssql" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md b/content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md new file mode 100644 index 00000000..07e8980b --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md @@ -0,0 +1,22 @@ +--- +title: "mssql" +draft: false +mermaid: true +type: examples +resourceType: "mssql" +provisionerType: "template" +flavor: "mssql" +excerpt: '' +description: 'Provisions a dedicated database on a shared MS SQL server instance.' +expectedOutputs: + - server + - port + - connection + - database + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mysql/score-compose.md b/content/en/examples/resource-provisioners/default/mysql/score-compose/template/mysql.md similarity index 57% rename from content/en/examples/resource-provisioners/default/mysql/score-compose.md rename to content/en/examples/resource-provisioners/default/mysql/score-compose/template/mysql.md index ee1401b3..bcd0d386 100644 --- a/content/en/examples/resource-provisioners/default/mysql/score-compose.md +++ b/content/en/examples/resource-provisioners/default/mysql/score-compose/template/mysql.md @@ -1,15 +1,24 @@ --- -title: "Score Compose" +title: "mysql" draft: false mermaid: true type: examples +resourceType: "mysql" +provisionerType: "template" +flavor: "mysql" excerpt: 'The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on startup.' +description: 'Provisions a dedicated MySQL database on a shared instance.' +expectedOutputs: + - host + - port + - name + - database + - username + - password hasMore: false -parent: "Mysql" -flavor: "Default" --- The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on startup. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mysql/score-k8s.md b/content/en/examples/resource-provisioners/default/mysql/score-k8s.md deleted file mode 100644 index 94f1bde0..00000000 --- a/content/en/examples/resource-provisioners/default/mysql/score-k8s.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Mysql" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md b/content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md new file mode 100644 index 00000000..3bb33e44 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md @@ -0,0 +1,22 @@ +--- +title: "mysql" +draft: false +mermaid: true +type: examples +resourceType: "mysql" +provisionerType: "template" +flavor: "mysql" +excerpt: '' +description: 'Provisions a dedicated MySQL database on a shared instance.' +expectedOutputs: + - host + - port + - name + - database + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md deleted file mode 100644 index 7a1070f0..00000000 --- a/content/en/examples/resource-provisioners/default/postgres-instance/score-compose.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score Compose" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Postgres Instance" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md new file mode 100644 index 00000000..817ab7c5 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md @@ -0,0 +1,20 @@ +--- +title: "postgres-instance" +draft: false +mermaid: true +type: examples +resourceType: "postgres-instance" +provisionerType: "template" +flavor: "postgres" +excerpt: '' +description: 'Provisions a dedicated PostgreSQL instance.' +expectedOutputs: + - host + - port + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md deleted file mode 100644 index 2c3e7dca..00000000 --- a/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Postgres Instance" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md new file mode 100644 index 00000000..613228a8 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md @@ -0,0 +1,20 @@ +--- +title: "postgres-instance" +draft: false +mermaid: true +type: examples +resourceType: "postgres-instance" +provisionerType: "template" +flavor: "postgres" +excerpt: '' +description: 'Provisions a dedicated PostgreSQL instance.' +expectedOutputs: + - host + - port + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres/score-compose.md b/content/en/examples/resource-provisioners/default/postgres/score-compose/template/postgres.md similarity index 57% rename from content/en/examples/resource-provisioners/default/postgres/score-compose.md rename to content/en/examples/resource-provisioners/default/postgres/score-compose/template/postgres.md index 96fe3805..bb1004a3 100644 --- a/content/en/examples/resource-provisioners/default/postgres/score-compose.md +++ b/content/en/examples/resource-provisioners/default/postgres/score-compose/template/postgres.md @@ -1,15 +1,24 @@ --- -title: "Score Compose" +title: "postgres" draft: false mermaid: true type: examples +resourceType: "postgres" +provisionerType: "template" +flavor: "postgres" excerpt: 'The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on startup.' +description: 'Provisions a dedicated database on a shared PostgreSQL instance.' +expectedOutputs: + - host + - port + - name + - database + - username + - password hasMore: false -parent: "Postgres" -flavor: "Default" --- The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on startup. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres/score-k8s.md b/content/en/examples/resource-provisioners/default/postgres/score-k8s.md deleted file mode 100644 index e1319473..00000000 --- a/content/en/examples/resource-provisioners/default/postgres/score-k8s.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Postgres" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md b/content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md new file mode 100644 index 00000000..3fd8521a --- /dev/null +++ b/content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md @@ -0,0 +1,22 @@ +--- +title: "postgres" +draft: false +mermaid: true +type: examples +resourceType: "postgres" +provisionerType: "template" +flavor: "postgres" +excerpt: '' +description: 'Provisions a dedicated database on a shared PostgreSQL instance.' +expectedOutputs: + - host + - port + - name + - database + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/rabbitmq/score-compose.md b/content/en/examples/resource-provisioners/default/rabbitmq/score-compose/template/rabbitmq.md similarity index 56% rename from content/en/examples/resource-provisioners/default/rabbitmq/score-compose.md rename to content/en/examples/resource-provisioners/default/rabbitmq/score-compose/template/rabbitmq.md index 3a86b574..1b07de82 100644 --- a/content/en/examples/resource-provisioners/default/rabbitmq/score-compose.md +++ b/content/en/examples/resource-provisioners/default/rabbitmq/score-compose/template/rabbitmq.md @@ -1,15 +1,23 @@ --- -title: "Score Compose" +title: "rabbitmq" draft: false mermaid: true type: examples +resourceType: "amqp" +provisionerType: "template" +flavor: "rabbitmq" excerpt: 'The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins.' +description: 'Provisions a dedicated RabbitMQ vhost on a shared instance.' +expectedOutputs: + - host + - port + - vhost + - username + - password hasMore: false -parent: "Rabbitmq" -flavor: "Default" --- The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md b/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md deleted file mode 100644 index bfb0aec7..00000000 --- a/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Rabbitmq" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md b/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md new file mode 100644 index 00000000..38485336 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md @@ -0,0 +1,21 @@ +--- +title: "rabbitmq" +draft: false +mermaid: true +type: examples +resourceType: "amqp" +provisionerType: "template" +flavor: "rabbitmq" +excerpt: '' +description: 'Provisions a dedicated RabbitMQ vhost on a shared instance.' +expectedOutputs: + - host + - port + - vhost + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/redis/score-compose.md b/content/en/examples/resource-provisioners/default/redis/score-compose/template/redis.md similarity index 60% rename from content/en/examples/resource-provisioners/default/redis/score-compose.md rename to content/en/examples/resource-provisioners/default/redis/score-compose/template/redis.md index 8cc56c8b..a285c9fa 100644 --- a/content/en/examples/resource-provisioners/default/redis/score-compose.md +++ b/content/en/examples/resource-provisioners/default/redis/score-compose/template/redis.md @@ -1,15 +1,22 @@ --- -title: "Score Compose" +title: "redis" draft: false mermaid: true type: examples +resourceType: "redis" +provisionerType: "template" +flavor: "redis" excerpt: 'The default redis provisioner adds a redis service to the project which returns a host, port, username, and password.' +description: 'Provisions a dedicated Redis instance.' +expectedOutputs: + - host + - port + - username + - password hasMore: false -parent: "Redis" -flavor: "Default" --- The default redis provisioner adds a redis service to the project which returns a host, port, username, and password. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/redis/score-k8s.md b/content/en/examples/resource-provisioners/default/redis/score-k8s.md deleted file mode 100644 index 480b79ec..00000000 --- a/content/en/examples/resource-provisioners/default/redis/score-k8s.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Redis" -flavor: "Default" - ---- - -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md b/content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md new file mode 100644 index 00000000..9c2fa3c8 --- /dev/null +++ b/content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md @@ -0,0 +1,20 @@ +--- +title: "redis" +draft: false +mermaid: true +type: examples +resourceType: "redis" +provisionerType: "template" +flavor: "redis" +excerpt: '' +description: 'Provisions a dedicated Redis instance.' +expectedOutputs: + - host + - port + - username + - password +hasMore: false + +--- + +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/route/score-compose.md b/content/en/examples/resource-provisioners/default/route/score-compose/template/route.md similarity index 69% rename from content/en/examples/resource-provisioners/default/route/score-compose.md rename to content/en/examples/resource-provisioners/default/route/score-compose/template/route.md index a614ceb1..02299b9a 100644 --- a/content/en/examples/resource-provisioners/default/route/score-compose.md +++ b/content/en/examples/resource-provisioners/default/route/score-compose/template/route.md @@ -1,15 +1,21 @@ --- -title: "Score Compose" +title: "route" draft: false mermaid: true type: examples +resourceType: "route" +provisionerType: "template" +flavor: "route" excerpt: 'The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps.' +description: 'Provisions a ingress route on a shared Nginx instance.' +supportedParams: + - host + - port + - path hasMore: false -parent: "Route" -flavor: "Default" --- The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/route/score-k8s.md b/content/en/examples/resource-provisioners/default/route/score-k8s/template/route.md similarity index 68% rename from content/en/examples/resource-provisioners/default/route/score-k8s.md rename to content/en/examples/resource-provisioners/default/route/score-k8s/template/route.md index c4e1ed0a..211d61c7 100644 --- a/content/en/examples/resource-provisioners/default/route/score-k8s.md +++ b/content/en/examples/resource-provisioners/default/route/score-k8s/template/route.md @@ -1,15 +1,21 @@ --- -title: "Score K8s" +title: "route" draft: false mermaid: true type: examples +resourceType: "route" +provisionerType: "template" +flavor: "route" excerpt: 'Routes could be implemented as either traditional ingress resources or using the newer gateway API. In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this.' +description: 'Provisions an HTTPRoute on a shared Nginx instance.' +supportedParams: + - host + - port + - path hasMore: false -parent: "Route" -flavor: "Default" --- Routes could be implemented as either traditional ingress resources or using the newer gateway API. In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/s3/score-compose.md b/content/en/examples/resource-provisioners/default/s3/score-compose/template/s3.md similarity index 67% rename from content/en/examples/resource-provisioners/default/s3/score-compose.md rename to content/en/examples/resource-provisioners/default/s3/score-compose/template/s3.md index 8b172ca5..cd1bf41a 100644 --- a/content/en/examples/resource-provisioners/default/s3/score-compose.md +++ b/content/en/examples/resource-provisioners/default/s3/score-compose/template/s3.md @@ -1,15 +1,25 @@ --- -title: "Score Compose" +title: "s3" draft: false mermaid: true type: examples +resourceType: "s3" +provisionerType: "template" +flavor: "s3" excerpt: 'This resource provides a minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. If the provider has a publish port annotation, it can expose a management port on the local network for debugging and connectivity.' +description: 'Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance.' +expectedOutputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key hasMore: false -parent: "S3" -flavor: "Default" --- This resource provides a minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. If the provider has a publish port annotation, it can expose a management port on the local network for debugging and connectivity. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/s3/score-k8s.md b/content/en/examples/resource-provisioners/default/s3/score-k8s/template/s3.md similarity index 66% rename from content/en/examples/resource-provisioners/default/s3/score-k8s.md rename to content/en/examples/resource-provisioners/default/s3/score-k8s/template/s3.md index fab2a38f..f6ada09e 100644 --- a/content/en/examples/resource-provisioners/default/s3/score-k8s.md +++ b/content/en/examples/resource-provisioners/default/s3/score-k8s/template/s3.md @@ -1,15 +1,25 @@ --- -title: "Score K8s" +title: "s3" draft: false mermaid: true type: examples +resourceType: "s3" +provisionerType: "template" +flavor: "s3" excerpt: 'This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket.' +description: 'Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance.' +expectedOutputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key hasMore: false -parent: "S3" -flavor: "Default" --- This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/service-port/score-compose.md b/content/en/examples/resource-provisioners/default/service-port/score-compose/template/service-port.md similarity index 66% rename from content/en/examples/resource-provisioners/default/service-port/score-compose.md rename to content/en/examples/resource-provisioners/default/service-port/score-compose/template/service-port.md index fe3ad9bf..614ac418 100644 --- a/content/en/examples/resource-provisioners/default/service-port/score-compose.md +++ b/content/en/examples/resource-provisioners/default/service-port/score-compose/template/service-port.md @@ -1,15 +1,23 @@ --- -title: "Score Compose" +title: "service-port" draft: false mermaid: true type: examples +resourceType: "service-port" +provisionerType: "template" +flavor: "service" excerpt: 'The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet.' +description: 'Outputs a hostname and port for connecting to another workload.' +expectedOutputs: + - hostname + - port +supportedParams: + - workload + - port hasMore: true -parent: "Service Port" -flavor: "Default" --- The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/service-port/score-k8s.md b/content/en/examples/resource-provisioners/default/service-port/score-k8s/template/service-port.md similarity index 66% rename from content/en/examples/resource-provisioners/default/service-port/score-k8s.md rename to content/en/examples/resource-provisioners/default/service-port/score-k8s/template/service-port.md index 310b4846..1b4f3796 100644 --- a/content/en/examples/resource-provisioners/default/service-port/score-k8s.md +++ b/content/en/examples/resource-provisioners/default/service-port/score-k8s/template/service-port.md @@ -1,15 +1,23 @@ --- -title: "Score K8s" +title: "service-port" draft: false mermaid: true type: examples +resourceType: "service-port" +provisionerType: "template" +flavor: "service" excerpt: 'The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet.' +description: 'Outputs a hostname and port for connecting to another workload.' +expectedOutputs: + - hostname + - port +supportedParams: + - workload + - port hasMore: true -parent: "Service Port" -flavor: "Default" --- The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/volume/score-compose.md b/content/en/examples/resource-provisioners/default/volume/score-compose/template/volume.md similarity index 71% rename from content/en/examples/resource-provisioners/default/volume/score-compose.md rename to content/en/examples/resource-provisioners/default/volume/score-compose/template/volume.md index 45209a44..b0d56723 100644 --- a/content/en/examples/resource-provisioners/default/volume/score-compose.md +++ b/content/en/examples/resource-provisioners/default/volume/score-compose/template/volume.md @@ -1,15 +1,20 @@ --- -title: "Score Compose" +title: "volume" draft: false mermaid: true type: examples +resourceType: "volume" +provisionerType: "template" +flavor: "volume" excerpt: 'The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source attribute that we can reference.' +description: 'Creates a persistent volume that can be mounted on a workload.' +expectedOutputs: + - source + - type hasMore: false -parent: "Volume" -flavor: "Default" --- The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source attribute that we can reference. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/volume/score-k8s.md b/content/en/examples/resource-provisioners/default/volume/score-k8s/template/volume.md similarity index 69% rename from content/en/examples/resource-provisioners/default/volume/score-k8s.md rename to content/en/examples/resource-provisioners/default/volume/score-k8s/template/volume.md index ed1a4b98..bf9bc92c 100644 --- a/content/en/examples/resource-provisioners/default/volume/score-k8s.md +++ b/content/en/examples/resource-provisioners/default/volume/score-k8s/template/volume.md @@ -1,15 +1,19 @@ --- -title: "Score K8s" +title: "volume" draft: false mermaid: true type: examples +resourceType: "volume" +provisionerType: "template" +flavor: "volume" excerpt: 'As an example we have a 'volume' type which returns an emptyDir volume. In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or persistent volume and claims.' +description: 'Creates a persistent volume that can be mounted on a workload.' +expectedOutputs: + - source hasMore: true -parent: "Volume" -flavor: "Default" --- As an example we have a 'volume' type which returns an emptyDir volume. In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or persistent volume and claims. -{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} +{{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/gen/examples-site/frontmatter-provisioners.js b/gen/examples-site/frontmatter-provisioners.js new file mode 100644 index 00000000..f4c358a7 --- /dev/null +++ b/gen/examples-site/frontmatter-provisioners.js @@ -0,0 +1,173 @@ +const fs = require("fs"); +const path = require("path"); +const matter = require("gray-matter"); +const { getGitHubUrl } = require("./github-utils"); +const { + getMetadataFromReadme, + removeNonExternalLinks, + getExcerpt, + addAliasesToMetadata, +} = require("./metadata-utils"); +const { isDirectory } = require("./file-utils"); + +const sourceFolder = process.argv[2]; +const DEFAULT_PROVISIONER_URL_SCORE_K8S = + "https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml"; +const DEFAULT_PROVISIONER_URL_SCORE_COMPOSE = + "https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml"; + +/** + * Converts an array to YAML array format. + * @param {Array} array - The array to convert. + * @returns {string} The YAML array format string. + */ +const convertToYamlArray = (array) => { + if (!array || !Array.isArray(array) || array.length === 0) return ""; + return array.map((item) => ` - ${item}`).join("\n"); +}; + +const generateFrontmatterContent = (config) => { + return `--- +title: "${config.title}" +draft: false +mermaid: true +type: examples +resourceType: "${config.resourceType}" +provisionerType: "${config.provisionerType}" +flavor: "${config.title.split("-")[0]}" +excerpt: '${config.excerpt}' +description: '${config.description}' +${ + config.expectedOutputs ? `expectedOutputs: \n${config.expectedOutputs}\n` : "" +}${ + config.supportedParams + ? `supportedParams: \n${config.supportedParams}\n` + : "" + }${config.tool ? `tool: ${config.tool}\n` : ""}hasMore: ${ + config.excerpt !== config.content.trim() ? "true" : "false" + } +${config.metadata} +--- + +${config.content}`; +}; + +/** + * Generates content for an example file using the example-file shortcode. + * @param {string} file - The filename of the example file. + * @param {string} dir - The directory containing the file. + * @param {string} githubUrl - The GitHub URL for the file. + * @returns {string} The generated example file content. + */ +const generateExampleFileContent = (file, dir, githubUrl) => { + return `{{% example-file filename="${file}" dir="${dir}" githubUrl="${githubUrl}" %}}`; +}; + +/** + * Processes the README.md file in the given path and extracts metadata, content, and excerpt. + * @param {string} dirPath - The path to the directory containing the README.md file. + * @returns {Object} An object containing excerpt, content, and metadata. + */ +const processReadme = (dirPath) => { + let excerpt = ""; + let content = ""; + let metadata = ""; + if (fs.existsSync(`${sourceFolder}/${dirPath}/README.md`)) { + const readme = fs.readFileSync( + `${sourceFolder}/${dirPath}/README.md`, + "utf8" + ); + const { metadata: readmeMetadata, parsedFrontmatter } = + getMetadataFromReadme(readme, dirPath.split("/")[0]); + metadata = readmeMetadata; + content = removeNonExternalLinks(parsedFrontmatter.content); + excerpt = getExcerpt(content); + metadata = addAliasesToMetadata(dirPath, metadata); + } + return { excerpt, content, metadata }; +}; + +/** + * Writes content to a file in the examples directory. + * @param {string} filePath - The path where the file should be written. + * @param {string} content - The content to write to the file. + */ +const writeContentToFile = (filePath, content) => { + const fullPath = `./content/en/examples/${filePath}.md`; + const dirPath = path.dirname(fullPath); + fs.mkdirSync(dirPath, { recursive: true }); + fs.writeFileSync(fullPath, content); +}; + +/** + * Determines the GitHub URL for a provisioner based on type and implementation. + * @param {string} dirPath - The path to the provisioner directory. + * @param {Object} options - Options object containing source. + * @param {string} implementation - The implementation type (e.g., "score-k8s", "score-compose"). + * @returns {string} The GitHub URL for the provisioner. + */ +const getProvisionerGitHubUrl = (dirPath, options, implementation) => { + let githubUrl = getGitHubUrl(dirPath); + if (options.source === "default") { + if (implementation === "score-k8s") { + githubUrl = DEFAULT_PROVISIONER_URL_SCORE_K8S; + } else if (implementation === "score-compose") { + githubUrl = DEFAULT_PROVISIONER_URL_SCORE_COMPOSE; + } + } + return githubUrl; +}; + +/** + * Builds frontmatter and content for an example page. + * @param {string} dirPath - The path to the example directory. + * @param {Object} [options] - Additional options for the frontmatter. + */ +const buildResourceProvisionerFiles = ( + dirPath, + options = { source: "default" } +) => { + // Get and parse the YAML file inside the path + const yamlFiles = fs + .readdirSync(`${sourceFolder}/${dirPath}`) + .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml")); + for (const yamlFile of yamlFiles) { + const yamlFilePath = `${sourceFolder}/${dirPath}/${yamlFile}`; + const yamlContent = fs.readFileSync(yamlFilePath, "utf8"); + const parsedYaml = matter(`---\n${yamlContent}\n---`).data[0]; + const implementation = dirPath.split("/").pop(); + const readmeConfig = processReadme(options.readmeLocation || dirPath); + const dir = options.fileLocation || dirPath.replace(/\.md$/, ""); + const githubUrl = getProvisionerGitHubUrl(dirPath, options, implementation); + const uriParts = parsedYaml.uri.split("/"); + let title = uriParts[uriParts.length - 1]; + let tool; + // Handle URIs with # (e.g., cmd://bash#example-provisioner) + if (title.includes("#")) { + title = title.split("#")[1]; + tool = title.split("-")[0]; + } + const provisionerType = parsedYaml.uri.split("://")[0]; + + const frontmatterContent = generateFrontmatterContent({ + ...readmeConfig, + title, + provisionerType, + resourceType: parsedYaml.type, + description: parsedYaml.description, + expectedOutputs: convertToYamlArray(parsedYaml.expected_outputs), + supportedParams: convertToYamlArray(parsedYaml.supported_params), + tool, + }); + + writeContentToFile( + `${dirPath}/${provisionerType}/${title}`, + `${frontmatterContent} + +${generateExampleFileContent(yamlFile, dir, githubUrl)}\n +` + ); + } +}; + +module.exports = { buildResourceProvisionerFiles }; diff --git a/gen/examples-site/gen-example-pages.js b/gen/examples-site/gen-example-pages.js index 9ba53646..895136b9 100644 --- a/gen/examples-site/gen-example-pages.js +++ b/gen/examples-site/gen-example-pages.js @@ -1,8 +1,8 @@ const fs = require("fs"); const { buildFrontmatter } = require("./frontmatter"); +const { buildResourceProvisionerFiles } = require("./frontmatter-provisioners"); const { parseConfig } = require("./config-parser"); const { - isDirectory, mkdirIfNotExistsSync, isValidFolder, hasSubdirectories, @@ -79,12 +79,19 @@ for (const categoryFolder of filteredCategoryFolders) { continue; } - buildFrontmatter( - subsubfolder, - `${categoryFolder}/${folder}/${subfolder}/${subsubfolder}`, - subfolder, - folder - ); + if (categoryFolder === "resource-provisioners") { + buildResourceProvisionerFiles( + `${categoryFolder}/${folder}/${subfolder}/${subsubfolder}`, + { source: folder } + ); + } else { + buildFrontmatter( + subsubfolder, + `${categoryFolder}/${folder}/${subfolder}/${subsubfolder}`, + subfolder, + folder + ); + } } } else { buildFrontmatter( diff --git a/gen/examples-site/transform-default-resource-provisioners.js b/gen/examples-site/transform-default-resource-provisioners.js index 6b01b2a2..06d4892d 100644 --- a/gen/examples-site/transform-default-resource-provisioners.js +++ b/gen/examples-site/transform-default-resource-provisioners.js @@ -83,17 +83,6 @@ function extractProvisionerSection(content, uri) { // Extract the provisioner YAML (without leading comments) const yamlLines = lines.slice(startIdx, endIdx); - // Remove the leading "- " from the first line to make it an object instead of an array - // and remove 2 spaces of indentation from all subsequent lines - if (yamlLines.length > 0 && yamlLines[0].startsWith("- ")) { - yamlLines[0] = yamlLines[0].substring(2); - // Remove 2 spaces from the beginning of all other lines - for (let i = 1; i < yamlLines.length; i++) { - if (yamlLines[i].startsWith(" ")) { - yamlLines[i] = yamlLines[i].substring(2); - } - } - } const yamlContent = yamlLines.join("\n").trim(); // Extract just the comments for README @@ -155,13 +144,11 @@ function splitProvisionersFile(sourceFile, baseDir, implementation) { if (yamlContent) { fs.writeFileSync(targetFile, yamlContent, "utf8"); - console.log(`Created: ${targetFile}`); // Create README.md with comments if (comments) { const readmeFile = path.join(targetDir, "README.md"); fs.writeFileSync(readmeFile, comments, "utf8"); - console.log(`Created: ${readmeFile}`); } } else { console.warn(`Could not extract YAML for ${provisioner.uri}`); @@ -182,11 +169,9 @@ const scoreComposeYaml = path.join( const scoreK8sYaml = path.join(scoreK8sDir, "zz-default.provisioners.yaml"); if (fs.existsSync(scoreComposeYaml)) { - console.log("\nProcessing score-compose provisioners..."); splitProvisionersFile(scoreComposeYaml, scoreComposeDir, "score-compose"); } if (fs.existsSync(scoreK8sYaml)) { - console.log("\nProcessing score-k8s provisioners..."); splitProvisionersFile(scoreK8sYaml, scoreK8sDir, "score-k8s"); } diff --git a/gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml index 4d870dbb..96d1d7ab 100644 --- a/gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/dns/score-compose/provisioners.yaml @@ -1,11 +1,11 @@ -uri: template://default-provisioners/dns -type: dns -description: Outputs a *.localhost domain as the hostname. -init: | - randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost -state: | - instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} -outputs: | - host: {{ .State.instanceHostname }} -expected_outputs: - - host \ No newline at end of file +- uri: template://default-provisioners/dns + type: dns + description: Outputs a *.localhost domain as the hostname. + init: | + randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost + state: | + instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} + outputs: | + host: {{ .State.instanceHostname }} + expected_outputs: + - host \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml index 4d870dbb..96d1d7ab 100644 --- a/gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/dns/score-k8s/provisioners.yaml @@ -1,11 +1,11 @@ -uri: template://default-provisioners/dns -type: dns -description: Outputs a *.localhost domain as the hostname. -init: | - randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost -state: | - instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} -outputs: | - host: {{ .State.instanceHostname }} -expected_outputs: - - host \ No newline at end of file +- uri: template://default-provisioners/dns + type: dns + description: Outputs a *.localhost domain as the hostname. + init: | + randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost + state: | + instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} + outputs: | + host: {{ .State.instanceHostname }} + expected_outputs: + - host \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml index 378b6007..842d86a4 100644 --- a/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/elasticsearch/score-compose/provisioners.yaml @@ -1,136 +1,136 @@ -uri: template://default-provisioners/elasticsearch -# By default, match all elasticsearch types regardless of class and id. -# If you want to override this, create another provisioner definition with a higher priority. -type: elasticsearch -description: Provisions a dedicated Elastic Search instance. -# Init template has the random service name and password if needed later -init: | - serviceName: elasticsearch - randomPassword: {{ randAlphaNum 16 | quote }} - clusterName: cluster-ecs-{{ randAlphaNum 6 }} - username: elastic - sk: default-provisioners-elasticsearch-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "9200" .Metadata | quote }} - license: {{ dig "annotations" "compose.score.dev/license" "basic" .Metadata | quote }} - stackVersion: {{ dig "annotations" "compose.score.dev/stack-version" "8.14.0" .Metadata | quote }} - esMemLimit: {{ dig "annotations" "compose.score.dev/es-mem-limit" "1073741824" .Metadata | quote }} -# The state for each elasticsearch resource is a unique host, port, and credentials -state: | - clusterName: {{ dig "clusterName" .Init.clusterName .State | quote }} - username: {{ dig "username" .Init.username .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - host: {{ dig "host" .Init.serviceName .State | quote }} -outputs: | - host: {{ .State.host }} - port: {{ .Init.publishPort }} - username: {{ .State.username | quote }} - password: {{ .State.password | quote }} -# Ensure the data volume exists -volumes: | - ecscerts: - driver: local - ecsdata: - driver: local -# Create 2 services, the first is the setup container which creates the certificates, the second is the elasticsearch itself -services: | - setup: - image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} - volumes: - - type: volume - source: ecscerts - target: /usr/share/elasticsearch/config/certs - user: "0" - command: - - "bash" - - "-c" - - | - if [ ! -f config/certs/ca.zip ]; then - echo "Creating CA"; - bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; - unzip config/certs/ca.zip -d config/certs; - fi; - if [ ! -f config/certs/certs.zip ]; then - echo "Creating certs"; - echo -ne \ - "instances:\n"\ - " - name: {{ .State.host }}\n"\ - " dns:\n"\ - " - {{ .State.host }}\n"\ - " - localhost\n"\ - " ip:\n"\ - " - 127.0.0.1\n"\ - > config/certs/instances.yml; - bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; - unzip config/certs/certs.zip -d config/certs; - fi; - echo "Setting file permissions" - chown -R root:root config/certs; - find . -type d -exec chmod 750 \{\} \;; - find . -type f -exec chmod 640 \{\} \;; - echo "Waiting for Elasticsearch availability"; - until curl -s --cacert config/certs/ca/ca.crt https://{{ .State.host }}:9200 | grep -q "missing authentication credentials"; do sleep 10; done; - echo "All done!"; - healthcheck: - test: ["CMD-SHELL", "[ -f config/certs/{{ .State.host }}/{{ .State.host }}.crt ]"] - interval: 1s - timeout: 5s - retries: 120 - {{ .State.host }}: - depends_on: - setup: - condition: service_healthy - image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} - labels: - co.elastic.logs/module: elasticsearch - volumes: - - type: volume - source: ecscerts - target: /usr/share/elasticsearch/config/certs - - type: volume - source: ecsdata - target: /usr/share/elasticsearch/data - ports: - - target: 9200 - published: {{ .Init.publishPort }} - environment: - - node.name={{ .State.host }} - - cluster.name={{ .State.clusterName }} - - discovery.type=single-node - - bootstrap.memory_lock=true - - ELASTIC_PASSWORD={{ .State.password }} - - xpack.security.enabled=true - - xpack.security.http.ssl.enabled=true - - xpack.security.http.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key - - xpack.security.http.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt - - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt - - xpack.security.transport.ssl.enabled=true - - xpack.security.transport.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key - - xpack.security.transport.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt - - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt - - xpack.security.transport.ssl.verification_mode=certificate - - xpack.license.self_generated.type={{ .Init.license }} - mem_limit: {{ .Init.esMemLimit }} - ulimits: - memlock: - soft: -1 - hard: -1 - healthcheck: - test: - [ - "CMD-SHELL", - "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", - ] - interval: 10s - timeout: 10s - retries: 120 -info_logs: | - - "{{.Uid}}: To connect to elasticsearch:\n - download certificate from container per command like: \n - \tdocker cp [CONTAINER-NAME]:/usr/share/elasticsearch/config/certs/ca/ca.crt /tmp/ \n - and than check connection per culr like: \n - \tcurl --cacert /tmp/ca.crt -u {{ .State.username }}:{{ .State.password }} https://localhost:{{ .Init.publishPort }}" -expected_outputs: - - host - - port - - username - - password \ No newline at end of file +- uri: template://default-provisioners/elasticsearch + # By default, match all elasticsearch types regardless of class and id. + # If you want to override this, create another provisioner definition with a higher priority. + type: elasticsearch + description: Provisions a dedicated Elastic Search instance. + # Init template has the random service name and password if needed later + init: | + serviceName: elasticsearch + randomPassword: {{ randAlphaNum 16 | quote }} + clusterName: cluster-ecs-{{ randAlphaNum 6 }} + username: elastic + sk: default-provisioners-elasticsearch-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "9200" .Metadata | quote }} + license: {{ dig "annotations" "compose.score.dev/license" "basic" .Metadata | quote }} + stackVersion: {{ dig "annotations" "compose.score.dev/stack-version" "8.14.0" .Metadata | quote }} + esMemLimit: {{ dig "annotations" "compose.score.dev/es-mem-limit" "1073741824" .Metadata | quote }} + # The state for each elasticsearch resource is a unique host, port, and credentials + state: | + clusterName: {{ dig "clusterName" .Init.clusterName .State | quote }} + username: {{ dig "username" .Init.username .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + host: {{ dig "host" .Init.serviceName .State | quote }} + outputs: | + host: {{ .State.host }} + port: {{ .Init.publishPort }} + username: {{ .State.username | quote }} + password: {{ .State.password | quote }} + # Ensure the data volume exists + volumes: | + ecscerts: + driver: local + ecsdata: + driver: local + # Create 2 services, the first is the setup container which creates the certificates, the second is the elasticsearch itself + services: | + setup: + image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} + volumes: + - type: volume + source: ecscerts + target: /usr/share/elasticsearch/config/certs + user: "0" + command: + - "bash" + - "-c" + - | + if [ ! -f config/certs/ca.zip ]; then + echo "Creating CA"; + bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; + unzip config/certs/ca.zip -d config/certs; + fi; + if [ ! -f config/certs/certs.zip ]; then + echo "Creating certs"; + echo -ne \ + "instances:\n"\ + " - name: {{ .State.host }}\n"\ + " dns:\n"\ + " - {{ .State.host }}\n"\ + " - localhost\n"\ + " ip:\n"\ + " - 127.0.0.1\n"\ + > config/certs/instances.yml; + bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; + unzip config/certs/certs.zip -d config/certs; + fi; + echo "Setting file permissions" + chown -R root:root config/certs; + find . -type d -exec chmod 750 \{\} \;; + find . -type f -exec chmod 640 \{\} \;; + echo "Waiting for Elasticsearch availability"; + until curl -s --cacert config/certs/ca/ca.crt https://{{ .State.host }}:9200 | grep -q "missing authentication credentials"; do sleep 10; done; + echo "All done!"; + healthcheck: + test: ["CMD-SHELL", "[ -f config/certs/{{ .State.host }}/{{ .State.host }}.crt ]"] + interval: 1s + timeout: 5s + retries: 120 + {{ .State.host }}: + depends_on: + setup: + condition: service_healthy + image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} + labels: + co.elastic.logs/module: elasticsearch + volumes: + - type: volume + source: ecscerts + target: /usr/share/elasticsearch/config/certs + - type: volume + source: ecsdata + target: /usr/share/elasticsearch/data + ports: + - target: 9200 + published: {{ .Init.publishPort }} + environment: + - node.name={{ .State.host }} + - cluster.name={{ .State.clusterName }} + - discovery.type=single-node + - bootstrap.memory_lock=true + - ELASTIC_PASSWORD={{ .State.password }} + - xpack.security.enabled=true + - xpack.security.http.ssl.enabled=true + - xpack.security.http.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key + - xpack.security.http.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt + - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.enabled=true + - xpack.security.transport.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key + - xpack.security.transport.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt + - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt + - xpack.security.transport.ssl.verification_mode=certificate + - xpack.license.self_generated.type={{ .Init.license }} + mem_limit: {{ .Init.esMemLimit }} + ulimits: + memlock: + soft: -1 + hard: -1 + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", + ] + interval: 10s + timeout: 10s + retries: 120 + info_logs: | + - "{{.Uid}}: To connect to elasticsearch:\n + download certificate from container per command like: \n + \tdocker cp [CONTAINER-NAME]:/usr/share/elasticsearch/config/certs/ca/ca.crt /tmp/ \n + and than check connection per culr like: \n + \tcurl --cacert /tmp/ca.crt -u {{ .State.username }}:{{ .State.password }} https://localhost:{{ .Init.publishPort }}" + expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml index 1fd3b1de..f2b2283e 100644 --- a/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/example-provisioner/score-k8s/provisioners.yaml @@ -1,11 +1,11 @@ -uri: cmd://bash#example-provisioner -type: example-provisioner-resource -description: Example provisioner that runs a bash script. -class: default -id: specific -# (Optional) additional args that the binary gets run with -# If any of the args are '' it will be replaced with "provision" -args: ["-c", "echo '{\"resource_outputs\":{\"key\":\"value\",\"secret\":\"🔐💬mysecret_mykey💬🔐\"},\"manifests\":[]}'"] -expected_outputs: - - key - - secret \ No newline at end of file +- uri: cmd://bash#example-provisioner + type: example-provisioner-resource + description: Example provisioner that runs a bash script. + class: default + id: specific + # (Optional) additional args that the binary gets run with + # If any of the args are '' it will be replaced with "provision" + args: ["-c", "echo '{\"resource_outputs\":{\"key\":\"value\",\"secret\":\"🔐💬mysecret_mykey💬🔐\"},\"manifests\":[]}'"] + expected_outputs: + - key + - secret \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml index 4fa9a699..08c3b6b1 100644 --- a/gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/kafka-topic/score-compose/provisioners.yaml @@ -1,61 +1,61 @@ -uri: template://default-provisioners/kafka-topic -type: kafka-topic -description: Provisions a dedicated Kafka topic on a shared Kafka broker. -init: | - brokerPort: 9092 - ctrlPort: 9093 -state: | - topic: {{ dig "topic" (print "topic-" (randAlphaNum 6)) .State | quote }} -shared: | - shared_kafka_instance_name: {{ dig "shared_kafka_instance_name" (print "kafka-" (randAlphaNum 6)) .Shared | quote }} -services: | - {{ .Shared.shared_kafka_instance_name }}: - image: bitnami/kafka:latest - restart: always - environment: - KAFKA_CFG_NODE_ID: "0" - KAFKA_CFG_PROCESS_ROLES: controller,broker - KAFKA_CFG_LISTENERS: "PLAINTEXT://:{{ .Init.brokerPort }},CONTROLLER://:{{ .Init.ctrlPort }}" - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "0@{{ .Shared.shared_kafka_instance_name }}:{{ .Init.ctrlPort }}" - KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "false" - healthcheck: - test: ["CMD", "kafka-topics.sh", "--list", "--bootstrap-server=localhost:{{ .Init.brokerPort }}"] - interval: 2s - timeout: 2s - retries: 10 - {{ $publishPort := (dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi) }} - {{ if ne $publishPort 0 }} - ports: - - target: {{ .Init.brokerPort }} - published: {{ $publishPort }} - {{ end }} - volumes: - - type: volume - source: {{ .Shared.shared_kafka_instance_name }}-data - target: /bitnami/kafka - {{ .State.topic }}-init: - image: bitnami/kafka:latest - entrypoint: ["/bin/sh"] - command: ["-c", "kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --describe || kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --create --partitions=3"] - network_mode: "service:{{ .Shared.shared_kafka_instance_name }}" - labels: - dev.score.compose.labels.is-init-container: "true" - depends_on: - {{ .Shared.shared_kafka_instance_name }}: - condition: service_healthy - restart: true -volumes: | - {{ .Shared.shared_kafka_instance_name }}-data: - driver: local -outputs: | - host: {{ .Shared.shared_kafka_instance_name }} - port: "{{ .Init.brokerPort }}" - name: {{ .State.topic }} - num_partitions: 3 -expected_outputs: - - host - - port - - name - - num_partitions \ No newline at end of file +- uri: template://default-provisioners/kafka-topic + type: kafka-topic + description: Provisions a dedicated Kafka topic on a shared Kafka broker. + init: | + brokerPort: 9092 + ctrlPort: 9093 + state: | + topic: {{ dig "topic" (print "topic-" (randAlphaNum 6)) .State | quote }} + shared: | + shared_kafka_instance_name: {{ dig "shared_kafka_instance_name" (print "kafka-" (randAlphaNum 6)) .Shared | quote }} + services: | + {{ .Shared.shared_kafka_instance_name }}: + image: bitnami/kafka:latest + restart: always + environment: + KAFKA_CFG_NODE_ID: "0" + KAFKA_CFG_PROCESS_ROLES: controller,broker + KAFKA_CFG_LISTENERS: "PLAINTEXT://:{{ .Init.brokerPort }},CONTROLLER://:{{ .Init.ctrlPort }}" + KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" + KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "0@{{ .Shared.shared_kafka_instance_name }}:{{ .Init.ctrlPort }}" + KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "false" + healthcheck: + test: ["CMD", "kafka-topics.sh", "--list", "--bootstrap-server=localhost:{{ .Init.brokerPort }}"] + interval: 2s + timeout: 2s + retries: 10 + {{ $publishPort := (dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi) }} + {{ if ne $publishPort 0 }} + ports: + - target: {{ .Init.brokerPort }} + published: {{ $publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .Shared.shared_kafka_instance_name }}-data + target: /bitnami/kafka + {{ .State.topic }}-init: + image: bitnami/kafka:latest + entrypoint: ["/bin/sh"] + command: ["-c", "kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --describe || kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --create --partitions=3"] + network_mode: "service:{{ .Shared.shared_kafka_instance_name }}" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ .Shared.shared_kafka_instance_name }}: + condition: service_healthy + restart: true + volumes: | + {{ .Shared.shared_kafka_instance_name }}-data: + driver: local + outputs: | + host: {{ .Shared.shared_kafka_instance_name }} + port: "{{ .Init.brokerPort }}" + name: {{ .State.topic }} + num_partitions: 3 + expected_outputs: + - host + - port + - name + - num_partitions \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml index 0e2c3d8b..ebf9eefa 100644 --- a/gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/mongo/score-k8s/provisioners.yaml @@ -1,156 +1,156 @@ -uri: template://default-provisioners/mongo -type: mongodb -description: Provisions a dedicated MongoDB database. -init: | - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - service: mongo-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ .State.service }} - port: 27017 - connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.service }}:27017/" - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "MONGO_INITDB_ROOT_PASSWORD" }} -manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: +- uri: template://default-provisioners/mongo + type: mongodb + description: Provisions a dedicated MongoDB database. + init: | + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: mongo-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 27017 + connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.service }}:27017/" + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MONGO_INITDB_ROOT_PASSWORD" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} + data: + MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: mongo-db - image: mirror.gcr.io/mongo:8 - ports: - - name: mongo - containerPort: 27017 - env: - - name: MONGO_INITDB_ROOT_USERNAME - value: {{ .State.username | quote }} - - name: MONGO_INITDB_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MONGO_INITDB_ROOT_PASSWORD - livenessProbe: - exec: - command: - - /bin/sh - - -c - - echo 'db.runCommand("ping").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD - initialDelaySeconds: 30 - timeoutSeconds: 5 - periodSeconds: 20 + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: mongo-db + image: mirror.gcr.io/mongo:8 + ports: + - name: mongo + containerPort: 27017 + env: + - name: MONGO_INITDB_ROOT_USERNAME + value: {{ .State.username | quote }} + - name: MONGO_INITDB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MONGO_INITDB_ROOT_PASSWORD + livenessProbe: + exec: + command: + - /bin/sh + - -c + - echo 'db.runCommand("ping").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD + initialDelaySeconds: 30 + timeoutSeconds: 5 + periodSeconds: 20 + securityContext: + runAsUser: 1001 + runAsGroup: 1001 + allowPrivilegeEscalation: false + privileged: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data/db + - name: tmp + mountPath: /tmp securityContext: - runAsUser: 1001 - runAsGroup: 1001 - allowPrivilegeEscalation: false - privileged: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - volumeMounts: - - name: data - mountPath: /data/db - - name: tmp - mountPath: /tmp - securityContext: - runAsNonRoot: true - fsGroup: 1001 - seccompProfile: - type: RuntimeDefault - volumes: - - name: tmp - emptyDir: {} - volumeClaimTemplates: - - metadata: - name: data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 27017 - targetPort: 27017 -expected_outputs: - - host - - port - - username - - password - - connection \ No newline at end of file + runAsNonRoot: true + fsGroup: 1001 + seccompProfile: + type: RuntimeDefault + volumes: + - name: tmp + emptyDir: {} + volumeClaimTemplates: + - metadata: + name: data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 27017 + targetPort: 27017 + expected_outputs: + - host + - port + - username + - password + - connection \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml index 38eec046..9f38e3ad 100644 --- a/gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/mongodb/score-compose/provisioners.yaml @@ -1,53 +1,53 @@ -uri: template://default-provisioners/mongodb -type: mongodb -description: Provisions a dedicated MongoDB database. -init: | - port: 27017 - randomServiceName: mongo-{{ randAlphaNum 6 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ .State.serviceName }} - port: {{ .Init.port }} - username: {{ .State.username | quote }} - password: {{ .State.password | quote }} - connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.serviceName }}:{{ .Init.port }}/" -volumes: | - {{ .State.serviceName }}-data: - name: {{ .State.serviceName }}-data - driver: local - labels: - dev.score.compose.res.uid: {{ .Uid }} -services: | - {{ .State.serviceName }}: - labels: - dev.score.compose.res.uid: {{ .Uid }} - image: mirror.gcr.io/mongo:8 - restart: always - environment: - MONGO_INITDB_ROOT_USERNAME: {{ .State.username | quote }} - MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | quote }} - healthcheck: - test: ["CMD-SHELL", "echo 'db.runCommand(\"ping\").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD"] - interval: 2s - timeout: 5s - retries: 15 - start_period: 10s - volumes: - - type: volume - source: {{ .State.serviceName }}-data - target: /data/db - volume: - nocopy: true -info_logs: | - - "{{.Uid}}: To connect to mongo: \"docker exec -ti {{ .ComposeProjectName }}-{{ .State.serviceName }}-1 mongosh -u {{ .State.username }} -p {{ .State.password }}\"" -expected_outputs: - - host - - port - - username - - password - - connection \ No newline at end of file +- uri: template://default-provisioners/mongodb + type: mongodb + description: Provisions a dedicated MongoDB database. + init: | + port: 27017 + randomServiceName: mongo-{{ randAlphaNum 6 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.serviceName }} + port: {{ .Init.port }} + username: {{ .State.username | quote }} + password: {{ .State.password | quote }} + connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.serviceName }}:{{ .Init.port }}/" + volumes: | + {{ .State.serviceName }}-data: + name: {{ .State.serviceName }}-data + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} + services: | + {{ .State.serviceName }}: + labels: + dev.score.compose.res.uid: {{ .Uid }} + image: mirror.gcr.io/mongo:8 + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: {{ .State.username | quote }} + MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | quote }} + healthcheck: + test: ["CMD-SHELL", "echo 'db.runCommand(\"ping\").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD"] + interval: 2s + timeout: 5s + retries: 15 + start_period: 10s + volumes: + - type: volume + source: {{ .State.serviceName }}-data + target: /data/db + volume: + nocopy: true + info_logs: | + - "{{.Uid}}: To connect to mongo: \"docker exec -ti {{ .ComposeProjectName }}-{{ .State.serviceName }}-1 mongosh -u {{ .State.username }} -p {{ .State.password }}\"" + expected_outputs: + - host + - port + - username + - password + - connection \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml index bfbdafec..0be30057 100644 --- a/gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/mssql/score-compose/provisioners.yaml @@ -1,54 +1,54 @@ -uri: template://default-provisioners/mssql -type: mssql -description: Provisions a dedicated database on a shared MS SQL server instance. -init: | - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-mssql - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} -state: | - service: {{ .Init.sk }} - database: master - username: sa - password: {{ dig "password" .Init.randomPassword .State | quote }} - publishPort: {{ .Init.publishPort }} -outputs: | - server: {{ .State.service }} - port: {{ .State.publishPort }} - connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};" - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ .State.password }} -volumes: | - {{ .Init.sk }}-data: - driver: local -services: | - {{ .Init.sk }}: - image: mcr.microsoft.com/mssql/server:latest - restart: always - environment: - ACCEPT_EULA: "Y" - MSSQL_ENABLE_HADR: "1" - MSSQL_AGENT_ENABLED: "1" - MSSQL_SA_PASSWORD: {{ .State.password }} +- uri: template://default-provisioners/mssql + type: mssql + description: Provisions a dedicated database on a shared MS SQL server instance. + init: | + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-mssql + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + state: | + service: {{ .Init.sk }} + database: master + username: sa + password: {{ dig "password" .Init.randomPassword .State | quote }} + publishPort: {{ .Init.publishPort }} + outputs: | + server: {{ .State.service }} + port: {{ .State.publishPort }} + connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};" + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} + volumes: | + {{ .Init.sk }}-data: + driver: local + services: | + {{ .Init.sk }}: + image: mcr.microsoft.com/mssql/server:latest + restart: always + environment: + ACCEPT_EULA: "Y" + MSSQL_ENABLE_HADR: "1" + MSSQL_AGENT_ENABLED: "1" + MSSQL_SA_PASSWORD: {{ .State.password }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 1433 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .Init.sk }}-data + target: /var/opt/mssql + info_logs: | + - "{{.Uid}}: To connect to mssql: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mcr.microsoft.com/mssql/server:latest mysql /opt/mssql-tools/bin/sqlcmd -S localhost -U {{ .State.username }} -p {{ .State.password | squote }}\"" {{ if ne .Init.publishPort "0" }} - ports: - - target: 1433 - published: {{ .Init.publishPort }} + - "{{.Uid}}: Or connect your mssql client to \" + Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};\"" {{ end }} - volumes: - - type: volume - source: {{ .Init.sk }}-data - target: /var/opt/mssql -info_logs: | - - "{{.Uid}}: To connect to mssql: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mcr.microsoft.com/mssql/server:latest mysql /opt/mssql-tools/bin/sqlcmd -S localhost -U {{ .State.username }} -p {{ .State.password | squote }}\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your mssql client to \" - Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};\"" - {{ end }} -expected_outputs: - - server - - port - - connection - - database - - username - - password \ No newline at end of file + expected_outputs: + - server + - port + - connection + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml index 1d816f7f..6a22d3b9 100644 --- a/gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/mssql/score-k8s/provisioners.yaml @@ -1,133 +1,133 @@ -uri: template://default-provisioners/mssql -type: mssql -description: Provisions a dedicated database on a shared MS SQL server instance. -init: | - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - service: mssql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - database: master - username: sa - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - server: {{ .State.service }} - port: 1433 - connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }}" - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }} -manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - MSSQL_SA_PASSWORD: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: +- uri: template://default-provisioners/mssql + type: mssql + description: Provisions a dedicated database on a shared MS SQL server instance. + init: | + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: mssql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: master + username: sa + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + server: {{ .State.service }} + port: 1433 + connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }}" + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - containers: - - name: mssql-db - image: mcr.microsoft.com/mssql/server:latest - ports: - - name: mssql - containerPort: 1433 - env: - - name: ACCEPT_EULA - value: "Y" - - name: MSSQL_ENABLE_HADR - value: "1" - - name: MSSQL_AGENT_ENABLED - value: "1" - - name: MSSQL_SA_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MSSQL_SA_PASSWORD - volumeMounts: - - name: mssql - mountPath: "/var/opt/mssql" - volumeClaimTemplates: - - metadata: - name: mssql - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} + data: + MSSQL_SA_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 1433 - targetPort: 1433 -expected_outputs: - - server - - port - - connection - - database - - username - - password \ No newline at end of file + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + containers: + - name: mssql-db + image: mcr.microsoft.com/mssql/server:latest + ports: + - name: mssql + containerPort: 1433 + env: + - name: ACCEPT_EULA + value: "Y" + - name: MSSQL_ENABLE_HADR + value: "1" + - name: MSSQL_AGENT_ENABLED + value: "1" + - name: MSSQL_SA_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MSSQL_SA_PASSWORD + volumeMounts: + - name: mssql + mountPath: "/var/opt/mssql" + volumeClaimTemplates: + - metadata: + name: mssql + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 1433 + targetPort: 1433 + expected_outputs: + - server + - port + - connection + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml index a65f3c6b..d184a6d0 100644 --- a/gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/mysql/score-compose/provisioners.yaml @@ -1,85 +1,85 @@ -uri: template://default-provisioners/mysql -# By default, match all mysql types regardless of class and id. If you want to override this, create another -# provisioner definition with a higher priority. -type: mysql -description: Provisions a dedicated MySQL database on a shared instance. -# Init template has the random service name and password if needed later -init: | - randomServiceName: mysql-{{ randAlphaNum 6 }} - randomDatabase: db{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - randomRootPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-mysql-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} -# The state for each database resource is a unique db name and credentials -state: | - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -# All instances agree on the shared state since there is no concurrency here -shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} - instanceRootPassword: {{ dig .Init.sk "instanceRootPassword" .Init.randomRootPassword .Shared | quote }} -# The outputs are the core database outputs. We output both name and database for broader compatibility. -outputs: | - host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} - port: 3306 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ .State.password }} -# Write out an idempotent create script per database -files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | - CREATE DATABASE IF NOT EXISTS {{ .State.database }}; - USE {{ .State.database }}; - CREATE USER IF NOT EXISTS '{{ .State.username }}'@'localhost' IDENTIFIED BY '{{ .State.password }}'; - GRANT ALL PRIVILEGES ON {{ .State.database }} TO '{{ .State.username }}'@'localhost'; - FLUSH PRIVILEGES; -# Ensure the data volume exists -volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local -# Create an services with the database itself -services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/mysql:8 - restart: always - environment: - MYSQL_DATABASE: {{ .State.database }} - MYSQL_USER: {{ .State.username }} - MYSQL_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} - MYSQL_ROOT_PASSWORD: {{ dig .Init.sk "instanceRootPassword" "" .Shared | quote }} +- uri: template://default-provisioners/mysql + # By default, match all mysql types regardless of class and id. If you want to override this, create another + # provisioner definition with a higher priority. + type: mysql + description: Provisions a dedicated MySQL database on a shared instance. + # Init template has the random service name and password if needed later + init: | + randomServiceName: mysql-{{ randAlphaNum 6 }} + randomDatabase: db{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + randomRootPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-mysql-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + # The state for each database resource is a unique db name and credentials + state: | + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + # All instances agree on the shared state since there is no concurrency here + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + instanceRootPassword: {{ dig .Init.sk "instanceRootPassword" .Init.randomRootPassword .Shared | quote }} + # The outputs are the core database outputs. We output both name and database for broader compatibility. + outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 3306 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} + # Write out an idempotent create script per database + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | + CREATE DATABASE IF NOT EXISTS {{ .State.database }}; + USE {{ .State.database }}; + CREATE USER IF NOT EXISTS '{{ .State.username }}'@'localhost' IDENTIFIED BY '{{ .State.password }}'; + GRANT ALL PRIVILEGES ON {{ .State.database }} TO '{{ .State.username }}'@'localhost'; + FLUSH PRIVILEGES; + # Ensure the data volume exists + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + # Create an services with the database itself + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/mysql:8 + restart: always + environment: + MYSQL_DATABASE: {{ .State.database }} + MYSQL_USER: {{ .State.username }} + MYSQL_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + MYSQL_ROOT_PASSWORD: {{ dig .Init.sk "instanceRootPassword" "" .Shared | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 3306 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /docker-entrypoint-initdb.d/ + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p$${MYSQL_ROOT_PASSWORD}"] + interval: 5s + timeout: 3s + retries: 10 + info_logs: | + - "{{.Uid}}: To connect to mysql, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mysql:8 mysql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -u {{ .State.username }} -p\"" {{ if ne .Init.publishPort "0" }} - ports: - - target: 3306 - published: {{ .Init.publishPort }} + - "{{.Uid}}: Or connect your mysql client to \" + mysql://{{ .State.username }}:{{ .State.password }}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:{{ .Init.publishPort }}/{{ .State.database }}\"" {{ end }} - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts - target: /docker-entrypoint-initdb.d/ - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /var/lib/mysql - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p$${MYSQL_ROOT_PASSWORD}"] - interval: 5s - timeout: 3s - retries: 10 -info_logs: | - - "{{.Uid}}: To connect to mysql, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mysql:8 mysql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -u {{ .State.username }} -p\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your mysql client to \" - mysql://{{ .State.username }}:{{ .State.password }}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:{{ .Init.publishPort }}/{{ .State.database }}\"" - {{ end }} -expected_outputs: - - host - - port - - name - - database - - username - - password \ No newline at end of file + expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml index 129685b2..d56b8e7f 100644 --- a/gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/mysql/score-k8s/provisioners.yaml @@ -1,147 +1,147 @@ -uri: template://default-provisioners/mysql -type: mysql -description: Provisions a dedicated MySQL database on a shared instance. -init: | - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - service: mysql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ .State.service }} - port: 3306 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "MYSQL_PASSWORD" }} -manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - MYSQL_PASSWORD: {{ .State.password | b64enc }} - MYSQL_ROOT_PASSWORD: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: +- uri: template://default-provisioners/mysql + type: mysql + description: Provisions a dedicated MySQL database on a shared instance. + init: | + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: mysql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 3306 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "MYSQL_PASSWORD" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - containers: - - name: mysql-db - image: mirror.gcr.io/mysql:8 - ports: - - name: mysql - containerPort: 3306 - env: - - name: MYSQL_USER - value: {{ .State.username | quote }} - - name: MYSQL_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MYSQL_PASSWORD - - name: MYSQL_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MYSQL_ROOT_PASSWORD - - name: MYSQL_DATABASE - value: {{ .State.database | quote }} - volumeMounts: - - name: data - mountPath: /var/lib/mysql - readinessProbe: - exec: - command: - - mysqladmin - - ping - - -h - - localhost - periodSeconds: 3 - volumeClaimTemplates: - - metadata: - name: data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} + data: + MYSQL_PASSWORD: {{ .State.password | b64enc }} + MYSQL_ROOT_PASSWORD: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 3306 - targetPort: 3306 -expected_outputs: - - host - - port - - name - - database - - username - - password \ No newline at end of file + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + containers: + - name: mysql-db + image: mirror.gcr.io/mysql:8 + ports: + - name: mysql + containerPort: 3306 + env: + - name: MYSQL_USER + value: {{ .State.username | quote }} + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MYSQL_PASSWORD + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + value: {{ .State.database | quote }} + volumeMounts: + - name: data + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: + - mysqladmin + - ping + - -h + - localhost + periodSeconds: 3 + volumeClaimTemplates: + - metadata: + name: data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 3306 + targetPort: 3306 + expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml index 8dd91204..9311c70d 100644 --- a/gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/postgres-instance/score-compose/provisioners.yaml @@ -1,58 +1,58 @@ -uri: template://default-provisioners/postgres-instance +- uri: template://default-provisioners/postgres-instance -type: postgres-instance -description: Provisions a dedicated PostgreSQL instance. -# Init template has the random service name and password if needed later -init: | - randomServiceName: pg-{{ randAlphaNum 6 }} - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-postgres-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} -# The state for each database resource is a unique db name and credentials -state: | - serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} - database: "postgres" - username: "postgres" - password: {{ dig "password" .Init.randomPassword .State | quote }} -# The outputs are the core database outputs. We output both name and database for broader compatibility. -outputs: | - host: {{ .State.serviceName }} - port: 5432 - username: postgres - password: {{ .State.password }} -# Ensure the data volume exists -volumes: | - {{ .State.serviceName }}-data: - driver: local -# Create 2 services, the first is the database itself, the second is the init container which runs the scripts -services: | - {{ .State.serviceName }}: - image: mirror.gcr.io/postgres:17-alpine - restart: always - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: {{ .State.password | quote }} + type: postgres-instance + description: Provisions a dedicated PostgreSQL instance. + # Init template has the random service name and password if needed later + init: | + randomServiceName: pg-{{ randAlphaNum 6 }} + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-postgres-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + # The state for each database resource is a unique db name and credentials + state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + database: "postgres" + username: "postgres" + password: {{ dig "password" .Init.randomPassword .State | quote }} + # The outputs are the core database outputs. We output both name and database for broader compatibility. + outputs: | + host: {{ .State.serviceName }} + port: 5432 + username: postgres + password: {{ .State.password }} + # Ensure the data volume exists + volumes: | + {{ .State.serviceName }}-data: + driver: local + # Create 2 services, the first is the database itself, the second is the init container which runs the scripts + services: | + {{ .State.serviceName }}: + image: mirror.gcr.io/postgres:17-alpine + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: {{ .State.password | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 5432 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ .State.serviceName }}-data + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + + info_logs: | + - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ .State.serviceName }} -U {{ .State.username }} --dbname {{ .State.database }}\"" {{ if ne .Init.publishPort "0" }} - ports: - - target: 5432 - published: {{ .Init.publishPort }} + - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" {{ end }} - volumes: - - type: volume - source: {{ .State.serviceName }}-data - target: /var/lib/postgresql/data - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres"] - -info_logs: | - - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ .State.serviceName }} -U {{ .State.username }} --dbname {{ .State.database }}\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" - {{ end }} -expected_outputs: - - host - - port - - username - - password \ No newline at end of file + expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml index 52302923..8237cca9 100644 --- a/gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/postgres-instance/score-k8s/provisioners.yaml @@ -1,148 +1,148 @@ -uri: template://default-provisioners/postgres-instance -type: postgres-instance -description: Provisions a dedicated PostgreSQL instance. -init: | - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ .State.service }} - port: 5432 - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "password" }} -manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - password: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: +- uri: template://default-provisioners/postgres-instance + type: postgres-instance + description: Provisions a dedicated PostgreSQL instance. + init: | + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 5432 + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: postgres-db - image: mirror.gcr.io/postgres:17-alpine - ports: - - name: postgres - containerPort: 5432 - env: - - name: PGDATA - value: /var/lib/postgresql/data/pgdata - - name: POSTGRES_USER - value: {{ .State.username | quote }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: password - volumeMounts: - - name: pv-data - mountPath: /var/lib/postgresql/data + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: postgres-db + image: mirror.gcr.io/postgres:17-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + - name: POSTGRES_USER + value: {{ .State.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: password + volumeMounts: + - name: pv-data + mountPath: /var/lib/postgresql/data + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .State.username | quote }} + periodSeconds: 3 securityContext: - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - privileged: false - capabilities: - drop: - - ALL - readinessProbe: - exec: - command: - - pg_isready - - -U - - {{ .State.username | quote }} - periodSeconds: 3 - securityContext: - runAsNonRoot: true - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - volumeClaimTemplates: - - metadata: - name: pv-data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 5432 - targetPort: 5432 -expected_outputs: - - host - - port - - username - - password \ No newline at end of file + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: pv-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml index 48935a6c..3d7f8ae5 100644 --- a/gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/postgres/score-compose/provisioners.yaml @@ -1,97 +1,97 @@ -uri: template://default-provisioners/postgres -# By default, match all redis types regardless of class and id. If you want to override this, create another -# provisioner definition with a higher priority. -type: postgres -description: Provisions a dedicated database on a shared PostgreSQL instance. -# Init template has the random service name and password if needed later -init: | - randomServiceName: pg-{{ randAlphaNum 6 }} - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-postgres-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} -# The state for each database resource is a unique db name and credentials -state: | - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -# All instances agree on the shared state since there is no concurrency here -shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} -# The outputs are the core database outputs. We output both name and database for broader compatibility. -outputs: | - host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} - port: 5432 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ .State.password }} -# Write out an idempotent create script per database -files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | - SELECT 'CREATE DATABASE "{{ .State.database }}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{{ .State.database }}')\gexec - SELECT $$CREATE USER "{{ .State.username }}" WITH PASSWORD '{{ .State.password }}'$$ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .State.username }}')\gexec - GRANT ALL PRIVILEGES ON DATABASE "{{ .State.database }}" TO "{{ .State.username }}"; - \connect "{{ .State.database }}"; - GRANT ALL ON SCHEMA public TO "{{ .State.username }}"; -# Ensure the data volume exists -volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local -# Create 2 services, the first is the database itself, the second is the init container which runs the scripts -services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/postgres:17-alpine - restart: always - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} +- uri: template://default-provisioners/postgres + # By default, match all redis types regardless of class and id. If you want to override this, create another + # provisioner definition with a higher priority. + type: postgres + description: Provisions a dedicated database on a shared PostgreSQL instance. + # Init template has the random service name and password if needed later + init: | + randomServiceName: pg-{{ randAlphaNum 6 }} + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-postgres-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} + # The state for each database resource is a unique db name and credentials + state: | + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + # All instances agree on the shared state since there is no concurrency here + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + # The outputs are the core database outputs. We output both name and database for broader compatibility. + outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 5432 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ .State.password }} + # Write out an idempotent create script per database + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | + SELECT 'CREATE DATABASE "{{ .State.database }}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{{ .State.database }}')\gexec + SELECT $$CREATE USER "{{ .State.username }}" WITH PASSWORD '{{ .State.password }}'$$ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .State.username }}')\gexec + GRANT ALL PRIVILEGES ON DATABASE "{{ .State.database }}" TO "{{ .State.username }}"; + \connect "{{ .State.database }}"; + GRANT ALL ON SCHEMA public TO "{{ .State.username }}"; + # Ensure the data volume exists + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + # Create 2 services, the first is the database itself, the second is the init container which runs the scripts + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/postgres:17-alpine + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + {{ if ne .Init.publishPort "0" }} + ports: + - target: 5432 + published: {{ .Init.publishPort }} + {{ end }} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres"] + interval: 2s + timeout: 2s + retries: 15 + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: mirror.gcr.io/postgres:17-alpine + entrypoint: ["/bin/sh"] + environment: + POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + command: + - "-c" + - | + cd /db-scripts + ls db-*.sql | xargs cat | psql "postgresql://postgres:$${POSTGRES_PASSWORD}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:5432/postgres" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /db-scripts + info_logs: | + - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -U {{ .State.username }} --dbname {{ .State.database }}\"" {{ if ne .Init.publishPort "0" }} - ports: - - target: 5432 - published: {{ .Init.publishPort }} + - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" {{ end }} - volumes: - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /var/lib/postgresql/data - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres"] - interval: 2s - timeout: 2s - retries: 15 - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: - image: mirror.gcr.io/postgres:17-alpine - entrypoint: ["/bin/sh"] - environment: - POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} - command: - - "-c" - - | - cd /db-scripts - ls db-*.sql | xargs cat | psql "postgresql://postgres:$${POSTGRES_PASSWORD}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:5432/postgres" - labels: - dev.score.compose.labels.is-init-container: "true" - depends_on: - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - condition: service_healthy - restart: true - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts - target: /db-scripts -info_logs: | - - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -U {{ .State.username }} --dbname {{ .State.database }}\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" - {{ end }} -expected_outputs: - - host - - port - - name - - database - - username - - password \ No newline at end of file + expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml index 75103f05..53a7e10d 100644 --- a/gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/postgres/score-k8s/provisioners.yaml @@ -1,158 +1,158 @@ -uri: template://default-provisioners/postgres -type: postgres -description: Provisions a dedicated database on a shared PostgreSQL instance. -init: | - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ .State.service }} - port: 5432 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "password" }} -manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - password: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: +- uri: template://default-provisioners/postgres + type: postgres + description: Provisions a dedicated database on a shared PostgreSQL instance. + init: | + randomDatabase: db-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + database: {{ dig "database" .Init.randomDatabase .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 5432 + name: {{ .State.database }} + database: {{ .State.database }} + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: postgres-db - image: mirror.gcr.io/postgres:17-alpine - ports: - - name: postgres - containerPort: 5432 - env: - - name: PGDATA - value: /var/lib/postgresql/data/pgdata - - name: POSTGRES_USER - value: {{ .State.username | quote }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: password - - name: POSTGRES_DB - value: {{ .State.database | quote }} - volumeMounts: - - name: pv-data - mountPath: /var/lib/postgresql/data + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: + - name: postgres-db + image: mirror.gcr.io/postgres:17-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + - name: POSTGRES_USER + value: {{ .State.username | quote }} + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .State.service }} + key: password + - name: POSTGRES_DB + value: {{ .State.database | quote }} + volumeMounts: + - name: pv-data + mountPath: /var/lib/postgresql/data + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + readinessProbe: + exec: + command: + - pg_isready + - -U + - {{ .State.username | quote }} + - -d + - {{ .State.database | quote }} + periodSeconds: 3 securityContext: - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - privileged: false - capabilities: - drop: - - ALL - readinessProbe: - exec: - command: - - pg_isready - - -U - - {{ .State.username | quote }} - - -d - - {{ .State.database | quote }} - periodSeconds: 3 - securityContext: - runAsNonRoot: true - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - volumeClaimTemplates: - - metadata: - name: pv-data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 5432 - targetPort: 5432 -expected_outputs: - - host - - port - - name - - database - - username - - password \ No newline at end of file + runAsNonRoot: true + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: pv-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + expected_outputs: + - host + - port + - name + - database + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml index cccf3255..22e3c820 100644 --- a/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/rabbitmq/score-compose/provisioners.yaml @@ -1,92 +1,92 @@ -uri: template://default-provisioners/rabbitmq -type: amqp -description: Provisions a dedicated RabbitMQ vhost on a shared instance. -init: | - randomServiceName: rabbitmq-{{ randAlphaNum 6 }} - randomVHost: vhost-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-rabbitmq - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} - publishManagementPort: {{ dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi }} -state: | - vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} - port: 5672 - vhost: {{ .State.vhost }} - username: {{ .State.username }} - password: {{ .State.password }} -shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instanceErlangCookie: {{ dig .Init.sk "instanceErlangCookie" (randAlpha 20) .Shared }} - {{ $publishPorts := (list) }} - {{ if ne .Init.publishPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 5672 "published" .Init.publishPort)) }}{{ end }} - {{ $x := (dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi) }} - {{ if ne .Init.publishManagementPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 15672 "published" .Init.publishManagementPort)) }}{{ end }} - publishPorts: {{ $publishPorts | toJson }} -volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local -files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.vhost }}.sh: | - while ! rabbitmqctl list_vhosts > /dev/null 2>&1; do - sleep 1 - done - rabbitmqctl list_vhosts | grep {{ .State.vhost }} || rabbitmqctl add_vhost {{ .State.vhost }} - rabbitmqctl list_users | grep {{ .State.username }} || rabbitmqctl add_user {{ .State.username }} {{ .State.password }} - rabbitmqctl set_user_tags {{ .State.username }} administrator - rabbitmqctl set_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" - rabbitmqctl set_topic_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" -services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/rabbitmq:3-management-alpine - restart: always - environment: - RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest - ports: {{ dig .Init.sk "publishPorts" "" .Shared | toJson}} - volumes: - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /var/lib/rabbitmq - healthcheck: - test: ["CMD-SHELL", "rabbitmq-diagnostics -q check_port_connectivity"] - interval: 2s - timeout: 5s - retries: 15 - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: - image: mirror.gcr.io/rabbitmq:3-management-alpine - entrypoint: ["/bin/sh"] - environment: - RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} - command: - - "-c" - - | - set -exu - for s in /db-scripts/*.sh; do source $$s; done - depends_on: - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - condition: service_healthy - restart: true - labels: - dev.score.compose.labels.is-init-container: "true" - network_mode: service:{{ dig .Init.sk "instanceServiceName" "" .Shared }} - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts - target: /db-scripts -info_logs: | - {{ if ne .Init.publishManagementPort 0 }} - - "{{.Uid}}: Browse the rabbitmq UI at \"http://localhost:{{ .Init.publishManagementPort }}\"" - {{ end }} -expected_outputs: - - host - - port - - vhost - - username - - password \ No newline at end of file +- uri: template://default-provisioners/rabbitmq + type: amqp + description: Provisions a dedicated RabbitMQ vhost on a shared instance. + init: | + randomServiceName: rabbitmq-{{ randAlphaNum 6 }} + randomVHost: vhost-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + sk: default-provisioners-rabbitmq + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} + publishManagementPort: {{ dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi }} + state: | + vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} + port: 5672 + vhost: {{ .State.vhost }} + username: {{ .State.username }} + password: {{ .State.password }} + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instanceErlangCookie: {{ dig .Init.sk "instanceErlangCookie" (randAlpha 20) .Shared }} + {{ $publishPorts := (list) }} + {{ if ne .Init.publishPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 5672 "published" .Init.publishPort)) }}{{ end }} + {{ $x := (dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi) }} + {{ if ne .Init.publishManagementPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 15672 "published" .Init.publishManagementPort)) }}{{ end }} + publishPorts: {{ $publishPorts | toJson }} + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.vhost }}.sh: | + while ! rabbitmqctl list_vhosts > /dev/null 2>&1; do + sleep 1 + done + rabbitmqctl list_vhosts | grep {{ .State.vhost }} || rabbitmqctl add_vhost {{ .State.vhost }} + rabbitmqctl list_users | grep {{ .State.username }} || rabbitmqctl add_user {{ .State.username }} {{ .State.password }} + rabbitmqctl set_user_tags {{ .State.username }} administrator + rabbitmqctl set_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" + rabbitmqctl set_topic_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/rabbitmq:3-management-alpine + restart: always + environment: + RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + ports: {{ dig .Init.sk "publishPorts" "" .Shared | toJson}} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /var/lib/rabbitmq + healthcheck: + test: ["CMD-SHELL", "rabbitmq-diagnostics -q check_port_connectivity"] + interval: 2s + timeout: 5s + retries: 15 + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: mirror.gcr.io/rabbitmq:3-management-alpine + entrypoint: ["/bin/sh"] + environment: + RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} + command: + - "-c" + - | + set -exu + for s in /db-scripts/*.sh; do source $$s; done + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + labels: + dev.score.compose.labels.is-init-container: "true" + network_mode: service:{{ dig .Init.sk "instanceServiceName" "" .Shared }} + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts + target: /db-scripts + info_logs: | + {{ if ne .Init.publishManagementPort 0 }} + - "{{.Uid}}: Browse the rabbitmq UI at \"http://localhost:{{ .Init.publishManagementPort }}\"" + {{ end }} + expected_outputs: + - host + - port + - vhost + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml index 97d574fd..ec03c515 100644 --- a/gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/rabbitmq/score-k8s/provisioners.yaml @@ -1,126 +1,126 @@ -uri: template://default-provisioners/rabbitmq -type: amqp -description: Provisions a dedicated RabbitMQ vhost on a shared instance. -init: | - randomVHost: vhost-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - service: rabbitmq-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ .State.service }} - port: 5672 - vhost: {{ .State.vhost }} - username: {{ .State.username }} - password: {{ .State.password }} -manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }}-secret - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }}-secret - app.kubernetes.io/instance: {{ .State.service }} - data: - RABBITMQ_DEFAULT_VHOST: {{ .State.vhost | b64enc }} - RABBITMQ_DEFAULT_USER: {{ .State.username | b64enc }} - RABBITMQ_DEFAULT_PASS: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - serviceName: {{ .State.service }} - replicas: 1 - selector: - matchLabels: +- uri: template://default-provisioners/rabbitmq + type: amqp + description: Provisions a dedicated RabbitMQ vhost on a shared instance. + init: | + randomVHost: vhost-{{ randAlpha 8 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: rabbitmq-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} + username: {{ dig "username" .Init.randomUsername .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 5672 + vhost: {{ .State.vhost }} + username: {{ .State.username }} + password: {{ .State.password }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }}-secret + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }}-secret app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} + data: + RABBITMQ_DEFAULT_VHOST: {{ .State.vhost | b64enc }} + RABBITMQ_DEFAULT_USER: {{ .State.username | b64enc }} + RABBITMQ_DEFAULT_PASS: {{ .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + serviceName: {{ .State.service }} + replicas: 1 + selector: + matchLabels: app.kubernetes.io/instance: {{ .State.service }} - spec: - containers: - - name: rabbitmq - image: mirror.gcr.io/rabbitmq:3-management-alpine - ports: - - name: amqp - containerPort: 5672 - - name: management - containerPort: 15672 - envFrom: - - secretRef: - name: {{ .State.service }}-secret - volumeMounts: - - name: data - mountPath: /var/lib/rabbitmq - readinessProbe: - exec: - command: - - rabbitmq-diagnostics - - -q - - check_port_connectivity - periodSeconds: 3 - initialDelaySeconds: 30 - timeoutSeconds: 5 - volumeClaimTemplates: - - metadata: - name: data + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 3Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - ports: - - port: 5672 - targetPort: 5672 - name: amqp - - port: 15672 - targetPort: 15672 - name: management - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP -expected_outputs: - - host - - port - - vhost - - username - - password \ No newline at end of file + containers: + - name: rabbitmq + image: mirror.gcr.io/rabbitmq:3-management-alpine + ports: + - name: amqp + containerPort: 5672 + - name: management + containerPort: 15672 + envFrom: + - secretRef: + name: {{ .State.service }}-secret + volumeMounts: + - name: data + mountPath: /var/lib/rabbitmq + readinessProbe: + exec: + command: + - rabbitmq-diagnostics + - -q + - check_port_connectivity + periodSeconds: 3 + initialDelaySeconds: 30 + timeoutSeconds: 5 + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 3Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + ports: + - port: 5672 + targetPort: 5672 + name: amqp + - port: 15672 + targetPort: 15672 + name: management + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + expected_outputs: + - host + - port + - vhost + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml index 6c6d9760..4b3fafa2 100644 --- a/gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/redis/score-compose/provisioners.yaml @@ -1,60 +1,60 @@ -uri: template://default-provisioners/redis -# By default, match all redis types regardless of class and id. If you want to override this, create another -# provisioner definition with a higher priority. -type: redis -description: Provisions a dedicated Redis instance. -# Init template has the default port and a random service name and password if needed later -init: | - port: 6379 - randomServiceName: redis-{{ randAlphaNum 6 }} - randomPassword: {{ randAlphaNum 16 | quote }} -# The only state we need to persist is the chosen random service name and password -state: | - serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} -# Return the outputs schema that consumers expect -outputs: | - host: {{ .State.serviceName }} - port: {{ .Init.port }} - username: default - password: {{ .State.password | quote }} -# write the config file to the mounts directory -files: | - {{ .State.serviceName }}/redis.conf: | - requirepass {{ .State.password }} - port {{ .Init.port }} - save 60 1 - loglevel warning -# add a volume for persistence of the redis data -volumes: | - {{ .State.serviceName }}-data: - name: {{ .State.serviceName }}-data - driver: local - labels: - dev.score.compose.res.uid: {{ .Uid }} -# And the redis service itself with volumes bound in -services: | - {{ .State.serviceName }}: - labels: - dev.score.compose.res.uid: {{ .Uid }} - image: mirror.gcr.io/redis:7-alpine - restart: always - entrypoint: ["redis-server"] - command: ["/usr/local/etc/redis/redis.conf"] - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ .State.serviceName }}/redis.conf - target: /usr/local/etc/redis/redis.conf - read_only: true - - type: volume - source: {{ .State.serviceName }}-data - target: /data - volume: - nocopy: true -info_logs: | - - "{{.Uid}}: To connect to redis: \"docker run -it --network {{ .ComposeProjectName }}_default --rm redis redis-cli -h {{ .State.serviceName | squote }} -a {{ .State.password | squote }}\"" -expected_outputs: - - host - - port - - username - - password \ No newline at end of file +- uri: template://default-provisioners/redis + # By default, match all redis types regardless of class and id. If you want to override this, create another + # provisioner definition with a higher priority. + type: redis + description: Provisions a dedicated Redis instance. + # Init template has the default port and a random service name and password if needed later + init: | + port: 6379 + randomServiceName: redis-{{ randAlphaNum 6 }} + randomPassword: {{ randAlphaNum 16 | quote }} + # The only state we need to persist is the chosen random service name and password + state: | + serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} + password: {{ dig "password" .Init.randomPassword .State | quote }} + # Return the outputs schema that consumers expect + outputs: | + host: {{ .State.serviceName }} + port: {{ .Init.port }} + username: default + password: {{ .State.password | quote }} + # write the config file to the mounts directory + files: | + {{ .State.serviceName }}/redis.conf: | + requirepass {{ .State.password }} + port {{ .Init.port }} + save 60 1 + loglevel warning + # add a volume for persistence of the redis data + volumes: | + {{ .State.serviceName }}-data: + name: {{ .State.serviceName }}-data + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} + # And the redis service itself with volumes bound in + services: | + {{ .State.serviceName }}: + labels: + dev.score.compose.res.uid: {{ .Uid }} + image: mirror.gcr.io/redis:7-alpine + restart: always + entrypoint: ["redis-server"] + command: ["/usr/local/etc/redis/redis.conf"] + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ .State.serviceName }}/redis.conf + target: /usr/local/etc/redis/redis.conf + read_only: true + - type: volume + source: {{ .State.serviceName }}-data + target: /data + volume: + nocopy: true + info_logs: | + - "{{.Uid}}: To connect to redis: \"docker run -it --network {{ .ComposeProjectName }}_default --rm redis redis-cli -h {{ .State.serviceName | squote }} -a {{ .State.password | squote }}\"" + expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml index da0920f3..1bead531 100644 --- a/gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/redis/score-k8s/provisioners.yaml @@ -1,147 +1,147 @@ -uri: template://default-provisioners/redis -type: redis -description: Provisions a dedicated Redis instance. -init: | - randomPassword: {{ randAlphaNum 16 | quote }} -state: | - service: redis-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - username: default - password: {{ dig "password" .Init.randomPassword .State | quote }} -outputs: | - host: {{ .State.service }} - port: 6379 - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "password" }} -manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - password: {{ .State.password | b64enc }} - redis.conf: {{ printf "requirepass %s\nport 6379\nsave 60 1\nloglevel warning\n" .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: +- uri: template://default-provisioners/redis + type: redis + description: Provisions a dedicated Redis instance. + init: | + randomPassword: {{ randAlphaNum 16 | quote }} + state: | + service: redis-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + username: default + password: {{ dig "password" .Init.randomPassword .State | quote }} + outputs: | + host: {{ .State.service }} + port: 6379 + username: {{ .State.username }} + password: {{ encodeSecretRef .State.service "password" }} + manifests: | + - apiVersion: v1 + kind: Secret + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} + data: + password: {{ .State.password | b64enc }} + redis.conf: {{ printf "requirepass %s\nport 6379\nsave 60 1\nloglevel warning\n" .State.password | b64enc }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + replicas: 1 + serviceName: {{ .State.service }} + selector: + matchLabels: app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: redis - image: mirror.gcr.io/redis:7-alpine - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - privileged: false - readOnlyRootFilesystem: true - ports: + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + spec: + automountServiceAccountToken: false + containers: - name: redis - containerPort: 6379 - volumeMounts: - - name: redis-data - mountPath: /data + image: mirror.gcr.io/redis:7-alpine + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + ports: + - name: redis + containerPort: 6379 + volumeMounts: + - name: redis-data + mountPath: /data + - name: config + mountPath: /usr/local/etc/redis + readinessProbe: + exec: + command: + - redis-cli + - ping + periodSeconds: 3 + securityContext: + fsGroup: 1000 + runAsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + volumes: - name: config - mountPath: /usr/local/etc/redis - readinessProbe: - exec: - command: - - redis-cli - - ping - periodSeconds: 3 - securityContext: - fsGroup: 1000 - runAsGroup: 1000 - runAsNonRoot: true - runAsUser: 1000 - seccompProfile: - type: RuntimeDefault - volumes: - - name: config - secret: - secretName: {{ .State.service }} - items: - - key: redis.conf - path: redis.conf - volumeClaimTemplates: - - metadata: - name: redis-data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 6379 - targetPort: 6379 -expected_outputs: - - host - - port - - username - - password \ No newline at end of file + secret: + secretName: {{ .State.service }} + items: + - key: redis.conf + path: redis.conf + volumeClaimTemplates: + - metadata: + name: redis-data + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Service + metadata: + name: {{ .State.service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.service }} + app.kubernetes.io/instance: {{ .State.service }} + spec: + selector: + app.kubernetes.io/instance: {{ .State.service }} + type: ClusterIP + ports: + - port: 6379 + targetPort: 6379 + expected_outputs: + - host + - port + - username + - password \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml index 5b7d48c2..ab76ab91 100644 --- a/gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/route/score-compose/provisioners.yaml @@ -1,104 +1,104 @@ -uri: template://default-provisioners/route -type: route -description: Provisions a ingress route on a shared Nginx instance. -init: | - randomServiceName: routing-{{ randAlphaNum 6 }} - sk: default-provisioners-routing-instance - {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} - {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} - {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} - {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} - {{ $port := index $ports (print .Params.port) }} - {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} - -shared: | - {{ .Init.sk }}: - instancePort: {{ dig .Init.sk "instancePort" 8080 .Shared }} - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - {{ $targetHost := (index .WorkloadServices .SourceWorkload).ServiceName }} +- uri: template://default-provisioners/route + type: route + description: Provisions a ingress route on a shared Nginx instance. + init: | + randomServiceName: routing-{{ randAlphaNum 6 }} + sk: default-provisioners-routing-instance + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} {{ $port := index $ports (print .Params.port) }} - {{ $targetPort := $port.TargetPort }} - {{ $target := (printf "%s:%d" $targetHost $targetPort) }} - {{ $hBefore := dig .Init.sk "hosts" (dict) .Shared }} - {{ $rBefore := dig .Params.host (dict) $hBefore }} - {{ $pathType := dig "compose.score.dev/route-provisioner-path-type" "Prefix" (dig "annotations" (dict) (.Metadata | default (dict))) }} - {{ $inner := dict "path" .Params.path "target" $target "port" $targetPort "path_type" $pathType }} - {{ $rAfter := (merge $rBefore (dict .Uid $inner)) }} - {{ $hAfter := (merge $hBefore (dict .Params.host $rAfter)) }} - hosts: {{ $hAfter | toRawJson }} -files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf: | - worker_processes 1; - worker_rlimit_nofile 8192; - events { - worker_connections 4096; - } - http { - resolver 127.0.0.11; + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} - {{ range $h, $r := (dig .Init.sk "hosts" "" .Shared) }} - server { - listen 80; - listen [::]:80; - server_name {{ $h }}; + shared: | + {{ .Init.sk }}: + instancePort: {{ dig .Init.sk "instancePort" 8080 .Shared }} + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + {{ $targetHost := (index .WorkloadServices .SourceWorkload).ServiceName }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ $port := index $ports (print .Params.port) }} + {{ $targetPort := $port.TargetPort }} + {{ $target := (printf "%s:%d" $targetHost $targetPort) }} + {{ $hBefore := dig .Init.sk "hosts" (dict) .Shared }} + {{ $rBefore := dig .Params.host (dict) $hBefore }} + {{ $pathType := dig "compose.score.dev/route-provisioner-path-type" "Prefix" (dig "annotations" (dict) (.Metadata | default (dict))) }} + {{ $inner := dict "path" .Params.path "target" $target "port" $targetPort "path_type" $pathType }} + {{ $rAfter := (merge $rBefore (dict .Uid $inner)) }} + {{ $hAfter := (merge $hBefore (dict .Params.host $rAfter)) }} + hosts: {{ $hAfter | toRawJson }} + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf: | + worker_processes 1; + worker_rlimit_nofile 8192; + events { + worker_connections 4096; + } + http { + resolver 127.0.0.11; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; - proxy_set_header Proxy ""; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - proxy_buffers 16 4k; - proxy_buffer_size 2k; - client_max_body_size 10m; - {{ dig "compose.score.dev/route-provisioner-server-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 4 }} + {{ range $h, $r := (dig .Init.sk "hosts" "" .Shared) }} + server { + listen 80; + listen [::]:80; + server_name {{ $h }}; - location = /favicon.ico { - return 204; - access_log off; - log_not_found off; - } + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; + proxy_set_header Proxy ""; + proxy_connect_timeout 5s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + proxy_buffers 16 4k; + proxy_buffer_size 2k; + client_max_body_size 10m; + {{ dig "compose.score.dev/route-provisioner-server-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 4 }} - {{ range $k, $v := $r }} - # the basic path variant, "/" or "/one/two" - location ~ ^{{ index $v "path" }}$ { - set $backend {{ index $v "target" }}; - proxy_pass http://$backend; - {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} - } + location = /favicon.ico { + return 204; + access_log off; + log_not_found off; + } + + {{ range $k, $v := $r }} + # the basic path variant, "/" or "/one/two" + location ~ ^{{ index $v "path" }}$ { + set $backend {{ index $v "target" }}; + proxy_pass http://$backend; + {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} + } - # The prefix match variants are included by default but can be excluded via 'compose.score.dev/route-provisioner-path-type' annotation - {{ if eq (index $v "path_type") "Prefix" }} - location ~ ^{{ index $v "path" | trimSuffix "/" }}/.* { - set $backend {{ index $v "target" }}; - proxy_pass http://$backend; - {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} + # The prefix match variants are included by default but can be excluded via 'compose.score.dev/route-provisioner-path-type' annotation + {{ if eq (index $v "path_type") "Prefix" }} + location ~ ^{{ index $v "path" | trimSuffix "/" }}/.* { + set $backend {{ index $v "target" }}; + proxy_pass http://$backend; + {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} + } + {{ end }} + {{ end }} } {{ end }} - {{ end }} } - {{ end }} - } -services: | - {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/nginx:1-alpine - restart: always - ports: - - published: {{ $p }} - target: 80 - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf - target: /etc/nginx/nginx.conf - readOnly: true -info_logs: | - {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} - - "{{.Uid}}: To connect to this route, http://{{ .Params.host }}:{{ $p }}{{ .Params.path }} (make sure {{ .Params.host }} resolves to localhost)" -supported_params: - - host - - port - - path \ No newline at end of file + services: | + {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: mirror.gcr.io/nginx:1-alpine + restart: always + ports: + - published: {{ $p }} + target: 80 + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf + target: /etc/nginx/nginx.conf + readOnly: true + info_logs: | + {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} + - "{{.Uid}}: To connect to this route, http://{{ .Params.host }}:{{ $p }}{{ .Params.path }} (make sure {{ .Params.host }} resolves to localhost)" + supported_params: + - host + - port + - path \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml index cae025b6..34bc23a5 100644 --- a/gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/route/score-k8s/provisioners.yaml @@ -1,45 +1,45 @@ -uri: template://default-provisioners/route -type: route -description: Provisions an HTTPRoute on a shared Nginx instance. -init: | - {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} - {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} - {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} - {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} - {{ $port := index $ports (print .Params.port) }} - {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} -state: | - routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} -manifests: | - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: {{ .State.routeName }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.routeName }} - app.kubernetes.io/instance: {{ .State.routeName }} - spec: - parentRefs: - - name: default - hostnames: - - {{ .Params.host | quote }} - rules: - - matches: - - path: - type: PathPrefix - value: {{ .Params.path | quote }} - backendRefs: - - name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} - port: {{ .Params.port }} -supported_params: - - host - - port - - path \ No newline at end of file +- uri: template://default-provisioners/route + type: route + description: Provisions an HTTPRoute on a shared Nginx instance. + init: | + {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} + {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} + {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} + {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} + {{ $port := index $ports (print .Params.port) }} + {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} + state: | + routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} + manifests: | + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: {{ .State.routeName }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + annotations: + k8s.score.dev/source-workload: {{ .SourceWorkload }} + k8s.score.dev/resource-uid: {{ .Uid }} + k8s.score.dev/resource-guid: {{ .Guid }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ .State.routeName }} + app.kubernetes.io/instance: {{ .State.routeName }} + spec: + parentRefs: + - name: default + hostnames: + - {{ .Params.host | quote }} + rules: + - matches: + - path: + type: PathPrefix + value: {{ .Params.path | quote }} + backendRefs: + - name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} + port: {{ .Params.port }} + supported_params: + - host + - port + - path \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml index b19ee83d..92426a9e 100644 --- a/gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/s3/score-compose/provisioners.yaml @@ -1,100 +1,100 @@ -uri: template://default-provisioners/s3 -type: s3 -description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. -# The init template contains some initial seed data that can be used it needed. -init: | - randomServiceName: minio-{{ randAlphaNum 6 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - randomBucket: bucket-{{ randAlpha 8 | lower }} - randomAccessKeyId: {{ randAlphaNum 20 | quote }} - randomSecretKey: {{ randAlphaNum 40 | quote }} - sk: default-provisioners-minio-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} -# The only instance state is the bucket name, for now we provision a single aws key across s3 resources. -state: | - bucket: {{ dig "bucket" .Init.randomBucket .State | quote }} -# The shared state contains the chosen service name and credentials -shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instanceUsername: {{ dig .Init.sk "instanceUsername" .Init.randomUsername .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} - instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" .Init.randomAccessKeyId .Shared | quote }} - instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" .Init.randomSecretKey .Shared | quote }} - publishPort: {{ with (dig .Init.sk "publishPort" 0 .Shared) }}{{ if ne . 0 }}{{ . }}{{ else }}{{ $.Init.publishPort }}{{ end }}{{ end }} -# the outputs that we can expose -outputs: | - bucket: {{ .State.bucket }} - access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} - secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} - endpoint: http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 - # for compatibility with Humanitec's existing s3 resource - region: "us-east-1" - aws_access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} - aws_secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} -# we store 2 files, 1 is always the same and overridden, the other is per bucket -files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/00-svcacct.sh: | - set -eu - mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} - mc admin user svcacct info myminio {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} || mc admin user svcacct add myminio {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} --access-key {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} --secret-key {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/10-bucket-{{ .State.bucket }}.sh: | - set -eu - mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} - mc mb -p myminio/{{ .State.bucket }} -volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local -# 2 services, the minio one, and the init container which ensures the service account and buckets exist -services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: quay.io/minio/minio - command: ["server", "/data", "--console-address", ":9001"] - restart: always +- uri: template://default-provisioners/s3 + type: s3 + description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. + # The init template contains some initial seed data that can be used it needed. + init: | + randomServiceName: minio-{{ randAlphaNum 6 }} + randomUsername: user-{{ randAlpha 8 }} + randomPassword: {{ randAlphaNum 16 | quote }} + randomBucket: bucket-{{ randAlpha 8 | lower }} + randomAccessKeyId: {{ randAlphaNum 20 | quote }} + randomSecretKey: {{ randAlphaNum 40 | quote }} + sk: default-provisioners-minio-instance + publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} + # The only instance state is the bucket name, for now we provision a single aws key across s3 resources. + state: | + bucket: {{ dig "bucket" .Init.randomBucket .State | quote }} + # The shared state contains the chosen service name and credentials + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} + instanceUsername: {{ dig .Init.sk "instanceUsername" .Init.randomUsername .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} + instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" .Init.randomAccessKeyId .Shared | quote }} + instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" .Init.randomSecretKey .Shared | quote }} + publishPort: {{ with (dig .Init.sk "publishPort" 0 .Shared) }}{{ if ne . 0 }}{{ . }}{{ else }}{{ $.Init.publishPort }}{{ end }}{{ end }} + # the outputs that we can expose + outputs: | + bucket: {{ .State.bucket }} + access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} + secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + endpoint: http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 + # for compatibility with Humanitec's existing s3 resource + region: "us-east-1" + aws_access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} + aws_secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + # we store 2 files, 1 is always the same and overridden, the other is per bucket + files: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/00-svcacct.sh: | + set -eu + mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} + mc admin user svcacct info myminio {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} || mc admin user svcacct add myminio {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} --access-key {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} --secret-key {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/10-bucket-{{ .State.bucket }}.sh: | + set -eu + mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} + mc mb -p myminio/{{ .State.bucket }} + volumes: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: + driver: local + # 2 services, the minio one, and the init container which ensures the service account and buckets exist + services: | + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + image: quay.io/minio/minio + command: ["server", "/data", "--console-address", ":9001"] + restart: always + {{ if ne .Init.publishPort 0 }} + ports: + - target: 9001 + published: {{ .Init.publishPort }} + {{ end }} + healthcheck: + test: ["CMD-SHELL", "mc alias set myminio http://localhost:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }}"] + interval: 2s + timeout: 2s + retries: 15 + environment: + MINIO_ROOT_USER: {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} + MINIO_ROOT_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} + volumes: + - type: volume + source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data + target: /data + {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: + image: quay.io/minio/minio + entrypoint: ["/bin/bash"] + command: + - "-c" + - "for s in $$(ls /setup-scripts -1); do sh /setup-scripts/$$s; done" + labels: + dev.score.compose.labels.is-init-container: "true" + depends_on: + {{ dig .Init.sk "instanceServiceName" "" .Shared }}: + condition: service_healthy + restart: true + volumes: + - type: bind + source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts + target: /setup-scripts + info_logs: | + - "{{.Uid}}: To connect with a minio client: use the myminio alias at \"docker run -it --network {{ .ComposeProjectName }}_default --rm --entrypoint /bin/bash quay.io/minio/minio -c 'mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceAccessKeyId" "" .Shared }} {{ dig .Init.sk "instanceSecretKey" "" .Shared }}; bash'\"" {{ if ne .Init.publishPort 0 }} - ports: - - target: 9001 - published: {{ .Init.publishPort }} + - "{{.Uid}}: Or enter {{ dig .Init.sk "instanceUsername" "" .Shared }} / {{ dig .Init.sk "instancePassword" "" .Shared }} at https://localhost:{{ .Init.publishPort }}" {{ end }} - healthcheck: - test: ["CMD-SHELL", "mc alias set myminio http://localhost:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }}"] - interval: 2s - timeout: 2s - retries: 15 - environment: - MINIO_ROOT_USER: {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} - MINIO_ROOT_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} - volumes: - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /data - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: - image: quay.io/minio/minio - entrypoint: ["/bin/bash"] - command: - - "-c" - - "for s in $$(ls /setup-scripts -1); do sh /setup-scripts/$$s; done" - labels: - dev.score.compose.labels.is-init-container: "true" - depends_on: - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - condition: service_healthy - restart: true - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts - target: /setup-scripts -info_logs: | - - "{{.Uid}}: To connect with a minio client: use the myminio alias at \"docker run -it --network {{ .ComposeProjectName }}_default --rm --entrypoint /bin/bash quay.io/minio/minio -c 'mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceAccessKeyId" "" .Shared }} {{ dig .Init.sk "instanceSecretKey" "" .Shared }}; bash'\"" - {{ if ne .Init.publishPort 0 }} - - "{{.Uid}}: Or enter {{ dig .Init.sk "instanceUsername" "" .Shared }} / {{ dig .Init.sk "instancePassword" "" .Shared }} at https://localhost:{{ .Init.publishPort }}" - {{ end }} -expected_outputs: - - bucket - - access_key_id - - secret_key - - endpoint - - region - - aws_access_key_id - - aws_secret_key \ No newline at end of file + expected_outputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml index afc42b05..0b9aa87d 100644 --- a/gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/s3/score-k8s/provisioners.yaml @@ -1,180 +1,180 @@ -uri: template://default-provisioners/s3 -type: s3 -description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. -# The init template contains some initial seed data that can be used t needed. -init: | - sk: default-provisioners-minio-instance -state: | - bucket: {{ dig "bucket" (printf "bucket-%s" .Guid) .State | quote }} -shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" (randAlpha 7 | lower | printf "minio-%s") .Shared | quote }} - instanceUsername: {{ dig .Init.sk "instanceUsername" (randAlpha 7 | printf "user-%s") .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" (randAlphaNum 16) .Shared | quote }} - instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" (randAlphaNum 20) .Shared | quote }} - instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" (randAlphaNum 40) .Shared | quote }} -outputs: | - {{ $shared := dig .Init.sk (dict) .Shared }} - {{ $service := $shared.instanceServiceName }} - bucket: {{ .State.bucket }} - access_key_id: {{ $shared.instanceAccessKeyId | quote }} - secret_key: {{ encodeSecretRef $service "secret_key" }} - endpoint: http://{{ $service }}:9000 - region: "us-east-1" - # for compatibility with Humanitec's existing s3 resource - aws_access_key_id: {{ $shared.instanceAccessKeyId | quote }} - aws_secret_key: {{ encodeSecretRef $service "secret_key" }} -manifests: | - {{ $shared := dig .Init.sk (dict) .Shared }} - {{ $service := $shared.instanceServiceName }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ $service | quote }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service | quote }} - app.kubernetes.io/instance: {{ $service | quote }} - spec: - replicas: 1 - serviceName: {{ $service | quote }} - selector: - matchLabels: +- uri: template://default-provisioners/s3 + type: s3 + description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. + # The init template contains some initial seed data that can be used t needed. + init: | + sk: default-provisioners-minio-instance + state: | + bucket: {{ dig "bucket" (printf "bucket-%s" .Guid) .State | quote }} + shared: | + {{ .Init.sk }}: + instanceServiceName: {{ dig .Init.sk "instanceServiceName" (randAlpha 7 | lower | printf "minio-%s") .Shared | quote }} + instanceUsername: {{ dig .Init.sk "instanceUsername" (randAlpha 7 | printf "user-%s") .Shared | quote }} + instancePassword: {{ dig .Init.sk "instancePassword" (randAlphaNum 16) .Shared | quote }} + instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" (randAlphaNum 20) .Shared | quote }} + instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" (randAlphaNum 40) .Shared | quote }} + outputs: | + {{ $shared := dig .Init.sk (dict) .Shared }} + {{ $service := $shared.instanceServiceName }} + bucket: {{ .State.bucket }} + access_key_id: {{ $shared.instanceAccessKeyId | quote }} + secret_key: {{ encodeSecretRef $service "secret_key" }} + endpoint: http://{{ $service }}:9000 + region: "us-east-1" + # for compatibility with Humanitec's existing s3 resource + aws_access_key_id: {{ $shared.instanceAccessKeyId | quote }} + aws_secret_key: {{ encodeSecretRef $service "secret_key" }} + manifests: | + {{ $shared := dig .Init.sk (dict) .Shared }} + {{ $service := $shared.instanceServiceName }} + - apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: {{ $service | quote }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} app.kubernetes.io/instance: {{ $service | quote }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service | quote }} + spec: + replicas: 1 + serviceName: {{ $service | quote }} + selector: + matchLabels: app.kubernetes.io/instance: {{ $service | quote }} - spec: - automountServiceAccountToken: false - containers: - - name: minio - image: quay.io/minio/minio - args: ["server", "/data", "--console-address", ":9001"] - ports: - - name: service - containerPort: 9000 - - name: console - containerPort: 9001 - env: - - name: MINIO_ROOT_USER - value: {{ $shared.instanceUsername | quote }} - - name: MINIO_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ $service | quote }} - key: password + template: + metadata: + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + automountServiceAccountToken: false + containers: + - name: minio + image: quay.io/minio/minio + args: ["server", "/data", "--console-address", ":9001"] + ports: + - name: service + containerPort: 9000 + - name: console + containerPort: 9001 + env: + - name: MINIO_ROOT_USER + value: {{ $shared.instanceUsername | quote }} + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: password + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + allowPrivilegeEscalation: false + privileged: false + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data securityContext: + runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 - allowPrivilegeEscalation: false - privileged: false - capabilities: - drop: - - ALL - volumeMounts: - - name: data - mountPath: /data - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - volumeClaimTemplates: - - metadata: - name: data - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service | quote }} - app.kubernetes.io/instance: {{ $service | quote }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Secret - metadata: - name: {{ $service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service }} - app.kubernetes.io/instance: {{ $service }} - data: - password: {{ $shared.instancePassword | b64enc }} - secret_key: {{ $shared.instanceSecretKey | b64enc }} - - apiVersion: v1 - kind: Service - metadata: - name: {{ $service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service }} - app.kubernetes.io/instance: {{ $service }} - spec: - selector: - app.kubernetes.io/instance: {{ $service }} - type: ClusterIP - ports: - - name: service - port: 9000 - targetPort: 9000 - - name: console - port: 9001 - targetPort: 9001 - - apiVersion: batch/v1 - kind: Job - metadata: - name: {{ printf "%s-bucket-%s" $service .Guid }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - spec: - template: - spec: - restartPolicy: OnFailure - containers: - - name: main - image: quay.io/minio/minio - command: - - /bin/bash - - -c - - | - set -eu - mc alias set myminio http://{{ $service }}:9000 {{ $shared.instanceUsername | quote }} $MINIO_ROOT_PASSWORD - mc admin user svcacct info myminio {{ $shared.instanceAccessKeyId | quote }} || mc admin user svcacct add myminio {{ $shared.instanceUsername | quote }} --access-key {{ $shared.instanceAccessKeyId | quote }} --secret-key $MINIO_SECRET_KEY - mc mb -p myminio/{{ .State.bucket }} - env: - - name: MINIO_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ $service | quote }} - key: password - - name: MINIO_SECRET_KEY - valueFrom: - secretKeyRef: - name: {{ $service | quote }} - key: secret_key -expected_outputs: - - bucket - - access_key_id - - secret_key - - endpoint - - region - - aws_access_key_id - - aws_secret_key \ No newline at end of file + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + volumeClaimTemplates: + - metadata: + name: data + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service | quote }} + app.kubernetes.io/instance: {{ $service | quote }} + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 1Gi + - apiVersion: v1 + kind: Secret + metadata: + name: {{ $service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service }} + app.kubernetes.io/instance: {{ $service }} + data: + password: {{ $shared.instancePassword | b64enc }} + secret_key: {{ $shared.instanceSecretKey | b64enc }} + - apiVersion: v1 + kind: Service + metadata: + name: {{ $service }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + app.kubernetes.io/name: {{ $service }} + app.kubernetes.io/instance: {{ $service }} + spec: + selector: + app.kubernetes.io/instance: {{ $service }} + type: ClusterIP + ports: + - name: service + port: 9000 + targetPort: 9000 + - name: console + port: 9001 + targetPort: 9001 + - apiVersion: batch/v1 + kind: Job + metadata: + name: {{ printf "%s-bucket-%s" $service .Guid }} + {{ if ne .Namespace "" }} + namespace: {{ .Namespace }} + {{ end }} + labels: + app.kubernetes.io/managed-by: score-k8s + spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: main + image: quay.io/minio/minio + command: + - /bin/bash + - -c + - | + set -eu + mc alias set myminio http://{{ $service }}:9000 {{ $shared.instanceUsername | quote }} $MINIO_ROOT_PASSWORD + mc admin user svcacct info myminio {{ $shared.instanceAccessKeyId | quote }} || mc admin user svcacct add myminio {{ $shared.instanceUsername | quote }} --access-key {{ $shared.instanceAccessKeyId | quote }} --secret-key $MINIO_SECRET_KEY + mc mb -p myminio/{{ .State.bucket }} + env: + - name: MINIO_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: password + - name: MINIO_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ $service | quote }} + key: secret_key + expected_outputs: + - bucket + - access_key_id + - secret_key + - endpoint + - region + - aws_access_key_id + - aws_secret_key \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml index 4ac4bf32..d26642fd 100644 --- a/gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/service-port/score-compose/provisioners.yaml @@ -1,18 +1,18 @@ -uri: template://default-provisioners/service-port -type: service-port -description: Outputs a hostname and port for connecting to another workload. -outputs: | - {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} - {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} - {{ $w := (index .WorkloadServices .Params.workload) }} - {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} - {{ $p := (index $w.Ports .Params.port) }} - {{ if not $p }}{{ fail "unknown service port" }}{{ end }} - hostname: {{ $w.ServiceName | quote }} - port: {{ $p.TargetPort }} -expected_outputs: - - hostname - - port -supported_params: - - workload - - port \ No newline at end of file +- uri: template://default-provisioners/service-port + type: service-port + description: Outputs a hostname and port for connecting to another workload. + outputs: | + {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} + {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} + {{ $w := (index .WorkloadServices .Params.workload) }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + {{ $p := (index $w.Ports .Params.port) }} + {{ if not $p }}{{ fail "unknown service port" }}{{ end }} + hostname: {{ $w.ServiceName | quote }} + port: {{ $p.TargetPort }} + expected_outputs: + - hostname + - port + supported_params: + - workload + - port \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml index 71be2eaf..95f500ed 100644 --- a/gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/service-port/score-k8s/provisioners.yaml @@ -1,18 +1,18 @@ -uri: template://default-provisioners/service-port -type: service-port -description: Outputs a hostname and port for connecting to another workload. -outputs: | - {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} - {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} - {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} - {{ if not $w }}{{ fail "unknown workload" }}{{ end }} - {{ $p := (index $w.Ports .Params.port) }} - {{ if not $p }}{{ fail "unknown service port" }}{{ end }} - hostname: {{ $w.ServiceName | quote }} - port: {{ $p.TargetPort }} -expected_outputs: - - hostname - - port -supported_params: - - workload - - port \ No newline at end of file +- uri: template://default-provisioners/service-port + type: service-port + description: Outputs a hostname and port for connecting to another workload. + outputs: | + {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} + {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} + {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} + {{ if not $w }}{{ fail "unknown workload" }}{{ end }} + {{ $p := (index $w.Ports .Params.port) }} + {{ if not $p }}{{ fail "unknown service port" }}{{ end }} + hostname: {{ $w.ServiceName | quote }} + port: {{ $p.TargetPort }} + expected_outputs: + - hostname + - port + supported_params: + - workload + - port \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml b/gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml index b4209539..0f441ccb 100644 --- a/gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/volume/score-compose/provisioners.yaml @@ -1,25 +1,25 @@ -uri: template://default-provisioners/volume -# By default, match all classes and ids of volume. If you want to override this, create another provisioner definition -# with a higher priority. -type: volume -description: Creates a persistent volume that can be mounted on a workload. -init: | - randomVolumeName: {{ .Id | replace "." "-" }}-{{ randAlphaNum 6 }} -# Store the random volume name if we haven't chosen one yet, otherwise use the one that exists already -state: | - name: {{ dig "name" .Init.randomVolumeName .State }} -# Return a source value with the volume name. This can be used in volume resource references now. -outputs: | +- uri: template://default-provisioners/volume + # By default, match all classes and ids of volume. If you want to override this, create another provisioner definition + # with a higher priority. type: volume - source: {{ .State.name }} -# Add a volume to the docker compose file. We assume our name is unique here. We also apply a label to help ensure -# that we can track the volume back to the workload and resource that created it. -volumes: | - {{ .State.name }}: - name: {{ .State.name }} - driver: local - labels: - dev.score.compose.res.uid: {{ .Uid }} -expected_outputs: - - source - - type \ No newline at end of file + description: Creates a persistent volume that can be mounted on a workload. + init: | + randomVolumeName: {{ .Id | replace "." "-" }}-{{ randAlphaNum 6 }} + # Store the random volume name if we haven't chosen one yet, otherwise use the one that exists already + state: | + name: {{ dig "name" .Init.randomVolumeName .State }} + # Return a source value with the volume name. This can be used in volume resource references now. + outputs: | + type: volume + source: {{ .State.name }} + # Add a volume to the docker compose file. We assume our name is unique here. We also apply a label to help ensure + # that we can track the volume back to the workload and resource that created it. + volumes: | + {{ .State.name }}: + name: {{ .State.name }} + driver: local + labels: + dev.score.compose.res.uid: {{ .Uid }} + expected_outputs: + - source + - type \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml b/gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml index a086fbad..f3bb17c5 100644 --- a/gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml +++ b/gen/external-content/resource-provisioners/default/volume/score-k8s/provisioners.yaml @@ -1,8 +1,8 @@ -uri: template://default-provisioners/volume -type: volume -description: Creates a persistent volume that can be mounted on a workload. -outputs: | - source: - emptyDir: {} -expected_outputs: - - source \ No newline at end of file +- uri: template://default-provisioners/volume + type: volume + description: Creates a persistent volume that can be mounted on a workload. + outputs: | + source: + emptyDir: {} + expected_outputs: + - source \ No newline at end of file From 1622846fae970c36f2d7bb3a496d36423c4b9202 Mon Sep 17 00:00:00 2001 From: Santiago Beroch Date: Wed, 22 Oct 2025 14:52:03 -0300 Subject: [PATCH 5/7] filters, list and single view Signed-off-by: Santiago Beroch --- config.toml | 2 +- content/en/examples/patch-templates/_index.md | 2 +- .../score-k8s/knative-service.md | 12 - .../template/redis-dapr-pubsub.md | 4 + .../template/rabbitmq-dapr-pubsub.md | 4 + .../score-k8s/template/redis-dapr-pubsub.md | 4 + .../template/redis-dapr-state-store.md | 4 + .../score-k8s/template/redis.md | 4 + .../template/dapr-subscription.md | 4 + .../score-k8s/template/dapr-subscription.md | 4 + .../dns/score-compose/cmd/dns-in-codespace.md | 4 + .../score-compose/template/dns-with-route.md | 4 + .../dns/score-k8s/cmd/dns-in-codespace.md | 4 + .../dns/score-k8s/template/dns-with-url.md | 4 + .../environment/score-compose/cmd/dotenv.md | 4 + .../environment/score-k8s/cmd/dotenv.md | 4 + .../score-compose/template/empty-hpa.md | 4 + .../score-k8s/template/default-hpa.md | 4 + .../score-k8s/cmd/helm-template-redis.md | 4 + .../redis/score-k8s/cmd/helm-upgrade-redis.md | 4 + .../route/score-k8s/template/ingress-route.md | 4 + .../template/ingress-with-net-pol-route.md | 4 + .../route-with-shared-gateway-with-netpol.md | 4 + .../template/route-with-shared-gateway.md | 4 + .../score-compose/template/static-service.md | 4 + .../template/static-service-with-netpol.md | 4 + .../score-k8s/template/static-service.md | 4 + .../default/dns/score-compose/template/dns.md | 4 + .../default/dns/score-k8s/template/dns.md | 4 + .../score-compose/template/elasticsearch.md | 4 + .../score-k8s/cmd/example-provisioner.md | 4 + .../score-compose/template/kafka-topic.md | 4 + .../default/mongo/score-k8s/template/mongo.md | 4 + .../mongodb/score-compose/template/mongodb.md | 4 + .../mssql/score-compose/template/mssql.md | 4 + .../default/mssql/score-k8s/template/mssql.md | 4 + .../mysql/score-compose/template/mysql.md | 4 + .../default/mysql/score-k8s/template/mysql.md | 4 + .../template/postgres-instance.md | 4 + .../score-k8s/template/postgres-instance.md | 4 + .../score-compose/template/postgres.md | 4 + .../postgres/score-k8s/template/postgres.md | 4 + .../score-compose/template/rabbitmq.md | 4 + .../rabbitmq/score-k8s/template/rabbitmq.md | 4 + .../redis/score-compose/template/redis.md | 4 + .../default/redis/score-k8s/template/redis.md | 4 + .../route/score-compose/template/route.md | 4 + .../default/route/score-k8s/template/route.md | 4 + .../default/s3/score-compose/template/s3.md | 4 + .../default/s3/score-k8s/template/s3.md | 4 + .../default/score-compose.md | 12 - .../default/score-k8s.md | 12 - .../score-compose/template/service-port.md | 4 + .../score-k8s/template/service-port.md | 4 + .../volume/score-compose/template/volume.md | 4 + .../volume/score-k8s/template/volume.md | 4 + data/examplesMeta.yml | 64 +- gen/examples-site/frontmatter-provisioners.js | 35 + gen/examples-site/metadata-utils.js | 49 +- ...transform-default-resource-provisioners.js | 3 + .../score-k8s/delete-default-manifests.tpl | 3 - .../score-k8s/knative-service.tpl | 22 - .../score-compose/default.provisioners.yaml | 996 ------------ .../score-k8s/zz-default.provisioners.yaml | 1390 ----------------- layouts/examples/list.html | 62 +- layouts/partials/examples/filters.html | 109 ++ layouts/shortcodes/example-file.html | 20 + .../resource-provisioner-content.html | 15 + 68 files changed, 529 insertions(+), 2483 deletions(-) delete mode 100644 content/en/examples/patch-templates/score-k8s/knative-service.md delete mode 100644 content/en/examples/resource-provisioners/default/score-compose.md delete mode 100644 content/en/examples/resource-provisioners/default/score-k8s.md delete mode 100644 gen/external-content/patch-templates/score-k8s/knative-service.tpl delete mode 100644 gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml delete mode 100644 gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml create mode 100644 layouts/shortcodes/resource-provisioner-content.html diff --git a/config.toml b/config.toml index 2cda0ccf..59729f27 100644 --- a/config.toml +++ b/config.toml @@ -172,7 +172,7 @@ url = "https://github.com/score-spec/community-patchers/blob/main" name = "score" labels = ["Specification", "Resources", "Provisioner"] [[Params.exampleTypeLabels]] -name = "resources-provisioners" +name = "resource-provisioners" labels = ["Source", "Implementation", "Provisioner Type", "Resource Type", "Flavor", "Tool"] [[Params.exampleTypeLabels]] name = "patch-templates" diff --git a/content/en/examples/patch-templates/_index.md b/content/en/examples/patch-templates/_index.md index 7ef208b2..21ef0254 100644 --- a/content/en/examples/patch-templates/_index.md +++ b/content/en/examples/patch-templates/_index.md @@ -4,6 +4,6 @@ draft: false type: examples --- -The examples below illustrate how to use patch templates for each Score implementation. +The examples below illustrate how to use patch templates with either [`score-compose`](https://docs.score.dev/docs/score-implementation/score-compose/patch-templates/) or [`score-k8s`](https://docs.score.dev/docs/score-implementation/score-k8s/patch-templates/). --- diff --git a/content/en/examples/patch-templates/score-k8s/knative-service.md b/content/en/examples/patch-templates/score-k8s/knative-service.md deleted file mode 100644 index e60c1f88..00000000 --- a/content/en/examples/patch-templates/score-k8s/knative-service.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: "Knative Service" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "score-k8s" - ---- - -{{% example-file filename="knative-service.tpl" dir="patch-templates/score-k8s" githubUrl="https://github.com/score-spec/community-patchers/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose/template/redis-dapr-pubsub.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose/template/redis-dapr-pubsub.md index 9d665849..02cfe203 100644 --- a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose/template/redis-dapr-pubsub.md +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-compose/template/redis-dapr-pubsub.md @@ -3,6 +3,8 @@ title: "redis-dapr-pubsub" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "dapr-pubsub" provisionerType: "template" flavor: "redis" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates a Dapr PubSub Component pointing to a Redis Service." type="dapr-pubsub" expectedOutputs="name" %}} + {{% example-file filename="10-redis-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/rabbitmq-dapr-pubsub.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/rabbitmq-dapr-pubsub.md index b1487f9b..c16a98f5 100644 --- a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/rabbitmq-dapr-pubsub.md +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/rabbitmq-dapr-pubsub.md @@ -3,6 +3,8 @@ title: "rabbitmq-dapr-pubsub" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "dapr-pubsub" provisionerType: "template" flavor: "rabbitmq" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates a Dapr PubSub Component pointing to a RabbitMQ StatefulSet." type="dapr-pubsub" expectedOutputs="name" %}} + {{% example-file filename="10-rabbitmq-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md index 4158645e..d3e7877b 100644 --- a/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md +++ b/content/en/examples/resource-provisioners/community/dapr-pubsub/score-k8s/template/redis-dapr-pubsub.md @@ -3,6 +3,8 @@ title: "redis-dapr-pubsub" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "dapr-pubsub" provisionerType: "template" flavor: "redis" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates a Dapr PubSub Component pointing to a Redis StatefulSet." type="dapr-pubsub" expectedOutputs="name" %}} + {{% example-file filename="10-redis-dapr-pubsub.provisioners.yaml" dir="resource-provisioners/community/dapr-pubsub/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose/template/redis-dapr-state-store.md b/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose/template/redis-dapr-state-store.md index 613fb140..b0e3ec6f 100644 --- a/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose/template/redis-dapr-state-store.md +++ b/content/en/examples/resource-provisioners/community/dapr-state-store/score-compose/template/redis-dapr-state-store.md @@ -3,6 +3,8 @@ title: "redis-dapr-state-store" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "dapr-state-store" provisionerType: "template" flavor: "redis" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates a Dapr StateStore Component pointing to a Redis Service." type="dapr-state-store" expectedOutputs="name" %}} + {{% example-file filename="10-redis-dapr-state-store.provisioners.yaml" dir="resource-provisioners/community/dapr-state-store/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s/template/redis.md b/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s/template/redis.md index bd734e98..2fc7d8e0 100644 --- a/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s/template/redis.md +++ b/content/en/examples/resource-provisioners/community/dapr-state-store/score-k8s/template/redis.md @@ -3,6 +3,8 @@ title: "redis" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "dapr-state-store" provisionerType: "template" flavor: "redis" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates a Dapr StateStore Component pointing to a Redis StatefulSet." type="dapr-state-store" expectedOutputs="name" %}} + {{% example-file filename="10-redis-dapr-state-store.provisioners.yaml" dir="resource-provisioners/community/dapr-state-store/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose/template/dapr-subscription.md b/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose/template/dapr-subscription.md index 3fff8bbb..6d9dd3f8 100644 --- a/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose/template/dapr-subscription.md +++ b/content/en/examples/resource-provisioners/community/dapr-subscription/score-compose/template/dapr-subscription.md @@ -3,6 +3,8 @@ title: "dapr-subscription" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "dapr-subscription" provisionerType: "template" flavor: "dapr" @@ -18,4 +20,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates a Dapr Subscription on a given Topic and PubSub." type="dapr-subscription" supportedParams="topic,pubsub" expectedOutputs="name,topic" %}} + {{% example-file filename="10-dapr-subscription.provisioners.yaml" dir="resource-provisioners/community/dapr-subscription/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s/template/dapr-subscription.md b/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s/template/dapr-subscription.md index f871df56..1bbd69d9 100644 --- a/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s/template/dapr-subscription.md +++ b/content/en/examples/resource-provisioners/community/dapr-subscription/score-k8s/template/dapr-subscription.md @@ -3,6 +3,8 @@ title: "dapr-subscription" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "dapr-subscription" provisionerType: "template" flavor: "dapr" @@ -18,4 +20,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates a Dapr Subscription on a given Topic and PubSub." type="dapr-subscription" supportedParams="topic,pubsub" expectedOutputs="name,topic" %}} + {{% example-file filename="10-dapr-subscription.provisioners.yaml" dir="resource-provisioners/community/dapr-subscription/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md b/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md index df429692..4f4526c2 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md +++ b/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md @@ -3,6 +3,8 @@ title: "dns-in-codespace" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "dns" provisionerType: "cmd" flavor: "dns" @@ -21,4 +23,6 @@ Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. +{{% resource-provisioner-content description="Get the forwarded port URL in current GitHub Codespace on port 8080" type="dns" expectedOutputs="host,url" %}} + {{% example-file filename="10-dns-in-codespace.provisioners.yaml" dir="resource-provisioners/community/dns/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md b/content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md index 37f4c697..a37afe35 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md +++ b/content/en/examples/resource-provisioners/community/dns/score-compose/template/dns-with-route.md @@ -3,6 +3,8 @@ title: "dns-with-route" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "dns" provisionerType: "template" flavor: "dns" @@ -20,4 +22,6 @@ Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. +{{% resource-provisioner-content description="Outputs a *.localhost domain as the hostname and associated URL in http on port 8080" type="dns" expectedOutputs="host,url" %}} + {{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md b/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md index ba093e49..2544c8db 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md +++ b/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md @@ -3,6 +3,8 @@ title: "dns-in-codespace" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "dns" provisionerType: "cmd" flavor: "dns" @@ -21,4 +23,6 @@ Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. +{{% resource-provisioner-content description="Get the forwarded port URL in current GitHub Codespace on port 80" type="dns" expectedOutputs="host,url" %}} + {{% example-file filename="10-dns-in-codespace.provisioners.yaml" dir="resource-provisioners/community/dns/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md b/content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md index ba4ba109..798310b5 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md +++ b/content/en/examples/resource-provisioners/community/dns/score-k8s/template/dns-with-url.md @@ -3,6 +3,8 @@ title: "dns-with-url" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "dns" provisionerType: "template" flavor: "dns" @@ -20,4 +22,6 @@ Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace. +{{% resource-provisioner-content description="Outputs a *.localhost domain as the hostname and associated URL in http on port 80" type="dns" expectedOutputs="host,url" %}} + {{% example-file filename="10-dns-with-url.provisioners.yaml" dir="resource-provisioners/community/dns/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/environment/score-compose/cmd/dotenv.md b/content/en/examples/resource-provisioners/community/environment/score-compose/cmd/dotenv.md index aacf84f8..af61b9af 100644 --- a/content/en/examples/resource-provisioners/community/environment/score-compose/cmd/dotenv.md +++ b/content/en/examples/resource-provisioners/community/environment/score-compose/cmd/dotenv.md @@ -3,6 +3,8 @@ title: "dotenv" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "environment" provisionerType: "cmd" flavor: "dotenv" @@ -18,4 +20,6 @@ Prerequisites: - Have `python` installed, this provisioner is using Python to load the `.env` file. +{{% resource-provisioner-content description="Loads environment variables from a local .env file." type="environment" %}} + {{% example-file filename="10-env.provisioners.yaml" dir="resource-provisioners/community/environment/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/environment/score-k8s/cmd/dotenv.md b/content/en/examples/resource-provisioners/community/environment/score-k8s/cmd/dotenv.md index b9f5453a..43586954 100644 --- a/content/en/examples/resource-provisioners/community/environment/score-k8s/cmd/dotenv.md +++ b/content/en/examples/resource-provisioners/community/environment/score-k8s/cmd/dotenv.md @@ -3,6 +3,8 @@ title: "dotenv" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "environment" provisionerType: "cmd" flavor: "dotenv" @@ -18,4 +20,6 @@ Prerequisites: - Have `python` installed, this provisioner is using Python to load the `.env` file. +{{% resource-provisioner-content description="Loads environment variables from a local .env file." type="environment" %}} + {{% example-file filename="10-env.provisioners.yaml" dir="resource-provisioners/community/environment/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/template/empty-hpa.md b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/template/empty-hpa.md index 73ba70bf..b7c4e023 100644 --- a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/template/empty-hpa.md +++ b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-compose/template/empty-hpa.md @@ -3,6 +3,8 @@ title: "empty-hpa" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "horizontal-pod-autoscaler" provisionerType: "template" flavor: "empty" @@ -12,4 +14,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates an empty object because HPA is not supported in Docker Compose." type="horizontal-pod-autoscaler" %}} + {{% example-file filename="10-hpa.provisioners.yaml" dir="resource-provisioners/community/horizontal-pod-autoscaler/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/template/default-hpa.md b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/template/default-hpa.md index d32cf4cd..1c0168c3 100644 --- a/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/template/default-hpa.md +++ b/content/en/examples/resource-provisioners/community/horizontal-pod-autoscaler/score-k8s/template/default-hpa.md @@ -3,6 +3,8 @@ title: "default-hpa" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "horizontal-pod-autoscaler" provisionerType: "template" flavor: "default" @@ -16,4 +18,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates an HorizontalPodAutoscaler manifest." type="horizontal-pod-autoscaler" supportedParams="maxReplicas,minReplicas,targetCPUUtilizationPercentage" %}} + {{% example-file filename="10-hpa.provisioners.yaml" dir="resource-provisioners/community/horizontal-pod-autoscaler/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md index c52bbc98..9fe82169 100644 --- a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md +++ b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md @@ -3,6 +3,8 @@ title: "helm-template-redis" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "redis" provisionerType: "cmd" flavor: "helm" @@ -35,4 +37,6 @@ Prerequisites: - Have access to a cluster where the Helm chart will be installed. - If you don't have one, you can deploy a `Kind` cluster locally by running this script: `.scripts/setup-kind-cluster.sh`. +{{% resource-provisioner-content description="Generates the manifests of the bitnami/redis Helm chart." type="redis" expectedOutputs="host,port,username,password" %}} + {{% example-file filename="10-redis-helm-template.provisioners.yaml" dir="resource-provisioners/community/redis/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md index 40959a9f..c66c5947 100644 --- a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md +++ b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md @@ -3,6 +3,8 @@ title: "helm-upgrade-redis" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "redis" provisionerType: "cmd" flavor: "helm" @@ -35,4 +37,6 @@ Prerequisites: - Have access to a cluster where the Helm chart will be installed. - If you don't have one, you can deploy a `Kind` cluster locally by running this script: `.scripts/setup-kind-cluster.sh`. +{{% resource-provisioner-content description="Deploys the bitnami/redis Helm chart in an existing cluster." type="redis" expectedOutputs="host,port,username,password" %}} + {{% example-file filename="10-redis-helm-upgrade.provisioners.yaml" dir="resource-provisioners/community/redis/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md index e4198995..e1e27f06 100644 --- a/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-route.md @@ -3,6 +3,8 @@ title: "ingress-route" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "route" provisionerType: "template" flavor: "ingress" @@ -16,4 +18,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions an Ingress route on a shared nginx instance." type="route" supportedParams="path,host,port" %}} + {{% example-file filename="10-ingress-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md index 17421d4b..e0c9f92b 100644 --- a/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/ingress-with-net-pol-route.md @@ -3,6 +3,8 @@ title: "ingress-with-net-pol-route" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "route" provisionerType: "template" flavor: "ingress" @@ -16,4 +18,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions an Ingress route on a shared nginx instance, and a NetworkPolicy between them." type="route" supportedParams="path,host,port" %}} + {{% example-file filename="10-ingress-with-netpol-route.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md index ba9b0df4..bdbfd20e 100644 --- a/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway-with-netpol.md @@ -3,6 +3,8 @@ title: "route-with-shared-gateway-with-netpol" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "route" provisionerType: "template" flavor: "route" @@ -16,4 +18,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates an HTTPRoute attached to a default Gateway in default Namespace, and a NetworkPolicy between them." type="route" supportedParams="path,host,port" %}} + {{% example-file filename="10-shared-gateway-httproute-with-netpol.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md index d96f19ce..9541839e 100644 --- a/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md +++ b/content/en/examples/resource-provisioners/community/route/score-k8s/template/route-with-shared-gateway.md @@ -3,6 +3,8 @@ title: "route-with-shared-gateway" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "route" provisionerType: "template" flavor: "route" @@ -16,4 +18,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Generates an HTTPRoute attached to a default Gateway in default Namespace." type="route" supportedParams="path,host,port" %}} + {{% example-file filename="10-shared-gateway-httproute.provisioners.yaml" dir="resource-provisioners/community/route/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/service/score-compose/template/static-service.md b/content/en/examples/resource-provisioners/community/service/score-compose/template/static-service.md index 6ea339aa..8d772d5b 100644 --- a/content/en/examples/resource-provisioners/community/service/score-compose/template/static-service.md +++ b/content/en/examples/resource-provisioners/community/service/score-compose/template/static-service.md @@ -3,6 +3,8 @@ title: "static-service" draft: false mermaid: true type: examples +source: "community" +implementation: "score-compose" resourceType: "service" provisionerType: "template" flavor: "static" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Outputs the name of the Workload dependency if it exists in the list of Workloads." type="service" expectedOutputs="name" %}} + {{% example-file filename="10-service.provisioners.yaml" dir="resource-provisioners/community/service/score-compose" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md index 284aeeeb..af7a7dc4 100644 --- a/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md +++ b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service-with-netpol.md @@ -3,6 +3,8 @@ title: "static-service-with-netpol" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "service" provisionerType: "template" flavor: "static" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Outputs the name of the Workload dependency if it exists in the list of Workloads, and generate NetworkPolicies between them." type="service" expectedOutputs="name" %}} + {{% example-file filename="10-service-with-netpol.provisioners.yaml" dir="resource-provisioners/community/service/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service.md b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service.md index 3df48f86..3e193209 100644 --- a/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service.md +++ b/content/en/examples/resource-provisioners/community/service/score-k8s/template/static-service.md @@ -3,6 +3,8 @@ title: "static-service" draft: false mermaid: true type: examples +source: "community" +implementation: "score-k8s" resourceType: "service" provisionerType: "template" flavor: "static" @@ -14,4 +16,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Outputs the name of the Workload dependency if it exists in the list of Workloads." type="service" expectedOutputs="name" %}} + {{% example-file filename="10-service.provisioners.yaml" dir="resource-provisioners/community/service/score-k8s" githubUrl="https://github.com/score-spec/community-provisioners/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/dns/score-compose/template/dns.md b/content/en/examples/resource-provisioners/default/dns/score-compose/template/dns.md index 28be1926..c92e643a 100644 --- a/content/en/examples/resource-provisioners/default/dns/score-compose/template/dns.md +++ b/content/en/examples/resource-provisioners/default/dns/score-compose/template/dns.md @@ -3,6 +3,8 @@ title: "dns" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "dns" provisionerType: "template" flavor: "dns" @@ -16,4 +18,6 @@ hasMore: true The default dns provisioner just outputs localhost as the hostname every time. This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This can be replaced by a new provisioner in the future. +{{% resource-provisioner-content description="Outputs a *.localhost domain as the hostname." type="dns" expectedOutputs="host" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/dns/score-k8s/template/dns.md b/content/en/examples/resource-provisioners/default/dns/score-k8s/template/dns.md index 64629c05..6553b5e0 100644 --- a/content/en/examples/resource-provisioners/default/dns/score-k8s/template/dns.md +++ b/content/en/examples/resource-provisioners/default/dns/score-k8s/template/dns.md @@ -3,6 +3,8 @@ title: "dns" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "dns" provisionerType: "template" flavor: "dns" @@ -16,4 +18,6 @@ hasMore: true The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is available. You should replace this with your own dns name generation that matches your external-dns controller. +{{% resource-provisioner-content description="Outputs a *.localhost domain as the hostname." type="dns" expectedOutputs="host" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/dns/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/elasticsearch/score-compose/template/elasticsearch.md b/content/en/examples/resource-provisioners/default/elasticsearch/score-compose/template/elasticsearch.md index d14eb0b9..cd10364f 100644 --- a/content/en/examples/resource-provisioners/default/elasticsearch/score-compose/template/elasticsearch.md +++ b/content/en/examples/resource-provisioners/default/elasticsearch/score-compose/template/elasticsearch.md @@ -3,6 +3,8 @@ title: "elasticsearch" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "elasticsearch" provisionerType: "template" flavor: "elasticsearch" @@ -19,4 +21,6 @@ hasMore: false The default elasticsearch provisioner adds a elasticsearch instance. +{{% resource-provisioner-content description="Provisions a dedicated Elastic Search instance." type="elasticsearch" expectedOutputs="host,port,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/elasticsearch/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md index 8e006afd..e6fa14c9 100644 --- a/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md +++ b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md @@ -3,6 +3,8 @@ title: "example-provisioner" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "example-provisioner-resource" provisionerType: "cmd" flavor: "example" @@ -18,4 +20,6 @@ hasMore: true The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory. +{{% resource-provisioner-content description="Example provisioner that runs a bash script." type="example-provisioner-resource" expectedOutputs="key,secret" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/example-provisioner/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md b/content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md index e739202b..fae52985 100644 --- a/content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md +++ b/content/en/examples/resource-provisioners/default/kafka-topic/score-compose/template/kafka-topic.md @@ -3,6 +3,8 @@ title: "kafka-topic" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "kafka-topic" provisionerType: "template" flavor: "kafka" @@ -17,4 +19,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated Kafka topic on a shared Kafka broker." type="kafka-topic" expectedOutputs="host,port,name,num_partitions" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/kafka-topic/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md b/content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md index eff401c2..eb504e5b 100644 --- a/content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md +++ b/content/en/examples/resource-provisioners/default/mongo/score-k8s/template/mongo.md @@ -3,6 +3,8 @@ title: "mongo" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "mongodb" provisionerType: "template" flavor: "mongo" @@ -18,4 +20,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated MongoDB database." type="mongodb" expectedOutputs="host,port,username,password,connection" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongo/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mongodb/score-compose/template/mongodb.md b/content/en/examples/resource-provisioners/default/mongodb/score-compose/template/mongodb.md index d4bcb782..a0019e17 100644 --- a/content/en/examples/resource-provisioners/default/mongodb/score-compose/template/mongodb.md +++ b/content/en/examples/resource-provisioners/default/mongodb/score-compose/template/mongodb.md @@ -3,6 +3,8 @@ title: "mongodb" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "mongodb" provisionerType: "template" flavor: "mongodb" @@ -20,4 +22,6 @@ hasMore: false The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string. +{{% resource-provisioner-content description="Provisions a dedicated MongoDB database." type="mongodb" expectedOutputs="host,port,username,password,connection" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mongodb/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md b/content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md index 36586bf8..bb693829 100644 --- a/content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md +++ b/content/en/examples/resource-provisioners/default/mssql/score-compose/template/mssql.md @@ -3,6 +3,8 @@ title: "mssql" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "mssql" provisionerType: "template" flavor: "mssql" @@ -19,4 +21,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated database on a shared MS SQL server instance." type="mssql" expectedOutputs="server,port,connection,database,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md b/content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md index 07e8980b..80c854bf 100644 --- a/content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md +++ b/content/en/examples/resource-provisioners/default/mssql/score-k8s/template/mssql.md @@ -3,6 +3,8 @@ title: "mssql" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "mssql" provisionerType: "template" flavor: "mssql" @@ -19,4 +21,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated database on a shared MS SQL server instance." type="mssql" expectedOutputs="server,port,connection,database,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mssql/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mysql/score-compose/template/mysql.md b/content/en/examples/resource-provisioners/default/mysql/score-compose/template/mysql.md index bcd0d386..dc6ceaba 100644 --- a/content/en/examples/resource-provisioners/default/mysql/score-compose/template/mysql.md +++ b/content/en/examples/resource-provisioners/default/mysql/score-compose/template/mysql.md @@ -3,6 +3,8 @@ title: "mysql" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "mysql" provisionerType: "template" flavor: "mysql" @@ -21,4 +23,6 @@ hasMore: false The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on startup. +{{% resource-provisioner-content description="Provisions a dedicated MySQL database on a shared instance." type="mysql" expectedOutputs="host,port,name,database,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md b/content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md index 3bb33e44..21cd6ed4 100644 --- a/content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md +++ b/content/en/examples/resource-provisioners/default/mysql/score-k8s/template/mysql.md @@ -3,6 +3,8 @@ title: "mysql" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "mysql" provisionerType: "template" flavor: "mysql" @@ -19,4 +21,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated MySQL database on a shared instance." type="mysql" expectedOutputs="host,port,name,database,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/mysql/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md index 817ab7c5..b25d2b90 100644 --- a/content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md +++ b/content/en/examples/resource-provisioners/default/postgres-instance/score-compose/template/postgres-instance.md @@ -3,6 +3,8 @@ title: "postgres-instance" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "postgres-instance" provisionerType: "template" flavor: "postgres" @@ -17,4 +19,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated PostgreSQL instance." type="postgres-instance" expectedOutputs="host,port,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md b/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md index 613228a8..2d023f17 100644 --- a/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md +++ b/content/en/examples/resource-provisioners/default/postgres-instance/score-k8s/template/postgres-instance.md @@ -3,6 +3,8 @@ title: "postgres-instance" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "postgres-instance" provisionerType: "template" flavor: "postgres" @@ -17,4 +19,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated PostgreSQL instance." type="postgres-instance" expectedOutputs="host,port,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres-instance/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres/score-compose/template/postgres.md b/content/en/examples/resource-provisioners/default/postgres/score-compose/template/postgres.md index bb1004a3..1c47e142 100644 --- a/content/en/examples/resource-provisioners/default/postgres/score-compose/template/postgres.md +++ b/content/en/examples/resource-provisioners/default/postgres/score-compose/template/postgres.md @@ -3,6 +3,8 @@ title: "postgres" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "postgres" provisionerType: "template" flavor: "postgres" @@ -21,4 +23,6 @@ hasMore: false The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on startup. +{{% resource-provisioner-content description="Provisions a dedicated database on a shared PostgreSQL instance." type="postgres" expectedOutputs="host,port,name,database,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md b/content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md index 3fd8521a..35c7af26 100644 --- a/content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md +++ b/content/en/examples/resource-provisioners/default/postgres/score-k8s/template/postgres.md @@ -3,6 +3,8 @@ title: "postgres" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "postgres" provisionerType: "template" flavor: "postgres" @@ -19,4 +21,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated database on a shared PostgreSQL instance." type="postgres" expectedOutputs="host,port,name,database,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/postgres/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/rabbitmq/score-compose/template/rabbitmq.md b/content/en/examples/resource-provisioners/default/rabbitmq/score-compose/template/rabbitmq.md index 1b07de82..fb7a3861 100644 --- a/content/en/examples/resource-provisioners/default/rabbitmq/score-compose/template/rabbitmq.md +++ b/content/en/examples/resource-provisioners/default/rabbitmq/score-compose/template/rabbitmq.md @@ -3,6 +3,8 @@ title: "rabbitmq" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "amqp" provisionerType: "template" flavor: "rabbitmq" @@ -20,4 +22,6 @@ hasMore: false The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins. +{{% resource-provisioner-content description="Provisions a dedicated RabbitMQ vhost on a shared instance." type="amqp" expectedOutputs="host,port,vhost,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md b/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md index 38485336..b25e5bee 100644 --- a/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md +++ b/content/en/examples/resource-provisioners/default/rabbitmq/score-k8s/template/rabbitmq.md @@ -3,6 +3,8 @@ title: "rabbitmq" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "amqp" provisionerType: "template" flavor: "rabbitmq" @@ -18,4 +20,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated RabbitMQ vhost on a shared instance." type="amqp" expectedOutputs="host,port,vhost,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/rabbitmq/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/redis/score-compose/template/redis.md b/content/en/examples/resource-provisioners/default/redis/score-compose/template/redis.md index a285c9fa..ba302e58 100644 --- a/content/en/examples/resource-provisioners/default/redis/score-compose/template/redis.md +++ b/content/en/examples/resource-provisioners/default/redis/score-compose/template/redis.md @@ -3,6 +3,8 @@ title: "redis" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "redis" provisionerType: "template" flavor: "redis" @@ -19,4 +21,6 @@ hasMore: false The default redis provisioner adds a redis service to the project which returns a host, port, username, and password. +{{% resource-provisioner-content description="Provisions a dedicated Redis instance." type="redis" expectedOutputs="host,port,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md b/content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md index 9c2fa3c8..7f682cec 100644 --- a/content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md +++ b/content/en/examples/resource-provisioners/default/redis/score-k8s/template/redis.md @@ -3,6 +3,8 @@ title: "redis" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "redis" provisionerType: "template" flavor: "redis" @@ -17,4 +19,6 @@ hasMore: false --- +{{% resource-provisioner-content description="Provisions a dedicated Redis instance." type="redis" expectedOutputs="host,port,username,password" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/redis/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/route/score-compose/template/route.md b/content/en/examples/resource-provisioners/default/route/score-compose/template/route.md index 02299b9a..296f9ee1 100644 --- a/content/en/examples/resource-provisioners/default/route/score-compose/template/route.md +++ b/content/en/examples/resource-provisioners/default/route/score-compose/template/route.md @@ -3,6 +3,8 @@ title: "route" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "route" provisionerType: "template" flavor: "route" @@ -18,4 +20,6 @@ hasMore: false The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps. +{{% resource-provisioner-content description="Provisions a ingress route on a shared Nginx instance." type="route" supportedParams="host,port,path" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/route/score-k8s/template/route.md b/content/en/examples/resource-provisioners/default/route/score-k8s/template/route.md index 211d61c7..58422558 100644 --- a/content/en/examples/resource-provisioners/default/route/score-k8s/template/route.md +++ b/content/en/examples/resource-provisioners/default/route/score-k8s/template/route.md @@ -3,6 +3,8 @@ title: "route" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "route" provisionerType: "template" flavor: "route" @@ -18,4 +20,6 @@ hasMore: false Routes could be implemented as either traditional ingress resources or using the newer gateway API. In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this. +{{% resource-provisioner-content description="Provisions an HTTPRoute on a shared Nginx instance." type="route" supportedParams="host,port,path" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/route/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/s3/score-compose/template/s3.md b/content/en/examples/resource-provisioners/default/s3/score-compose/template/s3.md index cd1bf41a..caf7ea5b 100644 --- a/content/en/examples/resource-provisioners/default/s3/score-compose/template/s3.md +++ b/content/en/examples/resource-provisioners/default/s3/score-compose/template/s3.md @@ -3,6 +3,8 @@ title: "s3" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "s3" provisionerType: "template" flavor: "s3" @@ -22,4 +24,6 @@ hasMore: false This resource provides a minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. If the provider has a publish port annotation, it can expose a management port on the local network for debugging and connectivity. +{{% resource-provisioner-content description="Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance." type="s3" expectedOutputs="bucket,access_key_id,secret_key,endpoint,region,aws_access_key_id,aws_secret_key" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/s3/score-k8s/template/s3.md b/content/en/examples/resource-provisioners/default/s3/score-k8s/template/s3.md index f6ada09e..e0d9a977 100644 --- a/content/en/examples/resource-provisioners/default/s3/score-k8s/template/s3.md +++ b/content/en/examples/resource-provisioners/default/s3/score-k8s/template/s3.md @@ -3,6 +3,8 @@ title: "s3" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "s3" provisionerType: "template" flavor: "s3" @@ -22,4 +24,6 @@ hasMore: false This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. This provides some common and well known outputs that can be used with any generic AWS s3 client. The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket. +{{% resource-provisioner-content description="Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance." type="s3" expectedOutputs="bucket,access_key_id,secret_key,endpoint,region,aws_access_key_id,aws_secret_key" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/s3/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/score-compose.md b/content/en/examples/resource-provisioners/default/score-compose.md deleted file mode 100644 index 895b1649..00000000 --- a/content/en/examples/resource-provisioners/default/score-compose.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: "Score Compose" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Default" - ---- - -{{% example-file filename="default.provisioners.yaml" dir="resource-provisioners/default/score-compose" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/score-k8s.md b/content/en/examples/resource-provisioners/default/score-k8s.md deleted file mode 100644 index 7e3bca08..00000000 --- a/content/en/examples/resource-provisioners/default/score-k8s.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: "Score K8s" -draft: false -mermaid: true -type: examples -excerpt: '' -hasMore: false -parent: "Default" - ---- - -{{% example-file filename="zz-default.provisioners.yaml" dir="resource-provisioners/default/score-k8s" githubUrl="https://github.com/score-spec/examples/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/default/service-port/score-compose/template/service-port.md b/content/en/examples/resource-provisioners/default/service-port/score-compose/template/service-port.md index 614ac418..6b881680 100644 --- a/content/en/examples/resource-provisioners/default/service-port/score-compose/template/service-port.md +++ b/content/en/examples/resource-provisioners/default/service-port/score-compose/template/service-port.md @@ -3,6 +3,8 @@ title: "service-port" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "service-port" provisionerType: "template" flavor: "service" @@ -20,4 +22,6 @@ hasMore: true The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. +{{% resource-provisioner-content description="Outputs a hostname and port for connecting to another workload." type="service-port" supportedParams="workload,port" expectedOutputs="hostname,port" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/service-port/score-k8s/template/service-port.md b/content/en/examples/resource-provisioners/default/service-port/score-k8s/template/service-port.md index 1b4f3796..7bba91af 100644 --- a/content/en/examples/resource-provisioners/default/service-port/score-k8s/template/service-port.md +++ b/content/en/examples/resource-provisioners/default/service-port/score-k8s/template/service-port.md @@ -3,6 +3,8 @@ title: "service-port" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "service-port" provisionerType: "template" flavor: "service" @@ -20,4 +22,6 @@ hasMore: true The default provisioner for service resources, this expects a workload and port name and will return the hostname and port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency relationship yet. +{{% resource-provisioner-content description="Outputs a hostname and port for connecting to another workload." type="service-port" supportedParams="workload,port" expectedOutputs="hostname,port" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/service-port/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/volume/score-compose/template/volume.md b/content/en/examples/resource-provisioners/default/volume/score-compose/template/volume.md index b0d56723..2758aecd 100644 --- a/content/en/examples/resource-provisioners/default/volume/score-compose/template/volume.md +++ b/content/en/examples/resource-provisioners/default/volume/score-compose/template/volume.md @@ -3,6 +3,8 @@ title: "volume" draft: false mermaid: true type: examples +source: "default" +implementation: "score-compose" resourceType: "volume" provisionerType: "template" flavor: "volume" @@ -17,4 +19,6 @@ hasMore: false The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source attribute that we can reference. +{{% resource-provisioner-content description="Creates a persistent volume that can be mounted on a workload." type="volume" expectedOutputs="source,type" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-compose" githubUrl="https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml" %}} diff --git a/content/en/examples/resource-provisioners/default/volume/score-k8s/template/volume.md b/content/en/examples/resource-provisioners/default/volume/score-k8s/template/volume.md index bf9bc92c..db59cb91 100644 --- a/content/en/examples/resource-provisioners/default/volume/score-k8s/template/volume.md +++ b/content/en/examples/resource-provisioners/default/volume/score-k8s/template/volume.md @@ -3,6 +3,8 @@ title: "volume" draft: false mermaid: true type: examples +source: "default" +implementation: "score-k8s" resourceType: "volume" provisionerType: "template" flavor: "volume" @@ -16,4 +18,6 @@ hasMore: true As an example we have a 'volume' type which returns an emptyDir volume. In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or persistent volume and claims. +{{% resource-provisioner-content description="Creates a persistent volume that can be mounted on a workload." type="volume" expectedOutputs="source" %}} + {{% example-file filename="provisioners.yaml" dir="resource-provisioners/default/volume/score-k8s" githubUrl="https://github.com/score-spec/score-k8s/blob/main/internal/provisioners/default/zz-default.provisioners.yaml" %}} diff --git a/data/examplesMeta.yml b/data/examplesMeta.yml index 9e26dfee..a9eb242e 100644 --- a/data/examplesMeta.yml +++ b/data/examplesMeta.yml @@ -1 +1,63 @@ -{} \ No newline at end of file +--- +resource-provisioners: + implementation: + - score-compose + - score-k8s + provisionerType: + - cmd + - template + resourceType: + - amqp + - dapr-pubsub + - dapr-state-store + - dapr-subscription + - dns + - elasticsearch + - environment + - example-provisioner-resource + - horizontal-pod-autoscaler + - kafka-topic + - mongodb + - mssql + - mysql + - postgres + - postgres-instance + - redis + - route + - s3 + - service + - service-port + - volume + flavor: + - dapr + - default + - dns + - dotenv + - elasticsearch + - empty + - example + - helm + - ingress + - kafka + - mongo + - mongodb + - mssql + - mysql + - postgres + - rabbitmq + - redis + - route + - s3 + - service + - static + - volume + tool: + - dns + - dotenv + - example + - helm + source: + - community + - default +--- + diff --git a/gen/examples-site/frontmatter-provisioners.js b/gen/examples-site/frontmatter-provisioners.js index f4c358a7..d35885c7 100644 --- a/gen/examples-site/frontmatter-provisioners.js +++ b/gen/examples-site/frontmatter-provisioners.js @@ -7,6 +7,7 @@ const { removeNonExternalLinks, getExcerpt, addAliasesToMetadata, + saveMetadata, } = require("./metadata-utils"); const { isDirectory } = require("./file-utils"); @@ -32,6 +33,8 @@ title: "${config.title}" draft: false mermaid: true type: examples +source: "${config.source}" +implementation: "${config.implementation}" resourceType: "${config.resourceType}" provisionerType: "${config.provisionerType}" flavor: "${config.title.split("-")[0]}" @@ -99,6 +102,20 @@ const writeContentToFile = (filePath, content) => { fs.writeFileSync(fullPath, content); }; +const generateResourceProvisionerContent = (parsedYaml) => { + return `{{% resource-provisioner-content description="${ + parsedYaml.description + }" type="${parsedYaml.type}" ${ + parsedYaml.supported_params + ? `supportedParams="${parsedYaml.supported_params}"` + : "" + } ${ + parsedYaml.expected_outputs + ? `expectedOutputs="${parsedYaml.expected_outputs}"` + : "" + } %}}`; +}; + /** * Determines the GitHub URL for a provisioner based on type and implementation. * @param {string} dirPath - The path to the provisioner directory. @@ -149,12 +166,28 @@ const buildResourceProvisionerFiles = ( } const provisionerType = parsedYaml.uri.split("://")[0]; + saveMetadata( + { + data: { + source: options.source, + implementation, + tool, + provisionerType, + resourceType: parsedYaml.type, + flavor: title.split("-")[0], + }, + }, + "resource-provisioners" + ); + const frontmatterContent = generateFrontmatterContent({ ...readmeConfig, title, provisionerType, + source: options.source, resourceType: parsedYaml.type, description: parsedYaml.description, + implementation, expectedOutputs: convertToYamlArray(parsedYaml.expected_outputs), supportedParams: convertToYamlArray(parsedYaml.supported_params), tool, @@ -164,6 +197,8 @@ const buildResourceProvisionerFiles = ( `${dirPath}/${provisionerType}/${title}`, `${frontmatterContent} +${generateResourceProvisionerContent(parsedYaml)} + ${generateExampleFileContent(yamlFile, dir, githubUrl)}\n ` ); diff --git a/gen/examples-site/metadata-utils.js b/gen/examples-site/metadata-utils.js index 202327aa..2db41f42 100644 --- a/gen/examples-site/metadata-utils.js +++ b/gen/examples-site/metadata-utils.js @@ -1,13 +1,13 @@ -const fs = require('fs'); -const matter = require('gray-matter'); +const fs = require("fs"); +const matter = require("gray-matter"); -const savedMetadataPath = './data/examplesMeta.yml'; -const aliasesPath = './data/examplesAliases.yaml'; -const aliasesFile = fs.readFileSync(aliasesPath, 'utf8'); +const savedMetadataPath = "./data/examplesMeta.yml"; +const aliasesPath = "./data/examplesAliases.yaml"; +const aliasesFile = fs.readFileSync(aliasesPath, "utf8"); const aliases = matter(`---\n${aliasesFile}\n---`).data; function saveMetadata(parsedFrontmatter, exampleType) { - const savedMetadataFile = fs.readFileSync(savedMetadataPath, 'utf8'); + const savedMetadataFile = fs.readFileSync(savedMetadataPath, "utf8"); const savedMetadata = matter(savedMetadataFile).data; // If the example type is not in the saved metadata, add it to the saved metadata: @@ -16,25 +16,25 @@ function saveMetadata(parsedFrontmatter, exampleType) { } for (const key in parsedFrontmatter.data) { - // If the key is not in the saved metadata, add it to the saved metadata: + if (!parsedFrontmatter.data[key]) { + continue; + } + // If the key is not in the saved metadata, add it to the saved metadata as an array: if (!savedMetadata[exampleType][key]) { - savedMetadata[exampleType][key] = parsedFrontmatter.data[key]; - if (Array.isArray(savedMetadata[key])) { - savedMetadata[exampleType][key] = savedMetadata[key].sort(); - } + savedMetadata[exampleType][key] = [parsedFrontmatter.data[key]].sort(); } else { // If the key is in the saved metadata and is an array, merge the arrays: if (Array.isArray(savedMetadata[exampleType][key])) { savedMetadata[exampleType][key] = Array.from( new Set([ ...savedMetadata[exampleType][key], - ...parsedFrontmatter.data[key], + parsedFrontmatter.data[key], ]) ).sort(); } } } - fs.writeFileSync(savedMetadataPath, matter.stringify('', savedMetadata)); + fs.writeFileSync(savedMetadataPath, matter.stringify("", savedMetadata)); } function saveRDTypeToMetadata(type, title) { @@ -43,12 +43,12 @@ function saveRDTypeToMetadata(type, title) { function getMetadataFromReadme(readmeContent, exampleType) { const parsedFrontmatter = matter(readmeContent); - let metadata = ''; + let metadata = ""; if (Object.keys(parsedFrontmatter.data).length > 0) { saveMetadata(parsedFrontmatter, exampleType); - const yamlMetadata = matter.stringify('', parsedFrontmatter.data); - metadata = yamlMetadata.replace(/---/g, '').trim(); + const yamlMetadata = matter.stringify("", parsedFrontmatter.data); + metadata = yamlMetadata.replace(/---/g, "").trim(); } return { metadata, parsedFrontmatter }; @@ -57,10 +57,10 @@ function getMetadataFromReadme(readmeContent, exampleType) { function addAliasesToMetadata(path, existingMetadata) { if (aliases[`/examples/${path}`]) { const aliasString = matter - .stringify('', { + .stringify("", { aliases: aliases[`/examples/${path}`], }) - .replace(/---/g, '') + .replace(/---/g, "") .trim(); return existingMetadata ? `${existingMetadata}\n${aliasString}` @@ -72,7 +72,7 @@ function addAliasesToMetadata(path, existingMetadata) { function removeNonExternalLinks(content) { const regex = /\[([^\]]+)\]\(([^)]+)\)/g; return content.replace(regex, (match, p1, p2) => { - if (p2.startsWith('http://') || p2.startsWith('https://')) { + if (p2.startsWith("http://") || p2.startsWith("https://")) { return match; } return `\`${p1}\``; @@ -81,10 +81,13 @@ function removeNonExternalLinks(content) { function getExcerpt(content) { const textWithoutHeadings = content - .replace(/^\s*#+.*$/gm, '') - .replace(/---/g, ''); - return textWithoutHeadings.trim().split('\n\n')[0].split('\r\n\r\n')[0]. - replace(/'/g, '''); + .replace(/^\s*#+.*$/gm, "") + .replace(/---/g, ""); + return textWithoutHeadings + .trim() + .split("\n\n")[0] + .split("\r\n\r\n")[0] + .replace(/'/g, "'"); } module.exports = { diff --git a/gen/examples-site/transform-default-resource-provisioners.js b/gen/examples-site/transform-default-resource-provisioners.js index 06d4892d..4792eaa6 100644 --- a/gen/examples-site/transform-default-resource-provisioners.js +++ b/gen/examples-site/transform-default-resource-provisioners.js @@ -175,3 +175,6 @@ if (fs.existsSync(scoreComposeYaml)) { if (fs.existsSync(scoreK8sYaml)) { splitProvisionersFile(scoreK8sYaml, scoreK8sDir, "score-k8s"); } + +fs.rmSync(scoreComposeDir, { recursive: true, force: true }); +fs.rmSync(scoreK8sDir, { recursive: true, force: true }); diff --git a/gen/external-content/patch-templates/score-k8s/delete-default-manifests.tpl b/gen/external-content/patch-templates/score-k8s/delete-default-manifests.tpl index f075ddb9..fc15a8d3 100644 --- a/gen/external-content/patch-templates/score-k8s/delete-default-manifests.tpl +++ b/gen/external-content/patch-templates/score-k8s/delete-default-manifests.tpl @@ -1,10 +1,7 @@ {{/* range in reverse order */}} {{ range $i, $m := (reverse .Manifests) }} -{{/* keep Namespace when --create-namespace is used */}} -{{ if ne $m.kind "Namespace" }} {{/* fix the index to be reversed as well */}} {{ $i := sub (len $.Manifests) (add $i 1) }} - op: delete path: {{ $i }} {{ end }} -{{ end }} diff --git a/gen/external-content/patch-templates/score-k8s/knative-service.tpl b/gen/external-content/patch-templates/score-k8s/knative-service.tpl deleted file mode 100644 index d15584a9..00000000 --- a/gen/external-content/patch-templates/score-k8s/knative-service.tpl +++ /dev/null @@ -1,22 +0,0 @@ -{{ range $i, $m := .Manifests }} -{{/* delete the default Service manifest */}} -{{ if eq $m.kind "Service" }} -- op: delete - path: {{ $i }} -{{ end }} -{{/* make the generated Deployment compliant with the Knative Service to reuse as many features as we can by default: env, liveness|readinessProbe, resources, etc. */}} -{{ if eq $m.kind "Deployment" }} -- op: set - path: {{ $i }}.kind - value: Service -- op: set - path: {{ $i }}.apiVersion - value: serving.knative.dev/v1 -- op: delete - path: {{ $i }}.spec.selector -- op: delete - path: {{ $i }}.spec.strategy -- op: delete - path: {{ $i }}.status -{{ end }} -{{ end }} \ No newline at end of file diff --git a/gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml b/gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml deleted file mode 100644 index 165dd5bf..00000000 --- a/gen/external-content/resource-provisioners/default/score-compose/default.provisioners.yaml +++ /dev/null @@ -1,996 +0,0 @@ -# This file is generated by score-compose when score-compose init is called. This presents the default set of -# template provisioners provided for a basic set of resource types. This file can be overridden by adding new -# *.provisioners.yaml files in this directory or by editing this file in place and checking it into the source code. - -# The default volume provisioner provided by score-compose allows basic volume resources to be created in the resources -# system. The volume resource just creates an ephemeral Docker volume with a random string as the name, and source -# attribute that we can reference. -- uri: template://default-provisioners/volume - # By default, match all classes and ids of volume. If you want to override this, create another provisioner definition - # with a higher priority. - type: volume - description: Creates a persistent volume that can be mounted on a workload. - init: | - randomVolumeName: {{ .Id | replace "." "-" }}-{{ randAlphaNum 6 }} - # Store the random volume name if we haven't chosen one yet, otherwise use the one that exists already - state: | - name: {{ dig "name" .Init.randomVolumeName .State }} - # Return a source value with the volume name. This can be used in volume resource references now. - outputs: | - type: volume - source: {{ .State.name }} - # Add a volume to the docker compose file. We assume our name is unique here. We also apply a label to help ensure - # that we can track the volume back to the workload and resource that created it. - volumes: | - {{ .State.name }}: - name: {{ .State.name }} - driver: local - labels: - dev.score.compose.res.uid: {{ .Uid }} - expected_outputs: - - source - - type - - -# The default provisioner for service resources, this expects a workload and port name and will return the hostname and -# port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency -# relationship yet. -- uri: template://default-provisioners/service-port - type: service-port - description: Outputs a hostname and port for connecting to another workload. - outputs: | - {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} - {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} - {{ $w := (index .WorkloadServices .Params.workload) }} - {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} - {{ $p := (index $w.Ports .Params.port) }} - {{ if not $p }}{{ fail "unknown service port" }}{{ end }} - hostname: {{ $w.ServiceName | quote }} - port: {{ $p.TargetPort }} - expected_outputs: - - hostname - - port - supported_params: - - workload - - port - -# The default redis provisioner adds a redis service to the project which returns a host, port, username, and password. -- uri: template://default-provisioners/redis - # By default, match all redis types regardless of class and id. If you want to override this, create another - # provisioner definition with a higher priority. - type: redis - description: Provisions a dedicated Redis instance. - # Init template has the default port and a random service name and password if needed later - init: | - port: 6379 - randomServiceName: redis-{{ randAlphaNum 6 }} - randomPassword: {{ randAlphaNum 16 | quote }} - # The only state we need to persist is the chosen random service name and password - state: | - serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - # Return the outputs schema that consumers expect - outputs: | - host: {{ .State.serviceName }} - port: {{ .Init.port }} - username: default - password: {{ .State.password | quote }} - # write the config file to the mounts directory - files: | - {{ .State.serviceName }}/redis.conf: | - requirepass {{ .State.password }} - port {{ .Init.port }} - save 60 1 - loglevel warning - # add a volume for persistence of the redis data - volumes: | - {{ .State.serviceName }}-data: - name: {{ .State.serviceName }}-data - driver: local - labels: - dev.score.compose.res.uid: {{ .Uid }} - # And the redis service itself with volumes bound in - services: | - {{ .State.serviceName }}: - labels: - dev.score.compose.res.uid: {{ .Uid }} - image: mirror.gcr.io/redis:7-alpine - restart: always - entrypoint: ["redis-server"] - command: ["/usr/local/etc/redis/redis.conf"] - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ .State.serviceName }}/redis.conf - target: /usr/local/etc/redis/redis.conf - read_only: true - - type: volume - source: {{ .State.serviceName }}-data - target: /data - volume: - nocopy: true - info_logs: | - - "{{.Uid}}: To connect to redis: \"docker run -it --network {{ .ComposeProjectName }}_default --rm redis redis-cli -h {{ .State.serviceName | squote }} -a {{ .State.password | squote }}\"" - expected_outputs: - - host - - port - - username - - password - -# The default postgres provisioner adds a postgres instance and then ensures that the required databases are created on -# startup. -- uri: template://default-provisioners/postgres - # By default, match all redis types regardless of class and id. If you want to override this, create another - # provisioner definition with a higher priority. - type: postgres - description: Provisions a dedicated database on a shared PostgreSQL instance. - # Init template has the random service name and password if needed later - init: | - randomServiceName: pg-{{ randAlphaNum 6 }} - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-postgres-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} - # The state for each database resource is a unique db name and credentials - state: | - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - # All instances agree on the shared state since there is no concurrency here - shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} - # The outputs are the core database outputs. We output both name and database for broader compatibility. - outputs: | - host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} - port: 5432 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ .State.password }} - # Write out an idempotent create script per database - files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | - SELECT 'CREATE DATABASE "{{ .State.database }}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{{ .State.database }}')\gexec - SELECT $$CREATE USER "{{ .State.username }}" WITH PASSWORD '{{ .State.password }}'$$ WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '{{ .State.username }}')\gexec - GRANT ALL PRIVILEGES ON DATABASE "{{ .State.database }}" TO "{{ .State.username }}"; - \connect "{{ .State.database }}"; - GRANT ALL ON SCHEMA public TO "{{ .State.username }}"; - # Ensure the data volume exists - volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local - # Create 2 services, the first is the database itself, the second is the init container which runs the scripts - services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/postgres:17-alpine - restart: always - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} - {{ if ne .Init.publishPort "0" }} - ports: - - target: 5432 - published: {{ .Init.publishPort }} - {{ end }} - volumes: - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /var/lib/postgresql/data - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres"] - interval: 2s - timeout: 2s - retries: 15 - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: - image: mirror.gcr.io/postgres:17-alpine - entrypoint: ["/bin/sh"] - environment: - POSTGRES_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} - command: - - "-c" - - | - cd /db-scripts - ls db-*.sql | xargs cat | psql "postgresql://postgres:$${POSTGRES_PASSWORD}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:5432/postgres" - labels: - dev.score.compose.labels.is-init-container: "true" - depends_on: - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - condition: service_healthy - restart: true - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts - target: /db-scripts - info_logs: | - - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -U {{ .State.username }} --dbname {{ .State.database }}\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" - {{ end }} - expected_outputs: - - host - - port - - name - - database - - username - - password - - -- uri: template://default-provisioners/postgres-instance - - type: postgres-instance - description: Provisions a dedicated PostgreSQL instance. - # Init template has the random service name and password if needed later - init: | - randomServiceName: pg-{{ randAlphaNum 6 }} - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-postgres-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} - # The state for each database resource is a unique db name and credentials - state: | - serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} - database: "postgres" - username: "postgres" - password: {{ dig "password" .Init.randomPassword .State | quote }} - # The outputs are the core database outputs. We output both name and database for broader compatibility. - outputs: | - host: {{ .State.serviceName }} - port: 5432 - username: postgres - password: {{ .State.password }} - # Ensure the data volume exists - volumes: | - {{ .State.serviceName }}-data: - driver: local - # Create 2 services, the first is the database itself, the second is the init container which runs the scripts - services: | - {{ .State.serviceName }}: - image: mirror.gcr.io/postgres:17-alpine - restart: always - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: {{ .State.password | quote }} - {{ if ne .Init.publishPort "0" }} - ports: - - target: 5432 - published: {{ .Init.publishPort }} - {{ end }} - volumes: - - type: volume - source: {{ .State.serviceName }}-data - target: /var/lib/postgresql/data - healthcheck: - test: ["CMD", "pg_isready", "-U", "postgres"] - - info_logs: | - - "{{.Uid}}: To connect to postgres, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm postgres:17-alpine psql -h {{ .State.serviceName }} -U {{ .State.username }} --dbname {{ .State.database }}\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your postgres client to \"postgres://{{ .State.username }}:{{ .State.password }}@localhost:{{ .Init.publishPort }}/{{ .State.database }}\"" - {{ end }} - expected_outputs: - - host - - port - - username - - password - -# This resource provides a minio based S3 bucket with AWS-style credentials. -# This provides some common and well known outputs that can be used with any generic AWS s3 client. -# If the provider has a publish port annotation, it can expose a management port on the local network for debugging and -# connectivity. -- uri: template://default-provisioners/s3 - type: s3 - description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. - # The init template contains some initial seed data that can be used it needed. - init: | - randomServiceName: minio-{{ randAlphaNum 6 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - randomBucket: bucket-{{ randAlpha 8 | lower }} - randomAccessKeyId: {{ randAlphaNum 20 | quote }} - randomSecretKey: {{ randAlphaNum 40 | quote }} - sk: default-provisioners-minio-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} - # The only instance state is the bucket name, for now we provision a single aws key across s3 resources. - state: | - bucket: {{ dig "bucket" .Init.randomBucket .State | quote }} - # The shared state contains the chosen service name and credentials - shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instanceUsername: {{ dig .Init.sk "instanceUsername" .Init.randomUsername .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} - instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" .Init.randomAccessKeyId .Shared | quote }} - instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" .Init.randomSecretKey .Shared | quote }} - publishPort: {{ with (dig .Init.sk "publishPort" 0 .Shared) }}{{ if ne . 0 }}{{ . }}{{ else }}{{ $.Init.publishPort }}{{ end }}{{ end }} - # the outputs that we can expose - outputs: | - bucket: {{ .State.bucket }} - access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} - secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} - endpoint: http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 - # for compatibility with Humanitec's existing s3 resource - region: "us-east-1" - aws_access_key_id: {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} - aws_secret_key: {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} - # we store 2 files, 1 is always the same and overridden, the other is per bucket - files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/00-svcacct.sh: | - set -eu - mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} - mc admin user svcacct info myminio {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} || mc admin user svcacct add myminio {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} --access-key {{ dig .Init.sk "instanceAccessKeyId" "" .Shared | quote }} --secret-key {{ dig .Init.sk "instanceSecretKey" "" .Shared | quote }} - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts/10-bucket-{{ .State.bucket }}.sh: | - set -eu - mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }} - mc mb -p myminio/{{ .State.bucket }} - volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local - # 2 services, the minio one, and the init container which ensures the service account and buckets exist - services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: quay.io/minio/minio - command: ["server", "/data", "--console-address", ":9001"] - restart: always - {{ if ne .Init.publishPort 0 }} - ports: - - target: 9001 - published: {{ .Init.publishPort }} - {{ end }} - healthcheck: - test: ["CMD-SHELL", "mc alias set myminio http://localhost:9000 {{ dig .Init.sk "instanceUsername" "" .Shared }} {{ dig .Init.sk "instancePassword" "" .Shared }}"] - interval: 2s - timeout: 2s - retries: 15 - environment: - MINIO_ROOT_USER: {{ dig .Init.sk "instanceUsername" "" .Shared | quote }} - MINIO_ROOT_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} - volumes: - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /data - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: - image: quay.io/minio/minio - entrypoint: ["/bin/bash"] - command: - - "-c" - - "for s in $$(ls /setup-scripts -1); do sh /setup-scripts/$$s; done" - labels: - dev.score.compose.labels.is-init-container: "true" - depends_on: - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - condition: service_healthy - restart: true - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-setup-scripts - target: /setup-scripts - info_logs: | - - "{{.Uid}}: To connect with a minio client: use the myminio alias at \"docker run -it --network {{ .ComposeProjectName }}_default --rm --entrypoint /bin/bash quay.io/minio/minio -c 'mc alias set myminio http://{{ dig .Init.sk "instanceServiceName" "" .Shared }}:9000 {{ dig .Init.sk "instanceAccessKeyId" "" .Shared }} {{ dig .Init.sk "instanceSecretKey" "" .Shared }}; bash'\"" - {{ if ne .Init.publishPort 0 }} - - "{{.Uid}}: Or enter {{ dig .Init.sk "instanceUsername" "" .Shared }} / {{ dig .Init.sk "instancePassword" "" .Shared }} at https://localhost:{{ .Init.publishPort }}" - {{ end }} - expected_outputs: - - bucket - - access_key_id - - secret_key - - endpoint - - region - - aws_access_key_id - - aws_secret_key - -# The default AMQP provisioner provides a simple rabbitmq instance with default configuration and plugins. -- uri: template://default-provisioners/rabbitmq - type: amqp - description: Provisions a dedicated RabbitMQ vhost on a shared instance. - init: | - randomServiceName: rabbitmq-{{ randAlphaNum 6 }} - randomVHost: vhost-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-rabbitmq - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi }} - publishManagementPort: {{ dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi }} - state: | - vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} - port: 5672 - vhost: {{ .State.vhost }} - username: {{ .State.username }} - password: {{ .State.password }} - shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instanceErlangCookie: {{ dig .Init.sk "instanceErlangCookie" (randAlpha 20) .Shared }} - {{ $publishPorts := (list) }} - {{ if ne .Init.publishPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 5672 "published" .Init.publishPort)) }}{{ end }} - {{ $x := (dig "annotations" "compose.score.dev/publish-management-port" "0" .Metadata | atoi) }} - {{ if ne .Init.publishManagementPort 0 }}{{ $publishPorts = (append $publishPorts (dict "target" 15672 "published" .Init.publishManagementPort)) }}{{ end }} - publishPorts: {{ $publishPorts | toJson }} - volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local - files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.vhost }}.sh: | - while ! rabbitmqctl list_vhosts > /dev/null 2>&1; do - sleep 1 - done - rabbitmqctl list_vhosts | grep {{ .State.vhost }} || rabbitmqctl add_vhost {{ .State.vhost }} - rabbitmqctl list_users | grep {{ .State.username }} || rabbitmqctl add_user {{ .State.username }} {{ .State.password }} - rabbitmqctl set_user_tags {{ .State.username }} administrator - rabbitmqctl set_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" - rabbitmqctl set_topic_permissions -p {{ .State.vhost }} {{ .State.username }} ".*" ".*" ".*" - services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/rabbitmq:3-management-alpine - restart: always - environment: - RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} - RABBITMQ_DEFAULT_USER: guest - RABBITMQ_DEFAULT_PASS: guest - ports: {{ dig .Init.sk "publishPorts" "" .Shared | toJson}} - volumes: - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /var/lib/rabbitmq - healthcheck: - test: ["CMD-SHELL", "rabbitmq-diagnostics -q check_port_connectivity"] - interval: 2s - timeout: 5s - retries: 15 - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-init: - image: mirror.gcr.io/rabbitmq:3-management-alpine - entrypoint: ["/bin/sh"] - environment: - RABBITMQ_ERLANG_COOKIE: {{ dig .Init.sk "instanceErlangCookie" "" .Shared }} - command: - - "-c" - - | - set -exu - for s in /db-scripts/*.sh; do source $$s; done - depends_on: - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - condition: service_healthy - restart: true - labels: - dev.score.compose.labels.is-init-container: "true" - network_mode: service:{{ dig .Init.sk "instanceServiceName" "" .Shared }} - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts - target: /db-scripts - info_logs: | - {{ if ne .Init.publishManagementPort 0 }} - - "{{.Uid}}: Browse the rabbitmq UI at \"http://localhost:{{ .Init.publishManagementPort }}\"" - {{ end }} - expected_outputs: - - host - - port - - vhost - - username - - password - -# The default dns provisioner just outputs localhost as the hostname every time. -# This is because without actual control of a dns resolver we can't do any accurate routing on any other name. This -# can be replaced by a new provisioner in the future. -- uri: template://default-provisioners/dns - type: dns - description: Outputs a *.localhost domain as the hostname. - init: | - randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost - state: | - instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} - outputs: | - host: {{ .State.instanceHostname }} - expected_outputs: - - host - -# The default route provisioner sets up an nginx service with an HTTP service that can route on our prefix paths. -# It assumes the hostnames and routes provided have no overlaps. Weird behavior may happen if there are overlaps. -- uri: template://default-provisioners/route - type: route - description: Provisions a ingress route on a shared Nginx instance. - init: | - randomServiceName: routing-{{ randAlphaNum 6 }} - sk: default-provisioners-routing-instance - {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} - {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} - {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} - {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} - {{ $port := index $ports (print .Params.port) }} - {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} - - shared: | - {{ .Init.sk }}: - instancePort: {{ dig .Init.sk "instancePort" 8080 .Shared }} - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - {{ $targetHost := (index .WorkloadServices .SourceWorkload).ServiceName }} - {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} - {{ $port := index $ports (print .Params.port) }} - {{ $targetPort := $port.TargetPort }} - {{ $target := (printf "%s:%d" $targetHost $targetPort) }} - {{ $hBefore := dig .Init.sk "hosts" (dict) .Shared }} - {{ $rBefore := dig .Params.host (dict) $hBefore }} - {{ $pathType := dig "compose.score.dev/route-provisioner-path-type" "Prefix" (dig "annotations" (dict) (.Metadata | default (dict))) }} - {{ $inner := dict "path" .Params.path "target" $target "port" $targetPort "path_type" $pathType }} - {{ $rAfter := (merge $rBefore (dict .Uid $inner)) }} - {{ $hAfter := (merge $hBefore (dict .Params.host $rAfter)) }} - hosts: {{ $hAfter | toRawJson }} - files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf: | - worker_processes 1; - worker_rlimit_nofile 8192; - events { - worker_connections 4096; - } - http { - resolver 127.0.0.11; - - {{ range $h, $r := (dig .Init.sk "hosts" "" .Shared) }} - server { - listen 80; - listen [::]:80; - server_name {{ $h }}; - - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for; - proxy_set_header Proxy ""; - proxy_connect_timeout 5s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; - proxy_buffers 16 4k; - proxy_buffer_size 2k; - client_max_body_size 10m; - {{ dig "compose.score.dev/route-provisioner-server-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 4 }} - - location = /favicon.ico { - return 204; - access_log off; - log_not_found off; - } - - {{ range $k, $v := $r }} - # the basic path variant, "/" or "/one/two" - location ~ ^{{ index $v "path" }}$ { - set $backend {{ index $v "target" }}; - proxy_pass http://$backend; - {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} - } - - # The prefix match variants are included by default but can be excluded via 'compose.score.dev/route-provisioner-path-type' annotation - {{ if eq (index $v "path_type") "Prefix" }} - location ~ ^{{ index $v "path" | trimSuffix "/" }}/.* { - set $backend {{ index $v "target" }}; - proxy_pass http://$backend; - {{ dig "compose.score.dev/route-provisioner-location-snippet" "" (dig "annotations" (dict) ($.Metadata | default (dict))) | indent 6 }} - } - {{ end }} - {{ end }} - } - {{ end }} - } - - services: | - {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/nginx:1-alpine - restart: always - ports: - - published: {{ $p }} - target: 80 - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}/nginx.conf - target: /etc/nginx/nginx.conf - readOnly: true - info_logs: | - {{ $p := (dig .Init.sk "instancePort" 0 .Shared) }} - - "{{.Uid}}: To connect to this route, http://{{ .Params.host }}:{{ $p }}{{ .Params.path }} (make sure {{ .Params.host }} resolves to localhost)" - supported_params: - - host - - port - - path - -# The default mongodb provisioner adds a mongodb service to the project which returns a host, port, username, and password, and connection string. -- uri: template://default-provisioners/mongodb - type: mongodb - description: Provisions a dedicated MongoDB database. - init: | - port: 27017 - randomServiceName: mongo-{{ randAlphaNum 6 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - serviceName: {{ dig "serviceName" .Init.randomServiceName .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ .State.serviceName }} - port: {{ .Init.port }} - username: {{ .State.username | quote }} - password: {{ .State.password | quote }} - connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.serviceName }}:{{ .Init.port }}/" - volumes: | - {{ .State.serviceName }}-data: - name: {{ .State.serviceName }}-data - driver: local - labels: - dev.score.compose.res.uid: {{ .Uid }} - services: | - {{ .State.serviceName }}: - labels: - dev.score.compose.res.uid: {{ .Uid }} - image: mirror.gcr.io/mongo:8 - restart: always - environment: - MONGO_INITDB_ROOT_USERNAME: {{ .State.username | quote }} - MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | quote }} - healthcheck: - test: ["CMD-SHELL", "echo 'db.runCommand(\"ping\").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD"] - interval: 2s - timeout: 5s - retries: 15 - start_period: 10s - volumes: - - type: volume - source: {{ .State.serviceName }}-data - target: /data/db - volume: - nocopy: true - info_logs: | - - "{{.Uid}}: To connect to mongo: \"docker exec -ti {{ .ComposeProjectName }}-{{ .State.serviceName }}-1 mongosh -u {{ .State.username }} -p {{ .State.password }}\"" - expected_outputs: - - host - - port - - username - - password - - connection - -# The default mysql provisioner adds a mysql instance and then ensures that the required databases are created on -# startup. -- uri: template://default-provisioners/mysql - # By default, match all mysql types regardless of class and id. If you want to override this, create another - # provisioner definition with a higher priority. - type: mysql - description: Provisions a dedicated MySQL database on a shared instance. - # Init template has the random service name and password if needed later - init: | - randomServiceName: mysql-{{ randAlphaNum 6 }} - randomDatabase: db{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - randomRootPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-mysql-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} - # The state for each database resource is a unique db name and credentials - state: | - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - # All instances agree on the shared state since there is no concurrency here - shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" .Init.randomServiceName .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" .Init.randomPassword .Shared | quote }} - instanceRootPassword: {{ dig .Init.sk "instanceRootPassword" .Init.randomRootPassword .Shared | quote }} - # The outputs are the core database outputs. We output both name and database for broader compatibility. - outputs: | - host: {{ dig .Init.sk "instanceServiceName" "" .Shared }} - port: 3306 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ .State.password }} - # Write out an idempotent create script per database - files: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts/{{ .State.database }}.sql: | - CREATE DATABASE IF NOT EXISTS {{ .State.database }}; - USE {{ .State.database }}; - CREATE USER IF NOT EXISTS '{{ .State.username }}'@'localhost' IDENTIFIED BY '{{ .State.password }}'; - GRANT ALL PRIVILEGES ON {{ .State.database }} TO '{{ .State.username }}'@'localhost'; - FLUSH PRIVILEGES; - # Ensure the data volume exists - volumes: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data: - driver: local - # Create an services with the database itself - services: | - {{ dig .Init.sk "instanceServiceName" "" .Shared }}: - image: mirror.gcr.io/mysql:8 - restart: always - environment: - MYSQL_DATABASE: {{ .State.database }} - MYSQL_USER: {{ .State.username }} - MYSQL_PASSWORD: {{ dig .Init.sk "instancePassword" "" .Shared | quote }} - MYSQL_ROOT_PASSWORD: {{ dig .Init.sk "instanceRootPassword" "" .Shared | quote }} - {{ if ne .Init.publishPort "0" }} - ports: - - target: 3306 - published: {{ .Init.publishPort }} - {{ end }} - volumes: - - type: bind - source: {{ .MountsDirectory }}/{{ dig .Init.sk "instanceServiceName" "" .Shared }}-db-scripts - target: /docker-entrypoint-initdb.d/ - - type: volume - source: {{ dig .Init.sk "instanceServiceName" "" .Shared }}-data - target: /var/lib/mysql - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p$${MYSQL_ROOT_PASSWORD}"] - interval: 5s - timeout: 3s - retries: 10 - info_logs: | - - "{{.Uid}}: To connect to mysql, enter password {{ .State.password | squote }} at: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mysql:8 mysql -h {{ dig .Init.sk "instanceServiceName" "" .Shared }} -u {{ .State.username }} -p\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your mysql client to \" - mysql://{{ .State.username }}:{{ .State.password }}@{{ dig .Init.sk "instanceServiceName" "" .Shared }}:{{ .Init.publishPort }}/{{ .State.database }}\"" - {{ end }} - expected_outputs: - - host - - port - - name - - database - - username - - password - -- uri: template://default-provisioners/kafka-topic - type: kafka-topic - description: Provisions a dedicated Kafka topic on a shared Kafka broker. - init: | - brokerPort: 9092 - ctrlPort: 9093 - state: | - topic: {{ dig "topic" (print "topic-" (randAlphaNum 6)) .State | quote }} - shared: | - shared_kafka_instance_name: {{ dig "shared_kafka_instance_name" (print "kafka-" (randAlphaNum 6)) .Shared | quote }} - services: | - {{ .Shared.shared_kafka_instance_name }}: - image: bitnami/kafka:latest - restart: always - environment: - KAFKA_CFG_NODE_ID: "0" - KAFKA_CFG_PROCESS_ROLES: controller,broker - KAFKA_CFG_LISTENERS: "PLAINTEXT://:{{ .Init.brokerPort }},CONTROLLER://:{{ .Init.ctrlPort }}" - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT" - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: "0@{{ .Shared.shared_kafka_instance_name }}:{{ .Init.ctrlPort }}" - KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "false" - healthcheck: - test: ["CMD", "kafka-topics.sh", "--list", "--bootstrap-server=localhost:{{ .Init.brokerPort }}"] - interval: 2s - timeout: 2s - retries: 10 - {{ $publishPort := (dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | atoi) }} - {{ if ne $publishPort 0 }} - ports: - - target: {{ .Init.brokerPort }} - published: {{ $publishPort }} - {{ end }} - volumes: - - type: volume - source: {{ .Shared.shared_kafka_instance_name }}-data - target: /bitnami/kafka - {{ .State.topic }}-init: - image: bitnami/kafka:latest - entrypoint: ["/bin/sh"] - command: ["-c", "kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --describe || kafka-topics.sh --topic={{.State.topic}} --bootstrap-server=localhost:{{ .Init.brokerPort }} --create --partitions=3"] - network_mode: "service:{{ .Shared.shared_kafka_instance_name }}" - labels: - dev.score.compose.labels.is-init-container: "true" - depends_on: - {{ .Shared.shared_kafka_instance_name }}: - condition: service_healthy - restart: true - volumes: | - {{ .Shared.shared_kafka_instance_name }}-data: - driver: local - outputs: | - host: {{ .Shared.shared_kafka_instance_name }} - port: "{{ .Init.brokerPort }}" - name: {{ .State.topic }} - num_partitions: 3 - expected_outputs: - - host - - port - - name - - num_partitions - -# The default elasticsearch provisioner adds a elasticsearch instance. -- uri: template://default-provisioners/elasticsearch - # By default, match all elasticsearch types regardless of class and id. - # If you want to override this, create another provisioner definition with a higher priority. - type: elasticsearch - description: Provisions a dedicated Elastic Search instance. - # Init template has the random service name and password if needed later - init: | - serviceName: elasticsearch - randomPassword: {{ randAlphaNum 16 | quote }} - clusterName: cluster-ecs-{{ randAlphaNum 6 }} - username: elastic - sk: default-provisioners-elasticsearch-instance - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "9200" .Metadata | quote }} - license: {{ dig "annotations" "compose.score.dev/license" "basic" .Metadata | quote }} - stackVersion: {{ dig "annotations" "compose.score.dev/stack-version" "8.14.0" .Metadata | quote }} - esMemLimit: {{ dig "annotations" "compose.score.dev/es-mem-limit" "1073741824" .Metadata | quote }} - # The state for each elasticsearch resource is a unique host, port, and credentials - state: | - clusterName: {{ dig "clusterName" .Init.clusterName .State | quote }} - username: {{ dig "username" .Init.username .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - host: {{ dig "host" .Init.serviceName .State | quote }} - outputs: | - host: {{ .State.host }} - port: {{ .Init.publishPort }} - username: {{ .State.username | quote }} - password: {{ .State.password | quote }} - # Ensure the data volume exists - volumes: | - ecscerts: - driver: local - ecsdata: - driver: local - # Create 2 services, the first is the setup container which creates the certificates, the second is the elasticsearch itself - services: | - setup: - image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} - volumes: - - type: volume - source: ecscerts - target: /usr/share/elasticsearch/config/certs - user: "0" - command: - - "bash" - - "-c" - - | - if [ ! -f config/certs/ca.zip ]; then - echo "Creating CA"; - bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip; - unzip config/certs/ca.zip -d config/certs; - fi; - if [ ! -f config/certs/certs.zip ]; then - echo "Creating certs"; - echo -ne \ - "instances:\n"\ - " - name: {{ .State.host }}\n"\ - " dns:\n"\ - " - {{ .State.host }}\n"\ - " - localhost\n"\ - " ip:\n"\ - " - 127.0.0.1\n"\ - > config/certs/instances.yml; - bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt --ca-key config/certs/ca/ca.key; - unzip config/certs/certs.zip -d config/certs; - fi; - echo "Setting file permissions" - chown -R root:root config/certs; - find . -type d -exec chmod 750 \{\} \;; - find . -type f -exec chmod 640 \{\} \;; - echo "Waiting for Elasticsearch availability"; - until curl -s --cacert config/certs/ca/ca.crt https://{{ .State.host }}:9200 | grep -q "missing authentication credentials"; do sleep 10; done; - echo "All done!"; - healthcheck: - test: ["CMD-SHELL", "[ -f config/certs/{{ .State.host }}/{{ .State.host }}.crt ]"] - interval: 1s - timeout: 5s - retries: 120 - {{ .State.host }}: - depends_on: - setup: - condition: service_healthy - image: docker.elastic.co/elasticsearch/elasticsearch:{{ .Init.stackVersion }} - labels: - co.elastic.logs/module: elasticsearch - volumes: - - type: volume - source: ecscerts - target: /usr/share/elasticsearch/config/certs - - type: volume - source: ecsdata - target: /usr/share/elasticsearch/data - ports: - - target: 9200 - published: {{ .Init.publishPort }} - environment: - - node.name={{ .State.host }} - - cluster.name={{ .State.clusterName }} - - discovery.type=single-node - - bootstrap.memory_lock=true - - ELASTIC_PASSWORD={{ .State.password }} - - xpack.security.enabled=true - - xpack.security.http.ssl.enabled=true - - xpack.security.http.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key - - xpack.security.http.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt - - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt - - xpack.security.transport.ssl.enabled=true - - xpack.security.transport.ssl.key=certs/{{ .State.host }}/{{ .State.host }}.key - - xpack.security.transport.ssl.certificate=certs/{{ .State.host }}/{{ .State.host }}.crt - - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt - - xpack.security.transport.ssl.verification_mode=certificate - - xpack.license.self_generated.type={{ .Init.license }} - mem_limit: {{ .Init.esMemLimit }} - ulimits: - memlock: - soft: -1 - hard: -1 - healthcheck: - test: - [ - "CMD-SHELL", - "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", - ] - interval: 10s - timeout: 10s - retries: 120 - info_logs: | - - "{{.Uid}}: To connect to elasticsearch:\n - download certificate from container per command like: \n - \tdocker cp [CONTAINER-NAME]:/usr/share/elasticsearch/config/certs/ca/ca.crt /tmp/ \n - and than check connection per culr like: \n - \tcurl --cacert /tmp/ca.crt -u {{ .State.username }}:{{ .State.password }} https://localhost:{{ .Init.publishPort }}" - expected_outputs: - - host - - port - - username - - password - -- uri: template://default-provisioners/mssql - type: mssql - description: Provisions a dedicated database on a shared MS SQL server instance. - init: | - randomPassword: {{ randAlphaNum 16 | quote }} - sk: default-provisioners-mssql - publishPort: {{ dig "annotations" "compose.score.dev/publish-port" "0" .Metadata | quote }} - state: | - service: {{ .Init.sk }} - database: master - username: sa - password: {{ dig "password" .Init.randomPassword .State | quote }} - publishPort: {{ .Init.publishPort }} - outputs: | - server: {{ .State.service }} - port: {{ .State.publishPort }} - connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};" - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ .State.password }} - volumes: | - {{ .Init.sk }}-data: - driver: local - services: | - {{ .Init.sk }}: - image: mcr.microsoft.com/mssql/server:latest - restart: always - environment: - ACCEPT_EULA: "Y" - MSSQL_ENABLE_HADR: "1" - MSSQL_AGENT_ENABLED: "1" - MSSQL_SA_PASSWORD: {{ .State.password }} - {{ if ne .Init.publishPort "0" }} - ports: - - target: 1433 - published: {{ .Init.publishPort }} - {{ end }} - volumes: - - type: volume - source: {{ .Init.sk }}-data - target: /var/opt/mssql - info_logs: | - - "{{.Uid}}: To connect to mssql: \"docker run -it --network {{ .ComposeProjectName }}_default --rm mcr.microsoft.com/mssql/server:latest mysql /opt/mssql-tools/bin/sqlcmd -S localhost -U {{ .State.username }} -p {{ .State.password | squote }}\"" - {{ if ne .Init.publishPort "0" }} - - "{{.Uid}}: Or connect your mssql client to \" - Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ .State.password }};\"" - {{ end }} - expected_outputs: - - server - - port - - connection - - database - - username - - password diff --git a/gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml b/gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml deleted file mode 100644 index 180f6a73..00000000 --- a/gen/external-content/resource-provisioners/default/score-k8s/zz-default.provisioners.yaml +++ /dev/null @@ -1,1390 +0,0 @@ -# This example provisioner is a fake resource type used to demonstrate the template provisioner mechanism. The URI should -# be a unique indicator of the provisioner. The scheme indicates how it is executed. -- uri: template://example-provisioners/example-provisioner - # (Required) Which resource type to match - type: example-provisioner-resource - description: Example provisioner that runs a template. - # (Optional) Which 'class' of the resource. Null will match any class, a non-empty value like 'default' will match - # only resources of that class. - class: null - # (Optional) The exact resource id to match. Null will match any resource, a non-empty value will only match - # the resource with exact same id. - id: null - # (Optional) The init template sets the initial context values on each provision request. This is a text template - # that must evaluate to a YAML/JSON key-value map. - init: | - key: value - # sprig functions are also supported - key2: {{ print "value" | upper }} - # other attributes are available such as Type, Class, Id, Uid, Guid. - my-uid: "{{ .Uid }}#{{ .Guid }}" - # (Optional) The state template gets evaluated next and sets the internal state of this resource based on the previous - # state and the init context. Like init, this evaluates to a YAML/JSON object. This is the template that allows - # state to be stored between each generate call. - state: | - stateKey: {{ .Init.key }} # will copy the value from init - stateKey2: {{ default 0 .State.stateKey2 | add 1 }} # will increment on each provision attempt - # (Optional) The shared state template is like state, but is a key-value structure shared between all resources. - # This can be used to coordinate shared resources and state between resources of the same or related types. - shared: | - section: - key: {{ .Shared.foo }} - # (Optional) The outputs template gets evaluated last and translates into the outputs available as placeholder - # references like ${resources.my-resource.key}. - outputs: | - plaintext: my-value - nested: - example: thing - # Instead of returning secret outputs as plaintext. They can be embedded as reference to Kubernetes Secrets. When - # these are detected, they can be used in environment variables or file contents securely. The format is - # 🔐💬_💬🔐. For example, the next line refers to a secret created in the manifests. - secret-reference: 🔐💬secret-{{ .Guid }}_password💬🔐 - # A template function also exists for generating these in the template provisioner. - secret-reference-alt: {{ encodeSecretRef (printf "secret-%s" .Guid) "password" }} - expected_outputs: - - plaintext - - nested - - secret-reference - - secret-reference-alt - # (Optional) The manifests template gets evaluated as a list of Kubernetes object manifests to be added to the output. - manifests: | - - apiVersion: v1 - kind: ConfigMap - metadata: - name: cfg-{{ .Guid }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - data: - key: {{ .Init.key }} - - apiVersion: v1 - kind: Secret - metadata: - name: secret-{{ .Guid }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - password: {{ b64enc "my-secret-password" }} - -# The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts -# with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory. -- uri: cmd://bash#example-provisioner - type: example-provisioner-resource - description: Example provisioner that runs a bash script. - class: default - id: specific - # (Optional) additional args that the binary gets run with - # If any of the args are '' it will be replaced with "provision" - args: ["-c", "echo '{\"resource_outputs\":{\"key\":\"value\",\"secret\":\"🔐💬mysecret_mykey💬🔐\"},\"manifests\":[]}'"] - expected_outputs: - - key - - secret - -# The default provisioner for service resources, this expects a workload and port name and will return the hostname and -# port required to contact it. This will validate that the workload and port exist, but won't enforce a dependency -# relationship yet. -- uri: template://default-provisioners/service-port - type: service-port - description: Outputs a hostname and port for connecting to another workload. - outputs: | - {{ if not .Params.workload }}{{ fail "expected 'workload' param for the target workload name" }}{{ end }} - {{ if not .Params.port }}{{ fail "expected 'port' param for the name of the target workload service port" }}{{ end }} - {{ if or (not $w) (not $w.ServiceName) }}{{ fail "unknown workload" }}{{ end }} - {{ if not $w }}{{ fail "unknown workload" }}{{ end }} - {{ $p := (index $w.Ports .Params.port) }} - {{ if not $p }}{{ fail "unknown service port" }}{{ end }} - hostname: {{ $w.ServiceName | quote }} - port: {{ $p.TargetPort }} - expected_outputs: - - hostname - - port - supported_params: - - workload - - port - -# As an example we have a 'volume' type which returns an emptyDir volume. -# In production or for real applications you may want to replace this with a provisioner for a tmpfs, host path, or -# persistent volume and claims. -- uri: template://default-provisioners/volume - type: volume - description: Creates a persistent volume that can be mounted on a workload. - outputs: | - source: - emptyDir: {} - expected_outputs: - - source - -# The default dns provisioner just outputs a random localhost domain because we don't know whether external-dns is -# available. You should replace this with your own dns name generation that matches your external-dns controller. -- uri: template://default-provisioners/dns - type: dns - description: Outputs a *.localhost domain as the hostname. - init: | - randomHostname: dns{{ randAlphaNum 6 | lower }}.localhost - state: | - instanceHostname: {{ dig "instanceHostname" .Init.randomHostname .State | quote }} - outputs: | - host: {{ .State.instanceHostname }} - expected_outputs: - - host - -# Routes could be implemented as either traditional ingress resources or using the newer gateway API. -# In this default provisioner we use the gateway API with some sensible defaults. But you may wish to replace this. -- uri: template://default-provisioners/route - type: route - description: Provisions an HTTPRoute on a shared Nginx instance. - init: | - {{ if not (regexMatch "^/|(/([^/]+))+$" .Params.path) }}{{ fail "params.path start with a / but cannot end with /" }}{{ end }} - {{ if not (regexMatch "^[a-z0-9_.-]{1,253}$" .Params.host) }}{{ fail (cat "params.host must be a valid hostname but was" .Params.host) }}{{ end }} - {{ $ports := (index .WorkloadServices .SourceWorkload).Ports }} - {{ if not $ports }}{{ fail "no service ports exist" }}{{ end }} - {{ $port := index $ports (print .Params.port) }} - {{ if not $port.TargetPort }}{{ fail "params.port is not a named service port" }}{{ end }} - state: | - routeName: route-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - manifests: | - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: {{ .State.routeName }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.routeName }} - app.kubernetes.io/instance: {{ .State.routeName }} - spec: - parentRefs: - - name: default - hostnames: - - {{ .Params.host | quote }} - rules: - - matches: - - path: - type: PathPrefix - value: {{ .Params.path | quote }} - backendRefs: - - name: {{ (index .WorkloadServices .SourceWorkload).ServiceName }} - port: {{ .Params.port }} - supported_params: - - host - - port - - path - -- uri: template://default-provisioners/postgres - type: postgres - description: Provisions a dedicated database on a shared PostgreSQL instance. - init: | - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ .State.service }} - port: 5432 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "password" }} - manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - password: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: - app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: postgres-db - image: mirror.gcr.io/postgres:17-alpine - ports: - - name: postgres - containerPort: 5432 - env: - - name: PGDATA - value: /var/lib/postgresql/data/pgdata - - name: POSTGRES_USER - value: {{ .State.username | quote }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: password - - name: POSTGRES_DB - value: {{ .State.database | quote }} - volumeMounts: - - name: pv-data - mountPath: /var/lib/postgresql/data - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - privileged: false - capabilities: - drop: - - ALL - readinessProbe: - exec: - command: - - pg_isready - - -U - - {{ .State.username | quote }} - - -d - - {{ .State.database | quote }} - periodSeconds: 3 - securityContext: - runAsNonRoot: true - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - volumeClaimTemplates: - - metadata: - name: pv-data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 5432 - targetPort: 5432 - expected_outputs: - - host - - port - - name - - database - - username - - password - -- uri: template://default-provisioners/postgres-instance - type: postgres-instance - description: Provisions a dedicated PostgreSQL instance. - init: | - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - service: pg-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ .State.service }} - port: 5432 - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "password" }} - manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - password: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: - app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: postgres-db - image: mirror.gcr.io/postgres:17-alpine - ports: - - name: postgres - containerPort: 5432 - env: - - name: PGDATA - value: /var/lib/postgresql/data/pgdata - - name: POSTGRES_USER - value: {{ .State.username | quote }} - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: password - volumeMounts: - - name: pv-data - mountPath: /var/lib/postgresql/data - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - privileged: false - capabilities: - drop: - - ALL - readinessProbe: - exec: - command: - - pg_isready - - -U - - {{ .State.username | quote }} - periodSeconds: 3 - securityContext: - runAsNonRoot: true - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - volumeClaimTemplates: - - metadata: - name: pv-data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 5432 - targetPort: 5432 - expected_outputs: - - host - - port - - username - - password - -- uri: template://default-provisioners/redis - type: redis - description: Provisions a dedicated Redis instance. - init: | - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - service: redis-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - username: default - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ .State.service }} - port: 6379 - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "password" }} - manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - password: {{ .State.password | b64enc }} - redis.conf: {{ printf "requirepass %s\nport 6379\nsave 60 1\nloglevel warning\n" .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: - app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: redis - image: mirror.gcr.io/redis:7-alpine - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - privileged: false - readOnlyRootFilesystem: true - ports: - - name: redis - containerPort: 6379 - volumeMounts: - - name: redis-data - mountPath: /data - - name: config - mountPath: /usr/local/etc/redis - readinessProbe: - exec: - command: - - redis-cli - - ping - periodSeconds: 3 - securityContext: - fsGroup: 1000 - runAsGroup: 1000 - runAsNonRoot: true - runAsUser: 1000 - seccompProfile: - type: RuntimeDefault - volumes: - - name: config - secret: - secretName: {{ .State.service }} - items: - - key: redis.conf - path: redis.conf - volumeClaimTemplates: - - metadata: - name: redis-data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 6379 - targetPort: 6379 - expected_outputs: - - host - - port - - username - - password - -- uri: template://default-provisioners/mysql - type: mysql - description: Provisions a dedicated MySQL database on a shared instance. - init: | - randomDatabase: db-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - service: mysql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - database: {{ dig "database" .Init.randomDatabase .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ .State.service }} - port: 3306 - name: {{ .State.database }} - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "MYSQL_PASSWORD" }} - manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - MYSQL_PASSWORD: {{ .State.password | b64enc }} - MYSQL_ROOT_PASSWORD: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: - app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - containers: - - name: mysql-db - image: mirror.gcr.io/mysql:8 - ports: - - name: mysql - containerPort: 3306 - env: - - name: MYSQL_USER - value: {{ .State.username | quote }} - - name: MYSQL_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MYSQL_PASSWORD - - name: MYSQL_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MYSQL_ROOT_PASSWORD - - name: MYSQL_DATABASE - value: {{ .State.database | quote }} - volumeMounts: - - name: data - mountPath: /var/lib/mysql - readinessProbe: - exec: - command: - - mysqladmin - - ping - - -h - - localhost - periodSeconds: 3 - volumeClaimTemplates: - - metadata: - name: data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 3306 - targetPort: 3306 - expected_outputs: - - host - - port - - name - - database - - username - - password - -- uri: template://default-provisioners/mongo - type: mongodb - description: Provisions a dedicated MongoDB database. - init: | - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - service: mongo-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ .State.service }} - port: 27017 - connection: "mongodb://{{ .State.username }}:{{ .State.password }}@{{ .State.service }}:27017/" - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "MONGO_INITDB_ROOT_PASSWORD" }} - manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - MONGO_INITDB_ROOT_PASSWORD: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: - app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - automountServiceAccountToken: false - containers: - - name: mongo-db - image: mirror.gcr.io/mongo:8 - ports: - - name: mongo - containerPort: 27017 - env: - - name: MONGO_INITDB_ROOT_USERNAME - value: {{ .State.username | quote }} - - name: MONGO_INITDB_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MONGO_INITDB_ROOT_PASSWORD - livenessProbe: - exec: - command: - - /bin/sh - - -c - - echo 'db.runCommand("ping").ok' | mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD - initialDelaySeconds: 30 - timeoutSeconds: 5 - periodSeconds: 20 - securityContext: - runAsUser: 1001 - runAsGroup: 1001 - allowPrivilegeEscalation: false - privileged: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - volumeMounts: - - name: data - mountPath: /data/db - - name: tmp - mountPath: /tmp - securityContext: - runAsNonRoot: true - fsGroup: 1001 - seccompProfile: - type: RuntimeDefault - volumes: - - name: tmp - emptyDir: {} - volumeClaimTemplates: - - metadata: - name: data - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 27017 - targetPort: 27017 - expected_outputs: - - host - - port - - username - - password - - connection - -- uri: template://default-provisioners/rabbitmq - type: amqp - description: Provisions a dedicated RabbitMQ vhost on a shared instance. - init: | - randomVHost: vhost-{{ randAlpha 8 }} - randomUsername: user-{{ randAlpha 8 }} - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - service: rabbitmq-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - vhost: {{ dig "vhost" .Init.randomVHost .State | quote }} - username: {{ dig "username" .Init.randomUsername .State | quote }} - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - host: {{ .State.service }} - port: 5672 - vhost: {{ .State.vhost }} - username: {{ .State.username }} - password: {{ .State.password }} - manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }}-secret - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }}-secret - app.kubernetes.io/instance: {{ .State.service }} - data: - RABBITMQ_DEFAULT_VHOST: {{ .State.vhost | b64enc }} - RABBITMQ_DEFAULT_USER: {{ .State.username | b64enc }} - RABBITMQ_DEFAULT_PASS: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - serviceName: {{ .State.service }} - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - containers: - - name: rabbitmq - image: mirror.gcr.io/rabbitmq:3-management-alpine - ports: - - name: amqp - containerPort: 5672 - - name: management - containerPort: 15672 - envFrom: - - secretRef: - name: {{ .State.service }}-secret - volumeMounts: - - name: data - mountPath: /var/lib/rabbitmq - readinessProbe: - exec: - command: - - rabbitmq-diagnostics - - -q - - check_port_connectivity - periodSeconds: 3 - initialDelaySeconds: 30 - timeoutSeconds: 5 - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 3Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - ports: - - port: 5672 - targetPort: 5672 - name: amqp - - port: 15672 - targetPort: 15672 - name: management - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - expected_outputs: - - host - - port - - vhost - - username - - password - -- uri: template://default-provisioners/mssql - type: mssql - description: Provisions a dedicated database on a shared MS SQL server instance. - init: | - randomPassword: {{ randAlphaNum 16 | quote }} - state: | - service: mssql-{{ .SourceWorkload }}-{{ substr 0 8 .Guid | lower }} - database: master - username: sa - password: {{ dig "password" .Init.randomPassword .State | quote }} - outputs: | - server: {{ .State.service }} - port: 1433 - connection: "Server=tcp:{{ .State.service }},1433;Initial Catalog={{ .State.database }};User ID={{ .State.username }};Password={{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }}" - database: {{ .State.database }} - username: {{ .State.username }} - password: {{ encodeSecretRef .State.service "MSSQL_SA_PASSWORD" }} - manifests: | - - apiVersion: v1 - kind: Secret - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - data: - MSSQL_SA_PASSWORD: {{ .State.password | b64enc }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - replicas: 1 - serviceName: {{ .State.service }} - selector: - matchLabels: - app.kubernetes.io/instance: {{ .State.service }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - spec: - containers: - - name: mssql-db - image: mcr.microsoft.com/mssql/server:latest - ports: - - name: mssql - containerPort: 1433 - env: - - name: ACCEPT_EULA - value: "Y" - - name: MSSQL_ENABLE_HADR - value: "1" - - name: MSSQL_AGENT_ENABLED - value: "1" - - name: MSSQL_SA_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .State.service }} - key: MSSQL_SA_PASSWORD - volumeMounts: - - name: mssql - mountPath: "/var/opt/mssql" - volumeClaimTemplates: - - metadata: - name: mssql - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Service - metadata: - name: {{ .State.service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - annotations: - k8s.score.dev/source-workload: {{ .SourceWorkload }} - k8s.score.dev/resource-uid: {{ .Uid }} - k8s.score.dev/resource-guid: {{ .Guid }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ .State.service }} - app.kubernetes.io/instance: {{ .State.service }} - spec: - selector: - app.kubernetes.io/instance: {{ .State.service }} - type: ClusterIP - ports: - - port: 1433 - targetPort: 1433 - expected_outputs: - - server - - port - - connection - - database - - username - - password - -# This resource provides an in-cluster minio based S3 bucket with AWS-style credentials. -# This provides some common and well known outputs that can be used with any generic AWS s3 client. -# The outputs of the provisioner are a stateful set, a service, a secret, and a job per bucket. -- uri: template://default-provisioners/s3 - type: s3 - description: Provisions a dedicated S3 bucket with AWS-style credentials on a shared MinIO instance. - # The init template contains some initial seed data that can be used t needed. - init: | - sk: default-provisioners-minio-instance - state: | - bucket: {{ dig "bucket" (printf "bucket-%s" .Guid) .State | quote }} - shared: | - {{ .Init.sk }}: - instanceServiceName: {{ dig .Init.sk "instanceServiceName" (randAlpha 7 | lower | printf "minio-%s") .Shared | quote }} - instanceUsername: {{ dig .Init.sk "instanceUsername" (randAlpha 7 | printf "user-%s") .Shared | quote }} - instancePassword: {{ dig .Init.sk "instancePassword" (randAlphaNum 16) .Shared | quote }} - instanceAccessKeyId: {{ dig .Init.sk "instanceAccessKeyId" (randAlphaNum 20) .Shared | quote }} - instanceSecretKey: {{ dig .Init.sk "instanceSecretKey" (randAlphaNum 40) .Shared | quote }} - outputs: | - {{ $shared := dig .Init.sk (dict) .Shared }} - {{ $service := $shared.instanceServiceName }} - bucket: {{ .State.bucket }} - access_key_id: {{ $shared.instanceAccessKeyId | quote }} - secret_key: {{ encodeSecretRef $service "secret_key" }} - endpoint: http://{{ $service }}:9000 - region: "us-east-1" - # for compatibility with Humanitec's existing s3 resource - aws_access_key_id: {{ $shared.instanceAccessKeyId | quote }} - aws_secret_key: {{ encodeSecretRef $service "secret_key" }} - manifests: | - {{ $shared := dig .Init.sk (dict) .Shared }} - {{ $service := $shared.instanceServiceName }} - - apiVersion: apps/v1 - kind: StatefulSet - metadata: - name: {{ $service | quote }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service | quote }} - app.kubernetes.io/instance: {{ $service | quote }} - spec: - replicas: 1 - serviceName: {{ $service | quote }} - selector: - matchLabels: - app.kubernetes.io/instance: {{ $service | quote }} - template: - metadata: - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service | quote }} - app.kubernetes.io/instance: {{ $service | quote }} - spec: - automountServiceAccountToken: false - containers: - - name: minio - image: quay.io/minio/minio - args: ["server", "/data", "--console-address", ":9001"] - ports: - - name: service - containerPort: 9000 - - name: console - containerPort: 9001 - env: - - name: MINIO_ROOT_USER - value: {{ $shared.instanceUsername | quote }} - - name: MINIO_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ $service | quote }} - key: password - securityContext: - runAsUser: 1000 - runAsGroup: 1000 - allowPrivilegeEscalation: false - privileged: false - capabilities: - drop: - - ALL - volumeMounts: - - name: data - mountPath: /data - securityContext: - runAsNonRoot: true - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - seccompProfile: - type: RuntimeDefault - volumeClaimTemplates: - - metadata: - name: data - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service | quote }} - app.kubernetes.io/instance: {{ $service | quote }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi - - apiVersion: v1 - kind: Secret - metadata: - name: {{ $service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service }} - app.kubernetes.io/instance: {{ $service }} - data: - password: {{ $shared.instancePassword | b64enc }} - secret_key: {{ $shared.instanceSecretKey | b64enc }} - - apiVersion: v1 - kind: Service - metadata: - name: {{ $service }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - app.kubernetes.io/name: {{ $service }} - app.kubernetes.io/instance: {{ $service }} - spec: - selector: - app.kubernetes.io/instance: {{ $service }} - type: ClusterIP - ports: - - name: service - port: 9000 - targetPort: 9000 - - name: console - port: 9001 - targetPort: 9001 - - apiVersion: batch/v1 - kind: Job - metadata: - name: {{ printf "%s-bucket-%s" $service .Guid }} - {{ if ne .Namespace "" }} - namespace: {{ .Namespace }} - {{ end }} - labels: - app.kubernetes.io/managed-by: score-k8s - spec: - template: - spec: - restartPolicy: OnFailure - containers: - - name: main - image: quay.io/minio/minio - command: - - /bin/bash - - -c - - | - set -eu - mc alias set myminio http://{{ $service }}:9000 {{ $shared.instanceUsername | quote }} $MINIO_ROOT_PASSWORD - mc admin user svcacct info myminio {{ $shared.instanceAccessKeyId | quote }} || mc admin user svcacct add myminio {{ $shared.instanceUsername | quote }} --access-key {{ $shared.instanceAccessKeyId | quote }} --secret-key $MINIO_SECRET_KEY - mc mb -p myminio/{{ .State.bucket }} - env: - - name: MINIO_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: {{ $service | quote }} - key: password - - name: MINIO_SECRET_KEY - valueFrom: - secretKeyRef: - name: {{ $service | quote }} - key: secret_key - expected_outputs: - - bucket - - access_key_id - - secret_key - - endpoint - - region - - aws_access_key_id - - aws_secret_key diff --git a/layouts/examples/list.html b/layouts/examples/list.html index 5072e4b3..b791ccf9 100644 --- a/layouts/examples/list.html +++ b/layouts/examples/list.html @@ -62,10 +62,10 @@

{{ end }} {{ if eq (index $section 2) "patch-templates" }} -

@@ -78,12 +78,54 @@

{{ end }}

{{ $excerpt := replaceRE `\[(?P
+ {{ end }} + {{ if eq (index $section 2) "resource-provisioners" }} +
+
+

+ + {{ .Title }} + +

+ {{ if .Params.source }} + {{ .Params.source | humanize }} + {{ end }} + {{ if .Params.resourceType }} + {{ .Params.resourceType }} + {{ end }} + {{ if .Params.implementation }} + {{ .Params.implementation }} + {{ end }} + {{ if .Params.provisionerType }} + {{ .Params.provisionerType }} + {{ end }} + {{ if .Params.tool }} + {{ .Params.tool }} + {{ end }} +
+ {{ $excerpt := replaceRE `\[(?P
{{ end }} {{ end }} diff --git a/layouts/partials/examples/filters.html b/layouts/partials/examples/filters.html index 14c391d8..5064aa60 100644 --- a/layouts/partials/examples/filters.html +++ b/layouts/partials/examples/filters.html @@ -132,4 +132,113 @@

{{ index $labels 0 }}

{{ end }} {{ end }} + {{ if eq $sectionWithoutBase "resource-provisioners" }} + {{ $sectionMetadata := index $metadata "resource-provisioners" }} +
+ {{ with site.GetPage .section }} +
+

{{ index $labels 0 }}

+ {{ range (index $sectionMetadata "source") }} +
+ + +
+ {{ end }} +
+
+

{{ index $labels 1 }}

+ {{ range (index $sectionMetadata "implementation") }} +
+ + +
+ {{ end }} +
+
+

{{ index $labels 2 }}

+ {{ range (index $sectionMetadata "provisionerType") }} +
+ + +
+ {{ end }} +
+
+

{{ index $labels 3 }}

+ {{ range (index $sectionMetadata "resourceType") }} +
+ + +
+ {{ end }} +
+
+

{{ index $labels 4 }}

+ {{ range (index $sectionMetadata "flavor") }} +
+ + +
+ {{ end }} +
+
+

{{ index $labels 5 }}

+ {{ range (index $sectionMetadata "tool") }} +
+ + +
+ {{ end }} +
+ {{ end }} +
+ {{ end }} \ No newline at end of file diff --git a/layouts/shortcodes/example-file.html b/layouts/shortcodes/example-file.html index 9aa8fe1a..e7b42eb8 100644 --- a/layouts/shortcodes/example-file.html +++ b/layouts/shortcodes/example-file.html @@ -34,6 +34,26 @@ "currentPage" $.Page ) -}} {{ end }} + {{ if eq (index (split (.Get "dir") "/") 0) "resource-provisioners" }} + {{ $updatedDir := delimit (after 2 (split (.Get "dir") "/")) "/" }} + {{ $updatedFolder := print "/" $updatedDir "/" (.Get "filename") }} + {{ if eq (index (split (.Get "dir") "/") 1) "default" }} + {{- partial "content/code-block" ( dict + "file" $folder + "gitHubUrl" (.Get "githubUrl" | default $exampleLibraryGitHubBaseUrl) + "currentPage" $.Page + ) -}} + {{ else }} + {{- partial "content/code-block" ( dict + "file" $folder + "gitHubUrl" (print + (.Get "githubUrl" | default $exampleLibraryGitHubBaseUrl) + ( strings.TrimPrefix $exampleLibrarySourcePath $updatedFolder ) + ) + "currentPage" $.Page + ) -}} + {{ end }} + {{ end }} {{ else }} {{ $dirSegments := split (.Get "dir") "/" }} {{ $filteredSegments := slice }} diff --git a/layouts/shortcodes/resource-provisioner-content.html b/layouts/shortcodes/resource-provisioner-content.html new file mode 100644 index 00000000..1646a9b6 --- /dev/null +++ b/layouts/shortcodes/resource-provisioner-content.html @@ -0,0 +1,15 @@ +
{{ .Get "description" }}
+
+type: {{ .Get "type" }}
+{{- if .Get "supportedParams" }}
+supported_params:
+{{- range split (.Get "supportedParams") "," }}
+  - {{ trim . " " }}
+{{- end }}
+{{- end }}
+{{- if .Get "expectedOutputs" }}
+expected_outputs:
+{{- range split (.Get "expectedOutputs") "," }}
+  - {{ trim . " " }}
+{{- end }}
+{{- end }}
\ No newline at end of file From da2fd88aa9d9730f6f5a9e692785c28390a6b2d5 Mon Sep 17 00:00:00 2001 From: Tobias Babin Date: Tue, 28 Oct 2025 10:02:18 +0100 Subject: [PATCH 6/7] Fixed merge conflict in README.md Signed-off-by: Tobias Babin --- README.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f0870e37..4fa9dad5 100644 --- a/README.md +++ b/README.md @@ -104,22 +104,16 @@ The commands for the initial integration of the repos are: ```bash git remote add -f -t main --no-tags examples https://github.com/score-spec/examples.git git remote add -f -t main --no-tags community-provisioners https://github.com/score-spec/community-provisioners.git -<<<<<<< HEAD git remote add -f -t main --no-tags score-compose https://github.com/score-spec/score-compose.git git remote add -f -t main --no-tags score-k8s https://github.com/score-spec/score-k8s.git -git read-tree --prefix=gen/external-content/score/specification -u examples/main:specification -git read-tree --prefix=gen/external-content/score/resources/default-provisioners -u examples/main:resources -git read-tree --prefix=gen/external-content/score/resources/community-provisioners -u community-provisioners/main -git read-tree --prefix=gen/external-content/resource-provisioners/community -u community-provisioners/main -git read-tree --prefix=gen/external-content/resource-provisioners/default/score-compose -u score-compose/main:internal/command -git read-tree --prefix=gen/external-content/resource-provisioners/default/score-k8s -u score-k8s/main:internal/provisioners/default -======= git remote add -f -t main --no-tags patch-templates https://github.com/score-spec/community-patchers.git git read-tree --prefix=gen/external-content/score/specification -u examples/main:specification +git read-tree --prefix=gen/external-content/resource-provisioners/default/score-compose -u score-compose/main:internal/command git read-tree --prefix=gen/external-content/score/resources/default-provisioners -u examples/main:resources git read-tree --prefix=gen/external-content/score/resources/community-provisioners -u community-provisioners/main +git read-tree --prefix=gen/external-content/resource-provisioners/default/score-k8s -u score-k8s/main:internal/provisioners/default +git read-tree --prefix=gen/external-content/resource-provisioners/community -u community-provisioners/main git read-tree --prefix=gen/external-content/patch-templates -u patch-templates/main ->>>>>>> main git add gen/external-content git commit -s -S -m "Integrating external content" ``` From f3a83720466550359b6b202c42a164b246448f50 Mon Sep 17 00:00:00 2001 From: Tobias Babin Date: Wed, 29 Oct 2025 16:31:47 +0100 Subject: [PATCH 7/7] Adjusted examples flavor calculation Signed-off-by: Tobias Babin --- .../patch-templates/score-k8s/knative-service.md | 12 ++++++++++++ .../dns/score-compose/cmd/dns-in-codespace.md | 2 +- .../community/dns/score-k8s/cmd/dns-in-codespace.md | 2 +- .../redis/score-k8s/cmd/helm-template-redis.md | 2 +- .../redis/score-k8s/cmd/helm-upgrade-redis.md | 2 +- .../score-k8s/cmd/example-provisioner.md | 2 +- data/examplesMeta.yml | 7 +++++-- gen/examples-site/frontmatter-provisioners.js | 11 +++++++---- 8 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 content/en/examples/patch-templates/score-k8s/knative-service.md diff --git a/content/en/examples/patch-templates/score-k8s/knative-service.md b/content/en/examples/patch-templates/score-k8s/knative-service.md new file mode 100644 index 00000000..e60c1f88 --- /dev/null +++ b/content/en/examples/patch-templates/score-k8s/knative-service.md @@ -0,0 +1,12 @@ +--- +title: "Knative Service" +draft: false +mermaid: true +type: examples +excerpt: '' +hasMore: false +parent: "score-k8s" + +--- + +{{% example-file filename="knative-service.tpl" dir="patch-templates/score-k8s" githubUrl="https://github.com/score-spec/community-patchers/blob/main" %}} diff --git a/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md b/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md index 4f4526c2..7243aa16 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md +++ b/content/en/examples/resource-provisioners/community/dns/score-compose/cmd/dns-in-codespace.md @@ -7,7 +7,7 @@ source: "community" implementation: "score-compose" resourceType: "dns" provisionerType: "cmd" -flavor: "dns" +flavor: "dns-in-codespace" excerpt: 'Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' description: 'Get the forwarded port URL in current GitHub Codespace on port 8080' diff --git a/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md b/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md index 2544c8db..c67b77af 100644 --- a/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md +++ b/content/en/examples/resource-provisioners/community/dns/score-k8s/cmd/dns-in-codespace.md @@ -7,7 +7,7 @@ source: "community" implementation: "score-k8s" resourceType: "dns" provisionerType: "cmd" -flavor: "dns" +flavor: "dns-in-codespace" excerpt: 'Prerequisites for `dns-in-codespace`: - Have `gh` installed, this provisioner is using the GitHub CLI to get the name of the current GitHub Codespace.' description: 'Get the forwarded port URL in current GitHub Codespace on port 80' diff --git a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md index 9fe82169..abce3ad8 100644 --- a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md +++ b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-template-redis.md @@ -7,7 +7,7 @@ source: "community" implementation: "score-k8s" resourceType: "redis" provisionerType: "cmd" -flavor: "helm" +flavor: "helm-template-redis" excerpt: 'Prerequisites: - Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). - Have `yq` installed locally.' diff --git a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md index c66c5947..223eabd6 100644 --- a/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md +++ b/content/en/examples/resource-provisioners/community/redis/score-k8s/cmd/helm-upgrade-redis.md @@ -7,7 +7,7 @@ source: "community" implementation: "score-k8s" resourceType: "redis" provisionerType: "cmd" -flavor: "helm" +flavor: "helm-upgrade-redis" excerpt: 'Prerequisites: - Have `helm` installed locally, this provisioner renders the manifests from the [Bitnami's Redis Helm chart](https://bitnami.com/stack/redis/helm). - Have `yq` installed locally.' diff --git a/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md index e6fa14c9..01c1e08f 100644 --- a/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md +++ b/content/en/examples/resource-provisioners/default/example-provisioner/score-k8s/cmd/example-provisioner.md @@ -7,7 +7,7 @@ source: "default" implementation: "score-k8s" resourceType: "example-provisioner-resource" provisionerType: "cmd" -flavor: "example" +flavor: "example-provisioner" excerpt: 'The 'cmd' scheme has a "host" + path component that indicates the path to the binary to execute. If the host starts with "." it is interpreted as a relative path, if it starts with "~" it resolves to the home directory.' description: 'Example provisioner that runs a bash script.' expectedOutputs: diff --git a/data/examplesMeta.yml b/data/examplesMeta.yml index a9eb242e..2118d472 100644 --- a/data/examplesMeta.yml +++ b/data/examplesMeta.yml @@ -32,11 +32,14 @@ resource-provisioners: - dapr - default - dns + - dns-in-codespace - dotenv - elasticsearch - empty - - example - - helm + - example-provisioner + - foo + - helm-template-redis + - helm-upgrade-redis - ingress - kafka - mongo diff --git a/gen/examples-site/frontmatter-provisioners.js b/gen/examples-site/frontmatter-provisioners.js index d35885c7..2c417ac6 100644 --- a/gen/examples-site/frontmatter-provisioners.js +++ b/gen/examples-site/frontmatter-provisioners.js @@ -37,7 +37,7 @@ source: "${config.source}" implementation: "${config.implementation}" resourceType: "${config.resourceType}" provisionerType: "${config.provisionerType}" -flavor: "${config.title.split("-")[0]}" +flavor: "${config.flavor}" excerpt: '${config.excerpt}' description: '${config.description}' ${ @@ -158,11 +158,13 @@ const buildResourceProvisionerFiles = ( const githubUrl = getProvisionerGitHubUrl(dirPath, options, implementation); const uriParts = parsedYaml.uri.split("/"); let title = uriParts[uriParts.length - 1]; + let flavor = title.split("-")[0]; let tool; // Handle URIs with # (e.g., cmd://bash#example-provisioner) if (title.includes("#")) { - title = title.split("#")[1]; - tool = title.split("-")[0]; + title = title.split("#")[1]; + flavor = title; + tool = title.split("-")[0]; } const provisionerType = parsedYaml.uri.split("://")[0]; @@ -174,7 +176,7 @@ const buildResourceProvisionerFiles = ( tool, provisionerType, resourceType: parsedYaml.type, - flavor: title.split("-")[0], + flavor, }, }, "resource-provisioners" @@ -188,6 +190,7 @@ const buildResourceProvisionerFiles = ( resourceType: parsedYaml.type, description: parsedYaml.description, implementation, + flavor, expectedOutputs: convertToYamlArray(parsedYaml.expected_outputs), supportedParams: convertToYamlArray(parsedYaml.supported_params), tool,