Skip to content

Commit 111762a

Browse files
gtherondclaude
andcommitted
[1953] feat: populate Expiring/Expired Conditions for Application Credentials
Wires the App Credential helpers (#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>
1 parent d94cbee commit 111762a

5 files changed

Lines changed: 63 additions & 31 deletions

File tree

graphify-out/cache/ast/17a14629d1b606109bda909287117739ee4aafcecbb5bbf6a71de9785943a572.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

graphify-out/cache/ast/9279aca6d66f2bed83ce1f7c4a4122902f78ecfe27a75497022be38443669323.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

graphify-out/cache/ast/ed0c2efd9673d21432d24502011daa8bf3fd7e2f14c26f38000f8d0016a95f91.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

k8s/migration/internal/controller/openstackcreds_controller.go

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ func (r *OpenstackCredsReconciler) applyValidationResult(ctx context.Context, sc
281281
return nil
282282
}
283283

284-
setConditionsForValidResult(&scope.OpenstackCreds.Status)
284+
setConditionsForValidResult(&scope.OpenstackCreds.Status, result.AppCred)
285285
ctxlog.Info("Updating status to success", "openstackcreds", scope.OpenstackCreds.Name)
286286
if err := r.Status().Update(ctx, scope.OpenstackCreds); err != nil {
287287
ctxlog.Error(err, "Error updating status of OpenstackCreds", "openstackcreds", scope.OpenstackCreds.Name)
@@ -525,18 +525,37 @@ func (r *OpenstackCredsReconciler) mapSecretToOpenstackCreds(ctx context.Context
525525
return requests
526526
}
527527

528-
// setConditionsForValidResult populates the OpenstackCreds Conditions for the
529-
// authenticated-and-validated case. Reasons specific to Application Credentials
530-
// (expiration, missing roles) are populated in the follow-up sub-issue #1953.
531-
//
532-
// Also writes the deprecated flat OpenStackValidationStatus/Message fields as a
533-
// derived view so the existing UI and pkg/vpwned proxy keep working through the
534-
// Conditions migration window.
535-
func setConditionsForValidResult(status *vjailbreakv1alpha1.OpenstackCredsStatus) {
528+
// authenticated-and-validated case. When an Application Credential is in use
529+
// and its metadata was fetched, Expiring / Expired are populated based on the
530+
// credential's expires_at; otherwise they remain NotApplicable. Also writes
531+
// the legacy flat OpenStackValidationStatus/Message fields as a derived view
532+
// so the existing UI and pkg/vpwned proxy keep working through the Conditions
533+
// migration window.
534+
func setConditionsForValidResult(status *vjailbreakv1alpha1.OpenstackCredsStatus, appCred *utils.AppCredentialDetails) {
536535
utils.SetCondition(&status.Conditions, utils.ConditionCredentialsParsed, metav1.ConditionTrue, utils.ReasonParsed, "Credential data parsed successfully")
537536
utils.SetCondition(&status.Conditions, utils.ConditionCredentialsValidated, metav1.ConditionTrue, utils.ReasonAuthSucceeded, "Authenticated to destination Keystone")
538-
utils.SetCondition(&status.Conditions, utils.ConditionExpiring, metav1.ConditionFalse, utils.ReasonNotApplicable, "Credential is not an Application Credential")
539-
utils.SetCondition(&status.Conditions, utils.ConditionExpired, metav1.ConditionFalse, utils.ReasonNotApplicable, "Credential is not an Application Credential")
537+
538+
if appCred == nil {
539+
utils.SetCondition(&status.Conditions, utils.ConditionExpiring, metav1.ConditionFalse, utils.ReasonNotApplicable, "Credential is not an Application Credential or details unavailable")
540+
utils.SetCondition(&status.Conditions, utils.ConditionExpired, metav1.ConditionFalse, utils.ReasonNotApplicable, "Credential is not an Application Credential or details unavailable")
541+
} else {
542+
within30, within7, expired := utils.EvaluateAppCredExpiration(appCred.ExpiresAt, time.Now())
543+
switch {
544+
case expired:
545+
utils.SetCondition(&status.Conditions, utils.ConditionExpired, metav1.ConditionTrue, utils.ReasonExpired, fmt.Sprintf("Application Credential %s expired at %s", appCred.ID, appCred.ExpiresAt.Format(time.RFC3339)))
546+
utils.SetCondition(&status.Conditions, utils.ConditionExpiring, metav1.ConditionFalse, utils.ReasonNotApplicable, "Credential already expired")
547+
case within7:
548+
utils.SetCondition(&status.Conditions, utils.ConditionExpiring, metav1.ConditionTrue, utils.ReasonWithin7Days, fmt.Sprintf("Application Credential %s expires at %s (within 7 days)", appCred.ID, appCred.ExpiresAt.Format(time.RFC3339)))
549+
utils.SetCondition(&status.Conditions, utils.ConditionExpired, metav1.ConditionFalse, utils.ReasonActive, "Credential is active")
550+
case within30:
551+
utils.SetCondition(&status.Conditions, utils.ConditionExpiring, metav1.ConditionTrue, utils.ReasonWithin30Days, fmt.Sprintf("Application Credential %s expires at %s (within 30 days)", appCred.ID, appCred.ExpiresAt.Format(time.RFC3339)))
552+
utils.SetCondition(&status.Conditions, utils.ConditionExpired, metav1.ConditionFalse, utils.ReasonActive, "Credential is active")
553+
default:
554+
utils.SetCondition(&status.Conditions, utils.ConditionExpiring, metav1.ConditionFalse, utils.ReasonNotApplicable, "Credential is not approaching expiration")
555+
utils.SetCondition(&status.Conditions, utils.ConditionExpired, metav1.ConditionFalse, utils.ReasonActive, "Credential is active")
556+
}
557+
}
558+
540559
status.OpenStackValidationStatus = string(corev1.PodSucceeded)
541560
status.OpenStackValidationMessage = "Successfully authenticated to Openstack"
542561
}
@@ -545,9 +564,9 @@ func setConditionsForValidResult(status *vjailbreakv1alpha1.OpenstackCredsStatus
545564
// validation-failure case. Distinguishes parse failures (CredentialsParsed=False
546565
// with a parse-specific Reason mapped from the sentinel error type returned by
547566
// utils.ParseCloudsYAML) from authentication failures (CredentialsParsed=True,
548-
// CredentialsValidated=False with an auth-specific Reason). Also writes the
549-
// deprecated flat OpenStackValidationStatus/Message fields as a derived view
550-
// for back-compat.
567+
// CredentialsValidated=False with a Reason from utils.MapKeystoneError). Also
568+
// writes the legacy flat OpenStackValidationStatus/Message fields as a derived
569+
// view for back-compat.
551570
func setConditionsForInvalidResult(status *vjailbreakv1alpha1.OpenstackCredsStatus, rawErr error, errMsg string) {
552571
if parsedFalseReason := parseFailureReason(rawErr); parsedFalseReason != "" {
553572
utils.SetCondition(&status.Conditions, utils.ConditionCredentialsParsed, metav1.ConditionFalse, parsedFalseReason, errMsg)
@@ -557,28 +576,16 @@ func setConditionsForInvalidResult(status *vjailbreakv1alpha1.OpenstackCredsStat
557576
return
558577
}
559578

560-
// Not a parse failure: parsing succeeded, authentication or environment
561-
// check is what failed. Heuristic Reason mapping on the error message.
562-
reason := utils.ReasonCredentialInvalidOrRevoked
563-
switch {
564-
case strings.Contains(errMsg, "401"), strings.Contains(errMsg, "invalid username"), strings.Contains(errMsg, "rejected or revoked"):
565-
reason = utils.ReasonCredentialInvalidOrRevoked
566-
case strings.Contains(errMsg, "404"), strings.Contains(errMsg, "Auth URL"):
567-
reason = utils.ReasonKeystoneUnreachable
568-
case strings.Contains(errMsg, "timeout"):
569-
reason = utils.ReasonKeystoneUnreachable
570-
case strings.Contains(errMsg, "x509"), strings.Contains(errMsg, "certificate"):
571-
reason = utils.ReasonTLSVerificationFailed
572-
}
579+
reason := utils.MapKeystoneError(rawErr)
573580
utils.SetCondition(&status.Conditions, utils.ConditionCredentialsParsed, metav1.ConditionTrue, utils.ReasonParsed, "Credential data parsed successfully")
574581
utils.SetCondition(&status.Conditions, utils.ConditionCredentialsValidated, metav1.ConditionFalse, reason, errMsg)
575582
status.OpenStackValidationStatus = constants.ValidationStatusFailed
576583
status.OpenStackValidationMessage = errMsg
577584
}
578585

579-
// parseFailureReason returns the appropriate ReasonInvalid* code when err
580-
// wraps one of the ParseCloudsYAML sentinel errors. Returns "" when err is not
581-
// a parse failure (caller treats as authentication-stage failure).
586+
// parseFailureReason returns the appropriate Reason code when err wraps one of
587+
// the ParseCloudsYAML sentinel errors. Returns "" when err is not a parse
588+
// failure (caller treats as authentication-stage failure).
582589
func parseFailureReason(err error) string {
583590
if err == nil {
584591
return ""

pkg/common/validation/openstack/validate.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ type ValidationResult struct {
3131
Valid bool
3232
Message string
3333
Error error
34+
// AppCred carries Application Credential metadata fetched after successful
35+
// authentication when auth_type is v3applicationcredential and the
36+
// credential's metadata could be retrieved. Nil for non-AppCred auth or
37+
// when the metadata fetch was unavailable. Drives the Expiring / Expired
38+
// Conditions on OpenstackCreds.status.
39+
AppCred *utils.AppCredentialDetails
3440
}
3541

3642
func authErrorMessage(err error, isTokenAuth bool) string {
@@ -254,11 +260,27 @@ func validateFromCloudsYAML(ctx context.Context, k8sClient client.Client, openst
254260
}
255261
}
256262

257-
return ValidationResult{
263+
result := ValidationResult{
258264
Valid: true,
259265
Message: "Successfully authenticated to Openstack",
260266
Error: nil,
261267
}
268+
269+
// For v3applicationcredential auth, try to fetch the credential's metadata
270+
// so the controller can populate the Expiring / Expired Conditions. A
271+
// failure here is non-fatal: the credential itself authenticated, and
272+
// the controller treats nil AppCred as "details unavailable".
273+
if cfg.AuthType == "v3applicationcredential" && cfg.AuthOptions.ApplicationCredentialID != "" {
274+
details, err := utils.FetchAppCredentialDetails(ctx, providerClient, cfg.AuthOptions.ApplicationCredentialID)
275+
if err != nil {
276+
ctrllog.FromContext(ctx).Info("could not fetch Application Credential details for status reporting",
277+
"credentialID", cfg.AuthOptions.ApplicationCredentialID, "err", err.Error())
278+
} else {
279+
result.AppCred = details
280+
}
281+
}
282+
283+
return result
262284
}
263285

264286
// getCredentialsFromSecret retrieves OpenStack credentials from a Kubernetes secret

0 commit comments

Comments
 (0)