Skip to content

Add cf-metadata parameter for user-defined CF labels and annotations#1879

Open
Yavor16 wants to merge 5 commits into
masterfrom
feature/LMCROSSITXSADEPLOY-1550
Open

Add cf-metadata parameter for user-defined CF labels and annotations#1879
Yavor16 wants to merge 5 commits into
masterfrom
feature/LMCROSSITXSADEPLOY-1550

Conversation

@Yavor16

@Yavor16 Yavor16 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Adds support for a new cf-metadata MTA parameter that lets descriptor authors
attach user-defined Cloud Foundry V3 labels and annotations to applications and
managed service instances directly from mtad.yaml.

modules:
  - name: my-app
    type: org.cloudfoundry.application
    parameters:
      cf-metadata:
        labels:
          commit-id: "abc123"
        annotations:
          description: "Built from the current descriptor"
resources:
  - name: my-service
    type: org.cloudfoundry.managed-service
    parameters:
      cf-metadata:
        labels:
          env: "prod"

Why

Previously, user-defined CF metadata (labels and annotations) could only be set
manually after deployment, which was error-prone and not reproducible from the
MTA descriptor. This change makes that metadata declarative and part of the
deployment lifecycle.

How

  • New cf-metadata module/resource parameter registered in SupportedParameters.
  • CfMetadataParser parses the labels and annotations sub-maps into a new
    CfUserMetadata value object.
  • CfMetadataValidator enforces CF metadata key/value constraints and rejects
    reserved mta.cloudfoundry.org/ keys with a clear error.
  • User metadata is threaded through CloudApplicationExtended and
    CloudServiceInstanceExtended, and merged into the built Metadata by
    ApplicationMetadataBuilder and ServiceMetadataBuilder.
  • Change detection in CreateOrUpdateAppStep and
    DetermineServiceCreateUpdateServiceActionsStep compares only user-defined
    keys, so updates to MTA-internal metadata no longer trigger spurious CF
    metadata API calls, and user metadata changes correctly trigger an update.

Behaviour notes

  • MTA-internal mta.cloudfoundry.org/ labels and annotations are always
    preserved; user descriptors cannot overwrite or delete them.
  • User-declared label/annotation values support standard descriptor variable
    interpolation.
  • MTA archives without cf-metadata deploy identically to prior behaviour.

Tests

  • CfMetadataParserTest, CfMetadataValidatorTest
  • ApplicationMetadataBuilderTest, ServiceMetadataBuilderTest,
    ApplicationCloudModelBuilderCfMetadataTest
  • CreateOrUpdateStepWithExistingAppTest,
    DetermineServiceCreateUpdateServiceActionsStepTest

JIRA:LMCROSSITXSADEPLOY-1550

Yavor16 added 4 commits July 16, 2026 13:24
Introduces the cf-metadata module/resource parameter allowing users to
attach user-defined CF V3 labels and annotations via mtad.yaml. Adds
CfUserMetadata value object, CfMetadataParser, CfMetadataValidator
with reserved-key and format enforcement. Merges user labels/annotations
into ApplicationMetadataBuilder and ServiceMetadataBuilder. Threads
userCfMetadata through CloudApplicationExtended and
CloudServiceInstanceExtended. Changes metadata change-detection in
CreateOrUpdateAppStep and DetermineServiceCreateUpdateServiceActionsStep
to compare only user-defined keys, preventing MTA-internal churn from
triggering spurious CF metadata API calls.

JIRA:LMCROSSITXSADEPLOY-1550
…MetadataBuilder

Fix RESERVED_KEY_NAMES in CfMetadataValidator to use HashSet construction
instead of Set.of() to tolerate duplicate string values that appear in both
MtaMetadataLabels and MtaMetadataAnnotations (e.g. "mta_id", "mta_namespace").

JIRA:LMCROSSITXSADEPLOY-1550
- ApplicationCloudModelBuilderCfMetadataTest: end-to-end test that cf-metadata
  in module parameters flows to getUserCfMetadata() on the built app
- CreateOrUpdateStepWithExistingAppTest: three tests verifying that user-label
  changes trigger updateApplicationMetadata, while MTA-internal-only changes
  do not
- DetermineServiceCreateUpdateServiceActionsStepTest: three tests verifying
  that user-label changes trigger UPDATE_METADATA action for services
- Fix filterUserKeys NPE when Metadata.getLabels()/getAnnotations() returns
  null (CF client v3 Metadata can return null for absent maps)

JIRA:LMCROSSITXSADEPLOY-1550

@Yavor16 Yavor16 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated code review by MTA Quality Agent — 1 finding.

@Yavor16

Yavor16 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

MTA Quality Report — cloudfoundry/multiapps-controller PR #1879

Jira: LMCROSSITXSADEPLOY-1550 — (backlog item could not be fetched in this environment)
Backlog alignment: WARN

  • Implements Jira scope? unclear — The Jira MCP tool was unavailable, so the backlog summary/description could not be retrieved to compare against the diff.
  • Changes outside Jira scope? unclear — Same reason; scope boundaries could not be verified. The PR description is internally consistent (adds cf-metadata parsing, validation, model threading, metadata building, change detection).

Code Review

6 finding(s): 1 posted inline · 5 general (see below).
See the inline review on the PR diff for posted findings.


Security

  • HIGHCfMetadataValidator.java:~21-25 (validateKeyNotReserved / RESERVED_KEY_NAMES): Reserved-key allowlist bypass allowing tampering with the platform autoscaler control label (metadata spoofing / functionality tampering, CWE-284 Improper Access Control). The builders write internal MTA metadata first, then apply user-supplied labels/annotations, so a colliding user key overwrites the internal value. RESERVED_KEY_NAMES does not include MtaMetadataLabels.AUTOSCALER_LABEL (app-autoscaler.cloudfoundry.org/disable-autoscaling); validateKeyNotReserved extracts name segment disable-autoscaling, which neither starts with mta_ nor is in the reserved set, so it passes validation. Change detection also treats AUTOSCALER_LABEL as internal and strips it before comparing, so a user-controlled value would not even be reconciled.
    Fix: Add MtaMetadataLabels.AUTOSCALER_LABEL to RESERVED_KEY_NAMES. Consider applying user metadata before internal MTA metadata so internal keys always win (defense in depth), and consolidate the three independent key lists into shared constants to prevent future drift.
  • LOW — Three independently maintained lists of internal-key names (validator RESERVED_KEY_NAMES + two step classes) — maintainability risk that will produce more bypasses as new internal labels are added.

SonarCloud

Check Status Result Annotations Details
Build and analyze in_progress ⏳ RUNNING link

Quality gate: UNKNOWN

The Build and analyze job (which runs the SonarCloud analysis) is still in_progress, so no quality gate conclusion or annotations are available yet. Re-run this check once the Build and analyze job completes.


Dependency CVEs

✅ No new CVEs — this PR does not modify any dependency files (pom.xml, build.gradle, lockfiles).


General findings

  • Severity: CRITICAL · Confidence: low · File: multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/CfMetadataValidator.java
    The code-reviewer reported a compile-breaking typo — Messages.CF_METADATA_INVALID_label_KEY_0 (mixed-case) instead of the declared CF_METADATA_INVALID_LABEL_KEY_0. This could not be confirmed against the diff: RIGHT-side line 63 of the file reads throw new ContentException(MessageFormat.format(Messages.CF_METADATA_INVALID_LABEL_KEY_0, key)); (correct all-caps), and Messages.java declares CF_METADATA_INVALID_LABEL_KEY_0. Demoted to a general note and not posted inline because the claimed evidence is contradicted by the actual source. Please double-check locally; if the constant reference is correct as shown, no action is needed.

  • Severity: IMPORTANT · File: ServicesCloudModelBuilder.java
    cf-metadata on a user-provided or existing service is silently ignored. createUserProvidedService and createExistingService hard-code null for userCfMetadata without parsing the parameter, so cf-metadata on those resource types is dropped rather than rejected.
    Fix: Either parse and pass through cf-metadata for those service types, or add explicit validation that throws a ContentException when cf-metadata is present on unsupported resource types.

  • Severity: IMPORTANT · File: CfMetadataParser.java + CloudApplicationExtended.java
    Inconsistent null-vs-empty contract on getUserCfMetadata() / CfMetadataParser. The parser returns an empty ImmutableCfUserMetadata when cf-metadata is absent, but getUserCfMetadata() is @Nullable. Callers that null-check (e.g. userCfMetadata != null in ApplicationMetadataBuilder) will never skip the branch even for modules that never declared cf-metadata, making the @Nullable annotation misleading.
    Fix: Pick one contract — either return null from the parser when absent (keeping @Nullable), or drop @Nullable and always pass an empty object.

  • Severity: IMPORTANT · File: CreateOrUpdateAppStep.java + DetermineServiceCreateUpdateServiceActionsStep.java
    Duplicated internal-key sets across two step classes plus the validator. Both steps inline identical Set<String> literals for internalLabelKeys / internalAnnotationKeys, re-created on every call, and the same concept lives in CfMetadataValidator.RESERVED_KEY_NAMES. Future additions must be updated in three places (this is also the root cause of the HIGH security finding above).
    Fix: Extract as shared private static final constants (e.g. in MtaMetadataLabels/MtaMetadataAnnotations or a new MtaInternalMetadataKeys class).

  • Severity: STYLE · File: ServicesCloudModelBuilder.java
    Import ordering violation (Spotless). The CfMetadataParser import (org.cloudfoundry.multiapps.controller.*) is placed after org.cloudfoundry.multiapps.mta.*, violating alphabetical ordering enforced by Spotless; CI will catch this.
    Fix: Move the CfMetadataParser import above the org.cloudfoundry.multiapps.mta.* imports.

The prior alternation was at the top level of the regex, so the
single-character alternative only matched bare unprefixed keys. A
key like foo.com/x (valid per CF) was incorrectly rejected because
the name-segment after the slash required at least two characters.

Wrapping the name-segment alternatives in a non-capturing group
ensures both the multi-char and single-char alternatives apply to
bare keys and to the name segment of prefixed keys equally.

JIRA:LMCROSSITXSADEPLOY-1550

@Yavor16 Yavor16 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated code review by MTA Quality Agent — 2 finding(s).

public class CfMetadataValidator {

private static final Pattern KEY_PATTERN = Pattern.compile(
"([a-zA-Z0-9][a-zA-Z0-9\\-_.]{0,251}[a-zA-Z0-9]/)?([a-zA-Z0-9][a-zA-Z0-9\\-_.]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9])");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Severity: IMPORTANT · Confidence: 85

The prefix sub-pattern [a-zA-Z0-9][a-zA-Z0-9\-_.]{0,251}[a-zA-Z0-9] requires a minimum of two characters, so a single-character DNS prefix such as a/mykey is incorrectly rejected as invalid. RFC 1123 allows single-character DNS labels, and the error message does not document a two-character minimum for the prefix, so users will receive a misleading "invalid key" error for valid keys.

Evidence: "([a-zA-Z0-9][a-zA-Z0-9\\-_.]{0,251}[a-zA-Z0-9]/)?([a-zA-Z0-9][a-zA-Z0-9\\-_.]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9])");

Fix:

Suggested change
"([a-zA-Z0-9][a-zA-Z0-9\\-_.]{0,251}[a-zA-Z0-9]/)?([a-zA-Z0-9][a-zA-Z0-9\\-_.]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9])");
"([a-zA-Z0-9]([a-zA-Z0-9\\-_.]{0,251}[a-zA-Z0-9])?/)?([a-zA-Z0-9][a-zA-Z0-9\\-_.]{0,61}[a-zA-Z0-9]|[a-zA-Z0-9])");

return;
}
if (!Objects.equals(existingApp.getV3Metadata(), app.getV3Metadata())) {
if (hasUserMetadataChanged(existingApp.getV3Metadata(), app.getV3Metadata())) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Severity: IMPORTANT · Confidence: 82

The sets internalLabelKeys and internalAnnotationKeys are defined as inline literals inside hasUserMetadataChanged, duplicating the identical definitions in DetermineServiceCreateUpdateServiceActionsStep.shouldUpdateMetadata. Any future addition of a new internal metadata key (or removal of an obsolete one) must be applied in both places; missing one silently changes the change-detection semantics for either apps or services. Both sets should be extracted to shared constants.

Evidence: Set<String> internalLabelKeys = Set.of(MtaMetadataLabels.MTA_ID, MtaMetadataLabels.MTA_NAMESPACE, MtaMetadataLabels.SPACE_GUID,

Fix: Extract both internalLabelKeys and internalAnnotationKeys as private static final Set<String> constants in a shared location (e.g. MtaMetadataLabels/MtaMetadataAnnotations or a new MtaInternalMetadataKeys class), and reuse them here, in DetermineServiceCreateUpdateServiceActionsStep, and in CfMetadataValidator.RESERVED_KEY_NAMES.

@Yavor16

Yavor16 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

MTA Quality Report — cloudfoundry/multiapps-controller PR #1879

Jira: LMCROSSITXSADEPLOY-1550 — (backlog item could not be fetched in this environment; the sap-jira MCP tool was unavailable)
Backlog alignment: WARN

  • Implements Jira scope? unclear — The Jira MCP tool was unavailable, so the backlog summary/description could not be retrieved to compare against the diff. The PR description is internally consistent (adds cf-metadata parsing, validation, model threading, metadata building, and change detection).
  • Changes outside Jira scope? unclear — Same reason; scope boundaries could not be verified against the backlog item.

Code Review

2 finding(s): 2 posted inline.
See the inline review on the PR diff for posted findings.


Security

  • HIGHCfMetadataValidator.java (RESERVED_KEY_NAMES): Authorization / security-control bypass (CWE-284: Improper Access Control). User-controlled cf-metadata labels are validated only against RESERVED_KEY_NAMES, which omits MtaMetadataLabels.AUTOSCALER_LABEL (app-autoscaler.cloudfoundry.org/disable-autoscaling). A user can therefore set this label via the cf-metadata parameter; the value flows unfiltered through CfMetadataParser.parse() into ApplicationMetadataBuilder.build() (userCfMetadata.getLabels().forEach(builder::label)) and is applied to the CF app. The app-autoscaler component reads exactly this label to suspend autoscaling, so an untrusted MTA descriptor can disable autoscaling on a deployed app — a controller-managed operational control the user is not meant to touch. The team's own code confirms the label is controller-internal: CreateOrUpdateAppStep.hasUserMetadataChanged lists AUTOSCALER_LABEL in internalLabelKeys to exclude it from user-change detection. The two sets are inconsistent (internal for change-detection, but not reserved for validation), which makes the gap concrete.
    Fix: Add MtaMetadataLabels.AUTOSCALER_LABEL to RESERVED_KEY_NAMES. Best practice: derive both the validator's reserved set and CreateOrUpdateAppStep's internalLabelKeys/internalAnnotationKeys from a single shared constant collection so the lists cannot drift out of sync again.
  • LOWCfMetadataParser.java (extractStringMap, unchecked cast): After per-entry type checks, the whole map is cast (Map<String, String>) rawMap. Functionally guarded by the preceding entry validation, so not exploitable as introduced by this PR. No action required for security.
  • LOWCfMetadataValidator.java (reserved-key check on name segment / exact key): validateKeyNotReserved checks the name segment, the full key, and the mta_ prefix. This correctly blocks prefixed variants; once AUTOSCALER_LABEL is added to the set, RESERVED_KEY_NAMES.contains(key) will match its full domain-prefixed form. No separate fix needed beyond the HIGH finding.

SonarCloud

Check Status Result Annotations Details
Build and analyze completed ❌ FAILED link
build completed ❌ FAILED link

Quality gate: UNKNOWN

The SonarCloud-associated job ("Build and analyze") failed at the build/workflow level before producing a quality-gate result. No SonarCloud quality-gate conclusion or per-line issue annotations were emitted.

Findings

No SonarCloud per-line code smells, bugs, or vulnerabilities were surfaced as check-run annotations.


Dependency CVEs

⏭️ CVE scan skipped on this review-fix re-run — dependencies were scanned on the initial quality run.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant