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

feat(action): check for any undefined environment variable in config #454

Merged
merged 1 commit into from
Dec 3, 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
40 changes: 37 additions & 3 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 @@ -19,8 +20,12 @@ import (
"github.com/go-playground/validator/v10"
)

// 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 is raised when JSONVerboseError can't find location of problem in JSON configuration file
ErrVerboseJSON = errors.New("unable to pinpoint the problem in JSON file")
// ErrEnvVarUndefined is raised when undefined environment variable is found in JSON configuration file
ErrEnvVarUndefined = errors.New("environment variable used in JSON file is not present in the environment")
)

// =================
// Data structures
Expand Down Expand Up @@ -221,6 +226,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 +248,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
Loading