mirror of
https://github.com/restic/restic.git
synced 2025-12-13 18:32:23 +00:00
feat: allow override env RESTIC_HOST with flag to filter all snapshots (#5541)
This commit is contained in:
12
changelog/unreleased/issue-5440
Normal file
12
changelog/unreleased/issue-5440
Normal file
@@ -0,0 +1,12 @@
|
||||
Enhancement: Allow overriding RESTIC_HOST environment variable with --host flag
|
||||
|
||||
When the `RESTIC_HOST` environment variable was set, there was no way to list or
|
||||
operate on snapshots from all hosts, as the environment variable would always
|
||||
filter to that specific host. Restic now allows overriding `RESTIC_HOST` by
|
||||
explicitly providing the `--host` flag with an empty string (e.g., `--host=""` or
|
||||
`--host=`), which will show snapshots from all hosts. This works for all commands
|
||||
that support snapshot filtering: `snapshots`, `forget`, `find`, `stats`, `copy`,
|
||||
`tag`, `repair snapshots`, `rewrite`, `mount`, `restore`, `dump`, and `ls`.
|
||||
|
||||
https://github.com/restic/restic/issues/5440
|
||||
https://github.com/restic/restic/pull/5541
|
||||
@@ -49,6 +49,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runForget(cmd.Context(), opts, pruneOpts, *globalOptions, globalOptions.term, args)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -96,7 +96,38 @@ func TestForgetOptionValues(t *testing.T) {
|
||||
|
||||
func TestForgetHostnameDefaulting(t *testing.T) {
|
||||
t.Setenv("RESTIC_HOST", "testhost")
|
||||
opts := ForgetOptions{}
|
||||
opts.AddFlags(pflag.NewFlagSet("test", pflag.ContinueOnError))
|
||||
rtest.Equals(t, []string{"testhost"}, opts.Hosts)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "env default when flag not set",
|
||||
args: nil,
|
||||
want: []string{"testhost"},
|
||||
},
|
||||
{
|
||||
name: "flag overrides env",
|
||||
args: []string{"--host", "flaghost"},
|
||||
want: []string{"flaghost"},
|
||||
},
|
||||
{
|
||||
name: "empty flag clears env",
|
||||
args: []string{"--host", ""},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
set := pflag.NewFlagSet(tt.name, pflag.ContinueOnError)
|
||||
opts := ForgetOptions{}
|
||||
opts.AddFlags(set)
|
||||
err := set.Parse(tt.args)
|
||||
rtest.Assert(t, err == nil, "expected no error for input")
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
rtest.Equals(t, tt.want, opts.Hosts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ Exit status is 12 if the password is incorrect.
|
||||
DisableAutoGenTag: true,
|
||||
GroupID: cmdGroupDefault,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runLs(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ Exit status is 12 if the password is incorrect.
|
||||
DisableAutoGenTag: true,
|
||||
GroupID: cmdGroupDefault,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runMount(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ Exit status is 12 if the password is incorrect.
|
||||
`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runRepairSnapshots(cmd.Context(), *globalOptions, opts, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runRestore(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runRewrite(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runSnapshots(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runStats(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ Exit status is 12 if the password is incorrect.
|
||||
GroupID: cmdGroupDefault,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||
return runTag(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,31 +12,39 @@ import (
|
||||
|
||||
// initMultiSnapshotFilter is used for commands that work on multiple snapshots
|
||||
// MUST be combined with restic.FindFilteredSnapshots or FindFilteredSnapshots
|
||||
// MUST be followed by finalizeSnapshotFilter after flag parsing
|
||||
func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter, addHostShorthand bool) {
|
||||
hostShorthand := "H"
|
||||
if !addHostShorthand {
|
||||
hostShorthand = ""
|
||||
}
|
||||
flags.StringArrayVarP(&filt.Hosts, "host", hostShorthand, nil, "only consider snapshots for this `host` (can be specified multiple times) (default: $RESTIC_HOST)")
|
||||
flags.StringArrayVarP(&filt.Hosts, "host", hostShorthand, nil, "only consider snapshots for this `host` (can be specified multiple times, use empty string to unset default value) (default: $RESTIC_HOST)")
|
||||
flags.Var(&filt.Tags, "tag", "only consider snapshots including `tag[,tag,...]` (can be specified multiple times)")
|
||||
flags.StringArrayVar(&filt.Paths, "path", nil, "only consider snapshots including this (absolute) `path` (can be specified multiple times, snapshots must include all specified paths)")
|
||||
|
||||
// set default based on env if set
|
||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||
filt.Hosts = []string{host}
|
||||
}
|
||||
}
|
||||
|
||||
// initSingleSnapshotFilter is used for commands that work on a single snapshot
|
||||
// MUST be combined with restic.FindFilteredSnapshot
|
||||
// MUST be followed by finalizeSnapshotFilter after flag parsing
|
||||
func initSingleSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter) {
|
||||
flags.StringArrayVarP(&filt.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when snapshot ID \"latest\" is given (can be specified multiple times) (default: $RESTIC_HOST)")
|
||||
flags.StringArrayVarP(&filt.Hosts, "host", "H", nil, "only consider snapshots for this `host`, when snapshot ID \"latest\" is given (can be specified multiple times, use empty string to unset default value) (default: $RESTIC_HOST)")
|
||||
flags.Var(&filt.Tags, "tag", "only consider snapshots including `tag[,tag,...]`, when snapshot ID \"latest\" is given (can be specified multiple times)")
|
||||
flags.StringArrayVar(&filt.Paths, "path", nil, "only consider snapshots including this (absolute) `path`, when snapshot ID \"latest\" is given (can be specified multiple times, snapshots must include all specified paths)")
|
||||
}
|
||||
|
||||
// set default based on env if set
|
||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||
filt.Hosts = []string{host}
|
||||
// finalizeSnapshotFilter applies RESTIC_HOST default only if --host flag wasn't explicitly set.
|
||||
// This allows users to override RESTIC_HOST by providing --host="" or --host with explicit values.
|
||||
func finalizeSnapshotFilter(filt *data.SnapshotFilter) {
|
||||
// Only apply RESTIC_HOST default if the --host flag wasn't changed by the user
|
||||
if filt.Hosts == nil {
|
||||
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||
filt.Hosts = []string{host}
|
||||
}
|
||||
}
|
||||
// If flag was set to empty string explicitly (e.g., --host=""),
|
||||
// filt.Hosts will be []string{""} which should be cleaned up to allow all hosts
|
||||
if len(filt.Hosts) == 1 && filt.Hosts[0] == "" {
|
||||
filt.Hosts = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,24 @@ func TestSnapshotFilter(t *testing.T) {
|
||||
[]string{"abc"},
|
||||
"def",
|
||||
},
|
||||
{
|
||||
"env set, empty flag overrides",
|
||||
[]string{"--host", ""},
|
||||
nil, // empty host filter means all hosts
|
||||
"envhost",
|
||||
},
|
||||
{
|
||||
"env set, multiple flags override",
|
||||
[]string{"--host", "host1", "--host", "host2"},
|
||||
[]string{"host1", "host2"},
|
||||
"envhost",
|
||||
},
|
||||
{
|
||||
"env set, multiple hosts including empty",
|
||||
[]string{"--host", "host1", "--host", ""},
|
||||
[]string{"host1", ""},
|
||||
"envhost",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Setenv("RESTIC_HOST", test.env)
|
||||
@@ -54,6 +72,9 @@ func TestSnapshotFilter(t *testing.T) {
|
||||
err := set.Parse(test.args)
|
||||
rtest.OK(t, err)
|
||||
|
||||
// Apply the finalization logic to handle env defaults
|
||||
finalizeSnapshotFilter(flt)
|
||||
|
||||
rtest.Equals(t, test.expected, flt.Hosts, "unexpected hosts")
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user