mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-09-08 09:15:43 +00:00
feat: support env variable substitution e.g. FOO=${MY_FOO_VAR}
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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...)
|
||||
|
||||
@@ -2,7 +2,9 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "github.com/garethgeorge/backrest/gen/go/v1"
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user