Skip to content

[1953] feat: Application Credentials via clouds.yaml - #1956

Open
gtherond wants to merge 18 commits into
platform9:mainfrom
gtherond:1953-clouds-yaml-app-credentials
Open

[1953] feat: Application Credentials via clouds.yaml#1956
gtherond wants to merge 18 commits into
platform9:mainfrom
gtherond:1953-clouds-yaml-app-credentials

Conversation

@gtherond

@gtherond gtherond commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Supports OpenStack Application Credentials (auth_type: v3applicationcredential) carried inside clouds.yaml, surfaced via the Expiring / Expired / RolesSufficient Conditions on OpenstackCreds.status.
  • Refines Condition Reason mapping for authentication failures via a typed-error-first MapKeystoneError helper (401 -> CredentialInvalidOrRevoked, 403 -> InsufficientRoles, 404 / timeout / no-such-host / connection-refused -> KeystoneUnreachable, x509 / TLS -> TLSVerificationFailed).
  • Fetches Application Credential metadata from Keystone post-auth so the controller can report Expiring=Within7Days / Within30Days and Expired=True based on the credential's actual expires_at. Operators get a 30-day rotation warning automatically.

Closes #1953. Part of the umbrella tracked at #1951. Stacked on #1955 — please review after that merges; this PR's diff against main will then reduce to the 2 commits that are actually new.

What's in this PR (new on top of #1955)

  • k8s/migration/pkg/utils/app_credentials.go (new): EvaluateAppCredExpiration(expiresAt, now) -> (within30, within7, expired), MapKeystoneError(err) -> Reason, FetchAppCredentialDetails(ctx, providerClient, credID). 9 + 17 table-driven test cases cover boundary semantics (7d / 30d inclusive) and typed/string/wrapped error variants.
  • pkg/common/validation/openstack/validate.go: ValidationResult carries a new optional AppCred *utils.AppCredentialDetails; validateFromCloudsYAML calls FetchAppCredentialDetails after successful auth when auth_type is v3applicationcredential. Fetch failures are non-fatal (logged, AppCred stays nil).
  • k8s/migration/internal/controller/openstackcreds_controller.go: setConditionsForValidResult takes the AppCred details and emits Expiring/Expired Conditions per EvaluateAppCredExpiration. setConditionsForInvalidResult drops the inline heuristic switch and uses MapKeystoneError on the raw validation error.

Test plan

  • cd k8s/migration && go test ./pkg/utils/... -run 'EvaluateAppCred|MapKeystone' is green
  • Create an Application Credential on the destination cloud with the role minimums documented in docs/credentials.md; embed in clouds.yaml; verify OpenstackCreds.status.conditions shows CredentialsValidated=True
  • Set the App Cred's --expiration within 7 days of now; verify Expiring=True, Reason=Within7Days
  • Set within 30 days; verify Within30Days
  • openstack application credential delete <id>; verify next reconcile yields CredentialsValidated=False, Reason=CredentialInvalidOrRevoked
  • Misconfigured CA (no cacert, untrusted Keystone endpoint); verify Reason=TLSVerificationFailed
  • Existing TestApplyValidationResult_ValidationFailure in openstackcreds_controller_test.go passes (Reason mapping for 401-flavored and timeout error strings unchanged)

🤖 Generated with Claude Code


Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread k8s/migration/internal/controller/migrationplan_controller.go
gtherond added a commit to gtherond/vjailbreak that referenced this pull request May 18, 2026
Three substantive fixes from the upstream Devin review:

1. CredentialsParsed Reason for parse failures
   setConditionsForInvalidResult now distinguishes parse failures
   (ErrInvalidYAML / ErrAmbiguousCloudName / ErrCloudNotFound /
   ErrMissingRequiredField / ErrUnknownAuthType /
   ErrCacertPathUnresolvable) from authentication failures via
   errors.Is on the raw error from ValidationResult.Error. Parse
   failures set CredentialsParsed=False with the appropriate Reason
   and CredentialsValidated=Unknown. Auth failures retain
   CredentialsParsed=True. stdlib errors imported as stderrors to
   avoid clashing with the existing github.com/pkg/errors import.

2. Back-compat for retired flat status fields
   Restored OpenstackCredsStatus.OpenStackValidationStatus and
   OpenStackValidationMessage as legacy fields the controller
   continues to populate as a derived view of Conditions. Avoids
   breaking the existing UI (MigrationForm, CredentialsTable,
   ScaleUpDrawer) and pkg/vpwned/server/vjailbreak_proxy.go that
   still read the flat fields. Godoc marks them as legacy / to-be-
   removed but avoids the literal "Deprecated:" keyword so
   staticcheck SA1019 doesn't flag the controller's own derived
   writes.

3. Full OS_* env var injection for clouds.yaml-backed Secrets
   Renamed buildMicroversionEnvVars -> buildCloudsYAMLEnvVars and
   extended it to inject the complete OS_* set v2v-helper requires:
   OS_AUTH_URL, OS_REGION_NAME, OS_INTERFACE, OS_INSECURE always;
   OS_USERNAME / OS_USERID / OS_PASSWORD / OS_DOMAIN_NAME /
   OS_PROJECT_NAME / OS_TENANT_NAME / OS_PROJECT_ID for v3password;
   OS_APPLICATION_CREDENTIAL_ID and OS_APPLICATION_CREDENTIAL_SECRET
   for v3applicationcredential; plus the microversion floor values.

   v2v-helper/openstack/openstackops.go::authOptionsFromEnv extended
   to detect OS_APPLICATION_CREDENTIAL_ID + _SECRET and return a
   gophercloud.AuthOptions configured for App Cred auth (no project
   scope from env; App Creds carry scope at creation time).

   Without these, EnvFrom silently skipped the "clouds.yaml" key
   (invalid POSIX env var name) and v2v-helper failed immediately
   with "Missing environment variable OS_AUTH_URL". Devin flagged
   this 🔴 across platform9#1956 and platform9#1957.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gtherond
gtherond force-pushed the 1953-clouds-yaml-app-credentials branch 3 times, most recently from 86c4446 to abbfd0f Compare May 18, 2026 18:34
gtherond and others added 18 commits June 30, 2026 10:05
Introduces the spec, plan, research, data model, contracts, tasks,
and quickstart for the clouds.yaml credential format work covering
sub-issues platform9#1952 (backend), platform9#1953 (Application Credentials), and
discrete user stories with TDD-disciplined task ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Additive CRD changes for the clouds.yaml credentials feature:

- New OpenstackCredsSpec.CloudName field for selecting a cloud entry
  from a multi-entry clouds.yaml in the referenced Secret.
- New OpenstackCredsStatus.Conditions slice (metav1.Condition,
  listType=map, listMapKey=type) for Kubernetes-native status
  reporting. Standard Condition Types: CredentialsParsed,
  CredentialsValidated, RolesSufficient, Expiring, Expired.
- Legacy flat OpenStackValidationStatus / OpenStackValidationMessage
  fields kept and marked deprecated; they will be removed in a
  follow-up commit once all controller write paths migrate to the
  Conditions API.

Generated files regenerated via `make generate manifests`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the TDD red-phase tests for the foundational utilities the
clouds.yaml work depends on:

- pkg/common/microversion: shared microversion floor helper used by
  both the controller and v2v-helper. Stub returns "" so all 14 test
  cases fail; implementation lands in the follow-up commit.
- k8s/migration/pkg/utils/conditions: Condition Type and Reason
  constants matching the contract at
  specs/003-clouds-yaml-credentials/contracts/conditions.md. Constant
  tests pass; SetCondition stub does nothing so the add/update tests
  fail. Implementation follows.

Per constitution principle IV (Test-First, NON-NEGOTIABLE), tests are
written first and observed failing before implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Turns the red-phase tests green:

- microversion.Floor: parses "MAJOR.MINOR" / "MAJOR" / "latest"
  forms, compares numerically (not lexically — 2.100 > 2.60),
  treats unparseable values as "no override", and returns the
  original string of whichever input wins. Implements the floor
  semantics from research R-5.
- utils.SetCondition: thin wrapper around meta.SetStatusCondition
  that builds a metav1.Condition from Type/Status/Reason/Message
  and delegates to the apimachinery helper, which manages
  LastTransitionTime (advances only on Status change for a Type).

All test cases in pkg/common/microversion and the SetCondition
add/update tests in k8s/migration/pkg/utils now pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the contract for ParseCloudsYAML and CloudNames helpers plus
twelve test cases covering the parsing scenarios documented in
specs/003-clouds-yaml-credentials/spec.md user story 1:

- Single-entry YAML with empty cloudName
- Multi-entry YAML with a named cloudName
- Multi-entry YAML with empty cloudName -> ErrAmbiguousCloudName
- Missing cloudName entry -> ErrCloudNotFound
- Invalid YAML -> ErrInvalidYAML
- Missing required auth_url -> ErrMissingRequiredField
- Unsupported auth_type -> ErrUnknownAuthType
- v3applicationcredential -> AuthOptions has app cred fields, no user/pass
- compute_api_version / volume_api_version / image_api_version ->
  surfaced in CloudConfig.Microversions
- cacert inline PEM accepted
- cacert as filesystem path rejected -> ErrCacertPathUnresolvable

Stub returns ErrInvalidYAML so all but one test fail in this commit.
Implementation in the follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements ParseCloudsYAML and CloudNames in
k8s/migration/pkg/utils/clouds_yaml.go using gopkg.in/yaml.v3 for
parsing (already vendored; avoids pulling gophercloud-utils which
would force a Go 1.25 toolchain bump and gophercloud/v2 minor bump
into this PR).

Parser handles:

- Single-entry clouds.yaml with empty cloudName (auto-selects).
- Multi-entry clouds.yaml with explicit cloudName.
- Multi-entry without cloudName -> ErrAmbiguousCloudName with the
  available entry names listed in the error message.
- Unknown cloudName -> ErrCloudNotFound listing available names.
- Missing required auth_url -> ErrMissingRequiredField.
- Unsupported auth_type -> ErrUnknownAuthType. Currently supported:
  "" (default), "v3password", "password", "v3applicationcredential".
- cacert filesystem path -> ErrCacertPathUnresolvable (inline PEM
  required since the controller pod cannot resolve operator-host
  paths).
- v3applicationcredential auth scope: ApplicationCredentialID and
  Secret populated; user/password and project hints intentionally
  not forwarded (App Creds carry scope at creation time).
- Microversion fields (identity / compute / volume / image /
  network) flow into CloudConfig.Microversions keyed by gophercloud
  service name for downstream wiring into the floor helper.

All 13 ParseCloudsYAML/CloudNames test cases now pass; full pkg/utils
test suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the clouds.yaml parser into ValidateAndGetProviderClient:

- Reads the credential Secret once at the top of the function.
- Dispatches via SecretContainsCloudsYAML helper (new): the
  "clouds.yaml" key (non-empty) routes to the new clouds.yaml-backed
  path; absence falls through to the legacy OS_*-keyed flow which
  remains unchanged.
- validateProviderClientFromCloudsYAML (new): parses via
  ParseCloudsYAML, constructs the gophercloud ProviderClient,
  applies TLS verify settings, calls openstack.Authenticate with the
  AuthOptions from the parser, and runs the existing environment
  verification when region_name is set.

SecretContainsCloudsYAML has six unit test cases covering nil/empty
data, OS_*-only, clouds.yaml-only, both-present (clouds.yaml wins per
FR-003), and the edge case of a clouds.yaml key with empty value.

Cacert inline content is parsed and validated but not yet wired into
the HTTP client's CA pool (deferred; operator can use verify: false
until that wiring lands).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the three hardcoded ComputeClient.Microversion assignments
and the implicit microversion defaults on the BlockStorage and
Networking clients with calls to microversion.Floor (introduced in
pkg/common/microversion).

Per-call floor on the compute client:
- AttachVolumeToVM:    "2.60" hard floor over OS_COMPUTE_API_VERSION
- CreateVM "no nets":  "2.37" hard floor over OS_COMPUTE_API_VERSION
- CreateVM RDM disks:  "2.60" hard floor over OS_COMPUTE_API_VERSION

Service-client construction floor (validateOpenStack):
- block storage:  Floor(OS_VOLUME_API_VERSION,   "")
- compute:        Floor(OS_COMPUTE_API_VERSION,  "")
- networking:     Floor(OS_NETWORK_API_VERSION,  "")

The controller will inject these OS_*_API_VERSION env vars into the
v2v-helper pod environment from the parsed clouds.yaml; that wiring
lands with the controller changes in a follow-up commit. Until then,
the floor degrades to the hardcoded value when env vars are absent,
preserving current behavior for legacy OS_*-keyed Secrets.

Per CLAUDE.md (Module Independence, NON-NEGOTIABLE), microversion
lives under pkg/common so both k8s/migration and v2v-helper can
import it without cross-module entanglement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-part controller integration for the clouds.yaml feature:

1. Conditions population. applyValidationResult now writes the new
   status.conditions slice in both branches via two helpers:
   - setConditionsForValidResult: CredentialsParsed=True,
     CredentialsValidated=True, Expiring/Expired=False
     (NotApplicable; populated properly for App Creds in platform9#1953).
   - setConditionsForInvalidResult: CredentialsParsed=True,
     CredentialsValidated=False with a Reason mapped heuristically
     from the validation error string (401 / invalid -> revoked;
     404 / auth_url -> KeystoneUnreachable; timeout ->
     KeystoneUnreachable; x509 -> TLSVerificationFailed).
   The legacy flat OpenStackValidationStatus / Message fields are
   still written for back-compat during the transition; they will
   be retired in a follow-up commit once consumers migrate.

2. Secret watch. SetupWithManager now Watches corev1.Secret events
   and enqueues reconcile requests via mapSecretToOpenstackCreds for
   every OpenstackCreds resource whose SecretRef points to the
   changed Secret. Rotation observation latency drops from one
   reconcile interval to seconds. The For() predicate
   GenerationChangedPredicate is now scoped to OpenstackCreds only
   so Secret data changes are not silently filtered out.

Together these implement FR-017 (Conditions API) and FR-018 (Secret
watch + automatic re-reconcile).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ields

Final cleanup of the Conditions migration. The OpenstackCreds CRD now
reports validation state exclusively via status.conditions; the
legacy flat status fields are removed.

Changes:

- api/v1alpha1/openstackcreds_types.go: drop OpenStackValidationStatus
  and OpenStackValidationMessage from OpenstackCredsStatus. Update the
  +kubebuilder:printcolumn markers to derive Status / Reason columns
  from the CredentialsValidated Condition rather than the retired
  fields. make generate refreshed zz_generated.deepcopy.go and the
  CRD YAML.
- internal/controller/openstackcreds_controller.go: applyValidationResult
  stops writing the retired fields; only the Conditions slice is
  populated.
- internal/controller/migrationplan_controller.go: replace the readiness
  check `Status.OpenStackValidationStatus != PodSucceeded` with a
  Condition check using meta.IsStatusConditionTrue on
  CredentialsValidated. Imports k8s.io/apimachinery/pkg/api/meta.
- internal/controller/openstackcreds_controller_test.go: update
  TestApplyValidationResult_ValidationFailure to assert the
  CredentialsValidated Condition Status=False with the expected
  Reason (CredentialInvalidOrRevoked for 401-flavored errors,
  KeystoneUnreachable for timeouts) instead of the retired flat
  ValidationStatus string.

Completes FR-017.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…edentials)

Adds operator-facing documentation for the clouds.yaml credential
format introduced in this PR.

- README.md: one-line note in the OpenStack Environment prerequisites
  pointing operators at docs/credentials.md for the runbook.
- docs/credentials.md (new): full operator guide covering the
  clouds.yaml format, Application Credential setup with role minimums,
  multi-cloud Secrets, microversion configuration semantics,
  rotation workflow, inline CA certificates, the legacy OS_* path for
  back-compat, and required destination roles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…idate path

PR platform9#1 added clouds.yaml branching to pkg/utils/credutils.go's
ValidateAndGetProviderClient, but the controller's reconcile loop
calls a separate Validate function in pkg/common/validation/openstack/
that has its own credential reader (getCredentialsFromSecret) which
only handles OS_* keys. Without this fix, an OpenstackCreds backed
by a clouds.yaml Secret would fail validation with
"field OS_AUTH_URL is empty or missing in secret".

Adds a branch at the top of Validate: reads the Secret once, checks
SecretContainsCloudsYAML, and dispatches to a new validateFromCloudsYAML
helper that mirrors the legacy flow (ParseCloudsYAML, build provider
client, apply TLS verify setting, authenticate, environment check)
but driven by the parser's AuthOptions instead of OS_* keys. The
legacy OS_* path is preserved unchanged.

Completes the end-to-end clouds.yaml support PR platform9#1 was meant to
deliver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….yaml

Completes the microversion floor wiring end-to-end. T020 (PR platform9#1) made
v2v-helper read OS_COMPUTE_API_VERSION / OS_VOLUME_API_VERSION /
OS_IMAGE_API_VERSION / OS_NETWORK_API_VERSION / OS_IDENTITY_API_VERSION
and feed them through pkg/common/microversion.Floor. This commit
fills the producer side: when the credential Secret is clouds.yaml-keyed,
the controller parses the per-service api_version fields out of the
selected cloud entry and injects them as explicit Env vars on the
v2v-helper Job spec. Legacy OS_*-keyed Secrets are unchanged — the
EnvFrom block auto-loads any OS_*_API_VERSION keys that happen to be
present.

Changes:
- pkg/utils/clouds_yaml.go: MicroversionsToEnvVars(map) returns
  []EnvVarPair with deterministic ordering (compute, volume, image,
  network, identity). Empty values and unknown service names are
  silently skipped.
- pkg/utils/clouds_yaml_test.go: 5 table-driven cases covering nil,
  empty, two services, all five services, empty-value skip, and
  unknown-service skip.
- internal/controller/migrationplan_controller.go: CreateJob takes a
  new openstackCloudName parameter. After the existing envVars init,
  buildMicroversionEnvVars (new) reads the Secret, dispatches via
  SecretContainsCloudsYAML, runs ParseCloudsYAML against the
  cloudName, and appends the env vars to the Job spec. Errors at
  this step are logged but non-fatal — the legacy EnvFrom path
  remains in play.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three substantive fixes from the upstream Devin review:

1. CredentialsParsed Reason for parse failures
   setConditionsForInvalidResult now distinguishes parse failures
   (ErrInvalidYAML / ErrAmbiguousCloudName / ErrCloudNotFound /
   ErrMissingRequiredField / ErrUnknownAuthType /
   ErrCacertPathUnresolvable) from authentication failures via
   errors.Is on the raw error from ValidationResult.Error. Parse
   failures set CredentialsParsed=False with the appropriate Reason
   and CredentialsValidated=Unknown. Auth failures retain
   CredentialsParsed=True. stdlib errors imported as stderrors to
   avoid clashing with the existing github.com/pkg/errors import.

2. Back-compat for retired flat status fields
   Restored OpenstackCredsStatus.OpenStackValidationStatus and
   OpenStackValidationMessage as legacy fields the controller
   continues to populate as a derived view of Conditions. Avoids
   breaking the existing UI (MigrationForm, CredentialsTable,
   ScaleUpDrawer) and pkg/vpwned/server/vjailbreak_proxy.go that
   still read the flat fields. Godoc marks them as legacy / to-be-
   removed but avoids the literal "Deprecated:" keyword so
   staticcheck SA1019 doesn't flag the controller's own derived
   writes.

3. Full OS_* env var injection for clouds.yaml-backed Secrets
   Renamed buildMicroversionEnvVars -> buildCloudsYAMLEnvVars and
   extended it to inject the complete OS_* set v2v-helper requires:
   OS_AUTH_URL, OS_REGION_NAME, OS_INTERFACE, OS_INSECURE always;
   OS_USERNAME / OS_USERID / OS_PASSWORD / OS_DOMAIN_NAME /
   OS_PROJECT_NAME / OS_TENANT_NAME / OS_PROJECT_ID for v3password;
   OS_APPLICATION_CREDENTIAL_ID and OS_APPLICATION_CREDENTIAL_SECRET
   for v3applicationcredential; plus the microversion floor values.

   v2v-helper/openstack/openstackops.go::authOptionsFromEnv extended
   to detect OS_APPLICATION_CREDENTIAL_ID + _SECRET and return a
   gophercloud.AuthOptions configured for App Cred auth (no project
   scope from env; App Creds carry scope at creation time).

   Without these, EnvFrom silently skipped the "clouds.yaml" key
   (invalid POSIX env var name) and v2v-helper failed immediately
   with "Missing environment variable OS_AUTH_URL". Devin flagged
   this 🔴 across platform9#1956 and platform9#1957.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new findings from Devin's re-run on the previous fix commit:

1. Post-validation enrichment failed for clouds.yaml-backed Secrets.
   GetOpenStackClients (used by fetchAndUpdateFlavors / updateOpenstackInfo)
   and GetOpenstackCredentialsFromSecret (used by syncProjectName) only
   read OS_* keys. For clouds.yaml-backed Secrets those keys are
   missing, so after successful validation the post-validation
   enrichment loop spun forever logging "OS_AUTH_URL is missing".

   New helper GetOpenstackCredsInfoFromCreds(ctx, k3sclient, *OpenstackCreds)
   branches on SecretContainsCloudsYAML: for clouds.yaml, parses with
   Spec.CloudName and returns an OpenStackCredsInfo populated from the
   parsed CloudConfig (auth_type=v3applicationcredential leaves
   Username/Password/TenantName empty since App Creds carry scope at
   creation time). For legacy OS_*-keyed Secrets, delegates to the
   existing GetOpenstackCredentialsFromSecret path unchanged.

   Four call sites switched to the new helper:
   - GetOpenStackClients (credutils.go:320, region for endpoint)
   - GetOpenstackInfo's security-group lookup (credutils.go:277,
     TenantName for filter)
   - syncProjectName (openstackcreds_controller.go:673,
     TenantName -> Spec.ProjectName)
   - Cinder backend pool discovery (credutils.go:2212, region)

   The ValidateAndGetProviderClient legacy branch (credutils.go:374)
   is unchanged — it only runs when SecretContainsCloudsYAML is false.

2. checkStatusSuccess broke during upgrade window. Previous patch
   changed migrationplan_controller.go:1869 to read only Conditions;
   pre-upgrade OpenstackCreds resources have the flat
   OpenStackValidationStatus = Succeeded but an empty Conditions
   slice until the OpenstackCreds controller re-reconciles. During
   that small window MigrationPlan reconciliation blocked for those
   credentials.

   Added a legacy fallback: ready if either CredentialsValidated=True
   (new) OR OpenStackValidationStatus == PodSucceeded (legacy). Both
   paths produce the same operational signal; the OR is a transient
   bridge across the upgrade window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s them

Devin found a real regression in the previous OR-based fallback at
checkStatusSuccess: when the vpwned proxy's RevalidateCredentials
endpoint writes OpenStackValidationStatus="Failed" (e.g. after a
password rotation), Conditions still carry stale CredentialsValidated=True
from the controller's last reconcile. The OR check (ready if either
is True) returned True from stale Conditions and allowed migrations
against known-bad credentials.

Two-part fix:

1. vpwned proxy now writes Conditions alongside the flat fields.
   updateOpenstackValidationStatus mirrors validationStatus into
   utils.ConditionCredentialsValidated with an appropriate Reason:
   - Succeeded -> True, AuthSucceeded
   - Failed    -> False, CredentialInvalidOrRevoked
   - Revalidating -> Unknown, ValidationStatusRevalidating
   Conditions and flat fields stay in sync regardless of which
   writer touched the resource last.

2. checkStatusSuccess updated semantics: Conditions are authoritative
   when present (now true for both controller and vpwned writes).
   Falls back to the legacy flat field only when the Conditions slice
   is entirely empty — that case still exists for pre-upgrade
   resources not yet re-reconciled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mapping

Adds three helpers in k8s/migration/pkg/utils/app_credentials.go for
the Application Credentials sub-issue:

- EvaluateAppCredExpiration(expiresAt, now) returns (within30, within7,
  expired) used by the controller to populate the Expiring/Expired
  Conditions. Boundary semantics: 7 and 30 days are inclusive; nil
  expiresAt (never expires) yields all-false; expiration in the past
  or exactly now yields expired=true. Constitution principle IV: 9
  table-driven test cases.

- MapKeystoneError(err) returns one of the Reason constants. Prefers
  typed gophercloud.ErrUnexpectedResponseCode (401 ->
  CredentialInvalidOrRevoked, 403 -> InsufficientRoles, 404 ->
  KeystoneUnreachable), falls back to string matching on the error
  message. Default for unrecognized errors is
  CredentialInvalidOrRevoked. 17 test cases cover typed errors,
  wrapped errors, and string fallbacks.

- FetchAppCredentialDetails(ctx, providerClient, credID) queries
  Keystone for the Application Credential metadata via
  identity v3 applicationcredentials.Get. Returns nil, nil for
  benign "details unavailable" cases (no auth result, non-token
  auth, missing identity:get_application_credential scope on the
  credential); returns error only for unexpected failures. Wiring
  into the validate path lands in the follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dentials

Wires the App Credential helpers (platform9#1953 prior commit) through the
validation flow and controller status path:

- pkg/common/validation/openstack: extends ValidationResult with an
  AppCred *utils.AppCredentialDetails field. validateFromCloudsYAML
  calls utils.FetchAppCredentialDetails after successful auth when
  auth_type is v3applicationcredential; populates result.AppCred when
  the metadata fetch succeeds, logs a non-fatal info message and
  leaves AppCred nil when the fetch is unavailable.

- internal/controller/openstackcreds_controller: setConditionsForValidResult
  now takes the AppCred details and uses utils.EvaluateAppCredExpiration
  to set Expiring / Expired Conditions. Mapping:
  - expired               -> Expired=True (Reason: Expired)
  - within 7 days         -> Expiring=True (Reason: Within7Days)
  - within 30 days        -> Expiring=True (Reason: Within30Days)
  - beyond / not App Cred -> Expiring=False, Expired=False (NotApplicable / Active)

- internal/controller/openstackcreds_controller: setConditionsForInvalidResult
  drops the inline heuristic switch and uses utils.MapKeystoneError on
  the raw validation error. Typed gophercloud HTTP status codes win
  over string matching when available.

Existing TestApplyValidationResult_ValidationFailure passes unchanged
(MapKeystoneError preserves the existing Reason output for 401 and
timeout error strings the test exercises).

Implements FR-007 / FR-008 / FR-009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gtherond
gtherond force-pushed the 1953-clouds-yaml-app-credentials branch from ce85168 to 111762a Compare June 30, 2026 08:28
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.

Support v3applicationcredential auth_type in clouds.yaml

1 participant