Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keep bindable environment references intact #146

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
})
}
}
Loading