mirror of
https://github.com/restic/restic.git
synced 2025-12-13 23:32:03 +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,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runCopy(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runDump(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runFind(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runForget(cmd.Context(), opts, pruneOpts, *globalOptions, globalOptions.term, args)
|
return runForget(cmd.Context(), opts, pruneOpts, *globalOptions, globalOptions.term, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,38 @@ func TestForgetOptionValues(t *testing.T) {
|
|||||||
|
|
||||||
func TestForgetHostnameDefaulting(t *testing.T) {
|
func TestForgetHostnameDefaulting(t *testing.T) {
|
||||||
t.Setenv("RESTIC_HOST", "testhost")
|
t.Setenv("RESTIC_HOST", "testhost")
|
||||||
opts := ForgetOptions{}
|
|
||||||
opts.AddFlags(pflag.NewFlagSet("test", pflag.ContinueOnError))
|
tests := []struct {
|
||||||
rtest.Equals(t, []string{"testhost"}, opts.Hosts)
|
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,
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runLs(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runLs(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runMount(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runMount(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
`,
|
`,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runRepairSnapshots(cmd.Context(), *globalOptions, opts, args, globalOptions.term)
|
return runRepairSnapshots(cmd.Context(), *globalOptions, opts, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runRestore(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
|
return runRestore(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runRewrite(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runRewrite(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runSnapshots(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runSnapshots(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runStats(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
return runStats(cmd.Context(), opts, *globalOptions, args, globalOptions.term)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ Exit status is 12 if the password is incorrect.
|
|||||||
GroupID: cmdGroupDefault,
|
GroupID: cmdGroupDefault,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
|
finalizeSnapshotFilter(&opts.SnapshotFilter)
|
||||||
return runTag(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
|
return runTag(cmd.Context(), opts, *globalOptions, globalOptions.term, args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,33 +12,41 @@ import (
|
|||||||
|
|
||||||
// initMultiSnapshotFilter is used for commands that work on multiple snapshots
|
// initMultiSnapshotFilter is used for commands that work on multiple snapshots
|
||||||
// MUST be combined with restic.FindFilteredSnapshots or FindFilteredSnapshots
|
// 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) {
|
func initMultiSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter, addHostShorthand bool) {
|
||||||
hostShorthand := "H"
|
hostShorthand := "H"
|
||||||
if !addHostShorthand {
|
if !addHostShorthand {
|
||||||
hostShorthand = ""
|
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.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)")
|
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
|
// initSingleSnapshotFilter is used for commands that work on a single snapshot
|
||||||
// MUST be combined with restic.FindFilteredSnapshot
|
// MUST be combined with restic.FindFilteredSnapshot
|
||||||
|
// MUST be followed by finalizeSnapshotFilter after flag parsing
|
||||||
func initSingleSnapshotFilter(flags *pflag.FlagSet, filt *data.SnapshotFilter) {
|
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.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)")
|
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
|
// 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 != "" {
|
if host := os.Getenv("RESTIC_HOST"); host != "" {
|
||||||
filt.Hosts = []string{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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
|
// FindFilteredSnapshots yields Snapshots, either given explicitly by `snapshotIDs` or filtered from the list of all snapshots.
|
||||||
func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *data.SnapshotFilter, snapshotIDs []string, printer progress.Printer) <-chan *data.Snapshot {
|
func FindFilteredSnapshots(ctx context.Context, be restic.Lister, loader restic.LoaderUnpacked, f *data.SnapshotFilter, snapshotIDs []string, printer progress.Printer) <-chan *data.Snapshot {
|
||||||
|
|||||||
@@ -39,6 +39,24 @@ func TestSnapshotFilter(t *testing.T) {
|
|||||||
[]string{"abc"},
|
[]string{"abc"},
|
||||||
"def",
|
"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.Run(test.name, func(t *testing.T) {
|
||||||
t.Setenv("RESTIC_HOST", test.env)
|
t.Setenv("RESTIC_HOST", test.env)
|
||||||
@@ -54,6 +72,9 @@ func TestSnapshotFilter(t *testing.T) {
|
|||||||
err := set.Parse(test.args)
|
err := set.Parse(test.args)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
|
// Apply the finalization logic to handle env defaults
|
||||||
|
finalizeSnapshotFilter(flt)
|
||||||
|
|
||||||
rtest.Equals(t, test.expected, flt.Hosts, "unexpected hosts")
|
rtest.Equals(t, test.expected, flt.Hosts, "unexpected hosts")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user