Skip to content

Commit

Permalink
feat: support env variable substitution e.g. FOO=${MY_FOO_VAR}
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed May 5, 2024
1 parent 802146a commit 8448f4c
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 56 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/stretchr/testify v1.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,12 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww=
Expand Down
18 changes: 18 additions & 0 deletions internal/orchestrator/repo/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package repo

import (
"os"
"regexp"
)

var (
envVarSubstRegex = regexp.MustCompile(`\${[^}]*}`)
)

// ExpandEnv expands environment variables of the form ${VAR} in a string.
func ExpandEnv(s string) string {
return envVarSubstRegex.ReplaceAllStringFunc(s, func(match string) string {
e, _ := os.LookupEnv(match[2 : len(match)-1])
return e
})
}
4 changes: 3 additions & 1 deletion internal/orchestrator/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ func NewRepoOrchestrator(config *v1.Config, repoConfig *v1.Repo, resticPath stri
}

if env := repoConfig.GetEnv(); len(env) != 0 {
opts = append(opts, restic.WithEnv(repoConfig.GetEnv()...))
for _, e := range env {
opts = append(opts, restic.WithEnv(ExpandEnv(e)))
}
}

repo := restic.NewRepo(resticPath, repoConfig.GetUri(), opts...)
Expand Down
50 changes: 50 additions & 0 deletions internal/orchestrator/repo/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package repo

import (
"context"
"os"
"slices"
"strings"
"testing"

v1 "github.com/garethgeorge/backrest/gen/go/v1"
Expand Down Expand Up @@ -140,3 +142,51 @@ func TestSnapshotParenting(t *testing.T) {
t.Errorf("expected 8 snapshots, got %d", len(snapshots))
}
}

func TestEnvVarPropagation(t *testing.T) {
t.Parallel()

repo := t.TempDir()
testData := test.CreateTestData(t)

// create a new repo with cache disabled for testing
r := &v1.Repo{
Id: "test",
Uri: repo,
Password: "test",
Flags: []string{"--no-cache"},
Env: []string{"RESTIC_PASSWORD=${MY_FOO}"},
}

plan := &v1.Plan{
Id: "test",
Repo: "test",
Paths: []string{testData},
}

orchestrator, err := NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t))
if err != nil {
t.Fatalf("failed to create repo orchestrator: %v", err)
}

_, err = orchestrator.Backup(context.Background(), plan, nil)
if err == nil || !strings.Contains(err.Error(), "an empty password is not a password") {
t.Fatalf("expected error about RESTIC_PASSWORD, got: %v", err)
}

// set the env var
os.Setenv("MY_FOO", "bar")
orchestrator, err = NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t))
if err != nil {
t.Fatalf("failed to create repo orchestrator: %v", err)
}

summary, err := orchestrator.Backup(context.Background(), plan, nil)
if err != nil {
t.Fatalf("backup error: %v", err)
}

if summary.SnapshotId == "" {
t.Fatal("expected snapshot id")
}
}
108 changes: 55 additions & 53 deletions webui/src/views/AddRepoModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,63 +282,65 @@ export const AddRepoModal = ({
</Tooltip>

{/* Repo.env */}
<Form.Item label="Env Vars">
<Form.List
name="env"
rules={[
{
validator: async (_, envVars) => {
return await envVarSetValidator(form, envVars);
<Tooltip title={"Environment variables that are passed to restic (e.g. to provide S3 or B2 credentials). References to parent-process env variables are supported as FOO=${MY_FOO_VAR}."}>
<Form.Item label="Env Vars">
<Form.List
name="env"
rules={[
{
validator: async (_, envVars) => {
return await envVarSetValidator(form, envVars);
},
},
},
]}
>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Form.Item
{...field}
validateTrigger={["onChange", "onBlur"]}
rules={[
{
required: true,
whitespace: true,
pattern: /^[\w-]+=.*$/,
message:
"Environment variable must be in format KEY=VALUE",
},
]}
noStyle
>
<Input
placeholder="KEY=VALUE"
onBlur={() => form.validateFields()}
style={{ width: "90%" }}
]}
>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Form.Item
{...field}
validateTrigger={["onChange", "onBlur"]}
rules={[
{
required: true,
whitespace: true,
pattern: /^[\w-]+=.*$/,
message:
"Environment variable must be in format KEY=VALUE",
},
]}
noStyle
>
<Input
placeholder="KEY=VALUE"
onBlur={() => form.validateFields()}
style={{ width: "90%" }}
/>
</Form.Item>
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(index)}
style={{ paddingLeft: "5px" }}
/>
</Form.Item>
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(index)}
style={{ paddingLeft: "5px" }}
/>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add("")}
style={{ width: "90%" }}
icon={<PlusOutlined />}
>
Set Environment Variable
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add("")}
style={{ width: "90%" }}
icon={<PlusOutlined />}
>
Set Environment Variable
</Button>
<Form.ErrorList errors={errors} />
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</Tooltip>

{/* Repo.flags */}
<Form.Item label="Flags">
Expand Down

0 comments on commit 8448f4c

Please sign in to comment.