Skip to content

Commit a08623f

Browse files
File write permission issue (#20)
* Add multi-platform Docker image support (amd64 + arm64) Enable native arm64 support for Apple Silicon users by building multi-architecture images via buildx. Uses TARGETARCH for explicit Go cross-compilation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update README.md with tag v0.0.7 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add QEMU setup for multi-platform Docker builds Required for arm64 emulation on amd64 runners. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Use cross-compilation for multi-platform Docker builds Replace QEMU emulation with native cross-compilation by adding --platform=$BUILDPLATFORM to the build stage. Go cross-compiles to the target architecture natively, eliminating slow emulation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update GitHub Actions to latest versions and add image output - actions/checkout v4 → v6 - actions/setup-go v4 → v6 (caching now enabled by default) - golangci/golangci-lint-action v8 → v9 - Add step to output pushed image registry path Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Added support for env hydration * Updated README.md * Code refactor * Fixed implementation * Fixed linter issues * Converted fields to unexported * Updated tests * Fixed linter issue * Added go doc for HydrateFile * Updated tests * Updated README.md and error logging * Fixed linter issue --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e8d0988 commit a08623f

File tree

3 files changed

+41
-48
lines changed

3 files changed

+41
-48
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ This is useful for injecting runtime configuration into static front-end apps wi
1919

2020
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.
2121

22+
**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).
23+
2224
#### Example
2325

2426
Given a `config.json` template:
@@ -60,7 +62,9 @@ FROM zopdev/static-server:v0.0.8
6062
COPY /app/out /static
6163

6264
# Set the path to the config file for environment variable hydration at startup
65+
# The config file must be writable by nonroot for hydration to work
6366
ENV CONFIG_FILE_PATH=/static/config.json
67+
COPY --chown=nonroot:nonroot /app/out/config.json $CONFIG_FILE_PATH
6468

6569
# The server listens on port 8000 by default; set HTTP_PORT to change it
6670

internal/config/hydrate_test.go

Lines changed: 36 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"io"
55
"os"
66
"path/filepath"
7-
"runtime"
87
"testing"
98

109
"github.com/stretchr/testify/require"
@@ -13,30 +12,30 @@ import (
1312
"gofr.dev/pkg/gofr/logging"
1413
)
1514

16-
func writeTempFile(t *testing.T, fs file.FileSystem, content string) string {
15+
func writeTempFile(t *testing.T, content string, permissions os.FileMode) string {
1716
t.Helper()
1817

1918
dir := t.TempDir()
2019
path := filepath.Join(dir, "config.json")
2120

22-
f, err := fs.Create(path)
21+
err := os.WriteFile(path, []byte(content), 0644)
2322
require.NoError(t, err)
2423

25-
_, err = f.Write([]byte(content))
26-
require.NoError(t, err)
27-
28-
require.NoError(t, f.Close())
24+
if permissions != 0 {
25+
require.NoError(t, os.Chmod(path, permissions))
26+
}
2927

3028
return path
3129
}
3230

3331
func TestConfig(t *testing.T) {
3432
tests := []struct {
35-
name string
36-
template string
37-
vars map[string]string
38-
expected string
39-
wantErr error
33+
name string
34+
template string
35+
vars map[string]string
36+
permissions os.FileMode
37+
expected string
38+
wantErr error
4039
}{
4140
{
4241
name: "all vars present",
@@ -45,9 +44,9 @@ func TestConfig(t *testing.T) {
4544
expected: `{"a":"1","b":"2"}`,
4645
},
4746
{
48-
name: "no config path is a no-op",
49-
vars: map[string]string{},
50-
wantErr: nil,
47+
name: "no config path is a no-op",
48+
template: `{"a":"${A}","b":"${B}"}`,
49+
vars: map[string]string{"CONFIG_FILE_PATH": ""},
5150
},
5251
{
5352
name: "extra vars ignored",
@@ -70,52 +69,42 @@ func TestConfig(t *testing.T) {
7069
wantErr: errMissingVars,
7170
},
7271
{
73-
name: "invalid config path",
74-
vars: map[string]string{"CONFIG_FILE_PATH": "/no/such/file"},
75-
wantErr: errReadConfig,
72+
name: "invalid config path",
73+
template: `{"a":"${A}"}`,
74+
vars: map[string]string{"CONFIG_FILE_PATH": "/no/such/file"},
75+
wantErr: errReadConfig,
76+
},
77+
{
78+
name: "write error on read-only file",
79+
template: `{"a":"${A}"}`,
80+
vars: map[string]string{"A": "1"},
81+
permissions: 0444,
82+
wantErr: errWriteConfig,
7683
},
7784
}
7885

7986
for _, tt := range tests {
8087
t.Run(tt.name, func(t *testing.T) {
8188
fs := file.New(logging.NewMockLogger(logging.ERROR))
8289

83-
if tt.template != "" {
84-
path := writeTempFile(t, fs, tt.template)
85-
tt.vars[filePathVar] = path
90+
// To not overwrite the file path if already present in the test case
91+
if _, ok := tt.vars[filePathVar]; !ok {
92+
tt.vars[filePathVar] = writeTempFile(t, tt.template, tt.permissions)
8693
}
8794

8895
err := HydrateFile(fs, config.NewMockConfig(tt.vars))
8996

9097
require.ErrorIs(t, err, tt.wantErr)
9198

92-
if tt.expected != "" {
93-
rf, readErr := fs.Open(tt.vars[filePathVar])
94-
require.NoError(t, readErr)
95-
got, readErr := io.ReadAll(rf)
96-
require.NoError(t, readErr)
97-
require.Equal(t, tt.expected, string(got))
99+
if tt.vars[filePathVar] == "" || tt.wantErr != nil {
100+
return
98101
}
99-
})
100-
}
101-
}
102-
103-
func TestHydrateFile_WriteError(t *testing.T) {
104-
if runtime.GOOS == "windows" {
105-
t.Skip("chmod not effective on Windows")
106-
}
107-
108-
fs := file.New(logging.NewMockLogger(logging.ERROR))
109-
path := writeTempFile(t, fs, `{"a":"${A}"}`)
110102

111-
err := os.Chmod(path, 0444)
112-
require.NoError(t, err)
113-
114-
vars := map[string]string{
115-
filePathVar: path,
116-
"A": "1",
103+
rf, readErr := os.Open(tt.vars[filePathVar])
104+
require.NoError(t, readErr)
105+
got, readErr := io.ReadAll(rf)
106+
require.NoError(t, readErr)
107+
require.Equal(t, tt.expected, string(got))
108+
})
117109
}
118-
119-
err = HydrateFile(fs, config.NewMockConfig(vars))
120-
require.ErrorIs(t, err, errWriteConfig)
121110
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ func main() {
2222

2323
app.OnStart(func(ctx *gofr.Context) error {
2424
if err := config.HydrateFile(ctx.File, app.Config); err != nil {
25-
ctx.Logger.Error(err)
25+
ctx.Logger.Error(err.Error())
2626
}
2727

2828
return nil

0 commit comments

Comments
 (0)