Skip to content

feat: propagate Crossplane default labels and user metadata to all Cloud Foundry resources#281

Open
gergely-szabo-sap wants to merge 16 commits into
mainfrom
feat/external-resource-labeling
Open

feat: propagate Crossplane default labels and user metadata to all Cloud Foundry resources#281
gergely-szabo-sap wants to merge 16 commits into
mainfrom
feat/external-resource-labeling

Conversation

@gergely-szabo-sap
Copy link
Copy Markdown
Contributor

@gergely-szabo-sap gergely-szabo-sap commented May 7, 2026

Summary

Adds automatic propagation of Crossplane default labels (crossplane-kind, crossplane-name, crossplane-provider-config) and user-specified metadata (labels/annotations) to all Cloud Foundry resources managed by the provider.

Default Crossplane labels

When a managed CF resource is created or updated, the provider automatically applies
Crossplane-recommended labels from resource.GetExternalTags() (crossplane-runtime
v1.20.0). These are:

Label Value Source
crossplane-kind . (e.g. app.cloudfoundry.crossplane.io) CR's GroupVersionKind
crossplane-name CR's .metadata.name (e.g. my-app) CR's name
crossplane-providerconfig ProviderConfig name CR's .spec.providerConfigRef.name (only when set)

Where they appear:

  • On the CF resource itself — visible via cf curl /v3/apps/ in the metadata.labels field, or any CF API client that reads resource metadata.
  • In the CR's status.atProvider.labels — populated on every Observe cycle from the CF API read-back.

How behavior changes:

  • Before this feature: CF resources created by the provider carried no Crossplane-identifying labels. There was no way to tell from the CF side that a resource was managed by Crossplane. Metadata drift was only checked for ServiceRouteBinding (using exact-map equality); all other resources ignored labels/annotations entirely.
  • After: Every Create and Update call merges default Crossplane labels into the resource's metadata alongside any user-specified labels. IsUpToDate in all 8 metadata-capable clients now checks metadata drift using subset semantics — it verifies that every desired label/annotation is present and correct in the actual resource, but ignores extra labels on the CF side that the provider didn't set. This prevents drift loops when the CF platform or other actors add labels the provider didn't ask for.

Backward compatibility:

  • Existing managed resources will receive the default labels on their next Update cycle after the provider upgrade. The controller detects the missing labels via IsMetadataUpToDate, triggers an Update, and the labels are applied. No manual intervention is needed — this is a one-time migration.
  • No CR spec changes — default labels are stripped by LateInitialize before writing into spec.forProvider.labels, so existing CRs remain unchanged.
  • No drift loops — the subset-check semantics mean labels added to the CF resource by the platform, other actors, or prior manual configuration will not cause the provider to attempt removal. The provider only asserts that its own desired labels are present; it does not enforce label exclusivity.
  • Export portability — the exporter strips default labels from exported CR manifests, since crossplane-kind/crossplane-name/crossplane-providerconfig are environment-specific and must be regenerated by the target controller at the destination.
  • Upgrade test coverage verifies that default labels are absent before upgrade and present after (see [BUG] Upgrade test framework does not support single-image provider build #279).

Resources without metadata support:

Six CF resource types (OrgQuota, SpaceQuota, OrgRole, SpaceRole, OrgMembers, SpaceMembers) do not support metadata in the CF API. These are excluded from label/annotation propagation — the ResourceMetadata struct is not embedded in their CRD types, and no metadata drift detection is performed.

Changes

Shared metadata infrastructure

  • internal/clients/metadata/ — new shared package providing:
    • BuildMetadata — merges Crossplane default labels with user-specified metadata
    • IsMetadataUpToDate — subset-aware drift detection (desired ⊆ actual)
    • DiffMetadata — produces the minimal merge-patch needed to align actual with desired
    • Nil pointer semantics for label/annotation deletion via the CF API

Resource metadata consolidation

  • apis/resources/v1alpha1/resource.go — introduces ResourceMetadata{ Labels, Annotations } struct with json:",inline" embedding
  • All 7 CRD types (App, Domain, Org, Space, Route, ServiceInstance, ServiceRouteBinding) now embed ResourceMetadata in both Parameters (spec) and Observation (status)
  • CRD YAML manifests regenerated
  • DeepCopy functions regenerated

Client and controller wiring

  • All 10 resource clients (app, domain, org, space, route, serviceinstance, servicebinding, serviceplan, serviceroutebinding) pass the managed resource through to BuildMetadata on Create and Update
  • Metadata read-back from CF API populated into Observation.Labels / Observation.Annotations
  • IsUpToDate in each client now checks metadata drift using subset semantics — extra labels from external actors won't trigger spurious updates

Exporter

  • cmd/exporter/cf/metadata/ strips Crossplane default labels (crossplane-kind, crossplane-name, crossplane-provider-config) from exported CR manifests, since these are environment-specific and should be regenerated by the target controller

Route controller refactoring

  • Split GetByIDOrSpec into FindRouteBySpec (backwards-compatibility fallback) and GetRouteByGUID (primary reconciliation path) for clearer separation of concerns

Tests

  • Unit tests: metadata package has 690 lines of test coverage including subset semantics, deletion markers, and edge cases
  • Unit tests: all 10 client packages updated to verify metadata propagation in Create/Update/IsUpToDate paths
  • E2e tests: label and annotation assertions for App, Org+Space, ServiceInstance+ServiceCredentialBinding, ServiceRouteBinding
  • Upgrade test: verifies Crossplane default labels are absent before upgrade and present after (see [BUG] Upgrade test framework does not support single-image provider build #279)

@gergely-szabo-sap gergely-szabo-sap self-assigned this May 7, 2026
@gergely-szabo-sap gergely-szabo-sap linked an issue May 7, 2026 that may be closed by this pull request
1 task
@gergely-szabo-sap gergely-szabo-sap marked this pull request as ready for review May 7, 2026 16:06
@gergely-szabo-sap gergely-szabo-sap force-pushed the feat/external-resource-labeling branch from a8b031a to 5c94f9c Compare May 8, 2026 08:48
@gergely-szabo-sap gergely-szabo-sap force-pushed the feat/external-resource-labeling branch from 5c94f9c to c31264d Compare May 8, 2026 12:08
Add internal/clients/metadata package with helpers for managing Cloud
Foundry resource metadata (labels and annotations) across all providers:

- BuildMetadata: merges Crossplane default labels (GetExternalTags)
  with user labels/annotations into *cfresource.Metadata; nil pointer
  values produce CF deletion markers via RemoveLabel/RemoveAnnotation

- MetadataMapEqual: exact equality check (nil == empty, nil-ptr != ptr(""))

- MetadataMapContains: subset check (desired ⊆ actual); extra keys in
  actual are ignored -- correct for merge-patch drift detection where
  CF resources may have system labels the provider doesn't manage

- IsMetadataUpToDate: convenience wrapper using MetadataMapContains
  for both labels and annotations

- DiffMetadata: computes merge-patch diff for Update calls; only
  processes keys in desired (keys in actual but not desired are left
  unchanged per CF API convention); includes nil-value deletion
  markers; accepts both label and annotation pairs

These helpers will replace per-controller duplicate implementations
(e.g. SRB controller's metadataMapEqual/isMetadataUpToDate) and
provide the foundation for issue #39 external resource labeling.
Add shared metadata client helpers (BuildMetadata, IsMetadataUpToDate,
DiffMetadata) for issue #39 external resource labeling support.

Standardize all 8 eligible resource types to use ResourceMetadata
inline struct consistently, replacing the mixed separate
Labels/Annotations fields on Space, Org, Domain, and ServiceInstance.
Update downstream consumers (client observation functions, domain
GenerateUpdate, exporter convert files) accordingly.
Move the "nil managed resource" test case from a nested position inside
the "nil pointer value overrides default label" test to a top-level
t.Run sibling in TestBuildMetadata.

Replace SRB controller's local isMetadataUpToDate and metadataMapEqual
helpers with metadata.IsMetadataUpToDate and metadata.MetadataMapEqual
from the shared metadata package. This adopts correct merge-patch
semantics (subset check via MetadataMapContains) instead of exact
equality, and removes ~45 lines of duplicated logic.

Update two SRB test expectations for subset semantics:
- ExtraLabelInActual: false -> true (desired is subset of actual)
- SpecWithoutMetadataBindingHasLabels: false -> true (empty desired
  is subset of anything)
…ayloads

All 7 eligible resource Update functions now accept a managed
resource parameter and call BuildMetadata, ensuring default
Crossplane labels (crossplane-kind, crossplane-name,
crossplane-providerconfig) and user-provided labels/annotations
from forProvider are included in CF Update API calls.

Controllers updated to pass the managed resource to Update calls.
Route RouteService interface Update signature extended with mg param.
SCB update replaces TODO with actual metadata handling.
SRB update now always sets metadata including defaults.
Replace hardcoded 'return true' stubs in Domain, Route, and SCB
IsUpToDate functions with real metadata comparison using the
shared metadata package. Thread the managed resource through
all IsUpToDate signatures so BuildMetadata can merge Crossplane
default labels with user-specified labels/annotations for
comparison against observed CF resource metadata.

For ServiceInstance, split the existing IsUpToDate into a
specUpToDate helper (name, plan, URLs) and a new IsUpToDate
that also checks metadata.

Controller observe tests updated to expect ResourceUpToDate: false
since fake mocks don't provide CF metadata, correctly reflecting
that metadata drift is now always detected.
…ceInstance

Copy Labels and Annotations from CF resource metadata into
the Crossplane observation during GenerateObservation /
UpdateObservation. This closes the metadata loop — without
read-back, IsUpToDate would see empty status.Labels and
perpetually flag metadata drift.

Domain, Org, Space, and SRB already had this from earlier
rounds. App, Route, ServiceCredentialBinding, and
ServiceInstance were missing it.
…le update guards

Org IsUpToDate now accepts the managed resource and compares
metadata via the shared package, completing the last holdout.
Org LateInitialize backfills spec labels/annotations from CF
when empty, preventing overwrite of pre-existing metadata.

Domain and Space controller Update guards removed: the
IsUpToDate check now properly gates all changes including
metadata, and DomainUpdate has no Name field so the old
name-change guard was a no-op.
Add metadata builder methods to App, Domain, Org, Route,
ServiceCredentialBinding, ServiceInstance, and Space fakes.
Each returns itself for chaining and nil-initializes Metadata.

Route also gets a new Route wrapper type with SetHost/SetGUID
alongside the existing FakeRoute constructor and MockRoute mock,
so controller tests can build routes with metadata via the
builder pattern.
Add GenerateCreate, GenerateUpdate, GenerateObservation, and
IsUpToDate tests for Domain, Org, and Space clients (new files).
Add TestIsUpToDate_Metadata to App, Route, SCB, and ServiceInstance
existing test files. Covers nil metadata, label drift/match,
annotation drift/match, and subset semantics where applicable.
Add e2e test coverage for Crossplane default labels and user-provided
labels/annotations on CF resources. New AssertDefaultLabels and
AssertLabelsAndAnnotations helpers verify crossplane-kind, crossplane-name,
and crossplane-providerconfig labels as well as user metadata.

- Add label/annotation assertions to app, orgspace, services, and SRB tests
- Add labels and annotations to all CR YAML manifests
- Poll for updated labels after SRB update (reconciler async)
- Skip label checks for observe-only resources (Org, Domain, etc.)
- Fix gofmt spacing, trailing whitespace, missing newlines
- Fix dangling close-paren syntax error in orgspace test
- Move SRB updated manifest to updated/ subdirectory
Verify that Crossplane default labels (crossplane-kind, crossplane-name,
crossplane-provider-config) are correctly applied to existing Space
resources after a provider upgrade.

- Pre-upgrade: assert no crossplane-* labels on CF resource
- Post-upgrade: assert all 3 default labels present with correct values
- Uses observe-only Org CR as Space dependency
- Follows CustomUpgradeTestBuilder pattern from existing upgrade tests
…rces

Add metadata.StripDefaultLabels() to remove crossplane-kind,
crossplane-name, and crossplane-provider-config labels from CF
resource metadata before writing exported CR manifests. These labels
are infrastructure metadata computed by the controller and should not
appear in ForProvider specs — a new CR gets different values and the
controller recomputes them on reconcile.

- New: cmd/exporter/cf/metadata/metadata.go — StripDefaultLabels helper
- Updated app, org, serviceinstance, and space exporters
- App exporter also gains previously missing ResourceMetadata block
- Group metadata import with other crossplane-provider-cloudfoundry
  imports in exporter convert files (goimports convention)
- Remove json.MarshalIndent debug logging from SRB e2e test poll loop
…eByGUID

Replace the hybrid GetByIDOrSpec lookup with two dedicated methods:
- FindRouteBySpec: backwards-compat lookup when external name is empty
- GetRouteByGUID: direct GUID lookup (primary path)

Moves late-initialization to the backwards-compat block, running before
GUID validation. GetRouteByGUID returns (obs, exists, error) instead of
the old nil-check convention.

Note: test mocks still reference GetByIDOrSpec and need updating.
…data check

- Add metadata.StripDefaultLabels to remove crossplane-kind/name/provider labels from late-initialized spec.Labels in org.LateInitialize
- Fix serviceroutebinding controller: call BuildMetadata before IsMetadataUpToDate to include default labels in comparison (was comparing bare user labels only)
- Update controller tests: add withDefaultMetadataLabels() modifier to set GVK, populate CF resource mocks with crossplane-* labels, correct ResourceUpToDate expectations from false to true
@gergely-szabo-sap gergely-szabo-sap force-pushed the feat/external-resource-labeling branch from c31264d to 5cd984c Compare May 11, 2026 10:48
@gergely-szabo-sap gergely-szabo-sap added the release-notes/new-feature Marks PR as new feature for release note generation label May 11, 2026
Both spaces had forProvider.name 'e2e-space', causing a CF naming collision within the same org. Renamed to e2e-space-ref and e2e-space-name to match their CR metadata names.
@mpechkurov
Copy link
Copy Markdown
Collaborator

This PR has been split into 6 smaller, independently reviewable PRs for easier review:

PR Title Depends On
#287 [281#1] Shared metadata helpers + API standardization None
#288 [281#2] Migrate SRB controller to shared metadata package #287
#289 [281#3] Wire Crossplane default labels into all CF resources (core feature) #288
#290 [281#4] Strip default labels from exporter #287
#291 [281#5] Route refactor (split GetByIDOrSpec) #289
#292 [281#6] Test coverage (unit, e2e, upgrade) #289

Merge order: #287#288#289#290#291#292

Each PR builds and passes all tests independently. Please review them in order.

@gergely-szabo-sap
Copy link
Copy Markdown
Contributor Author

PR Review Guide: Support External Resource Labeling

Feature Overview

This PR adds two capabilities to the provider:

  1. User-specified metadata propagation — Labels and annotations defined in
    spec.forProvider.labels / spec.forProvider.annotations are pushed to the
    Cloud Foundry resource on Create and Update, and drift-detected on Observe.

  2. Default Crossplane labels — Every Create and Update call also applies
    three Crossplane-recommended labels (crossplane-kind, crossplane-name,
    crossplane-providerconfig) to the CF resource. These are computed from the
    CR's identity by resource.GetExternalTags() (crossplane-runtime v1.20.0)
    and are NOT stored in the CR's spec.

Before this PR

  • CF resources created by the provider carried no Crossplane-identifying
    labels. There was no way to identify a managed resource from the CF side.
  • Only ServiceRouteBinding checked metadata drift, and it used exact-map
    equality
    — any extra label on the CF side caused IsUpToDate: false,
    triggering a spurious Update.
  • All other resource types ignored labels/annotations entirely.

After this PR

  • All 8 metadata-capable CF resources (App, Domain, Org, Space, Route,
    ServiceInstance, ServiceCredentialBinding, ServiceRouteBinding) propagate
    both user labels/annotations and default Crossplane labels.
  • All 8 clients use subset-check semantics for drift detection:
    desired ⊆ actual. Extra labels from the CF platform or other actors are
    ignored — no drift loops.
  • 6 resources without CF API metadata support (OrgQuota, SpaceQuota, OrgRole,
    SpaceRole, OrgMembers, SpaceMembers) are excluded entirely.

Step-by-Step Walkthrough

Step 1: Shared metadata type — ResourceMetadata

File: apis/resources/v1alpha1/resource.go

type ResourceMetadata struct {
    Annotations map[string]*string `json:"annotations,omitempty"`
    Labels      map[string]*string `json:"labels,omitempty"`
}

This struct is embedded via json:",inline" in both the Parameters (spec) and
Observation (status) of all 8 metadata-capable CRD types. The *string pointer
values are significant: a nil value acts as a deletion marker per CF API
convention.

Step 2: Shared metadata package — internal/clients/metadata/

File: internal/clients/metadata/metadata.go

Five public functions:

Function Purpose
BuildMetadata(mg, userLabels, userAnnotations) Merges default Crossplane labels with user metadata into a *cfresource.Metadata for Create/Update calls
IsMetadataUpToDate(desired, desired, actual, actual) Subset-check: returns true iff every key in desired exists and matches in actual
MetadataMapContains(desired, actual) Core subset check for a single map — used by IsMetadataUpToDate
MetadataMapEqual(desired, actual) Exact equality check — kept for test utility, not used in drift detection
DiffMetadata(desired, desired, actual, actual) Produces a minimal *cfresource.Metadata merge-patch for Update calls
StripDefaultLabels(labels) Removes crossplane-kind/crossplane-name/crossplane-providerconfig from a label map

Key design decisions:

  1. Subset semantics, not exact equality. MetadataMapContains only checks
    that desired keys are present and correct in actual. Extra keys in actual
    are ignored. This is critical because:

    • The CF platform may add its own labels
    • Other actors (humans, CI) may add labels the provider didn't set
    • With exact equality, any extra label → perpetual drift loop
  2. BuildMetadata always includes default labels. Even if
    spec.forProvider.labels is empty, BuildMetadata calls
    resource.GetExternalTags(mg) which returns at least crossplane-kind and
    crossplane-name. User labels override defaults on key collision.

  3. Nil pointer = deletion marker. In spec.forProvider.labels, a key with
    a nil value means "delete this label from the CF resource on Update."
    BuildMetadata passes these through via m.RemoveLabel().

  4. StripDefaultLabels prevents spec pollution. Used in LateInitialize
    and the exporter to strip default labels from maps that will be written into
    spec.forProvider.labels. The controller recomputes these on every
    reconcile — storing them in spec would be semantically incorrect.

Step 3: Client wiring — Create, Update, IsUpToDate

All 8 metadata-capable clients follow the same pattern. Using App as the
representative example:

Create (internal/clients/app/app.go):

appCreate.Metadata = metadata.BuildMetadata(mg, spec.Labels, spec.Annotations)

Update (internal/clients/app/app.go):

appUpdate.Metadata = metadata.BuildMetadata(mg, spec.Labels, spec.Annotations)

IsUpToDate (internal/clients/app/app.go):

desired := metadata.BuildMetadata(mg, spec.Labels, spec.Annotations)
metadataUpToDate := metadata.IsMetadataUpToDate(desired.Labels, desired.Annotations, status.Labels, status.Annotations)
return !changes.HasChanges() && metadataUpToDate, nil

Key observation: All three code paths use BuildMetadata(mg, spec.Labels, spec.Annotations) — the same function produces the desired metadata set. This
ensures consistency: what you create is what you check for drift.

Variations across resources:

Resource IsUpToDate pattern
App !changes.HasChanges() && metadataUpToDate
Domain Metadata-only check (no spec changes)
Org Short-circuits on spec.Name != observed.Name first
Space Short-circuits on name/SSH mismatch first
Route Returns metadata result directly
ServiceInstance Combined with specUpToDate and credentialsUpToDate
SCB Metadata-only check
SRB In controller's handleObservationState, not client IsUpToDate

Step 4: Observation population — reading labels back from CF

Each client's GenerateObservation / UpdateObservation function copies the
CF resource's metadata into the CR's status:

// From internal/clients/space/space.go
if o.Metadata != nil {
    obs.Labels = o.Metadata.Labels
    obs.Annotations = o.Metadata.Annotations
}

This makes labels/annotations visible in status.atProvider.labels and
status.atProvider.annotations.

Step 5: LateInitialize — preventing default label pollution in spec

Only Org currently has a LateInitialize that copies observed labels into
spec.forProvider.labels. It uses StripDefaultLabels:

// From internal/clients/org/org.go
if len(spec.Labels) == 0 && from.Metadata != nil {
    spec.Labels = metadata.StripDefaultLabels(from.Metadata.Labels)
}

Without StripDefaultLabels, the observed default labels (crossplane-kind,
crossplane-name, crossplane-providerconfig) would be copied into
spec.forProvider.labels, which is wrong because:

  1. The CR name may change — but the old crossplane-name value would be frozen
    in spec.
  2. The controller recomputes these labels on every reconcile from the CR's
    current identity — storing stale values in spec is semantically incorrect.
  3. If the CR is exported and re-imported with a different name/ProviderConfig,
    the stale default labels would be applied to the new resource.

Step 6: SRB controller — handleObservationState

The ServiceRouteBinding controller has a unique pattern: its IsUpToDate logic
lives in the controller (not the client), inside handleObservationState. This
is because SRB uses a last-operation state machine instead of a direct
IsUpToDate call.

CF Service Route Bindings are asynchronous-only operations. When you create
or delete an SRB, the CF API doesn't return a synchronous result — it starts a
"last operation" that transitions through Initial → InProgress → Succeeded|Failed.
The SRB controller must inspect binding.LastOperation.State to know whether
the resource is ready, still working, or failed. You can't just call IsUpToDate
unconditionally because:

  • If the last operation is InProgress, the resource is still being
    created/deleted — calling IsUpToDate would be meaningless and might trigger
    a redundant Update while the operation is running.
  • If the last operation is Failed, the resource is in an error state —
    IsUpToDate is irrelevant until the failure is resolved.
  • Only when the last operation is Succeeded does it make sense to ask "is the
    current state matching what I want?"

So the SRB controller uses a state machine (handleObservationState) with a
switch on the last operation state. The metadata drift check only runs inside
the LastOperationSucceeded branch. This is why the check lives in the
controller rather than the client — the decision of when it's safe to check
drift is tightly coupled to the state machine logic.

By contrast, the other 7 resources (App, Domain, Space, etc.) are synchronous —
the controller fetches the CF resource, populates the observation, then calls
client.IsUpToDate(cr, spec, status) as a single function call. Adding metadata
drift detection for those resources meant adding one line to the client's
IsUpToDate.

The LastOperationSucceeded branch now calls BuildMetadata and
IsMetadataUpToDate:

// From internal/controller/serviceroutebinding/controller.go
case v1alpha1.LastOperationSucceeded:
    // ...
    desired := metadata.BuildMetadata(cr, cr.Spec.ForProvider.Labels, cr.Spec.ForProvider.Annotations)
    upToDate := metadata.IsMetadataUpToDate(desired.Labels, desired.Annotations, actualLabels, actualAnnotations)
    return managed.ExternalObservation{ResourceExists: true, ResourceUpToDate: upToDate}, nil

Step 7: Exporter — stripping default labels from exported CRs

File: cmd/exporter/cf/metadata/metadata.go

The exporter strips default Crossplane labels from exported CR manifests. This
is the same StripDefaultLabels logic as in the client package, but lives in
the exporter's own package (no import cycle).

Why: Exported CRs may be imported into a different cluster with a different
name, namespace, or ProviderConfig. The default labels are environment-specific
and must be regenerated by the controller at the destination.

Step 8: Route controller refactoring

The route controller's GetByIDOrSpec was split into:

  • FindRouteBySpec(forProvider) — used during adoption (no external name set)
  • GetRouteByGUID(guid) — used during normal reconciliation

This is a structural change that was needed because the old combined method
didn't cleanly support the Create/Update metadata wiring. The split makes the
reconciliation paths explicit.

Step 9: Tests

Unit tests (internal/clients/metadata/metadata_test.go):

  • MetadataMapContains — subset semantics, nil handling, empty maps
  • MetadataMapEqual — exact equality
  • DiffMetadata — merge-patch generation
  • IsMetadataUpToDate — combined labels+annotations check

Controller tests — all 8 controllers updated:

  • args.mg uses withDefaultMetadataLabels() to set GVK (required because
    GetExternalTags uses the GVK to produce crossplane-kind)
  • Mock CF resources use .SetLabels(...) with matching default labels
  • want.mg also uses withDefaultMetadataLabels() (GVK persists after Observe)
  • ResourceUpToDate: true for cases where all conditions (spec + metadata) match
  • ResourceUpToDate: false kept for cases with spec mismatches that
    short-circuit before the metadata check

E2e tests (test/e2e/):

  • Existing YAML manifests extended with labels and annotations fields
  • Existing "ready" assess steps extended with AssertDefaultLabels checks
  • No separate label-update test steps — labels are verified inline during the
    normal reconciliation flow

Upgrade test (test/upgrade/label_upgrade_test.go):

  • Pre-upgrade: verifies no crossplane-* labels exist on existing resources
  • Post-upgrade: verifies all 3 default labels are present with correct values
  • Uses CustomUpgradeTestBuilder with custom pre/post assessment functions

Backward Compatibility

Scenario Impact
Existing managed resources Default labels are applied on next Update cycle after upgrade. No manual intervention needed.
CR spec (spec.forProvider) Unchanged — LateInitialize strips default labels before writing into spec.
Drift detection Changed from exact-match (SRB) or none (others) to subset-check. Extra labels on CF resources no longer trigger updates.
Exported CRs Default labels stripped — safe for import into different environments.
Observe-only resources (managementPolicies: [Observe]) No default labels applied — no impact.

Copy link
Copy Markdown
Collaborator

@Kukkerem Kukkerem left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

}
return !changes.HasChanges(), nil
desired := metadata.BuildMetadata(mg, spec.Labels, spec.Annotations)
metadataUpToDate := metadata.IsMetadataUpToDate(desired.Labels, desired.Annotations, status.Labels, status.Annotations)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this create an infinite loop? DetectChanges only checks the image and name, but if a Label or Annotation changed, HasChanges is false, Update will not call client.Update because in the controller, we have } else if changes.HasChanges() {, and it will do the same in the next reconciliation. Isn't a simple else enough in Update so it always updates when isUpToDate is false?

// return always true, as for now the Org resource is observe only

return spec.Name == observed.Name
func IsUpToDate(mg xpresource.Managed, spec v1alpha1.OrgParameters, observed *resource.Organization) bool {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Org is observe-only I think, so we couldn't reconcile the drift. Do we even need this here? Maybe a simple return spec.Name == observed.Name is enough to at least see if the Org name changed externally.

}
// TODO: ADD labels and annotations

if len(spec.Labels) == 0 && from.Metadata != nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we implement the same for Space, Domain, App, etc., but skip it for Org because it's observe-only?

for key, desiredVal := range desired {
actualVal, exists := actual[key]
if !exists {
return false
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we check if we are here because we have set desiredVal to nil - continue if
desiredVal is nil, or every deletion loops.

m := cfresource.NewMetadata()
m.Labels = diffMap(desiredLabels, actualLabels)
m.Annotations = diffMap(desiredAnnotations, actualAnnotations)
return m
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we return nil when both diffMaps are empty? Why return the Metadata when nothing changed?

Copy link
Copy Markdown
Collaborator

@SatabdiG SatabdiG left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work 🔥

} else if changes.HasChanges() {
_, err := c.client.Update(ctx, guid, cr.Spec.ForProvider)
_, err := c.client.Update(ctx, guid, cr, cr.Spec.ForProvider)
if err != nil {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the metadata gets changed, won't it fall through here silently as there is no else block. Won't this creates an infinite reconcile loop: Observe returns ResourceUpToDate: false forever but Update never fixes it.

// +mapType=granular
Labels map[string]*string `json:"labels,omitempty"`
// (Attributes) The metadata associated with the Cloud Foundry resource.
ResourceMetadata `json:",inline"`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that ResourceMetadata is missing // +mapType=granular, wouldn't this be a behaviour regression? Maybe we can add it to the struct's fields?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-notes/new-feature Marks PR as new feature for release note generation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support External Resource Labeling

4 participants