From 2fcf3582b57f2304b5a68249a6b1afa6a6a934f6 Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Tue, 14 Feb 2023 17:00:29 +0100 Subject: [PATCH] osbuild: add shell.init stage Add support for the org.osbuild.shell.init stage and test validator. --- internal/osbuild/shell_init_stage.go | 57 +++++++ internal/osbuild/shell_init_stage_test.go | 184 ++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 internal/osbuild/shell_init_stage.go create mode 100644 internal/osbuild/shell_init_stage_test.go diff --git a/internal/osbuild/shell_init_stage.go b/internal/osbuild/shell_init_stage.go new file mode 100644 index 000000000..fb95c9ab8 --- /dev/null +++ b/internal/osbuild/shell_init_stage.go @@ -0,0 +1,57 @@ +package osbuild + +import ( + "fmt" + "regexp" +) + +const filenameRegex = "^[a-zA-Z0-9\\.\\-_]{1,250}$" +const envVarRegex = "^[A-Z][A-Z0-9_]*$" + +type ShellInitStageOptions struct { + Files map[string]ShellInitFile `json:"files"` +} + +func (ShellInitStageOptions) isStageOptions() {} + +func (options ShellInitStageOptions) validate() error { + fre := regexp.MustCompile(filenameRegex) + vre := regexp.MustCompile(envVarRegex) + for fname, kvs := range options.Files { + if !fre.MatchString(fname) { + return fmt.Errorf("filename %q doesn't conform to schema (%s)", fname, filenameRegex) + } + + if len(kvs.Env) == 0 { + return fmt.Errorf("at least one environment variable must be specified for each file") + } + + for _, kv := range kvs.Env { + if !vre.MatchString(kv.Key) { + return fmt.Errorf("variable name %q doesn't conform to schema (%s)", kv.Key, envVarRegex) + } + } + } + + return nil +} + +type ShellInitFile struct { + Env []EnvironmentVariable `json:"env"` +} + +type EnvironmentVariable struct { + Key string `json:"key"` + Value string `json:"value"` +} + +func NewShellInitStage(options *ShellInitStageOptions) *Stage { + if err := options.validate(); err != nil { + panic(err) + } + + return &Stage{ + Type: "org.osbuild.shell.init", + Options: options, + } +} diff --git a/internal/osbuild/shell_init_stage_test.go b/internal/osbuild/shell_init_stage_test.go new file mode 100644 index 000000000..5f5db0131 --- /dev/null +++ b/internal/osbuild/shell_init_stage_test.go @@ -0,0 +1,184 @@ +package osbuild + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidShellInitStageOptions(t *testing.T) { + tests := []ShellInitStageOptions{ + { + Files: map[string]ShellInitFile{ + "filename": { + Env: []EnvironmentVariable{ + { + Key: "KEY", + Value: "value", + }, + { + Key: "KEY2", + Value: "value2", + }, + { + Key: "EMPTY", + Value: "", + }, + }, + }, + "filename2": { + Env: []EnvironmentVariable{ + { + Key: "KEY21", + Value: "value21", + }, + { + Key: "KEY22", + Value: "value22", + }, + { + Key: "EMPTY", + Value: "", + }, + }, + }, + }, + }, + { + Files: map[string]ShellInitFile{ + "gawk.sh": { + Env: []EnvironmentVariable{ + { + Key: "AWKPATH", + Value: "$AWKPATH:$*", + }, + { + Key: "AWKLIBPATH", + Value: "$AWKLIBPATH:$*", + }, + }, + }, + "flatpak.sh": { + Env: []EnvironmentVariable{ + { + Key: "XDG_DATA_DIRS", + Value: "${new_dirs:+${new_dirs}:}${XDG_DATA_DIRS:-/usr/local/share:/usr/share}", + }, + }, + }, + }, + }, + } + + assert := assert.New(t) + for idx := range tests { + tt := tests[idx] + name := fmt.Sprintf("ValidShellInitStage-%d", idx) + t.Run(name, func(t *testing.T) { + assert.NoErrorf(tt.validate(), "%q returned an error [idx: %d]", name, idx) + assert.NotPanics(func() { NewShellInitStage(&tt) }) + }) + } +} + +func TestInvalidShellInitStageOptions(t *testing.T) { + tests := []ShellInitStageOptions{ + { + Files: map[string]ShellInitFile{ + + "path/filename": { + Env: []EnvironmentVariable{ + { + Key: "DOESNT", + Value: "matter", + }, + }, + }, + "ok": { + Env: []EnvironmentVariable{ + { + Key: "EMPTYOK", + Value: "", + }, + }, + }, + }, + }, + { + Files: map[string]ShellInitFile{ + "gawk.sh": { + Env: []EnvironmentVariable{ + { + Key: "", + Value: "badkey", + }, + }, + }, + }, + }, + { + Files: map[string]ShellInitFile{ + "$FILENAME": { + Env: []EnvironmentVariable{ + { + Key: "BAD", + Value: "filename", + }, + }, + }, + }, + }, + { + Files: map[string]ShellInitFile{ + "FILENAME": { + Env: []EnvironmentVariable{ + { + Key: "bad.var", + Value: "okval", + }, + }, + }, + }, + }, + { + Files: map[string]ShellInitFile{ + "FILENAME": { + Env: []EnvironmentVariable{ + { + Key: "BAD.VAR", + Value: "okval", + }, + }, + }, + }, + }, + { + Files: map[string]ShellInitFile{ + "me.sh": { + Env: []EnvironmentVariable{ + { + Key: "-SH", + Value: "", + }, + }, + }, + }, + }, + { + Files: map[string]ShellInitFile{ + "empty.sh": {}, + }, + }, + } + + assert := assert.New(t) + for idx := range tests { + tt := tests[idx] + name := fmt.Sprintf("InvalidShellInitStage-%d", idx) + t.Run(name, func(t *testing.T) { + assert.Errorf(tt.validate(), "%q didn't return an error [idx: %d]", name, idx) + assert.Panics(func() { NewShellInitStage(&tt) }) + }) + } +}