Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/bases/placement.openstack.org_placementapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ spec:
description: APITimeout for HAProxy, Apache
minimum: 10
type: integer
auth:
description: Auth - Parameters related to authentication
properties:
applicationCredentialSecret:
description: ApplicationCredentialSecret - Secret containing Application
Credential ID and Secret
type: string
type: object
containerImage:
description: PlacementAPI Container Image URL (will be set to environmental
default if empty)
Expand Down
17 changes: 15 additions & 2 deletions api/v1beta1/placementapi_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ limitations under the License.
package v1beta1

import (
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
"github.com/openstack-k8s-operators/lib-common/modules/common/tls"
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
"k8s.io/apimachinery/pkg/util/validation/field"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
)

const (
Expand Down Expand Up @@ -126,6 +126,11 @@ type PlacementAPISpecCore struct {
// TLS - Parameters related to the TLS
TLS tls.API `json:"tls,omitempty"`

// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec
// Auth - Parameters related to authentication
Auth AuthSpec `json:"auth,omitempty"`

// +kubebuilder:validation:Optional
// TopologyRef to apply the Topology defined by the associated CR referenced
// by name
Expand All @@ -139,6 +144,14 @@ type APIOverrideSpec struct {
Service map[service.Endpoint]service.RoutedOverrideSpec `json:"service,omitempty"`
}

// AuthSpec defines authentication parameters
type AuthSpec struct {
// +kubebuilder:validation:Optional
// +operator-sdk:csv:customresourcedefinitions:type=spec
// ApplicationCredentialSecret - Secret containing Application Credential ID and Secret
ApplicationCredentialSecret string `json:"applicationCredentialSecret,omitempty"`
}

// PasswordSelector to identify the DB and AdminUser password from the Secret
type PasswordSelector struct {
// +kubebuilder:validation:Optional
Expand Down
1 change: 0 additions & 1 deletion api/v1beta1/placementapi_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ func (spec *PlacementAPISpec) Default() {
if spec.APITimeout == 0 {
spec.APITimeout = placementAPIDefaults.APITimeout
}

}

// Default - set defaults for this PlacementAPI core spec (this version is used by the OpenStackControlplane webhook)
Expand Down
16 changes: 16 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions config/crd/bases/placement.openstack.org_placementapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ spec:
description: APITimeout for HAProxy, Apache
minimum: 10
type: integer
auth:
description: Auth - Parameters related to authentication
properties:
applicationCredentialSecret:
description: ApplicationCredentialSecret - Secret containing Application
Credential ID and Secret
type: string
type: object
containerImage:
description: PlacementAPI Container Image URL (will be set to environmental
default if empty)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ spec:
kind: PlacementAPI
name: placementapis.placement.openstack.org
specDescriptors:
- description: Auth - Parameters related to authentication
displayName: Auth
path: auth
- description: ApplicationCredentialSecret - Secret containing Application Credential
ID and Secret
displayName: Application Credential Secret
path: auth.applicationCredentialSecret
- description: TLS - Parameters related to the TLS
displayName: TLS
path: tls
Expand Down
45 changes: 45 additions & 0 deletions internal/controller/placementapi_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package controller

import (
"context"
"errors"
"fmt"
"maps"
"time"
Expand Down Expand Up @@ -68,6 +69,12 @@ import (
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
)

// Static errors for Application Credential handling
var (
ErrACSecretNotFound = errors.New("ApplicationCredential secret not found")
ErrACSecretMissingKeys = errors.New("ApplicationCredential secret missing required keys")
)

type conditionUpdater interface {
Set(c *condition.Condition)
MarkTrue(t condition.Type, messageFormat string, messageArgs ...any)
Expand Down Expand Up @@ -849,6 +856,7 @@ const (
tlsAPIInternalField = ".spec.tls.api.internal.secretName"
tlsAPIPublicField = ".spec.tls.api.public.secretName"
topologyField = ".spec.topologyRef.Name"
authAppCredSecretField = ".spec.auth.applicationCredentialSecret" // #nosec G101
)

var allWatchFields = []string{
Expand All @@ -857,6 +865,7 @@ var allWatchFields = []string{
tlsAPIInternalField,
tlsAPIPublicField,
topologyField,
authAppCredSecretField,
}

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

// index authAppCredSecretField
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &placementv1.PlacementAPI{}, authAppCredSecretField, func(rawObj client.Object) []string {
// Extract the application credential secret name from the spec, if one is provided
cr := rawObj.(*placementv1.PlacementAPI)
if cr.Spec.Auth.ApplicationCredentialSecret == "" {
return nil
}
return []string{cr.Spec.Auth.ApplicationCredentialSecret}
}); err != nil {
return err
}

return ctrl.NewControllerManagedBy(mgr).
For(&placementv1.PlacementAPI{}).
Owns(&mariadbv1.MariaDBDatabase{}).
Expand Down Expand Up @@ -1379,6 +1400,30 @@ func (r *PlacementAPIReconciler) generateServiceConfigMaps(
),
}

templateParameters["UseApplicationCredentials"] = false
// Try to get Application Credential for this service
if instance.Spec.Auth.ApplicationCredentialSecret != "" {
acSecretObj, _, err := secret.GetSecret(ctx, h, instance.Spec.Auth.ApplicationCredentialSecret, instance.Namespace)
if err != nil {
if k8s_errors.IsNotFound(err) {
h.GetLogger().Info("ApplicationCredential secret not found, waiting", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
return fmt.Errorf("%w: %s", ErrACSecretNotFound, instance.Spec.Auth.ApplicationCredentialSecret)
}
h.GetLogger().Error(err, "Failed to get ApplicationCredential secret", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
return err
}
acID, okID := acSecretObj.Data[keystonev1.ACIDSecretKey]
acSecretData, okSecret := acSecretObj.Data[keystonev1.ACSecretSecretKey]
if !okID || len(acID) == 0 || !okSecret || len(acSecretData) == 0 {
h.GetLogger().Info("ApplicationCredential secret missing required keys", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
return fmt.Errorf("%w: %s", ErrACSecretMissingKeys, instance.Spec.Auth.ApplicationCredentialSecret)
}
templateParameters["UseApplicationCredentials"] = true
templateParameters["ACID"] = string(acID)
templateParameters["ACSecret"] = string(acSecretData)
h.GetLogger().Info("Using ApplicationCredentials auth", "secret", instance.Spec.Auth.ApplicationCredentialSecret)
}

// create httpd vhost template parameters
httpdVhostConfig := map[string]any{}
for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} {
Expand Down
18 changes: 12 additions & 6 deletions templates/placementapi/config/placement.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@ connection = {{ .DatabaseConnection }}
auth_strategy = keystone

[keystone_authtoken]
project_domain_name = Default
user_domain_name = Default
project_name = service
www_authenticate_uri = {{ .KeystonePublicURL }}
auth_url = {{ .KeystoneInternalURL }}
{{ if .UseApplicationCredentials -}}
auth_type = v3applicationcredential
application_credential_id = {{ .ACID }}
application_credential_secret = {{ .ACSecret }}
{{ else -}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

+1, changes are reflecting.

auth_type = password
username = {{ .ServiceUser }}
password = {{ .PlacementPassword }}
user_domain_name = Default
project_domain_name = Default
project_name = service
{{- end }}
{{ if (index . "Region") -}}
region_name = {{ .Region }}
{{ end -}}
www_authenticate_uri = {{ .KeystonePublicURL }}
auth_url = {{ .KeystoneInternalURL }}
auth_type = password
interface = internal

[oslo_policy]
Expand Down
62 changes: 59 additions & 3 deletions test/functional/placementapi_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,11 +374,10 @@ var _ = Describe("PlacementAPI controller", func() {
// Verify region_name is set in [keystone_authtoken] section
// GetRegion() returns Status.Region, so check that
Expect(keystoneAPI.Status.Region).ToNot(BeEmpty(), "KeystoneAPI should have a region set in status")
// The region_name should appear in the [keystone_authtoken] section
// It's conditionally rendered, so check it appears between password and www_authenticate_uri
// The region_name should appear in the [keystone_authtoken] section (before [oslo_policy])
Expect(conf).Should(
MatchRegexp(fmt.Sprintf(
"password = .*\\nregion_name = %s\\n", keystoneAPI.Status.Region)))
`\[keystone_authtoken\][\s\S]*region_name = %s[\s\S]*\[oslo_policy\]`, keystoneAPI.Status.Region)))
})

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

})

When("an ApplicationCredential is created for Placement", func() {
BeforeEach(func() {
DeferCleanup(
k8sClient.Delete, ctx, CreatePlacementAPISecret(names.Namespace, SecretName))
keystoneAPIName := keystone.CreateKeystoneAPI(names.Namespace)
DeferCleanup(keystone.DeleteKeystoneAPI, keystoneAPIName)

acSecretName := fmt.Sprintf("ac-%s-secret", placement.ServiceName)
acSecret := th.CreateSecret(
types.NamespacedName{Namespace: names.Namespace, Name: acSecretName},
map[string][]byte{
keystonev1.ACIDSecretKey: []byte("test-ac-id"),
keystonev1.ACSecretSecretKey: []byte("test-ac-secret"),
},
)
DeferCleanup(th.DeleteInstance, acSecret)

spec := GetDefaultPlacementAPISpec()
spec["auth"] = map[string]any{
"applicationCredentialSecret": acSecretName,
}

DeferCleanup(th.DeleteInstance, CreatePlacementAPI(names.PlacementAPIName, spec))

DeferCleanup(
mariadb.DeleteDBService,
mariadb.CreateDBService(
names.Namespace,
GetDefaultPlacementAPISpec()["databaseInstance"].(string),
corev1.ServiceSpec{
Ports: []corev1.ServicePort{{Port: 3306}},
},
),
)
mariadb.SimulateMariaDBDatabaseCompleted(names.MariaDBDatabaseName)
mariadb.SimulateMariaDBAccountCompleted(names.MariaDBAccount)
})

It("should render ApplicationCredential auth in placement.conf", func() {
configSecret := th.GetSecret(names.ConfigMapName)
conf := configSecret.Data["placement.conf"]

// AC auth is configured
Expect(conf).To(ContainSubstring("auth_type = v3applicationcredential"))
Expect(conf).To(ContainSubstring("application_credential_id = test-ac-id"))
Expect(conf).To(ContainSubstring("application_credential_secret = test-ac-secret"))
Comment thread
mrkisaolamb marked this conversation as resolved.

// Password auth fields should not be present
Expect(conf).NotTo(ContainSubstring("auth_type = password"))
Expect(conf).NotTo(ContainSubstring("username ="))
Expect(conf).NotTo(ContainSubstring("password ="))
Expect(conf).NotTo(ContainSubstring("project_name ="))
Expect(conf).NotTo(ContainSubstring("user_domain_name ="))
Expect(conf).NotTo(ContainSubstring("project_domain_name ="))
})
})

})