From 975c726095dd6156373aba6965c972e2ffcb8ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Th=C3=B6mmes?= Date: Wed, 25 Sep 2024 09:14:33 +0200 Subject: [PATCH] Keep bindable environment references intact App Platform allows for bindable variables in environment variables (see https://docs.digitalocean.com/products/app-platform/how-to/use-environment-variables/#using-bindable-variables-within-environment-variables). They look like "normal" env-var references though and so our env-var-expansion process destroyed those references since generally the respective value isn't present on the action's env. This fixes that by not expanding the respective env vars if no value is present in the environment and the reference looks like a bindable reference. --- README.md | 2 ++ deploy/main.go | 2 +- deploy/main_test.go | 16 ++++++++++++++++ utils/env.go | 40 ++++++++++++++++++++++++++++++++++++++++ utils/env_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 utils/env.go create mode 100644 utils/env_test.go diff --git a/README.md b/README.md index 02a1cac..7f45521 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ The following action deploys the app whenever a new commit is pushed to the main In this case, a secret of the repository named `SOME_SECRET_FROM_REPOSITORY` will also be passed into the app via its environment variables as `SOME_SECRET`. It is passed to the action's environment via the `${{ secrets.KEY }}` notation and then substituted into the spec itself via the environment variable reference in `value`. Make sure to define the respective env var's type as `SECRET` in the spec to ensure the value is stored in an encrypted way. +**Note:** `APP_DOMAIN`, `APP_URL` and `APP_NAME` are predefined [App-wide variables](https://docs.digitalocean.com/products/app-platform/how-to/use-environment-variables/#app-wide-variables). Avoid overriding them in the action's environment to avoid the env-var-expansion process of the Github Action to interfere with that of the platform itself. + ```yaml name: Update App diff --git a/deploy/main.go b/deploy/main.go index a7f99d8..ccc91dc 100644 --- a/deploy/main.go +++ b/deploy/main.go @@ -93,7 +93,7 @@ func (d *deployer) createSpec(ctx context.Context) (*godo.AppSpec, error) { if err != nil { return nil, fmt.Errorf("failed to get app spec content: %w", err) } - appSpecExpanded := os.ExpandEnv(string(appSpec)) + appSpecExpanded := utils.ExpandEnvRetainingBindables(string(appSpec)) if err := yaml.Unmarshal([]byte(appSpecExpanded), &spec); err != nil { return nil, fmt.Errorf("failed to parse app spec: %w", err) } diff --git a/deploy/main_test.go b/deploy/main_test.go index 46c61ea..50df721 100644 --- a/deploy/main_test.go +++ b/deploy/main_test.go @@ -20,6 +20,10 @@ import ( func TestCreateSpecFromFile(t *testing.T) { spec := &godo.AppSpec{ Name: "foo", + Envs: []*godo.AppVariableDefinition{{ + Key: "GLOBAL_ENV_VAR", + Value: "${APP_DOMAIN}", + }}, Services: []*godo.AppServiceSpec{{ Name: "web", Image: &godo.ImageSourceSpec{ @@ -28,6 +32,10 @@ func TestCreateSpecFromFile(t *testing.T) { Repository: "bar", Tag: "${ENV_VAR}", }, + Envs: []*godo.AppVariableDefinition{{ + Key: "SERVICE_ENV_VAR", + Value: "${web2.HOSTNAME}", + }}, }, { Name: "web2", Image: &godo.ImageSourceSpec{ @@ -61,6 +69,10 @@ func TestCreateSpecFromFile(t *testing.T) { expected := &godo.AppSpec{ Name: "foo", + Envs: []*godo.AppVariableDefinition{{ + Key: "GLOBAL_ENV_VAR", + Value: "${APP_DOMAIN}", // Bindable reference stayed intact. + }}, Services: []*godo.AppServiceSpec{{ Name: "web", Image: &godo.ImageSourceSpec{ @@ -69,6 +81,10 @@ func TestCreateSpecFromFile(t *testing.T) { Repository: "bar", Tag: "v1", // Tag was updated. }, + Envs: []*godo.AppVariableDefinition{{ + Key: "SERVICE_ENV_VAR", + Value: "${web2.HOSTNAME}", // Bindable reference stayed intact. + }}, }, { Name: "web2", Image: &godo.ImageSourceSpec{ diff --git a/utils/env.go b/utils/env.go new file mode 100644 index 0000000..f97a13d --- /dev/null +++ b/utils/env.go @@ -0,0 +1,40 @@ +package utils + +import ( + "fmt" + "os" + "strings" +) + +// appWideVariables are the environment variables that are shared across all components. +// See https://docs.digitalocean.com/products/app-platform/how-to/use-environment-variables/#app-wide-variables. +var appWideVariables = map[string]struct{}{ + "APP_DOMAIN": {}, + "APP_URL": {}, + "APP_NAME": {}, +} + +// ExpandEnvRetainingBindables expands the environment variables in s, but it +// keeps bindable variables intact. +// Since bindable variables look like env vars, notation-wise, we just don't +// expand them at all. +func ExpandEnvRetainingBindables(s string) string { + return os.Expand(s, func(name string) string { + value := os.Getenv(name) + if value == "" { + if _, ok := appWideVariables[name]; ok || looksLikeBindable(name) { + // If the environment variable is not set, keep the respective + // reference intact. + return fmt.Sprintf("${%s}", name) + } + } + return value + }) +} + +// looksLikeBindable returns true if the key looks like a bindable variable. +// Environment variables can't usually contain dots, so if they do, we're +// fairly confident that it's a bindable variable. +func looksLikeBindable(key string) bool { + return strings.Contains(key, ".") +} diff --git a/utils/env_test.go b/utils/env_test.go new file mode 100644 index 0000000..b1904d6 --- /dev/null +++ b/utils/env_test.go @@ -0,0 +1,41 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestExpandEnvRetainingBindables(t *testing.T) { + t.Setenv("FOO", "bar") + t.Setenv("APP_URL", "baz") + + tests := []struct { + name string + in string + out string + }{{ + name: "simple", + in: "hello $FOO", + out: "hello bar", + }, { + name: "bindable", + in: "hello ${FOO.bar}", + out: "hello ${FOO.bar}", + }, { + name: "global bindable, unset", + in: "hello ${APP_DOMAIN}", + out: "hello ${APP_DOMAIN}", + }, { + name: "global bindable, overridden in env", + in: "hello ${APP_URL}", + out: "hello baz", + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := ExpandEnvRetainingBindables(test.in) + require.Equal(t, test.out, got) + }) + } +}