Skip to content

Commit

Permalink
Keep bindable environment references intact
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
markusthoemmes committed Sep 25, 2024
1 parent 9484e02 commit 975c726
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion deploy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions deploy/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand Down Expand Up @@ -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{
Expand All @@ -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{
Expand Down
40 changes: 40 additions & 0 deletions utils/env.go
Original file line number Diff line number Diff line change
@@ -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, ".")
}
41 changes: 41 additions & 0 deletions utils/env_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}

0 comments on commit 975c726

Please sign in to comment.