Skip to content

[1952] feat: clouds.yaml credential format for OpenstackCreds - #1955

Open
gtherond wants to merge 16 commits into
platform9:mainfrom
gtherond:1952-clouds-yaml-credentials
Open

[1952] feat: clouds.yaml credential format for OpenstackCreds#1955
gtherond wants to merge 16 commits into
platform9:mainfrom
gtherond:1952-clouds-yaml-credentials

Conversation

@gtherond

@gtherond gtherond commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Introduces clouds.yaml as the credential format for the OpenstackCreds CRD, with full back-compat for the existing OS_*-keyed Secrets.
  • Replaces the flat OpenStackValidationStatus / OpenStackValidationMessage status fields with a Kubernetes-style status.conditions slice (metav1.Condition).
  • Wires per-service API version values from clouds.yaml (compute_api_version, volume_api_version, etc.) as an operator-configurable floor over the internal hardcoded microversion values in v2v-helper. Operators can bump compute microversion at will via the Secret without rebuilding vjailbreak; misconfigured low values can't lower the floor for operations that require a specific microversion.
  • Adds a Kubernetes Secret watch so credential rotation (in particular Application Credential rotation in Support v3applicationcredential auth_type in clouds.yaml #1953) is observed and re-validated within seconds.

Closes #1952. Part of the umbrella tracked at #1951.

Design artifacts (spec, plan, research, data model, contracts, tasks) ship in this PR under specs/003-clouds-yaml-credentials/.

What's in this PR

  • CRD: new OpenstackCredsSpec.CloudName field; new OpenstackCredsStatus.Conditions slice; retired flat status fields; updated kubebuilder printcolumns to derive from Conditions.
  • pkg/common/microversion (new): shared Floor(configValue, hardcodedValue) string helper used by both the controller and v2v-helper. Handles MAJOR.MINOR / MAJOR / latest, numeric comparison (so 2.100 > 2.60), unparseable-fallback semantics.
  • k8s/migration/pkg/utils: Condition Type / Reason constants per specs/.../contracts/conditions.md, SetCondition wrapper over meta.SetStatusCondition, ParseCloudsYAML parser (custom-built on gopkg.in/yaml.v3 to avoid pulling gophercloud/utils/v2 which would force Go 1.25 + gophercloud minor bumps), SecretContainsCloudsYAML for dispatch, MicroversionsToEnvVars for env injection.
  • pkg/common/validation/openstack/validate.go and k8s/migration/pkg/utils/credutils.go: parser branching — clouds.yaml key in Secret takes precedence over OS_*; legacy path unchanged.
  • v2v-helper/openstack/openstackops.go + v2v-helper/pkg/utils/openstackopsutils.go: per-call hardcoded floors (2.60, 2.37) and service-client construction now honor microversion.Floor.
  • k8s/migration/internal/controller/openstackcreds_controller.go: setConditionsForValidResult / setConditionsForInvalidResult populate the new Conditions; Watches(&corev1.Secret{}, EnqueueRequestsFromMapFunc(...)) wires the Secret watch.
  • k8s/migration/internal/controller/migrationplan_controller.go: CreateJob derives OS_*_API_VERSION env vars from clouds.yaml and injects them into the v2v-helper Job spec.
  • Docs: new docs/credentials.md operator runbook (clouds.yaml + Application Credentials + multi-cloud + microversion + rotation + inline cacert + legacy fallback + role minimums).

Test plan

  • cd k8s/migration && make test and make test-v2v-helper pass on the changes
  • Apply an OpenstackCreds referencing a Secret with a single-entry clouds.yaml; verify status.conditions show CredentialsParsed=True, CredentialsValidated=True, Expiring=False, Expired=False
  • Apply a multi-entry clouds.yaml with cloudName set; verify the selected cloud's auth values authenticate against the destination Keystone
  • Apply a multi-entry clouds.yaml without cloudName; verify CredentialsParsed=False, Reason=AmbiguousCloudName and the message lists available cloud names
  • Apply a legacy OS_*-keyed Secret (existing deployment shape); verify validation succeeds with the same Conditions as the clouds.yaml path
  • Edit the Secret data with kubectl apply; verify the controller re-reconciles within seconds (Secret watch)
  • Set compute_api_version: "2.95" in clouds.yaml; trigger a migration that hits the AttachVolumeToVM code path; verify the X-OpenStack-Nova-API-Version header on the resulting POST /os-volume_attachments is 2.95, not 2.60
  • Walk through the new docs/credentials.md quickstart end-to-end against a real Keystone

Pre-existing items observed during this work

  • pkg/utils/bmprovisionerutils.go:227 has a pre-existing go vet failure (copylocks on a protobuf message containing sync.Mutex); make test baseline on main is broken because of it. Not introduced by this PR — golangci-lint run does not flag it.
  • .githooks/pre-commit line 11 (GRAPHIFY_BIN=$(command -v graphify 2>/dev/null)) aborts the hook under set -e when graphify isn't installed; external contributors see git commit exit 1 with no useful output. Worked around locally by installing the graphifyy PyPI package. A one-line ... || true fix would unblock external contributors.

🤖 Generated with Claude Code


Open in Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

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>

@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 2 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment thread k8s/migration/pkg/utils/credutils.go
Comment thread k8s/migration/internal/controller/migrationplan_controller.go Outdated
gtherond added a commit to gtherond/vjailbreak that referenced this pull request May 18, 2026
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>
gtherond and others added 16 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>
@gtherond
gtherond force-pushed the 1952-clouds-yaml-credentials branch from ae7f094 to ff5fd22 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.

Accept clouds.yaml in OpenstackCreds Secret

1 participant