diff --git a/README.md b/README.md index 095d8d9..467e26f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ This is useful for injecting runtime configuration into static front-end apps wi If any placeholders have no matching environment variable, the server still writes the file (substituting empty strings for missing values) and logs an error listing the unresolved variables. +**Important:** Since the static-server image runs as `nonroot:nonroot`, the config file must be writable by the `nonroot` user. Use `COPY --chown=nonroot:nonroot` when copying the config file in your Dockerfile (see [example](#1-build-a-docker-image) below). + #### Example Given a `config.json` template: @@ -60,7 +62,9 @@ FROM zopdev/static-server:v0.0.8 COPY /app/out /static # Set the path to the config file for environment variable hydration at startup +# The config file must be writable by nonroot for hydration to work ENV CONFIG_FILE_PATH=/static/config.json +COPY --chown=nonroot:nonroot /app/out/config.json $CONFIG_FILE_PATH # The server listens on port 8000 by default; set HTTP_PORT to change it diff --git a/internal/config/hydrate_test.go b/internal/config/hydrate_test.go index 613e3f2..f8906cb 100644 --- a/internal/config/hydrate_test.go +++ b/internal/config/hydrate_test.go @@ -4,7 +4,6 @@ import ( "io" "os" "path/filepath" - "runtime" "testing" "github.com/stretchr/testify/require" @@ -13,30 +12,30 @@ import ( "gofr.dev/pkg/gofr/logging" ) -func writeTempFile(t *testing.T, fs file.FileSystem, content string) string { +func writeTempFile(t *testing.T, content string, permissions os.FileMode) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config.json") - f, err := fs.Create(path) + err := os.WriteFile(path, []byte(content), 0644) require.NoError(t, err) - _, err = f.Write([]byte(content)) - require.NoError(t, err) - - require.NoError(t, f.Close()) + if permissions != 0 { + require.NoError(t, os.Chmod(path, permissions)) + } return path } func TestConfig(t *testing.T) { tests := []struct { - name string - template string - vars map[string]string - expected string - wantErr error + name string + template string + vars map[string]string + permissions os.FileMode + expected string + wantErr error }{ { name: "all vars present", @@ -45,9 +44,9 @@ func TestConfig(t *testing.T) { expected: `{"a":"1","b":"2"}`, }, { - name: "no config path is a no-op", - vars: map[string]string{}, - wantErr: nil, + name: "no config path is a no-op", + template: `{"a":"${A}","b":"${B}"}`, + vars: map[string]string{"CONFIG_FILE_PATH": ""}, }, { name: "extra vars ignored", @@ -70,9 +69,17 @@ func TestConfig(t *testing.T) { wantErr: errMissingVars, }, { - name: "invalid config path", - vars: map[string]string{"CONFIG_FILE_PATH": "/no/such/file"}, - wantErr: errReadConfig, + name: "invalid config path", + template: `{"a":"${A}"}`, + vars: map[string]string{"CONFIG_FILE_PATH": "/no/such/file"}, + wantErr: errReadConfig, + }, + { + name: "write error on read-only file", + template: `{"a":"${A}"}`, + vars: map[string]string{"A": "1"}, + permissions: 0444, + wantErr: errWriteConfig, }, } @@ -80,42 +87,24 @@ func TestConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { fs := file.New(logging.NewMockLogger(logging.ERROR)) - if tt.template != "" { - path := writeTempFile(t, fs, tt.template) - tt.vars[filePathVar] = path + // To not overwrite the file path if already present in the test case + if _, ok := tt.vars[filePathVar]; !ok { + tt.vars[filePathVar] = writeTempFile(t, tt.template, tt.permissions) } err := HydrateFile(fs, config.NewMockConfig(tt.vars)) require.ErrorIs(t, err, tt.wantErr) - if tt.expected != "" { - rf, readErr := fs.Open(tt.vars[filePathVar]) - require.NoError(t, readErr) - got, readErr := io.ReadAll(rf) - require.NoError(t, readErr) - require.Equal(t, tt.expected, string(got)) + if tt.vars[filePathVar] == "" || tt.wantErr != nil { + return } - }) - } -} - -func TestHydrateFile_WriteError(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("chmod not effective on Windows") - } - - fs := file.New(logging.NewMockLogger(logging.ERROR)) - path := writeTempFile(t, fs, `{"a":"${A}"}`) - err := os.Chmod(path, 0444) - require.NoError(t, err) - - vars := map[string]string{ - filePathVar: path, - "A": "1", + rf, readErr := os.Open(tt.vars[filePathVar]) + require.NoError(t, readErr) + got, readErr := io.ReadAll(rf) + require.NoError(t, readErr) + require.Equal(t, tt.expected, string(got)) + }) } - - err = HydrateFile(fs, config.NewMockConfig(vars)) - require.ErrorIs(t, err, errWriteConfig) } diff --git a/main.go b/main.go index 249a62c..ed76816 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ func main() { app.OnStart(func(ctx *gofr.Context) error { if err := config.HydrateFile(ctx.File, app.Config); err != nil { - ctx.Logger.Error(err) + ctx.Logger.Error(err.Error()) } return nil