Files
kopia/cli/command_policy_show.go
Donatas Abraitis 67d8399127 fix(cli): Fix crash when doing kopia policy show <path> (#4560)
```
~# ./kopia policy show /home/donatas/tests
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x18b7b84]

goroutine 1 [running]:
github.com/kopia/kopia/cli.appendOSSnapshotPolicyRows({0xc005759008, 0x26, 0x55}, 0xc005754600?, 0xc005764008)
	/home/donatas/projects/kopia/cli/command_policy_show.go:490 +0x44
github.com/kopia/kopia/cli.printPolicy(0xc0000f5d48, 0xc005754600, 0xc005764008)
	/home/donatas/projects/kopia/cli/command_policy_show.go:135 +0x5e5
github.com/kopia/kopia/cli.(*commandPolicyShow).run(0xc0000f5d10, {0x2311ef8, 0xc0002e5320}, {0x7fb83829f3d0, 0xc0052eeb00})
	/home/donatas/projects/kopia/cli/command_policy_show.go:46 +0x18d
github.com/kopia/kopia/cli.(*App).repositoryReaderAction.func1({0x2311ef8?, 0xc0002e5320?}, {0x7fb83829f3d0?, 0xc0052eeb00?})
	/home/donatas/projects/kopia/cli/app.go:477 +0x2b
github.com/kopia/kopia/cli.(*App).repositoryReaderAction.(*App).maybeRepositoryAction.func2({0x2311ef8, 0xc0002e5320})
	/home/donatas/projects/kopia/cli/app.go:571 +0xbd
github.com/kopia/kopia/cli.(*App).repositoryReaderAction.(*App).maybeRepositoryAction.(*App).baseActionWithContext.func3.1.1()
	/home/donatas/projects/kopia/cli/app.go:554 +0x6e
github.com/kopia/kopia/cli.(*profileFlags).withProfiling(0x0?, 0xc0004a09c0?)
	/home/donatas/projects/kopia/cli/profile.go:45 +0x2b9
github.com/kopia/kopia/cli.(*App).repositoryReaderAction.(*App).maybeRepositoryAction.(*App).baseActionWithContext.func3.1({0x2311ef8?, 0xc0002e5320?})
	/home/donatas/projects/kopia/cli/app.go:549 +0x50
github.com/kopia/kopia/cli.(*App).runAppWithContext.func1(0xc0000f48d0?, 0xc0000f4808, 0xc0000efd30, {0x2311ef8, 0xc0002e52f0})
	/home/donatas/projects/kopia/cli/app.go:521 +0x186
github.com/kopia/kopia/cli.(*App).runAppWithContext(0xc0000f4808, 0xc0002b88f0, 0xc0000efd30)
	/home/donatas/projects/kopia/cli/app.go:522 +0x1a9
github.com/kopia/kopia/cli.(*App).repositoryReaderAction.(*App).maybeRepositoryAction.(*App).baseActionWithContext.func3(0x22e2801?)
	/home/donatas/projects/kopia/cli/app.go:548 +0x3c
github.com/alecthomas/kingpin/v2.(*actionMixin).applyActions(...)
	/home/donatas/go/pkg/mod/github.com/alecthomas/kingpin/v2@v2.4.0/actions.go:28
github.com/alecthomas/kingpin/v2.(*Application).applyActions(0xc0001b8500?, 0xc000453050)
	/home/donatas/go/pkg/mod/github.com/alecthomas/kingpin/v2@v2.4.0/app.go:568 +0xd0
github.com/alecthomas/kingpin/v2.(*Application).execute(0xc0001b8500, 0xc000453050, {0xc0004b6d80, 0x2, 0x2})
	/home/donatas/go/pkg/mod/github.com/alecthomas/kingpin/v2@v2.4.0/app.go:398 +0x65
github.com/alecthomas/kingpin/v2.(*Application).Parse(0xc0001b8500, {0xc00014e090?, 0xc0001b8500?, 0x22e8c80?})
	/home/donatas/go/pkg/mod/github.com/alecthomas/kingpin/v2@v2.4.0/app.go:230 +0x14a
main.main()
	/home/donatas/projects/kopia/main.go:77 +0x1cc
```

This is happening if we apply this before:

```
~# ./kopia policy set --inherit=false --clear-ignore /home/donatas/tests
```

Default value for `p.OSSnapshotPolicy.VolumeShadowCopy.Enable` is nil, so it's not set
which is inherited from a global one and is 0 (never).

Signed-off-by: Donatas Abraitis <donatas@opensourcerouting.org>
2025-05-12 23:34:28 +00:00

510 lines
16 KiB
Go

package cli
import (
"context"
"fmt"
"strings"
"time"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/units"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/snapshot/policy"
)
type commandPolicyShow struct {
policyTargetFlags
jo jsonOutput
out textOutput
}
func (c *commandPolicyShow) setup(svc appServices, parent commandParent) {
cmd := parent.Command("show", "Show snapshot policy.").Alias("get")
c.policyTargetFlags.setup(cmd)
c.jo.setup(svc, cmd)
c.out.setup(svc)
cmd.Action(svc.repositoryReaderAction(c.run))
}
func (c *commandPolicyShow) run(ctx context.Context, rep repo.Repository) error {
targets, err := c.policyTargets(ctx, rep)
if err != nil {
return err
}
for _, target := range targets {
effective, definition, _, err := policy.GetEffectivePolicy(ctx, rep, target)
if err != nil {
return errors.Wrapf(err, "can't get effective policy for %q", target)
}
if c.jo.jsonOutput {
c.out.printStdout("%s\n", c.jo.jsonBytes(effective))
} else {
printPolicy(&c.out, effective, definition)
}
}
return nil
}
type policyTableRow struct {
name string
value string
def string
}
func alignedPolicyTableRows(v []policyTableRow) string {
var nameValueLen int
const (
nameValueSpace = " "
defSpace = " "
)
for _, it := range v {
if it.value == "" {
continue
}
t := it.name
if it.value != "" {
t += nameValueSpace + it.value
}
if len(t) > nameValueLen {
nameValueLen = len(t)
}
}
var lines []string
for _, it := range v {
l := it.name
if it.value != "" || it.def != "" {
if spaces := nameValueLen - len(l) - len(it.value); spaces > 0 {
l += strings.Repeat(" ", spaces)
}
l += it.value
}
if it.def != "" {
l += defSpace
l += it.def
}
lines = append(lines, l)
}
return strings.Join(lines, "\n")
}
func definitionPointToString(target, src snapshot.SourceInfo) string {
if src == target {
return "(defined for this target)"
}
return "inherited from " + src.String()
}
func printPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
var rows []policyTableRow
rows = appendRetentionPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendFilesPolicyValue(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendErrorHandlingPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendSchedulingPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendUploadPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendCompressionPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendMetadataCompressionPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendSplitterPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendActionsPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendOSSnapshotPolicyRows(rows, p, def)
rows = append(rows, policyTableRow{})
rows = appendLoggingPolicyRows(rows, p, def)
out.printStdout("Policy for %v:\n\n%v\n", p.Target(), alignedPolicyTableRows(rows))
}
func appendRetentionPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
return append(rows,
policyTableRow{"Retention:", "", ""},
policyTableRow{" Annual snapshots:", valueOrNotSet(p.RetentionPolicy.KeepAnnual), definitionPointToString(p.Target(), def.RetentionPolicy.KeepAnnual)},
policyTableRow{" Monthly snapshots:", valueOrNotSet(p.RetentionPolicy.KeepMonthly), definitionPointToString(p.Target(), def.RetentionPolicy.KeepMonthly)},
policyTableRow{" Weekly snapshots:", valueOrNotSet(p.RetentionPolicy.KeepWeekly), definitionPointToString(p.Target(), def.RetentionPolicy.KeepWeekly)},
policyTableRow{" Daily snapshots:", valueOrNotSet(p.RetentionPolicy.KeepDaily), definitionPointToString(p.Target(), def.RetentionPolicy.KeepDaily)},
policyTableRow{" Hourly snapshots:", valueOrNotSet(p.RetentionPolicy.KeepHourly), definitionPointToString(p.Target(), def.RetentionPolicy.KeepHourly)},
policyTableRow{" Latest snapshots:", valueOrNotSet(p.RetentionPolicy.KeepLatest), definitionPointToString(p.Target(), def.RetentionPolicy.KeepLatest)},
policyTableRow{" Ignore identical snapshots:", boolToString(p.RetentionPolicy.IgnoreIdenticalSnapshots.OrDefault(false)), definitionPointToString(p.Target(), def.RetentionPolicy.IgnoreIdenticalSnapshots)},
)
}
func boolToString(v bool) string {
if v {
return "true"
}
return "false"
}
func logDetailToString(v policy.LogDetail) string {
return fmt.Sprintf("%v", v)
}
func appendFilesPolicyValue(items []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
items = append(items,
policyTableRow{"Files policy:", "", ""},
policyTableRow{
" Ignore cache directories:",
boolToString(p.FilesPolicy.IgnoreCacheDirectories.OrDefault(true)),
definitionPointToString(p.Target(), def.FilesPolicy.IgnoreCacheDirectories),
})
if len(p.FilesPolicy.IgnoreRules) > 0 {
items = append(items, policyTableRow{
" Ignore rules:", "", definitionPointToString(p.Target(), def.FilesPolicy.IgnoreRules),
})
for _, rule := range p.FilesPolicy.IgnoreRules {
items = append(items, policyTableRow{" " + rule, "", ""})
}
} else {
items = append(items, policyTableRow{" No ignore rules:", "", ""})
}
if len(p.FilesPolicy.DotIgnoreFiles) > 0 {
items = append(items, policyTableRow{
" Read ignore rules from files:", "",
definitionPointToString(p.Target(), def.FilesPolicy.DotIgnoreFiles),
})
for _, dotFile := range p.FilesPolicy.DotIgnoreFiles {
items = append(items, policyTableRow{" " + dotFile, "", ""})
}
}
if maxSize := p.FilesPolicy.MaxFileSize; maxSize > 0 {
items = append(items, policyTableRow{
" Ignore files above:",
units.BytesString(maxSize),
definitionPointToString(p.Target(), def.FilesPolicy.MaxFileSize),
})
}
items = append(items, policyTableRow{
" Scan one filesystem only:",
boolToString(p.FilesPolicy.OneFileSystem.OrDefault(false)),
definitionPointToString(p.Target(), def.FilesPolicy.OneFileSystem),
})
return items
}
func appendErrorHandlingPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
return append(rows,
policyTableRow{"Error handling policy:", "", ""},
policyTableRow{
" Ignore file read errors:",
boolToString(p.ErrorHandlingPolicy.IgnoreFileErrors.OrDefault(false)),
definitionPointToString(p.Target(), def.ErrorHandlingPolicy.IgnoreFileErrors),
},
policyTableRow{
" Ignore directory read errors:",
boolToString(p.ErrorHandlingPolicy.IgnoreDirectoryErrors.OrDefault(false)),
definitionPointToString(p.Target(), def.ErrorHandlingPolicy.IgnoreDirectoryErrors),
},
policyTableRow{
" Ignore unknown types:",
boolToString(p.ErrorHandlingPolicy.IgnoreUnknownTypes.OrDefault(true)),
definitionPointToString(p.Target(), def.ErrorHandlingPolicy.IgnoreUnknownTypes),
},
)
}
func appendLoggingPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
return append(rows,
policyTableRow{
fmt.Sprintf("Logging details (%v-none, %v-maximum):", policy.LogDetailNone, policy.LogDetailMax), "", "",
},
policyTableRow{
" Directory snapshotted:",
logDetailToString(p.LoggingPolicy.Directories.Snapshotted.OrDefault(policy.LogDetailNone)),
definitionPointToString(p.Target(), def.LoggingPolicy.Directories.Snapshotted),
},
policyTableRow{
" Directory ignored:",
logDetailToString(p.LoggingPolicy.Directories.Ignored.OrDefault(policy.LogDetailNone)),
definitionPointToString(p.Target(), def.LoggingPolicy.Directories.Ignored),
},
policyTableRow{
" Entry snapshotted:",
logDetailToString(p.LoggingPolicy.Entries.Snapshotted.OrDefault(policy.LogDetailNone)),
definitionPointToString(p.Target(), def.LoggingPolicy.Entries.Snapshotted),
},
policyTableRow{
" Entry ignored:",
logDetailToString(p.LoggingPolicy.Entries.Ignored.OrDefault(policy.LogDetailNone)),
definitionPointToString(p.Target(), def.LoggingPolicy.Entries.Ignored),
},
policyTableRow{
" Entry cache hit:",
logDetailToString(p.LoggingPolicy.Entries.CacheHit.OrDefault(policy.LogDetailNone)),
definitionPointToString(p.Target(), def.LoggingPolicy.Entries.CacheHit),
},
policyTableRow{
" Entry cache miss:",
logDetailToString(p.LoggingPolicy.Entries.CacheMiss.OrDefault(policy.LogDetailNone)),
definitionPointToString(p.Target(), def.LoggingPolicy.Entries.CacheMiss),
},
)
}
func appendUploadPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
return append(rows,
policyTableRow{"Uploads:", "", ""},
policyTableRow{" Max parallel snapshots (server/UI):", valueOrNotSet(p.UploadPolicy.MaxParallelSnapshots), definitionPointToString(p.Target(), def.UploadPolicy.MaxParallelSnapshots)},
policyTableRow{" Max parallel file reads:", valueOrNotSet(p.UploadPolicy.MaxParallelFileReads), definitionPointToString(p.Target(), def.UploadPolicy.MaxParallelFileReads)},
policyTableRow{" Parallel upload above size:", valueOrNotSetOptionalInt64Bytes(p.UploadPolicy.ParallelUploadAboveSize), definitionPointToString(p.Target(), def.UploadPolicy.ParallelUploadAboveSize)},
)
}
func appendSchedulingPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
rows = append(rows, policyTableRow{"Scheduling policy:", "", ""})
hasAny := false
rows = append(rows, policyTableRow{" Scheduled snapshots:", "", ""})
if p.SchedulingPolicy.Interval() != 0 {
rows = append(rows, policyTableRow{
" Snapshot interval:",
p.SchedulingPolicy.Interval().String(),
definitionPointToString(p.Target(), def.SchedulingPolicy.IntervalSeconds),
})
hasAny = true
}
if len(p.SchedulingPolicy.TimesOfDay) > 0 {
rows = append(rows,
policyTableRow{
" Run missed snapshots:",
boolToString(p.SchedulingPolicy.RunMissed.OrDefault(false)),
definitionPointToString(p.Target(), def.SchedulingPolicy.RunMissed),
},
policyTableRow{
" Snapshot times:",
"",
definitionPointToString(p.Target(), def.SchedulingPolicy.TimesOfDay),
})
for _, tod := range p.SchedulingPolicy.TimesOfDay {
rows = append(rows, policyTableRow{" " + tod.String(), "", ""})
}
hasAny = true
}
if len(p.SchedulingPolicy.Cron) > 0 {
rows = append(rows, policyTableRow{" Crontab expressions:", "", definitionPointToString(p.Target(), def.SchedulingPolicy.Cron)})
for _, cron := range p.SchedulingPolicy.Cron {
rows = append(rows, policyTableRow{" " + cron, "", ""})
}
hasAny = true
}
if !hasAny {
rows = append(rows, policyTableRow{" None.", "", ""})
}
rows = append(rows, policyTableRow{" Manual snapshot:", boolToString(p.SchedulingPolicy.Manual), definitionPointToString(p.Target(), def.SchedulingPolicy.Manual)})
return rows
}
func appendCompressionPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
if p.CompressionPolicy.CompressorName == "" || p.CompressionPolicy.CompressorName == "none" {
rows = append(rows, policyTableRow{"Compression disabled.", "", ""})
return rows
}
rows = append(rows,
policyTableRow{"Compression:", "", ""},
policyTableRow{" Compressor:", string(p.CompressionPolicy.CompressorName), definitionPointToString(p.Target(), def.CompressionPolicy.CompressorName)})
switch {
case len(p.CompressionPolicy.OnlyCompress) > 0:
rows = append(rows, policyTableRow{
" Only compress files with the following extensions:", "",
definitionPointToString(p.Target(), def.CompressionPolicy.OnlyCompress),
})
for _, rule := range p.CompressionPolicy.OnlyCompress {
rows = append(rows, policyTableRow{" - " + rule, "", ""})
}
case len(p.CompressionPolicy.NeverCompress) > 0:
rows = append(rows, policyTableRow{
" Compress all files except the following extensions:", "",
definitionPointToString(p.Target(), def.CompressionPolicy.NeverCompress),
})
for _, rule := range p.CompressionPolicy.NeverCompress {
rows = append(rows, policyTableRow{" " + rule, "", ""})
}
default:
rows = append(rows, policyTableRow{" Compress files regardless of extensions.", "", ""})
}
switch {
case p.CompressionPolicy.MaxSize > 0:
rows = append(rows, policyTableRow{fmt.Sprintf(
" Only compress files between %v and %v.",
units.BytesString(p.CompressionPolicy.MinSize),
units.BytesString(p.CompressionPolicy.MaxSize)), "", ""})
case p.CompressionPolicy.MinSize > 0:
rows = append(rows, policyTableRow{fmt.Sprintf(
" Only compress files bigger than %v.",
units.BytesString(p.CompressionPolicy.MinSize)), "", ""})
default:
rows = append(rows, policyTableRow{" Compress files of all sizes.", "", ""})
}
return rows
}
func appendMetadataCompressionPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
if p.MetadataCompressionPolicy.CompressorName == "" || p.MetadataCompressionPolicy.CompressorName == "none" {
rows = append(rows, policyTableRow{"Metadata compression disabled.", "", ""})
return rows
}
return append(rows,
policyTableRow{"Metadata compression:", "", ""},
policyTableRow{" Compressor:", string(p.MetadataCompressionPolicy.CompressorName), definitionPointToString(p.Target(), def.MetadataCompressionPolicy.CompressorName)})
}
func appendSplitterPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
algorithm := p.SplitterPolicy.Algorithm
if algorithm == "" {
algorithm = "(repository default)"
}
rows = append(rows,
policyTableRow{"Splitter:", "", ""},
policyTableRow{" Algorithm override:", algorithm, definitionPointToString(p.Target(), def.SplitterPolicy.Algorithm)})
return rows
}
func appendActionsPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
var anyActions bool
if h := p.Actions.BeforeSnapshotRoot; h != nil {
rows = append(rows,
policyTableRow{"Run command before snapshot root:", "", definitionPointToString(p.Target(), def.Actions.BeforeSnapshotRoot)})
rows = appendActionCommandRows(rows, h)
anyActions = true
}
if h := p.Actions.AfterSnapshotRoot; h != nil {
rows = append(rows, policyTableRow{"Run command after snapshot root:", "", definitionPointToString(p.Target(), def.Actions.AfterSnapshotRoot)})
rows = appendActionCommandRows(rows, h)
anyActions = true
}
if h := p.Actions.BeforeFolder; h != nil {
rows = append(rows, policyTableRow{"Run command before this folder:", "", "(non-inheritable)"})
rows = appendActionCommandRows(rows, h)
anyActions = true
}
if h := p.Actions.AfterFolder; h != nil {
rows = append(rows, policyTableRow{"Run command after this folder:", "", "(non-inheritable)"})
rows = appendActionCommandRows(rows, h)
anyActions = true
}
if !anyActions {
rows = append(rows, policyTableRow{"No actions defined.", "", ""})
}
return rows
}
func appendActionCommandRows(rows []policyTableRow, h *policy.ActionCommand) []policyTableRow {
if h.Script != "" {
rows = append(rows,
policyTableRow{" Embedded script (stored in repository):", "", ""},
policyTableRow{indentMultilineString(h.Script, " "), "", ""},
)
} else {
rows = append(rows,
policyTableRow{" Command:", "", ""},
policyTableRow{" " + h.Command + " " + strings.Join(h.Arguments, " "), "", ""})
}
actualMode := h.Mode
if actualMode == "" {
actualMode = "sync"
}
rows = append(rows,
policyTableRow{" Mode:", actualMode, ""},
policyTableRow{" Timeout:", (time.Second * time.Duration(h.TimeoutSeconds)).String(), ""},
policyTableRow{"", "", ""},
)
return rows
}
func appendOSSnapshotPolicyRows(rows []policyTableRow, p *policy.Policy, def *policy.Definition) []policyTableRow {
rows = append(rows,
policyTableRow{"OS-level snapshot support:", "", ""},
policyTableRow{
" Volume Shadow Copy:",
p.OSSnapshotPolicy.VolumeShadowCopy.Enable.OrDefault(policy.OSSnapshotNever).String(),
definitionPointToString(p.Target(), def.OSSnapshotPolicy.VolumeShadowCopy.Enable),
},
)
return rows
}
func valueOrNotSet(p *policy.OptionalInt) string {
if p == nil {
return "-"
}
return fmt.Sprintf("%v", *p)
}
func valueOrNotSetOptionalInt64Bytes(p *policy.OptionalInt64) string {
if p == nil {
return "-"
}
return units.BytesString(*p)
}