Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion pkg/apis/velero/v1/backup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ type BackupSpec struct {
// +nullable
SnapshotVolumes *bool `json:"snapshotVolumes,omitempty"`

// TTL is a time.Duration-parseable string describing how long
// TTL is a string describing how long
// the Backup should be retained for.
// +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/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
94 changes: 94 additions & 0 deletions pkg/cmd/util/flag/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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]int64{
"yr": 365 * 24 * 3600,
"mo": 30 * 24 * 3600,
"w": 7 * 24 * 3600,
"d": 24 * 3600,
"h": 3600,
"m": 60,
"s": 1,
}

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

var total int64
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.ParseInt(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) * time.Second}, 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" }
185 changes: 185 additions & 0 deletions pkg/cmd/util/flag/duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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: "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: "100d",
expected: 100 * 24 * time.Hour,
wantErr: false,
},
{
name: "all units combined",
input: "1yr2mo1w3d4h5m6s",
expected: 365*24*time.Hour + 2*30*24*time.Hour + 7*24*time.Hour + 3*24*time.Hour + 4*time.Hour + 5*time.Minute + 6*time.Second,
wantErr: false,
},

// Error cases
{
name: "invalid character at start",
input: "abc",
expected: 0,
wantErr: true,
},
{
name: "missing 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: "empty unit",
input: "5",
expected: 0,
wantErr: true,
},
{
name: "negative number",
input: "-5s",
expected: 0,
wantErr: true,
},
{
name: "decimal number",
input: "5.5s",
expected: 0,
wantErr: true,
},
{
name: "unit without number",
input: "s",
expected: 0,
wantErr: true,
},
{
name: "mixed valid and invalid",
input: "5s10x",
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
Loading