Skip to content

Commit 7828661

Browse files
authored
Merge pull request #1174 from fluxcd/migrate-api-version
ssa: introduce support for migrating API version
2 parents 9b7ca2d + 18b0e9d commit 7828661

3 files changed

Lines changed: 531 additions & 4 deletions

File tree

ssa/manager_apply.go

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@ type ApplyOptions struct {
6868
// CustomStageKinds defines a set of Kubernetes resource types that should be applied
6969
// in a separate stage after CRDs and before namespaced objects.
7070
CustomStageKinds map[schema.GroupKind]struct{} `json:"customStageKinds,omitempty"`
71+
72+
// MigrateAPIVersion, when enabled, rewrites every managed fields entry
73+
// on the existing object to match the API version of the applied object
74+
// before each apply.
75+
//
76+
// This is needed after a CRD adds a new version that introduces fields
77+
// with default values: without migration, any managed fields entry still
78+
// tagged with the old API version causes the API server to fail the
79+
// apply with "field not declared in schema" for the new defaulted field.
80+
MigrateAPIVersion bool `json:"migrateAPIVersion,omitempty"`
7181
}
7282

7383
// ApplyCleanupOptions defines which metadata entries are to be removed before applying objects.
@@ -108,6 +118,15 @@ func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstru
108118
return m.changeSetEntry(object, SkippedAction), nil
109119
}
110120

121+
var patched bool
122+
if opts.MigrateAPIVersion && getError == nil {
123+
var err error
124+
patched, err = m.migrateAPIVersion(ctx, existingObject, object.GetAPIVersion())
125+
if err != nil {
126+
return nil, fmt.Errorf("%s failed to migrate API version: %w", utils.FmtUnstructured(existingObject), err)
127+
}
128+
}
129+
111130
dryRunObject := object.DeepCopy()
112131
if err := m.dryRunApply(ctx, dryRunObject); err != nil {
113132
if !errors.IsNotFound(getError) && m.shouldForceApply(object, existingObject, opts, err) {
@@ -121,11 +140,12 @@ func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstru
121140
return nil, ssaerrors.NewDryRunErr(err, dryRunObject)
122141
}
123142

124-
patched, err := m.cleanupMetadata(ctx, object, existingObject, opts.Cleanup)
143+
patchedCleanupMetadata, err := m.cleanupMetadata(ctx, object, existingObject, opts.Cleanup)
125144
if err != nil {
126145
return nil, fmt.Errorf("%s metadata.managedFields cleanup failed: %w",
127146
utils.FmtUnstructured(existingObject), err)
128147
}
148+
patched = patched || patchedCleanupMetadata
129149

130150
// do not apply objects that have not drifted to avoid bumping the resource version
131151
if !patched && !m.hasDrifted(existingObject, dryRunObject) {
@@ -172,6 +192,15 @@ func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.
172192
return nil
173193
}
174194

195+
var patched bool
196+
if opts.MigrateAPIVersion && getError == nil {
197+
var err error
198+
patched, err = m.migrateAPIVersion(ctx, existingObject, object.GetAPIVersion())
199+
if err != nil {
200+
return fmt.Errorf("%s failed to migrate API version: %w", utils.FmtUnstructured(existingObject), err)
201+
}
202+
}
203+
175204
dryRunObject := object.DeepCopy()
176205
if err := m.dryRunApply(ctx, dryRunObject); err != nil {
177206
// We cannot have an immutable error (and therefore shouldn't force-apply) if the resource doesn't
@@ -207,11 +236,12 @@ func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.
207236
}
208237
}
209238

210-
patched, err := m.cleanupMetadata(ctx, object, existingObject, opts.Cleanup)
239+
patchedCleanupMetadata, err := m.cleanupMetadata(ctx, object, existingObject, opts.Cleanup)
211240
if err != nil {
212241
return fmt.Errorf("%s metadata.managedFields cleanup failed: %w",
213242
utils.FmtUnstructured(existingObject), err)
214243
}
244+
patched = patched || patchedCleanupMetadata
215245

216246
if patched || m.hasDrifted(existingObject, dryRunObject) {
217247
toApply[i] = object
@@ -345,6 +375,40 @@ func (m *ResourceManager) apply(ctx context.Context, object *unstructured.Unstru
345375
return m.client.Patch(ctx, object, client.Apply, opts...)
346376
}
347377

378+
// migrateAPIVersion rewrites every managed fields entry on existingObject
379+
// to desiredAPIVersion via a JSON patch. See ApplyOptions.MigrateAPIVersion
380+
// for the motivation. Every entry is rewritten, regardless of the field
381+
// manager that owns it or whether it sits on a subresource: any entry
382+
// left at an older API version can make the next apply fail with "field
383+
// not declared in schema". On success existingObject is updated in-place
384+
// with the server's response. Returns whether a patch was actually
385+
// applied (false means there was nothing to migrate).
386+
func (m *ResourceManager) migrateAPIVersion(ctx context.Context,
387+
existingObject *unstructured.Unstructured,
388+
desiredAPIVersion string) (bool, error) {
389+
390+
// Build patch.
391+
patch, err := PatchMigrateToVersion(existingObject, desiredAPIVersion)
392+
if err != nil {
393+
return false, fmt.Errorf("failed to create patch for migrating managed fields API version: %w", err)
394+
}
395+
if len(patch) == 0 {
396+
return false, nil
397+
}
398+
399+
// Apply patch.
400+
patchBytes, err := json.Marshal(patch)
401+
if err != nil {
402+
return false, fmt.Errorf("failed to marshal patch for migrating managed fields API version: %w", err)
403+
}
404+
rawPatch := client.RawPatch(types.JSONPatchType, patchBytes)
405+
if err := m.client.Patch(ctx, existingObject, rawPatch, client.FieldOwner(m.owner.Field)); err != nil {
406+
return false, fmt.Errorf("failed to migrate managed fields API version to %s: %w", desiredAPIVersion, err)
407+
}
408+
409+
return true, nil
410+
}
411+
348412
// cleanupMetadata performs an HTTP PATCH request to remove entries from metadata annotations, labels and managedFields.
349413
func (m *ResourceManager) cleanupMetadata(ctx context.Context,
350414
desiredObject *unstructured.Unstructured,

0 commit comments

Comments
 (0)