mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-09-20 06:54:32 +00:00
Merge pull request #9132 from mjnagel/crd-upgrade
Run the E2E test on kind / get-go-version (push) Failing after 56s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 13s
Main CI / Build (push) Failing after 25s
Close stale issues and PRs / stale (push) Successful in 12s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m36s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m16s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m13s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m4s
Run the E2E test on kind / get-go-version (push) Failing after 56s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 13s
Main CI / Build (push) Failing after 25s
Close stale issues and PRs / stale (push) Successful in 12s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m36s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m16s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m13s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m4s
feat: add apply flag to install command
This commit is contained in:
@@ -102,6 +102,11 @@ type StatusUpdater interface {
|
||||
UpdateStatus(obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
// Applier applies changes to an object using server-side apply
|
||||
type Applier interface {
|
||||
Apply(name string, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
// Dynamic contains client methods that Velero needs for backing up and restoring resources.
|
||||
type Dynamic interface {
|
||||
Creator
|
||||
@@ -111,6 +116,7 @@ type Dynamic interface {
|
||||
Patcher
|
||||
Deletor
|
||||
StatusUpdater
|
||||
Applier
|
||||
}
|
||||
|
||||
// dynamicResourceClient implements Dynamic.
|
||||
@@ -136,6 +142,10 @@ func (d *dynamicResourceClient) Get(name string, opts metav1.GetOptions) (*unstr
|
||||
return d.resourceClient.Get(context.TODO(), name, opts)
|
||||
}
|
||||
|
||||
func (d *dynamicResourceClient) Apply(name string, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) {
|
||||
return d.resourceClient.Apply(context.TODO(), name, obj, opts)
|
||||
}
|
||||
|
||||
func (d *dynamicResourceClient) Patch(name string, data []byte) (*unstructured.Unstructured, error) {
|
||||
return d.resourceClient.Patch(context.TODO(), name, types.MergePatchType, data, metav1.PatchOptions{})
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ type Options struct {
|
||||
ConcurrentBackups int
|
||||
NodeAgentDisableHostPath bool
|
||||
kubeletRootDir string
|
||||
Apply bool
|
||||
ServerPriorityClassName string
|
||||
NodeAgentPriorityClassName string
|
||||
}
|
||||
@@ -102,6 +103,7 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) {
|
||||
flags.StringVar(&o.BucketName, "bucket", o.BucketName, "Name of the object storage bucket where backups should be stored")
|
||||
flags.StringVar(&o.SecretFile, "secret-file", o.SecretFile, "File containing credentials for backup and volume provider. If not specified, --no-secret must be used for confirmation. Optional.")
|
||||
flags.BoolVar(&o.NoSecret, "no-secret", o.NoSecret, "Flag indicating if a secret should be created. Must be used as confirmation if --secret-file is not provided. Optional.")
|
||||
flags.BoolVar(&o.Apply, "apply", o.Apply, "Flag indicating if resources should be applied instead of created. This can be used for updating existing resources.")
|
||||
flags.BoolVar(&o.NoDefaultBackupLocation, "no-default-backup-location", o.NoDefaultBackupLocation, "Flag indicating if a default backup location should be created. Must be used as confirmation if --bucket or --provider are not provided. Optional.")
|
||||
flags.StringVar(&o.Image, "image", o.Image, "Image to use for the Velero and node agent pods. Optional.")
|
||||
flags.StringVar(&o.Prefix, "prefix", o.Prefix, "Prefix under which all Velero data should be stored within the bucket. Optional.")
|
||||
@@ -416,7 +418,7 @@ func (o *Options) Run(c *cobra.Command, f client.Factory) error {
|
||||
|
||||
errorMsg := fmt.Sprintf("\n\nError installing Velero. Use `kubectl logs deploy/velero -n %s` to check the deploy logs", o.Namespace)
|
||||
|
||||
err = install.Install(dynamicFactory, kbClient, resources, os.Stdout)
|
||||
err = install.Install(dynamicFactory, kbClient, resources, os.Stdout, o.Apply)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, errorMsg)
|
||||
}
|
||||
|
||||
+31
-15
@@ -278,30 +278,45 @@ func GroupResources(resources *unstructured.UnstructuredList) *ResourceGroup {
|
||||
return rg
|
||||
}
|
||||
|
||||
// createResource attempts to create a resource in the cluster.
|
||||
// If the resource already exists in the cluster, it's merely logged.
|
||||
func createResource(r *unstructured.Unstructured, factory client.DynamicFactory, w io.Writer) error {
|
||||
// createOrApplyResource attempts to create or apply a resource in the cluster.
|
||||
// If apply is true, it uses server-side apply to update existing resources.
|
||||
// If apply is false and the resource already exists in the cluster, it's merely logged.
|
||||
func createOrApplyResource(r *unstructured.Unstructured, factory client.DynamicFactory, w io.Writer, apply bool) error {
|
||||
id := fmt.Sprintf("%s/%s", r.GetKind(), r.GetName())
|
||||
|
||||
// Helper to reduce boilerplate message about the same object
|
||||
log := func(f string, a ...any) {
|
||||
format := strings.Join([]string{id, ": ", f, "\n"}, "")
|
||||
fmt.Fprintf(w, format, a...)
|
||||
log := func(f string) {
|
||||
fmt.Fprintf(w, "%s: %s\n", id, f)
|
||||
}
|
||||
log("attempting to create resource")
|
||||
|
||||
c, err := CreateClient(r, factory, w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.Create(r); apierrors.IsAlreadyExists(err) {
|
||||
log("already exists, proceeding")
|
||||
} else if err != nil {
|
||||
return errors.Wrapf(err, "Error creating resource %s", id)
|
||||
if apply {
|
||||
log("attempting to apply resource")
|
||||
// Set field manager for server-side apply and force to override conflicts
|
||||
applyOpts := metav1.ApplyOptions{
|
||||
FieldManager: "velero-cli",
|
||||
Force: true,
|
||||
}
|
||||
|
||||
if _, err := c.Apply(r.GetName(), r, applyOpts); err != nil {
|
||||
return errors.Wrapf(err, "Error applying resource %s", id)
|
||||
}
|
||||
log("applied")
|
||||
} else {
|
||||
log("attempting to create resource")
|
||||
if _, err := c.Create(r); apierrors.IsAlreadyExists(err) {
|
||||
log("already exists, proceeding")
|
||||
} else if err != nil {
|
||||
return errors.Wrapf(err, "Error creating resource %s", id)
|
||||
} else {
|
||||
log("created")
|
||||
}
|
||||
}
|
||||
|
||||
log("created")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -335,13 +350,14 @@ func CreateClient(r *unstructured.Unstructured, factory client.DynamicFactory, w
|
||||
// An unstructured list of resources is sent, one at a time, to the server. These are assumed to be in the preferred order already.
|
||||
// Resources will be sorted into CustomResourceDefinitions and any other resource type, and the function will wait up to 1 minute
|
||||
// for CRDs to be ready before proceeding.
|
||||
// If apply is true, it uses server-side apply to update existing resources.
|
||||
// An io.Writer can be used to output to a log or the console.
|
||||
func Install(dynamicFactory client.DynamicFactory, kbClient kbclient.Client, resources *unstructured.UnstructuredList, w io.Writer) error {
|
||||
func Install(dynamicFactory client.DynamicFactory, kbClient kbclient.Client, resources *unstructured.UnstructuredList, w io.Writer, apply bool) error {
|
||||
rg := GroupResources(resources)
|
||||
|
||||
//Install CRDs first
|
||||
for _, r := range rg.CRDResources {
|
||||
if err := createResource(r, dynamicFactory, w); err != nil {
|
||||
if err := createOrApplyResource(r, dynamicFactory, w, apply); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -357,7 +373,7 @@ func Install(dynamicFactory client.DynamicFactory, kbClient kbclient.Client, res
|
||||
|
||||
// Install all other resources
|
||||
for _, r := range rg.OtherResources {
|
||||
if err = createResource(r, dynamicFactory, w); err != nil {
|
||||
if err = createOrApplyResource(r, dynamicFactory, w, apply); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
+235
-1
@@ -1,6 +1,8 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -12,9 +14,11 @@ import (
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
v1crds "github.com/vmware-tanzu/velero/config/crd/v1/crds"
|
||||
@@ -53,7 +57,7 @@ func TestInstall(t *testing.T) {
|
||||
require.NoError(t, appendUnstructured(resources, v1crds.CRDs[0]))
|
||||
require.NoError(t, appendUnstructured(resources, Namespace("velero")))
|
||||
|
||||
assert.NoError(t, Install(factory, c, resources, os.Stdout))
|
||||
assert.NoError(t, Install(factory, c, resources, os.Stdout, false))
|
||||
}
|
||||
|
||||
func Test_crdsAreReady(t *testing.T) {
|
||||
@@ -168,3 +172,233 @@ func TestNodeAgentWindowsIsReady(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.True(t, ready)
|
||||
}
|
||||
|
||||
func TestCreateOrApplyResourceError(t *testing.T) {
|
||||
r := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-configmap",
|
||||
"namespace": "velero",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dc := &test.FakeDynamicClient{}
|
||||
expectedErr := errors.New("create error")
|
||||
dc.On("Create", mock.Anything).Return(&unstructured.Unstructured{}, expectedErr)
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := createOrApplyResource(r, factory, &buf, false)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), expectedErr.Error())
|
||||
}
|
||||
|
||||
func TestCreateOrApplyResourceAlreadyExists(t *testing.T) {
|
||||
r := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-configmap",
|
||||
"namespace": "velero",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dc := &test.FakeDynamicClient{}
|
||||
alreadyExistsErr := apierrors.NewAlreadyExists(schema.GroupResource{Resource: "configmaps"}, "test-configmap")
|
||||
// We need to return a non-nil unstructured object even though it's not used
|
||||
dc.On("Create", mock.Anything).Return(&unstructured.Unstructured{}, alreadyExistsErr)
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := createOrApplyResource(r, factory, &buf, false)
|
||||
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateOrApplyResourceClientError(t *testing.T) {
|
||||
r := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-configmap",
|
||||
"namespace": "velero",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
expectedErr := errors.New("client creation error")
|
||||
// Return error from ClientForGroupVersionResource
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(&test.FakeDynamicClient{}, expectedErr)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := createOrApplyResource(r, factory, &buf, false)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), expectedErr.Error())
|
||||
}
|
||||
|
||||
func TestCreateOrApplyResourceApplyError(t *testing.T) {
|
||||
r := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-configmap",
|
||||
"namespace": "velero",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
dc := &test.FakeDynamicClient{}
|
||||
expectedErr := errors.New("apply error")
|
||||
// Mock Apply to return an error
|
||||
dc.On("Apply", mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{}, expectedErr)
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := createOrApplyResource(r, factory, &buf, true) // true for apply flag to use Apply
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), expectedErr.Error())
|
||||
}
|
||||
|
||||
func TestInstallErrorAfterCreateClient(t *testing.T) {
|
||||
// Create a test non-CRD resource
|
||||
nonCRDResource := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-configmap",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resources := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{*nonCRDResource},
|
||||
}
|
||||
|
||||
// Mock the factory to return a client that will succeed on ClientForGroupVersionResource
|
||||
// but fail on Create
|
||||
dc := &test.FakeDynamicClient{}
|
||||
expectedErr := errors.New("create error after successful client creation")
|
||||
dc.On("Create", mock.Anything).Return(&unstructured.Unstructured{}, expectedErr)
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
|
||||
|
||||
c := fake.NewClientBuilder().Build()
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := Install(factory, c, resources, &buf, false)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), expectedErr.Error())
|
||||
}
|
||||
|
||||
func TestInstallErrorOnCRDResource(t *testing.T) {
|
||||
crdResource := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "apiextensions.k8s.io/v1",
|
||||
"kind": "CustomResourceDefinition",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-crd",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resources := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{*crdResource},
|
||||
}
|
||||
|
||||
dc := &test.FakeDynamicClient{}
|
||||
expectedErr := errors.New("error creating CRD resource")
|
||||
// We need to return a non-nil unstructured object even though it's not used
|
||||
dc.On("Create", mock.Anything).Return(&unstructured.Unstructured{}, expectedErr)
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
|
||||
|
||||
c := fake.NewClientBuilder().Build()
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := Install(factory, c, resources, &buf, false)
|
||||
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), expectedErr.Error())
|
||||
}
|
||||
|
||||
func TestInstallWithApplyFlag(t *testing.T) {
|
||||
// Create a test resource
|
||||
testResource := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-configmap",
|
||||
"namespace": "velero",
|
||||
},
|
||||
"data": map[string]any{
|
||||
"key1": "value1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resources := &unstructured.UnstructuredList{
|
||||
Items: []unstructured.Unstructured{*testResource},
|
||||
}
|
||||
|
||||
// Test case 1: Without apply flag (create)
|
||||
{
|
||||
dc := &test.FakeDynamicClient{}
|
||||
// Expect Create to be called
|
||||
dc.On("Create", mock.Anything).Return(testResource, nil)
|
||||
// Apply should not be called
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
|
||||
|
||||
c := fake.NewClientBuilder().Build()
|
||||
|
||||
err := Install(factory, c, resources, os.Stdout, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that Create was called and Apply was not
|
||||
dc.AssertCalled(t, "Create", mock.Anything)
|
||||
dc.AssertNotCalled(t, "Apply", mock.Anything, mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
// Test case 2: With apply flag
|
||||
{
|
||||
dc := &test.FakeDynamicClient{}
|
||||
// Create should not be called
|
||||
// Expect Apply to be called
|
||||
dc.On("Apply", mock.Anything, mock.Anything, mock.Anything).Return(testResource, nil)
|
||||
|
||||
factory := &test.FakeDynamicFactory{}
|
||||
factory.On("ClientForGroupVersionResource", mock.Anything, mock.Anything, mock.Anything).Return(dc, nil)
|
||||
|
||||
c := fake.NewClientBuilder().Build()
|
||||
|
||||
err := Install(factory, c, resources, os.Stdout, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that Apply was called and Create was not
|
||||
dc.AssertCalled(t, "Apply", mock.Anything, mock.Anything, mock.Anything)
|
||||
dc.AssertNotCalled(t, "Create", mock.Anything)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,3 +83,8 @@ func (c *FakeDynamicClient) UpdateStatus(obj *unstructured.Unstructured, opts me
|
||||
args := c.Called(obj, opts)
|
||||
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
|
||||
}
|
||||
|
||||
func (c *FakeDynamicClient) Apply(name string, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) {
|
||||
args := c.Called(name, obj, opts)
|
||||
return args.Get(0).(*unstructured.Unstructured), args.Error(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user