Skip to content

Commit

Permalink
feat(action): check for any undefined environment variable in config
Browse files Browse the repository at this point in the history
- until now it was rather hard to debug when the issue was one or more
  missing environment variables
- with this patch, all environment detected variables in the JSON
  configuration file must be defined at the time of execution

Signed-off-by: AtomicFS <vojtech.vesely@9elements.com>
  • Loading branch information
AtomicFS committed Dec 3, 2024
1 parent cd01c93 commit 99b7ddb
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 5 deletions.
37 changes: 35 additions & 2 deletions action/recipes/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"log/slog"
"os"
"path/filepath"
"regexp"
"strings"

"dagger.io/dagger"
Expand All @@ -20,7 +21,10 @@ import (
)

// ErrVerboseJSON is raised when JSONVerboseError can't find location of problem in JSON configuration file
var ErrVerboseJSON = errors.New("unable to pinpoint the problem in JSON file")
var (
ErrVerboseJSON = errors.New("unable to pinpoint the problem in JSON file")
ErrEnvVarUndefined = errors.New("environment variable used in JSON file is not present in the environment")
)

// =================
// Data structures
Expand Down Expand Up @@ -221,6 +225,16 @@ func ValidateConfig(conf Config) error {
return nil
}

// FindAllEnvVars returns all environment variables found in the provided string
func FindAllEnvVars(text string) []string {
pattern := regexp.MustCompile(`\${?([a-zA-Z0-9_]+)}?`)
result := pattern.FindAllString(text, -1)
for index, value := range result {
result[index] = pattern.ReplaceAllString(value, "$1")
}
return result
}

// ReadConfig is for reading and parsing JSON configuration file into Config struct
func ReadConfig(filepath string) (*Config, error) {
// Read JSON file
Expand All @@ -233,8 +247,27 @@ func ReadConfig(filepath string) (*Config, error) {
return nil, err
}

// Expand environment variables
contentStr := string(content)

// Check if all environment variables are defined
envVars := FindAllEnvVars(contentStr)
undefinedVarFound := false
for _, envVar := range envVars {
_, found := os.LookupEnv(envVar)
if !found {
slog.Error(
fmt.Sprintf("environment variable '%s' is undefined", envVar),
slog.String("suggestion", "define the environment variable in the environment"),
slog.Any("error", ErrEnvVarUndefined),
)
undefinedVarFound = true
}
}
if undefinedVarFound {
return nil, ErrEnvVarUndefined
}

// Expand environment variables
contentStr = os.ExpandEnv(contentStr)

// Decode JSON
Expand Down
64 changes: 61 additions & 3 deletions action/recipes/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -120,6 +121,53 @@ func TestConfigReadAndWrite(t *testing.T) {
}
}

func TestFindAllEnvVars(t *testing.T) {
testCases := []struct {
name string
text string
expectedEnvVars []string
}{
{
name: "no env vars",
text: "dummy string",
expectedEnvVars: []string{},
},
{
name: "one env var",
text: "dummy string with $MY_VAR",
expectedEnvVars: []string{"MY_VAR"},
},
{
name: "one env var with brackets",
text: "dummy string with ${MY_VAR}",
expectedEnvVars: []string{"MY_VAR"},
},
{
name: "two env vars",
text: "dummy string with $MY_VAR and ${MY_VAR}",
expectedEnvVars: []string{"MY_VAR", "MY_VAR"},
},
{
name: "two env vars with numbers",
text: "dummy string with $MY_VAR1 and ${MY_VAR2}",
expectedEnvVars: []string{"MY_VAR1", "MY_VAR2"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
foundVars := FindAllEnvVars(tc.text)
fmt.Println(foundVars)
fmt.Println(tc.expectedEnvVars)

assert.Equal(t, len(tc.expectedEnvVars), len(foundVars))
// If both slices are of zero length, then the comparison fails for whatever reason
if len(tc.expectedEnvVars) > 0 {
assert.True(t, reflect.DeepEqual(tc.expectedEnvVars, foundVars))
}
})
}
}

func TestConfigEnvVars(t *testing.T) {
commonDummy := CommonOpts{
SdkURL: "ghcr.io/9elements/firmware-action/coreboot_4.19:main",
Expand Down Expand Up @@ -182,6 +230,15 @@ func TestConfigEnvVars(t *testing.T) {
"TEST_ENV_VAR_REPOPATH": "dummy/dir/",
},
},
{
name: "undefined env var",
wantErr: ErrEnvVarUndefined,
url: "ghcr.io/$TEST_ENV_VAR/coreboot_4.19:main",
urlExpected: commonDummy.SdkURL,
repoPath: commonDummy.RepoPath,
repoPathExpected: commonDummy.RepoPath,
envVars: map[string]string{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -213,12 +270,13 @@ func TestConfigEnvVars(t *testing.T) {
assert.NoError(t, err)
// Read
optsConverted, err := ReadConfig(configFilepath)
assert.NoError(t, err)

// err = ValidateConfig(optsConverted)
assert.ErrorIs(t, err, tc.wantErr)
assert.Equal(t, tc.urlExpected, optsConverted.Coreboot["coreboot-A"].SdkURL)
assert.Equal(t, tc.repoPathExpected, optsConverted.Coreboot["coreboot-A"].RepoPath)
if tc.wantErr == nil {
assert.Equal(t, tc.urlExpected, optsConverted.Coreboot["coreboot-A"].SdkURL)
assert.Equal(t, tc.repoPathExpected, optsConverted.Coreboot["coreboot-A"].RepoPath)
}
})
}
}
Expand Down

0 comments on commit 99b7ddb

Please sign in to comment.