Skip to content

Commit 554b04e

Browse files
authored
Merge pull request #9132 from mjnagel/crd-upgrade
feat: add apply flag to install command
2 parents c594026 + 3244cc6 commit 554b04e

File tree

8 files changed

+365
-17
lines changed

8 files changed

+365
-17
lines changed

changelogs/unreleased/9132-mjnagel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `--apply` flag to `install` command, allowing usage of Kubernetes apply to make changes to existing installs

design/Implemented/apply-flag.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Apply flag for install command
2+
3+
## Abstract
4+
Add an `--apply` flag to the install command that enables applying existing resources rather than creating them. This can be useful as part of the upgrade process for existing installations.
5+
6+
## Background
7+
The current Velero install command creates resources but doesn't provide a direct way to apply updates to an existing installation.
8+
Users attempting to run the install command on an existing installation receive "already exists" messages.
9+
Upgrade steps for existing installs typically involve a three (or more) step process to apply updated CRDs (using `--dry-run` and piping to `kubectl apply`) and then updating/setting images on the Velero deployment and node-agent.
10+
11+
## Goals
12+
- Provide a simple flag to enable applying resources on an existing Velero installation.
13+
- Use server-side apply to update existing resources rather than attempting to create them.
14+
- Maintain consistency with the regular install flow.
15+
16+
## Non Goals
17+
- Implement special logic for specific version-to-version upgrades (i.e. resource deletion, etc).
18+
- Add complex upgrade validation or pre/post-upgrade hooks.
19+
- Provide rollback capabilities.
20+
21+
## High-Level Design
22+
The `--apply` flag will be added to the Velero install command.
23+
When this flag is set, the installation process will use server-side apply to update existing resources instead of using create on new resources.
24+
This flag can be used as _part_ of the upgrade process, but will not always fully handle an upgrade.
25+
26+
## Detailed Design
27+
The implementation adds a new boolean flag `--apply` to the install command.
28+
This flag will be passed through to the underlying install functions where the resource creation logic resides.
29+
30+
When the flag is set to true:
31+
- The `createOrApplyResource` function will use server-side apply with field manager "velero-cli" and `force=true` to update resources.
32+
- Resources will be applied in the same order as they would be created during installation.
33+
- Custom Resource Definitions will still be processed first, and the system will wait for them to be established before continuing.
34+
35+
The server-side apply approach with `force=true` ensures that resources are updated even if there are conflicts with the last applied state.
36+
This provides a best-effort mechanism to apply resources that follows the same flow as installation but updates resources instead of creating them.
37+
38+
No special handling is added for specific versions or resource structures, making this a general-purpose mechanism for applying resources.
39+
40+
## Alternatives Considered
41+
1. Creating a separate `upgrade` command that would duplicate much of the install command logic.
42+
- Rejected due to code duplication and maintenance overhead.
43+
44+
2. Implementing version-specific upgrade logic to handle breaking changes between versions.
45+
- Rejected as overly complex and difficult to maintain across multiple version paths.
46+
- This could be considered again in the future, but is not in the scope of the current design.
47+
48+
3. Adding automatic detection of existing resources and switching to apply mode.
49+
- Rejected as it could lead to unexpected behavior and confusion if users unintentionally apply changes to existing resources.
50+
51+
## Security Considerations
52+
The apply flag maintains the same security profile as the install command.
53+
No additional permissions are required beyond what is needed for resource creation.
54+
The use of `force=true` with server-side apply could potentially override manual changes made to resources, but this is a necessary trade-off to ensure apply is successful.
55+
56+
## Compatibility
57+
This enhancement is compatible with all existing Velero installations as it is a new opt-in flag.
58+
It does not change any resource formats or API contracts.
59+
The apply process is best-effort and does not guarantee compatibility between arbitrary versions of Velero.
60+
Users should still consult release notes for any breaking changes that may require manual intervention.
61+
This flag could be adopted by the helm chart, specifically for CRD updates, to simplify the CRD update job.
62+
63+
## Implementation
64+
The implementation involves:
65+
1. Adding support for `Apply` to the existing Kubernetes client code.
66+
1. Adding the `--apply` flag to the install command options.
67+
1. Changing `createResource` to `createOrApplyResource` and updating it to use server-side apply when the `apply` boolean is set.
68+
69+
The implementation is straightforward and follows existing code patterns.
70+
No migration of state or special handling of specific resources is required.

pkg/client/dynamic.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ type StatusUpdater interface {
102102
UpdateStatus(obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*unstructured.Unstructured, error)
103103
}
104104

105+
// Applier applies changes to an object using server-side apply
106+
type Applier interface {
107+
Apply(name string, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error)
108+
}
109+
105110
// Dynamic contains client methods that Velero needs for backing up and restoring resources.
106111
type Dynamic interface {
107112
Creator
@@ -111,6 +116,7 @@ type Dynamic interface {
111116
Patcher
112117
Deletor
113118
StatusUpdater
119+
Applier
114120
}
115121

116122
// dynamicResourceClient implements Dynamic.
@@ -136,6 +142,10 @@ func (d *dynamicResourceClient) Get(name string, opts metav1.GetOptions) (*unstr
136142
return d.resourceClient.Get(context.TODO(), name, opts)
137143
}
138144

145+
func (d *dynamicResourceClient) Apply(name string, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) {
146+
return d.resourceClient.Apply(context.TODO(), name, obj, opts)
147+
}
148+
139149
func (d *dynamicResourceClient) Patch(name string, data []byte) (*unstructured.Unstructured, error) {
140150
return d.resourceClient.Patch(context.TODO(), name, types.MergePatchType, data, metav1.PatchOptions{})
141151
}

pkg/cmd/cli/install/install.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ type Options struct {
9292
ConcurrentBackups int
9393
NodeAgentDisableHostPath bool
9494
kubeletRootDir string
95+
Apply bool
9596
ServerPriorityClassName string
9697
NodeAgentPriorityClassName string
9798
}
@@ -102,6 +103,7 @@ func (o *Options) BindFlags(flags *pflag.FlagSet) {
102103
flags.StringVar(&o.BucketName, "bucket", o.BucketName, "Name of the object storage bucket where backups should be stored")
103104
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.")
104105
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.")
106+
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.")
105107
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.")
106108
flags.StringVar(&o.Image, "image", o.Image, "Image to use for the Velero and node agent pods. Optional.")
107109
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 {
416418

417419
errorMsg := fmt.Sprintf("\n\nError installing Velero. Use `kubectl logs deploy/velero -n %s` to check the deploy logs", o.Namespace)
418420

419-
err = install.Install(dynamicFactory, kbClient, resources, os.Stdout)
421+
err = install.Install(dynamicFactory, kbClient, resources, os.Stdout, o.Apply)
420422
if err != nil {
421423
return errors.Wrap(err, errorMsg)
422424
}

pkg/install/install.go

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -278,30 +278,45 @@ func GroupResources(resources *unstructured.UnstructuredList) *ResourceGroup {
278278
return rg
279279
}
280280

281-
// createResource attempts to create a resource in the cluster.
282-
// If the resource already exists in the cluster, it's merely logged.
283-
func createResource(r *unstructured.Unstructured, factory client.DynamicFactory, w io.Writer) error {
281+
// createOrApplyResource attempts to create or apply a resource in the cluster.
282+
// If apply is true, it uses server-side apply to update existing resources.
283+
// If apply is false and the resource already exists in the cluster, it's merely logged.
284+
func createOrApplyResource(r *unstructured.Unstructured, factory client.DynamicFactory, w io.Writer, apply bool) error {
284285
id := fmt.Sprintf("%s/%s", r.GetKind(), r.GetName())
285286

286287
// Helper to reduce boilerplate message about the same object
287-
log := func(f string, a ...any) {
288-
format := strings.Join([]string{id, ": ", f, "\n"}, "")
289-
fmt.Fprintf(w, format, a...)
288+
log := func(f string) {
289+
fmt.Fprintf(w, "%s: %s\n", id, f)
290290
}
291-
log("attempting to create resource")
292291

293292
c, err := CreateClient(r, factory, w)
294293
if err != nil {
295294
return err
296295
}
297296

298-
if _, err := c.Create(r); apierrors.IsAlreadyExists(err) {
299-
log("already exists, proceeding")
300-
} else if err != nil {
301-
return errors.Wrapf(err, "Error creating resource %s", id)
297+
if apply {
298+
log("attempting to apply resource")
299+
// Set field manager for server-side apply and force to override conflicts
300+
applyOpts := metav1.ApplyOptions{
301+
FieldManager: "velero-cli",
302+
Force: true,
303+
}
304+
305+
if _, err := c.Apply(r.GetName(), r, applyOpts); err != nil {
306+
return errors.Wrapf(err, "Error applying resource %s", id)
307+
}
308+
log("applied")
309+
} else {
310+
log("attempting to create resource")
311+
if _, err := c.Create(r); apierrors.IsAlreadyExists(err) {
312+
log("already exists, proceeding")
313+
} else if err != nil {
314+
return errors.Wrapf(err, "Error creating resource %s", id)
315+
} else {
316+
log("created")
317+
}
302318
}
303319

304-
log("created")
305320
return nil
306321
}
307322

@@ -335,13 +350,14 @@ func CreateClient(r *unstructured.Unstructured, factory client.DynamicFactory, w
335350
// An unstructured list of resources is sent, one at a time, to the server. These are assumed to be in the preferred order already.
336351
// Resources will be sorted into CustomResourceDefinitions and any other resource type, and the function will wait up to 1 minute
337352
// for CRDs to be ready before proceeding.
353+
// If apply is true, it uses server-side apply to update existing resources.
338354
// An io.Writer can be used to output to a log or the console.
339-
func Install(dynamicFactory client.DynamicFactory, kbClient kbclient.Client, resources *unstructured.UnstructuredList, w io.Writer) error {
355+
func Install(dynamicFactory client.DynamicFactory, kbClient kbclient.Client, resources *unstructured.UnstructuredList, w io.Writer, apply bool) error {
340356
rg := GroupResources(resources)
341357

342358
//Install CRDs first
343359
for _, r := range rg.CRDResources {
344-
if err := createResource(r, dynamicFactory, w); err != nil {
360+
if err := createOrApplyResource(r, dynamicFactory, w, apply); err != nil {
345361
return err
346362
}
347363
}
@@ -357,7 +373,7 @@ func Install(dynamicFactory client.DynamicFactory, kbClient kbclient.Client, res
357373

358374
// Install all other resources
359375
for _, r := range rg.OtherResources {
360-
if err = createResource(r, dynamicFactory, w); err != nil {
376+
if err = createOrApplyResource(r, dynamicFactory, w, apply); err != nil {
361377
return err
362378
}
363379
}

0 commit comments

Comments
 (0)