Skip to content

Commit 22e9b75

Browse files
committed
Application Credential support
Signed-off-by: Veronika Fisarova <[email protected]>
1 parent e4cdf96 commit 22e9b75

File tree

10 files changed

+274
-9
lines changed

10 files changed

+274
-9
lines changed

api/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,5 @@ require (
7878
// mschuppert: map to latest commit from release-4.16 tag
7979
// must consistent within modules and service operators
8080
replace github.com/openshift/api => github.com/openshift/api v0.0.0-20240830023148-b7d0481c9094 //allow-merging
81+
82+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20250718124530-83b0609d1c8c

config/rbac/role.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ rules:
1212
- get
1313
- list
1414
- watch
15+
- apiGroups:
16+
- ""
17+
resources:
18+
- secrets
19+
verbs:
20+
- get
21+
- list
22+
- watch
1523
- apiGroups:
1624
- ""
1725
resources:
@@ -146,6 +154,14 @@ rules:
146154
- get
147155
- list
148156
- watch
157+
- apiGroups:
158+
- keystone.openstack.org
159+
resources:
160+
- keystoneapplicationcredentials
161+
verbs:
162+
- get
163+
- list
164+
- watch
149165
- apiGroups:
150166
- keystone.openstack.org
151167
resources:

controllers/swift_common.go

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,26 @@ package controllers
1919
import (
2020
"context"
2121
"fmt"
22-
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
23-
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
24-
"github.com/openstack-k8s-operators/lib-common/modules/common/secret"
25-
"k8s.io/apimachinery/pkg/types"
22+
"strings"
2623
"time"
2724

2825
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
26+
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
27+
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2928
"github.com/openstack-k8s-operators/lib-common/modules/common/env"
29+
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
30+
"github.com/openstack-k8s-operators/lib-common/modules/common/secret"
31+
32+
corev1 "k8s.io/api/core/v1"
3033
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34+
"k8s.io/apimachinery/pkg/types"
3135
ctrl "sigs.k8s.io/controller-runtime"
36+
"sigs.k8s.io/controller-runtime/pkg/builder"
3237
"sigs.k8s.io/controller-runtime/pkg/client"
38+
"sigs.k8s.io/controller-runtime/pkg/handler"
3339
"sigs.k8s.io/controller-runtime/pkg/log"
40+
"sigs.k8s.io/controller-runtime/pkg/predicate"
41+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3442
)
3543

3644
// fields to index to reconcile when change
@@ -140,3 +148,51 @@ func verifyServiceSecret(
140148
(*envVars)[secretName.Name] = env.SetValue(hash)
141149
return ctrl.Result{}, nil
142150
}
151+
152+
// AddACWatches adds KeystoneApplicationCredential + Secret watches to the passed controller builder
153+
func AddACWatches(b *builder.Builder) *builder.Builder {
154+
const (
155+
acPrefix = "ac-"
156+
acSecSuffix = "-secret"
157+
)
158+
159+
acMap := handler.MapFunc(func(_ context.Context, obj client.Object) []reconcile.Request {
160+
name := obj.GetName()
161+
ns := obj.GetNamespace()
162+
163+
// must begin with "ac-"
164+
if !strings.HasPrefix(name, acPrefix) {
165+
return nil
166+
}
167+
trim := strings.TrimPrefix(name, acPrefix)
168+
// for Secrets also strip "-secret"
169+
if _, isSecret := obj.(*corev1.Secret); isSecret {
170+
if !strings.HasSuffix(trim, acSecSuffix) {
171+
return nil
172+
}
173+
trim = strings.TrimSuffix(trim, acSecSuffix)
174+
}
175+
176+
// enqueue reconcile only for swift proxy controller
177+
svc := trim
178+
return []reconcile.Request{
179+
{NamespacedName: types.NamespacedName{Namespace: ns, Name: svc + "-proxy"}},
180+
}
181+
})
182+
183+
// watch the AC CR
184+
b = b.Watches(
185+
&keystonev1.KeystoneApplicationCredential{},
186+
handler.EnqueueRequestsFromMapFunc(acMap),
187+
builder.WithPredicates(predicate.GenerationChangedPredicate{}),
188+
)
189+
190+
// watch the AC kube Secret
191+
b = b.Watches(
192+
&corev1.Secret{},
193+
handler.EnqueueRequestsFromMapFunc(acMap),
194+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
195+
)
196+
197+
return b
198+
}

controllers/swift_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ func (r *SwiftReconciler) GetLogger(ctx context.Context) logr.Logger {
5959
//+kubebuilder:rbac:groups=swift.openstack.org,resources=swifts,verbs=get;list;watch;create;update;patch;delete
6060
//+kubebuilder:rbac:groups=swift.openstack.org,resources=swifts/status,verbs=get;update;patch
6161
//+kubebuilder:rbac:groups=swift.openstack.org,resources=swifts/finalizers,verbs=update;patch
62+
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
63+
//+kubebuilder:rbac:groups=keystone.openstack.org,resources=keystoneapplicationcredentials,verbs=get;list;watch
6264

6365
// service account, role, rolebinding
6466
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch

controllers/swiftproxy_controller.go

Lines changed: 167 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
appsv1 "k8s.io/api/apps/v1"
4040
corev1 "k8s.io/api/core/v1"
4141
apierrors "k8s.io/apimachinery/pkg/api/errors"
42+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
4243
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4344
ctrl "sigs.k8s.io/controller-runtime"
4445

@@ -511,6 +512,19 @@ func (r *SwiftProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
511512
return ctrlResult, err
512513
}
513514

515+
// Check for Application Credentials
516+
ctrlResult, err = r.verifyApplicationCredentials(
517+
ctx,
518+
r.GetLogger(ctx),
519+
helper.GetClient(),
520+
instance.Namespace,
521+
"swift",
522+
&envVars,
523+
)
524+
if (err != nil || ctrlResult != ctrl.Result{}) {
525+
return ctrlResult, err
526+
}
527+
514528
// Get the service password and pass it to the template
515529
sps, _, err := secret.GetSecret(ctx, helper, instance.Spec.Secret, instance.Namespace)
516530
if err != nil {
@@ -570,6 +584,20 @@ func (r *SwiftProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
570584
return ctrl.Result{}, err
571585
}
572586

587+
// Get Application Credential data if available
588+
acID, acSecret, useAC, err := r.getApplicationCredentialData(
589+
ctx,
590+
helper.GetClient(),
591+
instance.Namespace,
592+
"swift",
593+
)
594+
if err != nil {
595+
Log.Info("Failed to get application credentials, continuing with password auth", "error", err.Error())
596+
useAC = false
597+
acID = ""
598+
acSecret = ""
599+
}
600+
573601
// Create a Secret populated with content from templates/
574602
tpl := swiftproxy.SecretTemplates(
575603
instance,
@@ -582,6 +610,9 @@ func (r *SwiftProxyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
582610
secretRef,
583611
os.GetRegion(),
584612
transportURLString,
613+
useAC,
614+
acID,
615+
acSecret,
585616
)
586617
err = secret.EnsureSecrets(ctx, helper, instance, tpl, &envVars)
587618
if err != nil {
@@ -835,7 +866,7 @@ func (r *SwiftProxyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma
835866
return nil
836867
}
837868

838-
return ctrl.NewControllerManagedBy(mgr).
869+
b := ctrl.NewControllerManagedBy(mgr).
839870
For(&swiftv1beta1.SwiftProxy{}).
840871
Owns(&corev1.Secret{}).
841872
Owns(&keystonev1.KeystoneService{}).
@@ -855,8 +886,9 @@ func (r *SwiftProxyReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma
855886
builder.WithPredicates(predicate.GenerationChangedPredicate{})).
856887
Watches(&keystonev1.KeystoneAPI{},
857888
handler.EnqueueRequestsFromMapFunc(r.findObjectForSrc),
858-
builder.WithPredicates(keystonev1.KeystoneAPIStatusChangedPredicate)).
859-
Complete(r)
889+
builder.WithPredicates(keystonev1.KeystoneAPIStatusChangedPredicate))
890+
b = AddACWatches(b)
891+
return b.Complete(r)
860892
}
861893

862894
func (r *SwiftProxyReconciler) findObjectsForSrc(ctx context.Context, src client.Object) []reconcile.Request {
@@ -1020,3 +1052,135 @@ func (r *SwiftProxyReconciler) transportURLCreateOrUpdate(
10201052

10211053
return transportURL, op, err
10221054
}
1055+
1056+
// verifyApplicationCredentials handles Application Credentials validation
1057+
// It only uses AC if it's in a complete/ready state, otherwise continues with password auth
1058+
func (r *SwiftProxyReconciler) verifyApplicationCredentials(
1059+
ctx context.Context,
1060+
log logr.Logger,
1061+
client client.Client,
1062+
namespace string,
1063+
serviceName string,
1064+
configVars *map[string]env.Setter,
1065+
) (ctrl.Result, error) {
1066+
// Check for Application Credential - only use it if it's fully ready
1067+
acName := fmt.Sprintf("ac-%s", serviceName)
1068+
ac := &keystonev1.KeystoneApplicationCredential{}
1069+
1070+
if err := client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: acName}, ac); err == nil {
1071+
// AC CR exists - check if it's in ready state
1072+
if r.isACReady(ctx, log, client, ac) {
1073+
// AC is ready - add it to configVars for hash tracking
1074+
secretKey := types.NamespacedName{Namespace: namespace, Name: ac.Status.SecretName}
1075+
hash, res, err := secret.VerifySecret(
1076+
ctx,
1077+
secretKey,
1078+
[]string{"AC_ID", "AC_SECRET"},
1079+
client,
1080+
10*time.Second,
1081+
)
1082+
if err != nil {
1083+
log.Info("ApplicationCredential secret verification failed, continuing with password auth", "error", err.Error())
1084+
} else if res.RequeueAfter > 0 {
1085+
return res, nil
1086+
} else {
1087+
// AC is ready and verified - add to configVars for change tracking
1088+
(*configVars)["secret-"+ac.Status.SecretName] = env.SetValue(hash)
1089+
log.Info("Using ApplicationCredential authentication")
1090+
}
1091+
} else {
1092+
// AC exists but not ready - wait for it
1093+
log.Info("ApplicationCredential exists but not ready, waiting")
1094+
return ctrl.Result{RequeueAfter: time.Duration(10) * time.Second}, nil
1095+
}
1096+
} else if !k8s_errors.IsNotFound(err) {
1097+
return ctrl.Result{}, err
1098+
}
1099+
1100+
return ctrl.Result{}, nil
1101+
}
1102+
1103+
// isACReady checks if ApplicationCredential is in a ready state with all required components
1104+
func (r *SwiftProxyReconciler) isACReady(ctx context.Context, log logr.Logger, client client.Client, ac *keystonev1.KeystoneApplicationCredential) bool {
1105+
// Check if AC has completed setup (secret name is populated)
1106+
if ac.Status.SecretName == "" {
1107+
log.V(1).Info("AC not ready: SecretName not populated", "ac", ac.Name)
1108+
return false
1109+
}
1110+
1111+
secret := &corev1.Secret{}
1112+
secretKey := types.NamespacedName{Namespace: ac.Namespace, Name: ac.Status.SecretName}
1113+
if err := client.Get(ctx, secretKey, secret); err != nil {
1114+
log.V(1).Info("AC not ready: Secret not found", "secret", secretKey, "error", err)
1115+
return false
1116+
}
1117+
1118+
acID, acIDExists := secret.Data["AC_ID"]
1119+
acSecret, acSecretExists := secret.Data["AC_SECRET"]
1120+
1121+
if !acIDExists || !acSecretExists {
1122+
log.V(1).Info("AC not ready: Missing required fields", "secret", secretKey)
1123+
return false
1124+
}
1125+
1126+
if len(acID) == 0 || len(acSecret) == 0 {
1127+
log.V(1).Info("AC not ready: Empty required fields", "secret", secretKey)
1128+
return false
1129+
}
1130+
1131+
log.V(1).Info("AC is ready", "secret", secretKey)
1132+
return true
1133+
}
1134+
1135+
// getApplicationCredentialData retrieves AC data from secret if available and ready
1136+
func (r *SwiftProxyReconciler) getApplicationCredentialData(
1137+
ctx context.Context,
1138+
client client.Client,
1139+
namespace string,
1140+
serviceName string,
1141+
) (acID string, acSecret string, useAC bool, err error) {
1142+
// Check for Application Credential
1143+
acName := fmt.Sprintf("ac-%s", serviceName)
1144+
ac := &keystonev1.KeystoneApplicationCredential{}
1145+
1146+
if err := client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: acName}, ac); err != nil {
1147+
if k8s_errors.IsNotFound(err) {
1148+
return "", "", false, nil // AC not found, use password auth
1149+
}
1150+
return "", "", false, err
1151+
}
1152+
1153+
// AC exists, check if it's ready
1154+
if ac.Status.SecretName == "" {
1155+
return "", "", false, nil // AC not ready, use password auth
1156+
}
1157+
1158+
// Look up the AC secret
1159+
secret := &corev1.Secret{}
1160+
secName := types.NamespacedName{Namespace: ac.Namespace, Name: ac.Status.SecretName}
1161+
if err := client.Get(ctx, secName, secret); err != nil {
1162+
if k8s_errors.IsNotFound(err) {
1163+
return "", "", false, fmt.Errorf("%w: ApplicationCredential Secret %s", util.ErrNotFound, secName)
1164+
}
1165+
return "", "", false, err
1166+
}
1167+
1168+
// Validate required AC secret keys exist and are not empty
1169+
acIDData, acIDExists := secret.Data["AC_ID"]
1170+
acSecretData, acSecretExists := secret.Data["AC_SECRET"]
1171+
1172+
if !acIDExists {
1173+
return "", "", false, fmt.Errorf("%w: field AC_ID not found in ApplicationCredential Secret %s", util.ErrFieldNotFound, secName)
1174+
}
1175+
if !acSecretExists {
1176+
return "", "", false, fmt.Errorf("%w: field AC_SECRET not found in ApplicationCredential Secret %s", util.ErrFieldNotFound, secName)
1177+
}
1178+
if len(acIDData) == 0 {
1179+
return "", "", false, fmt.Errorf("%w: field AC_ID is empty in ApplicationCredential Secret %s", util.ErrFieldNotFound, secName)
1180+
}
1181+
if len(acSecretData) == 0 {
1182+
return "", "", false, fmt.Errorf("%w: field AC_SECRET is empty in ApplicationCredential Secret %s", util.ErrFieldNotFound, secName)
1183+
}
1184+
1185+
return string(acIDData), string(acSecretData), true, nil
1186+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,5 @@ replace github.com/openshift/api => github.com/openshift/api v0.0.0-202408300231
9191

9292
// custom RabbitmqClusterSpecCore for OpenStackControlplane (v2.6.0_patches_tag)
9393
replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250717122149-12f70b7f3d8d //allow-merging
94+
95+
replace github.com/openstack-k8s-operators/keystone-operator/api => github.com/Deydra71/keystone-operator/api v0.0.0-20250718124530-83b0609d1c8c

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/Deydra71/keystone-operator/api v0.0.0-20250718124530-83b0609d1c8c h1:283YBuEmIfplwstu3oQvYWATABVCV7QS1jQztP7teFg=
2+
github.com/Deydra71/keystone-operator/api v0.0.0-20250718124530-83b0609d1c8c/go.mod h1:0/8UQBzOyJYqHjarBfm2Rsw4xbQ8tywtKxSbTFbLbO0=
13
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
24
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
35
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -82,8 +84,6 @@ github.com/openstack-k8s-operators/barbican-operator/api v0.6.1-0.20250804160755
8284
github.com/openstack-k8s-operators/barbican-operator/api v0.6.1-0.20250804160755-893ae5639aec/go.mod h1:Yns9+BfjPFaHNdDgSH1hmCYGl3V6/HTBHlKS9K82Js8=
8385
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20250802211136-f5a38d83e342 h1:YnvkneTck4ir4kEbwuSCZFgKG/Iwy75PCyVjI1+r1oY=
8486
github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20250802211136-f5a38d83e342/go.mod h1:Dv8qpmBIQy3Jv/EyQnOyc0w61X8vyfxpjcIQONP5CwY=
85-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20250802061907-896a24e4fc36 h1:Z9aVKpwjQq1m8WKm/pOctbYHxYm6YKV+5tOPFE+13qQ=
86-
github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20250802061907-896a24e4fc36/go.mod h1:Xic8tNbumSe5WFoA8MlNp1jRJpUtGHzGYyJKMilOqx8=
8787
github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.20250730071847-837b07f8d72f h1:7w9WseH9LSKnLX3UW1AfWPaozN7fimdVoUtOYHnsQKY=
8888
github.com/openstack-k8s-operators/lib-common/modules/ansible v0.6.1-0.20250730071847-837b07f8d72f/go.mod h1:0bajRHochTUT6Ecfriw27l3vL0yezVrnUmt3bcIpu4w=
8989
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20250730071847-837b07f8d72f h1:DW8aNjEtDFrWiZ6vWuOXwdRB4eBD0n+bA9foQkOEx6U=

pkg/swiftproxy/templates.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ func SecretTemplates(
3838
secretRef string,
3939
keystoneRegion string,
4040
transportURL string,
41+
useApplicationCredentials bool,
42+
applicationCredentialID string,
43+
applicationCredentialSecret string,
4144
) []util.Template {
4245
templateParameters := make(map[string]interface{})
4346
templateParameters["ServiceUser"] = instance.Spec.ServiceUser
@@ -51,6 +54,13 @@ func SecretTemplates(
5154
templateParameters["KeystoneRegion"] = keystoneRegion
5255
templateParameters["TransportURL"] = transportURL
5356

57+
// Application Credential parameters
58+
templateParameters["UseApplicationCredentials"] = useApplicationCredentials
59+
if useApplicationCredentials {
60+
templateParameters["ApplicationCredentialID"] = applicationCredentialID
61+
templateParameters["ApplicationCredentialSecret"] = applicationCredentialSecret
62+
}
63+
5464
// MTLS params
5565
if mc.Status.MTLSCert != "" {
5666
templateParameters["MemcachedAuthCert"] = fmt.Sprint(memcachedv1.CertMountPath())

0 commit comments

Comments
 (0)