mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-26 01:44:19 +00:00
Merge branch 'main' into fix/backup-name-validation
This commit is contained in:
@@ -416,8 +416,19 @@ func ParseOrderedResources(orderMapStr string) (map[string]string, error) {
|
||||
return nil, fmt.Errorf("invalid OrderedResources '%s'", entry)
|
||||
}
|
||||
kind := strings.TrimSpace(kv[0])
|
||||
order := strings.TrimSpace(kv[1])
|
||||
orderedResources[kind] = order
|
||||
orderParts := strings.Split(kv[1], ",")
|
||||
cleaned := make([]string, 0, len(orderParts))
|
||||
for _, part := range orderParts {
|
||||
name := strings.TrimSpace(part)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, name)
|
||||
}
|
||||
if kind == "" || len(cleaned) == 0 {
|
||||
return nil, fmt.Errorf("invalid OrderedResources '%s'", entry)
|
||||
}
|
||||
orderedResources[kind] = strings.Join(cleaned, ",")
|
||||
}
|
||||
return orderedResources, nil
|
||||
}
|
||||
|
||||
@@ -234,6 +234,14 @@ func TestCreateOptions_OrderedResources(t *testing.T) {
|
||||
"persistentvolumes": "pv1,pv2",
|
||||
}
|
||||
assert.Equal(t, expectedMixedResources, orderedResources)
|
||||
|
||||
// Spaces after commas in the resource list must be trimmed.
|
||||
orderedResources, err = ParseOrderedResources("pods=ns1/p1, ns1/p2 ; persistentvolumeclaims= ns2/pvc1, ns2/pvc2")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, map[string]string{
|
||||
"pods": "ns1/p1,ns1/p2",
|
||||
"persistentvolumeclaims": "ns2/pvc1,ns2/pvc2",
|
||||
}, orderedResources)
|
||||
}
|
||||
|
||||
func TestCreateCommand(t *testing.T) {
|
||||
|
||||
@@ -111,7 +111,7 @@ func NewAddCommand(f client.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
// add the plugin as an init container
|
||||
plugin := *builder.ForPluginContainer(args[0], corev1api.PullPolicy(imagePullPolicyFlag.String())).Result()
|
||||
plugin := *builder.ForPluginContainer(args[0], corev1api.PullPolicy(imagePullPolicyFlag.String()), veleroDeploy.Spec.Template.Spec.InitContainers).Result()
|
||||
|
||||
veleroDeploy.Spec.Template.Spec.InitContainers = append(veleroDeploy.Spec.Template.Spec.InitContainers, plugin)
|
||||
|
||||
|
||||
@@ -57,13 +57,11 @@ var resToDelete = []kbclient.ObjectList{}
|
||||
|
||||
// uninstallOptions collects all the options for uninstalling Velero from a Kubernetes cluster.
|
||||
type uninstallOptions struct {
|
||||
wait bool // deprecated
|
||||
force bool
|
||||
}
|
||||
|
||||
// BindFlags adds command line values to the options struct.
|
||||
func (o *uninstallOptions) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.BoolVar(&o.wait, "wait", o.wait, "Wait for Velero uninstall to be ready. Optional. Deprecated.")
|
||||
flags.BoolVar(&o.force, "force", o.force, "Forces the Velero uninstall. Optional.")
|
||||
}
|
||||
|
||||
@@ -81,10 +79,6 @@ Use '--force' to skip the prompt confirming if you want to uninstall Velero.
|
||||
`,
|
||||
Example: ` # velero uninstall --namespace staging`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if o.wait {
|
||||
fmt.Println("Warning: the \"--wait\" option is deprecated and will be removed in a future release. The uninstall command always waits for the uninstall to complete.")
|
||||
}
|
||||
|
||||
// Confirm if not asked to force-skip confirmation
|
||||
if !o.force {
|
||||
fmt.Println("You are about to uninstall Velero.")
|
||||
|
||||
@@ -28,6 +28,11 @@ const (
|
||||
defaultPodVolumeOperationTimeout = 240 * time.Minute
|
||||
defaultResourceTerminatingTimeout = 10 * time.Minute
|
||||
|
||||
// DefaultResourceTimeout is the default for --resource-timeout. It matches
|
||||
// defaultResourceTerminatingTimeout so controller fallbacks stay aligned with
|
||||
// server defaults (see pkg/cmd/server/config/config.go).
|
||||
DefaultResourceTimeout = defaultResourceTerminatingTimeout
|
||||
|
||||
// server's client default qps and burst
|
||||
defaultClientQPS float32 = 100.0
|
||||
defaultClientBurst int = 100
|
||||
@@ -41,7 +46,7 @@ const (
|
||||
defaultCSISnapshotTimeout = 10 * time.Minute
|
||||
defaultItemOperationTimeout = 4 * time.Hour
|
||||
|
||||
resourceTimeout = 10 * time.Minute
|
||||
resourceTimeout = defaultResourceTerminatingTimeout
|
||||
|
||||
defaultMaxConcurrentK8SConnections = 30
|
||||
defaultDisableInformerCache = false
|
||||
@@ -183,6 +188,7 @@ type Config struct {
|
||||
ConcurrentBackups int
|
||||
GlobalBackupVolumePoliciesConfigMap string
|
||||
DefaultResourceModifierConfigMap string
|
||||
MaxBackupExtractionSize int
|
||||
}
|
||||
|
||||
func GetDefaultConfig() *Config {
|
||||
@@ -289,4 +295,10 @@ func (c *Config) BindFlags(flags *pflag.FlagSet) {
|
||||
c.DefaultResourceModifierConfigMap,
|
||||
"The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores. Ignored when a per-restore resource modifier is specified.",
|
||||
)
|
||||
flags.IntVar(
|
||||
&c.MaxBackupExtractionSize,
|
||||
"max-backup-extraction-size",
|
||||
c.MaxBackupExtractionSize,
|
||||
"Maximum size of a backup extraction in megabytes. If not set, default value (16GB) will be used.",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ import (
|
||||
"github.com/vmware-tanzu/velero/internal/storage"
|
||||
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
|
||||
"github.com/vmware-tanzu/velero/pkg/archive"
|
||||
"github.com/vmware-tanzu/velero/pkg/backup"
|
||||
"github.com/vmware-tanzu/velero/pkg/buildinfo"
|
||||
"github.com/vmware-tanzu/velero/pkg/client"
|
||||
@@ -935,6 +936,11 @@ func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string
|
||||
}
|
||||
}
|
||||
|
||||
if s.config.MaxBackupExtractionSize > 0 {
|
||||
s.logger.Infof("Setting backup data extraction cap as %v MB", s.config.MaxBackupExtractionSize)
|
||||
archive.SetMaxExtractionSize(int64(s.config.MaxBackupExtractionSize) * 1024 * 1024)
|
||||
}
|
||||
|
||||
s.logger.Info("Server starting...")
|
||||
|
||||
if err := s.mgr.Start(s.ctx); err != nil {
|
||||
|
||||
@@ -40,6 +40,12 @@ import (
|
||||
// not found
|
||||
var ErrNotFound = errors.New("file not found")
|
||||
var ErrDownloadRequestDownloadURLTimeout = errors.New("download request download url timeout, check velero server logs for errors. backup storage location may not be available")
|
||||
var unzipLimit int64 = 1024 * 1024 * 1024 // 1GB limit
|
||||
|
||||
// ErrDownloadRequestFailed is returned when the server refused the request and gave no
|
||||
// reason. The controller sets a message in every path that fails today, so this is a
|
||||
// fallback rather than the usual case.
|
||||
var ErrDownloadRequestFailed = errors.New("download request failed, check velero server logs for errors")
|
||||
|
||||
func Stream(
|
||||
ctx context.Context,
|
||||
@@ -114,6 +120,16 @@ func getDownloadURL(
|
||||
if updated.Status.DownloadURL != "" {
|
||||
return updated.Status.DownloadURL, nil
|
||||
}
|
||||
|
||||
// Failed is terminal. Waiting for a URL that will never be signed would end in
|
||||
// ErrDownloadRequestDownloadURLTimeout, which blames the storage location for
|
||||
// something the status already explains.
|
||||
if updated.Status.Phase == veleroV1api.DownloadRequestPhaseFailed {
|
||||
if updated.Status.Message != "" {
|
||||
return "", errors.New(updated.Status.Message)
|
||||
}
|
||||
return "", ErrDownloadRequestFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,17 +218,35 @@ func download(
|
||||
return errors.Errorf("request failed: %v", string(body))
|
||||
}
|
||||
|
||||
reader := resp.Body
|
||||
var r io.Reader = resp.Body
|
||||
var gzipReader *gzip.Reader
|
||||
if kind != veleroV1api.DownloadTargetKindBackupContents {
|
||||
// need to decompress logs
|
||||
gzipReader, err := gzip.NewReader(resp.Body)
|
||||
var err error
|
||||
gzipReader, err = gzip.NewReader(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
reader = gzipReader
|
||||
|
||||
r = io.LimitReader(gzipReader, unzipLimit)
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, reader)
|
||||
return err
|
||||
_, err = io.Copy(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if gzipReader != nil {
|
||||
var buf [1]byte
|
||||
n, err := gzipReader.Read(buf[:])
|
||||
if n > 0 || err == nil {
|
||||
return errors.Errorf("decompressed data exceeds the limit")
|
||||
}
|
||||
if err != io.EOF {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -463,6 +463,7 @@ func TestDownload(t *testing.T) {
|
||||
expectedContent string
|
||||
expectedError bool
|
||||
errorType error
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "successful download with gzip for logs",
|
||||
@@ -474,6 +475,16 @@ func TestDownload(t *testing.T) {
|
||||
expectedContent: testContent,
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "error decompressed data exceeds the limit",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(compressedContent.Bytes())
|
||||
},
|
||||
target: velerov1api.DownloadTargetKindBackupLog,
|
||||
expectedError: true,
|
||||
expectedErrMsg: "decompressed data exceeds the limit",
|
||||
},
|
||||
{
|
||||
name: "successful download without gzip for backup contents",
|
||||
serverHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -506,6 +517,12 @@ func TestDownload(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
originalLimit := unzipLimit
|
||||
if tc.expectedErrMsg == "decompressed data exceeds the limit" {
|
||||
unzipLimit = 10
|
||||
}
|
||||
defer func() { unzipLimit = originalLimit }()
|
||||
|
||||
server := httptest.NewServer(tc.serverHandler)
|
||||
defer server.Close()
|
||||
|
||||
@@ -525,6 +542,9 @@ func TestDownload(t *testing.T) {
|
||||
if tc.errorType != nil {
|
||||
assert.Equal(t, tc.errorType, err)
|
||||
}
|
||||
if tc.expectedErrMsg != "" {
|
||||
assert.Contains(t, err.Error(), tc.expectedErrMsg)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedContent, buf.String())
|
||||
|
||||
@@ -90,8 +90,12 @@ func printBackup(backup *velerov1api.Backup) []metav1.TableRow {
|
||||
if backup.Status.Expiration != nil {
|
||||
expiration = backup.Status.Expiration.Time
|
||||
}
|
||||
if expiration.IsZero() && backup.Spec.TTL.Duration > 0 {
|
||||
expiration = backup.CreationTimestamp.Add(backup.Spec.TTL.Duration)
|
||||
// Only estimate expiration from TTL after the backup has started. Backups
|
||||
// stalled in New have no Status.Expiration yet; using CreationTimestamp
|
||||
// would incorrectly show them as already expired (issue #3555).
|
||||
if expiration.IsZero() && backup.Spec.TTL.Duration > 0 &&
|
||||
backup.Status.StartTimestamp != nil && !backup.Status.StartTimestamp.Time.IsZero() {
|
||||
expiration = backup.Status.StartTimestamp.Time.Add(backup.Spec.TTL.Duration)
|
||||
}
|
||||
|
||||
status := string(backup.Status.Phase)
|
||||
|
||||
@@ -76,6 +76,26 @@ func TestPrintBackupWithoutStartTimestamp(t *testing.T) {
|
||||
assert.Equal(t, string(velerov1api.BackupPhaseFailedValidation), rows[0].Cells[1])
|
||||
}
|
||||
|
||||
func TestPrintBackupExpiresForStalledNewBackup(t *testing.T) {
|
||||
created := metav1.NewTime(time.Now().Add(-20 * 24 * time.Hour))
|
||||
backup := &velerov1api.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "clusterstate-20210128123759",
|
||||
CreationTimestamp: created,
|
||||
},
|
||||
Spec: velerov1api.BackupSpec{
|
||||
TTL: metav1.Duration{Duration: 10 * 24 * time.Hour},
|
||||
},
|
||||
Status: velerov1api.BackupStatus{
|
||||
Phase: velerov1api.BackupPhaseNew,
|
||||
},
|
||||
}
|
||||
|
||||
rows := printBackup(backup)
|
||||
require.Len(t, rows, 1)
|
||||
assert.Equal(t, "n/a", rows[0].Cells[5], "stalled New backup should not show expiration in the past")
|
||||
}
|
||||
|
||||
func TestPrintBackupWithStartTimestamp(t *testing.T) {
|
||||
started := metav1.NewTime(time.Date(2026, 8, 8, 21, 6, 28, 0, time.UTC))
|
||||
backup := &velerov1api.Backup{
|
||||
|
||||
Reference in New Issue
Block a user