Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelogs/unreleased/9345-itrooz
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make TTL accept day/week/month/year units
3 changes: 2 additions & 1 deletion config/crd/v1/bases/velero.io_backups.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -495,8 +495,9 @@ spec:
type: string
ttl:
description: |-
TTL is a time.Duration-parseable string describing how long
TTL is a time.Duration-compatible string describing how long
the Backup should be retained for.
Supports time.Duration units + day, week, month, and year.
type: string
uploaderConfig:
description: UploaderConfig specifies the configuration for the uploader.
Expand Down
3 changes: 2 additions & 1 deletion config/crd/v1/bases/velero.io_schedules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,9 @@ spec:
type: string
ttl:
description: |-
TTL is a time.Duration-parseable string describing how long
TTL is a time.Duration-compatible string describing how long
the Backup should be retained for.
Supports time.Duration units + day, week, month, and year.
type: string
uploaderConfig:
description: UploaderConfig specifies the configuration for the
Expand Down
4 changes: 2 additions & 2 deletions config/crd/v1/crds/crds.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pkg/apis/velero/v1/backup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ type BackupSpec struct {
// +nullable
SnapshotVolumes *bool `json:"snapshotVolumes,omitempty"`

// TTL is a time.Duration-parseable string describing how long
// TTL is a time.Duration-compatible string describing how long
// the Backup should be retained for.
// Supports time.Duration units + day, week, month, and year.
// +optional
TTL metav1.Duration `json:"ttl,omitempty"`

Expand Down
6 changes: 3 additions & 3 deletions pkg/cmd/cli/backup/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func NewCreateCommand(f client.Factory, use string) *cobra.Command {

type CreateOptions struct {
Name string
TTL time.Duration
TTL flag.Duration
SnapshotVolumes flag.OptionalBool
SnapshotMoveData flag.OptionalBool
DataMover string
Expand Down Expand Up @@ -121,7 +121,7 @@ func NewCreateOptions() *CreateOptions {
}

func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
flags.DurationVar(&o.TTL, "ttl", o.TTL, "How long before the backup can be garbage collected.")
flags.Var(&o.TTL, "ttl", "How long before the backup can be garbage collected.")
flags.Var(&o.IncludeNamespaces, "include-namespaces", "Namespaces to include in the backup (use '*' for all namespaces).")
flags.Var(&o.ExcludeNamespaces, "exclude-namespaces", "Namespaces to exclude from the backup.")
flags.Var(&o.IncludeResources, "include-resources", "Resources to include in the backup, formatted as resource.group, such as storageclasses.storage.k8s.io (use '*' for all resources). Cannot work with include-cluster-scoped-resources, exclude-cluster-scoped-resources, include-namespace-scoped-resources and exclude-namespace-scoped-resources.")
Expand Down Expand Up @@ -388,7 +388,7 @@ func (o *CreateOptions) BuildBackup(namespace string) (*velerov1api.Backup, erro
ExcludedNamespaceScopedResources(o.ExcludeNamespaceScopedResources...).
LabelSelector(o.Selector.LabelSelector).
OrLabelSelector(o.OrSelector.OrLabelSelectors).
TTL(o.TTL).
TTL(o.TTL.Duration).
StorageLocation(o.StorageLocation).
VolumeSnapshotLocations(o.SnapshotLocations...).
CSISnapshotTimeout(o.CSISnapshotTimeout).
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/cli/backup/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestCreateOptions_BuildBackup(t *testing.T) {
require.NoError(t, err)

assert.Equal(t, velerov1api.BackupSpec{
TTL: metav1.Duration{Duration: o.TTL},
TTL: metav1.Duration{Duration: o.TTL.Duration},
IncludedNamespaces: []string(o.IncludeNamespaces),
SnapshotVolumes: o.SnapshotVolumes.Value,
IncludeClusterResources: o.IncludeClusterResources.Value,
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/cli/schedule/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
LabelSelector: o.BackupOptions.Selector.LabelSelector,
OrLabelSelectors: o.BackupOptions.OrSelector.OrLabelSelectors,
SnapshotVolumes: o.BackupOptions.SnapshotVolumes.Value,
TTL: metav1.Duration{Duration: o.BackupOptions.TTL},
TTL: metav1.Duration{Duration: o.BackupOptions.TTL.Duration},
StorageLocation: o.BackupOptions.StorageLocation,
VolumeSnapshotLocations: o.BackupOptions.SnapshotLocations,
DefaultVolumesToFsBackup: o.BackupOptions.DefaultVolumesToFsBackup.Value,
Expand Down
99 changes: 99 additions & 0 deletions pkg/cmd/util/flag/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package flag

import (
"fmt"
"strconv"
"strings"
"time"
"unicode"

"k8s.io/apimachinery/pkg/util/duration"
)

// Wrapper around time.Duration with a parser that accepts days, months, years as valid units.
type Duration struct {
time.Duration
}

// unit map: symbol -> seconds
var unitMap = map[string]uint64{
"ns": uint64(time.Nanosecond),
"us": uint64(time.Microsecond),
"µs": uint64(time.Microsecond), // U+00B5 = micro symbol
"μs": uint64(time.Microsecond), // U+03BC = Greek letter mu
"ms": uint64(time.Millisecond),
"s": uint64(time.Second),
"m": uint64(time.Minute),
"h": uint64(time.Hour),
"d": uint64(24 * time.Hour),
"w": uint64(7 * 24 * time.Hour),
"mo": uint64(30 * 24 * time.Hour),
"yr": uint64(365 * 24 * time.Hour),
}

// ParseDuration parses strings like "2d5h10m"
func ParseDuration(s string) (Duration, error) {
s = strings.TrimSpace(s)
if s == "" {
return Duration{Duration: 0}, nil
}

var total uint64
i := 0
n := len(s)

for i < n {
// Get number
j := i
for j < n && unicode.IsDigit(rune(s[j])) {
j++
}
if j == i {
return Duration{}, fmt.Errorf("expected number at pos %d", i)
}
numStr := s[i:j]
num, err := strconv.ParseUint(numStr, 10, 64)
if err != nil {
return Duration{}, err
}

// Get unit
k := j
for k < n && unicode.IsLetter(rune(s[k])) {
k++
}
if k == j {
return Duration{}, fmt.Errorf("missing unit after number at pos %d", j)
}
unit := strings.ToLower(s[j:k])

// Query value for unit
val, ok := unitMap[unit]
if !ok {
return Duration{}, fmt.Errorf("unknown unit %q", unit)
}
// Add to total
total += num * val

i = k
for i < n && s[i] == ' ' {
i++
}
}

// Convert integer seconds into time.Duration
return Duration{Duration: time.Duration(total)}, nil
}

func (d *Duration) String() string { return duration.ShortHumanDuration(d.Duration) }

func (d *Duration) Set(s string) error {
parsed, err := ParseDuration(s)
if err != nil {
return err
}
*d = parsed
return nil
}

func (d *Duration) Type() string { return "duration" }
173 changes: 173 additions & 0 deletions pkg/cmd/util/flag/duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package flag

import (
"testing"
"time"
)

func TestParseDuration(t *testing.T) {
tests := []struct {
name string
input string
expected time.Duration
wantErr bool
}{
// Valid cases
{
name: "empty string",
input: "",
expected: 0,
wantErr: false,
},
{
name: "whitespace only",
input: " ",
expected: 0,
wantErr: false,
},
{
name: "nanoseconds only",
input: "30ns",
expected: 30 * time.Nanosecond,
wantErr: false,
},
{
name: "seconds only",
input: "30s",
expected: 30 * time.Second,
wantErr: false,
},
{
name: "minutes only",
input: "5m",
expected: 5 * time.Minute,
wantErr: false,
},
{
name: "hours only",
input: "2h",
expected: 2 * time.Hour,
wantErr: false,
},
{
name: "days only",
input: "3d",
expected: 3 * 24 * time.Hour,
wantErr: false,
},
{
name: "weeks only",
input: "1w",
expected: 7 * 24 * time.Hour,
wantErr: false,
},
{
name: "months only",
input: "2mo",
expected: 2 * 30 * 24 * time.Hour,
wantErr: false,
},
{
name: "years only",
input: "1yr",
expected: 365 * 24 * time.Hour,
wantErr: false,
},
{
name: "combined units",
input: "2d5h10m30s",
expected: 2*24*time.Hour + 5*time.Hour + 10*time.Minute + 30*time.Second,
wantErr: false,
},
{
name: "combined with spaces",
input: "1d 12h 30m",
expected: 24*time.Hour + 12*time.Hour + 30*time.Minute,
wantErr: false,
},
{
name: "mixed case units",
input: "1D 2h 3M 4s",
expected: 24*time.Hour + 2*time.Hour + 3*time.Minute + 4*time.Second,
wantErr: false,
},
{
name: "zero values",
input: "0d0h0m0s",
expected: 0,
wantErr: false,
},
{
name: "large numbers",
input: "10000000s",
expected: 10000000 * time.Second,
wantErr: false,
},

// Error cases
{
name: "invalid character at start",
input: "abc",
expected: 0,
wantErr: true,
},
{
name: "unit without number",
input: "s",
expected: 0,
wantErr: true,
},
{
name: "number without unit",
input: "123",
expected: 0,
wantErr: true,
},
{
name: "invalid unit",
input: "5x",
expected: 0,
wantErr: true,
},
{
name: "number with invalid character",
input: "5a5s",
expected: 0,
wantErr: true,
},
{
name: "negative number",
input: "-5s",
expected: 0,
wantErr: true,
},
{
name: "decimal number",
input: "5.5s",
expected: 0,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParseDuration(tt.input)

if tt.wantErr {
if err == nil {
t.Errorf("ParseDuration(%q) expected error, got nil", tt.input)
}
return
}

if err != nil {
t.Errorf("ParseDuration(%q) unexpected error: %v", tt.input, err)
return
}

if result.Duration != tt.expected {
t.Errorf("ParseDuration(%q) = %v, want %v", tt.input, result.Duration, tt.expected)
}
})
}
}
2 changes: 1 addition & 1 deletion site/content/docs/main/how-velero-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ When you create a backup, you can specify a TTL (time to live) by adding the fla
* All PersistentVolume snapshots
* All associated Restores

The TTL flag allows the user to specify the backup retention period with the value specified in hours, minutes and seconds in the form `--ttl 24h0m0s`. If not specified, a default TTL value of 30 days will be applied.
The TTL flag allows the user to specify the backup retention period with the value in the form `--ttl 1d0h0m0s`. If not specified, a default TTL value of 30 days will be applied.

The effects of expiration are not applied immediately, they are applied when the gc-controller runs its reconciliation loop every hour by default. If needed, you can adjust the frequency of the reconciliation loop using the `--garbage-collection-frequency
<DURATION>` flag.
Expand Down