feat: allow override env RESTIC_HOST with flag to filter all snapshots (#5541)

This commit is contained in:
Srigovind Nayak
2025-10-05 16:52:50 +05:30
committed by GitHub
parent a2a49cf784
commit 22f254c9ca
16 changed files with 98 additions and 13 deletions

View 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

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
})
}
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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)
},
}

View File

@@ -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
}
}

View File

@@ -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")
}
})