Skip to content

Commit 7b903f2

Browse files
xekcursoragent
authored andcommitted
Application Credential support
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0015cb1 commit 7b903f2

9 files changed

Lines changed: 170 additions & 12 deletions

File tree

api/bases/placement.openstack.org_placementapis.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ spec:
5757
description: APITimeout for HAProxy, Apache
5858
minimum: 10
5959
type: integer
60+
auth:
61+
description: Auth - Parameters related to authentication
62+
properties:
63+
applicationCredentialSecret:
64+
description: ApplicationCredentialSecret - Secret containing Application
65+
Credential ID and Secret
66+
type: string
67+
type: object
6068
containerImage:
6169
description: PlacementAPI Container Image URL (will be set to environmental
6270
default if empty)

api/v1beta1/placementapi_types.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
2021
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2122
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
2223
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
2324
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
24-
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
25-
"k8s.io/apimachinery/pkg/util/validation/field"
2625
corev1 "k8s.io/api/core/v1"
2726
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/util/validation/field"
2828
)
2929

3030
const (
@@ -126,6 +126,11 @@ type PlacementAPISpecCore struct {
126126
// TLS - Parameters related to the TLS
127127
TLS tls.API `json:"tls,omitempty"`
128128

129+
// +kubebuilder:validation:Optional
130+
// +operator-sdk:csv:customresourcedefinitions:type=spec
131+
// Auth - Parameters related to authentication
132+
Auth AuthSpec `json:"auth,omitempty"`
133+
129134
// +kubebuilder:validation:Optional
130135
// TopologyRef to apply the Topology defined by the associated CR referenced
131136
// by name
@@ -139,6 +144,14 @@ type APIOverrideSpec struct {
139144
Service map[service.Endpoint]service.RoutedOverrideSpec `json:"service,omitempty"`
140145
}
141146

147+
// AuthSpec defines authentication parameters
148+
type AuthSpec struct {
149+
// +kubebuilder:validation:Optional
150+
// +operator-sdk:csv:customresourcedefinitions:type=spec
151+
// ApplicationCredentialSecret - Secret containing Application Credential ID and Secret
152+
ApplicationCredentialSecret string `json:"applicationCredentialSecret,omitempty"`
153+
}
154+
142155
// PasswordSelector to identify the DB and AdminUser password from the Secret
143156
type PasswordSelector struct {
144157
// +kubebuilder:validation:Optional

api/v1beta1/placementapi_webhook.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ func (spec *PlacementAPISpec) Default() {
6868
if spec.APITimeout == 0 {
6969
spec.APITimeout = placementAPIDefaults.APITimeout
7070
}
71-
7271
}
7372

7473
// Default - set defaults for this PlacementAPI core spec (this version is used by the OpenStackControlplane webhook)

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/placement.openstack.org_placementapis.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ spec:
5757
description: APITimeout for HAProxy, Apache
5858
minimum: 10
5959
type: integer
60+
auth:
61+
description: Auth - Parameters related to authentication
62+
properties:
63+
applicationCredentialSecret:
64+
description: ApplicationCredentialSecret - Secret containing Application
65+
Credential ID and Secret
66+
type: string
67+
type: object
6068
containerImage:
6169
description: PlacementAPI Container Image URL (will be set to environmental
6270
default if empty)

config/manifests/bases/placement-operator.clusterserviceversion.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ spec:
2424
kind: PlacementAPI
2525
name: placementapis.placement.openstack.org
2626
specDescriptors:
27+
- description: Auth - Parameters related to authentication
28+
displayName: Auth
29+
path: auth
30+
- description: ApplicationCredentialSecret - Secret containing Application Credential
31+
ID and Secret
32+
displayName: Application Credential Secret
33+
path: auth.applicationCredentialSecret
2734
- description: TLS - Parameters related to the TLS
2835
displayName: TLS
2936
path: tls

internal/controller/placementapi_controller.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controller
1919

2020
import (
2121
"context"
22+
"errors"
2223
"fmt"
2324
"maps"
2425
"time"
@@ -68,6 +69,12 @@ import (
6869
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
6970
)
7071

72+
// Static errors for Application Credential handling
73+
var (
74+
ErrACSecretNotFound = errors.New("ApplicationCredential secret not found")
75+
ErrACSecretMissingKeys = errors.New("ApplicationCredential secret missing required keys")
76+
)
77+
7178
type conditionUpdater interface {
7279
Set(c *condition.Condition)
7380
MarkTrue(t condition.Type, messageFormat string, messageArgs ...any)
@@ -849,6 +856,7 @@ const (
849856
tlsAPIInternalField = ".spec.tls.api.internal.secretName"
850857
tlsAPIPublicField = ".spec.tls.api.public.secretName"
851858
topologyField = ".spec.topologyRef.Name"
859+
authAppCredSecretField = ".spec.auth.applicationCredentialSecret" // #nosec G101
852860
)
853861

854862
var allWatchFields = []string{
@@ -857,6 +865,7 @@ var allWatchFields = []string{
857865
tlsAPIInternalField,
858866
tlsAPIPublicField,
859867
topologyField,
868+
authAppCredSecretField,
860869
}
861870

862871
// SetupWithManager sets up the controller with the Manager.
@@ -921,6 +930,18 @@ func (r *PlacementAPIReconciler) SetupWithManager(mgr ctrl.Manager) error {
921930
return err
922931
}
923932

933+
// index authAppCredSecretField
934+
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &placementv1.PlacementAPI{}, authAppCredSecretField, func(rawObj client.Object) []string {
935+
// Extract the application credential secret name from the spec, if one is provided
936+
cr := rawObj.(*placementv1.PlacementAPI)
937+
if cr.Spec.Auth.ApplicationCredentialSecret == "" {
938+
return nil
939+
}
940+
return []string{cr.Spec.Auth.ApplicationCredentialSecret}
941+
}); err != nil {
942+
return err
943+
}
944+
924945
return ctrl.NewControllerManagedBy(mgr).
925946
For(&placementv1.PlacementAPI{}).
926947
Owns(&mariadbv1.MariaDBDatabase{}).
@@ -1379,6 +1400,30 @@ func (r *PlacementAPIReconciler) generateServiceConfigMaps(
13791400
),
13801401
}
13811402

1403+
templateParameters["UseApplicationCredentials"] = false
1404+
// Try to get Application Credential for this service
1405+
if instance.Spec.Auth.ApplicationCredentialSecret != "" {
1406+
acSecretObj, _, err := secret.GetSecret(ctx, h, instance.Spec.Auth.ApplicationCredentialSecret, instance.Namespace)
1407+
if err != nil {
1408+
if k8s_errors.IsNotFound(err) {
1409+
h.GetLogger().Info("ApplicationCredential secret not found, waiting", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
1410+
return fmt.Errorf("%w: %s", ErrACSecretNotFound, instance.Spec.Auth.ApplicationCredentialSecret)
1411+
}
1412+
h.GetLogger().Error(err, "Failed to get ApplicationCredential secret", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
1413+
return err
1414+
}
1415+
acID, okID := acSecretObj.Data[keystonev1.ACIDSecretKey]
1416+
acSecretData, okSecret := acSecretObj.Data[keystonev1.ACSecretSecretKey]
1417+
if !okID || len(acID) == 0 || !okSecret || len(acSecretData) == 0 {
1418+
h.GetLogger().Info("ApplicationCredential secret missing required keys", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
1419+
return fmt.Errorf("%w: %s", ErrACSecretMissingKeys, instance.Spec.Auth.ApplicationCredentialSecret)
1420+
}
1421+
templateParameters["UseApplicationCredentials"] = true
1422+
templateParameters["ACID"] = string(acID)
1423+
templateParameters["ACSecret"] = string(acSecretData)
1424+
h.GetLogger().Info("Using ApplicationCredentials auth", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
1425+
}
1426+
13821427
// create httpd vhost template parameters
13831428
httpdVhostConfig := map[string]any{}
13841429
for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} {

templates/placementapi/config/placement.conf

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,23 @@ connection = {{ .DatabaseConnection }}
1515
auth_strategy = keystone
1616

1717
[keystone_authtoken]
18-
project_domain_name = Default
19-
user_domain_name = Default
20-
project_name = service
18+
www_authenticate_uri = {{ .KeystonePublicURL }}
19+
auth_url = {{ .KeystoneInternalURL }}
20+
{{ if .UseApplicationCredentials -}}
21+
auth_type = v3applicationcredential
22+
application_credential_id = {{ .ACID }}
23+
application_credential_secret = {{ .ACSecret }}
24+
{{ else -}}
25+
auth_type = password
2126
username = {{ .ServiceUser }}
2227
password = {{ .PlacementPassword }}
28+
user_domain_name = Default
29+
project_domain_name = Default
30+
project_name = service
31+
{{- end }}
2332
{{ if (index . "Region") -}}
2433
region_name = {{ .Region }}
2534
{{ end -}}
26-
www_authenticate_uri = {{ .KeystonePublicURL }}
27-
auth_url = {{ .KeystoneInternalURL }}
28-
auth_type = password
2935
interface = internal
3036

3137
[oslo_policy]

test/functional/placementapi_controller_test.go

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,10 @@ var _ = Describe("PlacementAPI controller", func() {
374374
// Verify region_name is set in [keystone_authtoken] section
375375
// GetRegion() returns Status.Region, so check that
376376
Expect(keystoneAPI.Status.Region).ToNot(BeEmpty(), "KeystoneAPI should have a region set in status")
377-
// The region_name should appear in the [keystone_authtoken] section
378-
// It's conditionally rendered, so check it appears between password and www_authenticate_uri
377+
// The region_name should appear in the [keystone_authtoken] section (before [oslo_policy])
379378
Expect(conf).Should(
380379
MatchRegexp(fmt.Sprintf(
381-
"password = .*\\nregion_name = %s\\n", keystoneAPI.Status.Region)))
380+
`\[keystone_authtoken\][\s\S]*region_name = %s[\s\S]*\[oslo_policy\]`, keystoneAPI.Status.Region)))
382381
})
383382

384383
It("creates service account, role and rolebindig", func() {
@@ -1470,4 +1469,61 @@ var _ = Describe("PlacementAPI reconfiguration", func() {
14701469

14711470
})
14721471

1472+
When("an ApplicationCredential is created for Placement", func() {
1473+
BeforeEach(func() {
1474+
DeferCleanup(
1475+
k8sClient.Delete, ctx, CreatePlacementAPISecret(names.Namespace, SecretName))
1476+
keystoneAPIName := keystone.CreateKeystoneAPI(names.Namespace)
1477+
DeferCleanup(keystone.DeleteKeystoneAPI, keystoneAPIName)
1478+
1479+
acSecretName := fmt.Sprintf("ac-%s-secret", placement.ServiceName)
1480+
acSecret := th.CreateSecret(
1481+
types.NamespacedName{Namespace: names.Namespace, Name: acSecretName},
1482+
map[string][]byte{
1483+
keystonev1.ACIDSecretKey: []byte("test-ac-id"),
1484+
keystonev1.ACSecretSecretKey: []byte("test-ac-secret"),
1485+
},
1486+
)
1487+
DeferCleanup(th.DeleteInstance, acSecret)
1488+
1489+
spec := GetDefaultPlacementAPISpec()
1490+
spec["auth"] = map[string]any{
1491+
"applicationCredentialSecret": acSecretName,
1492+
}
1493+
1494+
DeferCleanup(th.DeleteInstance, CreatePlacementAPI(names.PlacementAPIName, spec))
1495+
1496+
DeferCleanup(
1497+
mariadb.DeleteDBService,
1498+
mariadb.CreateDBService(
1499+
names.Namespace,
1500+
GetDefaultPlacementAPISpec()["databaseInstance"].(string),
1501+
corev1.ServiceSpec{
1502+
Ports: []corev1.ServicePort{{Port: 3306}},
1503+
},
1504+
),
1505+
)
1506+
mariadb.SimulateMariaDBDatabaseCompleted(names.MariaDBDatabaseName)
1507+
mariadb.SimulateMariaDBAccountCompleted(names.MariaDBAccount)
1508+
})
1509+
1510+
It("should render ApplicationCredential auth in placement.conf", func() {
1511+
configSecret := th.GetSecret(names.ConfigMapName)
1512+
conf := configSecret.Data["placement.conf"]
1513+
1514+
// AC auth is configured
1515+
Expect(conf).To(ContainSubstring("auth_type = v3applicationcredential"))
1516+
Expect(conf).To(ContainSubstring("application_credential_id = test-ac-id"))
1517+
Expect(conf).To(ContainSubstring("application_credential_secret = test-ac-secret"))
1518+
1519+
// Password auth fields should not be present
1520+
Expect(conf).NotTo(ContainSubstring("auth_type = password"))
1521+
Expect(conf).NotTo(ContainSubstring("username ="))
1522+
Expect(conf).NotTo(ContainSubstring("password ="))
1523+
Expect(conf).NotTo(ContainSubstring("project_name ="))
1524+
Expect(conf).NotTo(ContainSubstring("user_domain_name ="))
1525+
Expect(conf).NotTo(ContainSubstring("project_domain_name ="))
1526+
})
1527+
})
1528+
14731529
})

0 commit comments

Comments
 (0)