Skip to content
Open
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
5 changes: 5 additions & 0 deletions apis/capabilities/v1beta1/application_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ type ApplicationSpec struct {
// Suspend application if true suspends application, if false resumes application.
//+optional
Suspend bool `json:"suspend,omitempty"`

// AuthSecretRef reference to the API credentials secret. This secret is
// used only once when creating a new application
//+optional
AuthSecretRef *corev1.LocalObjectReference `json:"authSecretRef"`
}

// ApplicationStatus defines the observed state of Application
Expand Down
5 changes: 5 additions & 0 deletions apis/capabilities/v1beta1/zz_generated.deepcopy.go

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

13 changes: 13 additions & 0 deletions bundle/manifests/capabilities.3scale.net_applications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ spec:
applicationPlanName:
description: ApplicationPlanName name of application plan that the application will use
type: string
authSecretRef:
description: |-
AuthSecretRef reference to the API credentials secret. This secret is
used only once when creating a new application
properties:
name:
description: |-
Name of the referent.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?
type: string
type: object
x-kubernetes-map-type: atomic
description:
description: Description human-readable text of the application
type: string
Expand Down
13 changes: 13 additions & 0 deletions config/crd/bases/capabilities.3scale.net_applications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ spec:
description: ApplicationPlanName name of application plan that the
application will use
type: string
authSecretRef:
description: |-
AuthSecretRef reference to the API credentials secret. This secret is
used only once when creating a new application
properties:
name:
description: |-
Name of the referent.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
TODO: Add other useful fields. apiVersion, kind, uid?
type: string
type: object
x-kubernetes-map-type: atomic
description:
description: Description human-readable text of the application
type: string
Expand Down
24 changes: 23 additions & 1 deletion controllers/capabilities/application_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,29 @@ func (r *ApplicationReconciler) applicationReconciler(applicationResource *capab
return nil, err
}

reconciler := NewApplicationReconciler(r.BaseReconciler, applicationResource, *accountResource.Status.ID, *productResource.Status.ID, threescaleAPIClient)
var authParams map[string]string
if applicationResource.Spec.AuthSecretRef != nil {
authSecretObj, err := helper.GetSecret(applicationResource.Spec.AuthSecretRef.Name, applicationResource.Namespace, r.Client())
if err != nil {
return nil, err
}

authMode, err := extractApplicationCredentialType(productResource)
if err != nil {
return nil, err
}

if err := validateApplicationCrendentialSecret(authSecretObj, authMode); err != nil {
return nil, err
}

authParams, err = handleCredentials(authSecretObj, authMode)
if err != nil {
return nil, err
}
}

reconciler := NewApplicationReconciler(r.BaseReconciler, applicationResource, authParams, *accountResource.Status.ID, *productResource.Status.ID, threescaleAPIClient)
return reconciler.Reconcile()
}

Expand Down
115 changes: 115 additions & 0 deletions controllers/capabilities/application_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package controllers

import (
"fmt"

capabilitiesv1beta1 "github.com/3scale/3scale-operator/apis/capabilities/v1beta1"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
ThreescaleCredentialTypeUserKey = "1"
ThreescaleCredentialTypeAppID = "2"
ThreescaleCredentialTypeOIDC = "oidc"
)

const (
ThreescaleCredentialParamUserKey = "user_key"
ThreescaleCredentialParamAppID = "application_id"
ThreescaleCredentialParamAppKey = "application_key"
)

const (
CredentialSecretKeyNameUserKey = "UserKey"
CredentialSecretKeyNameAppID = "ApplicationID"
CredentialSecretKeyNameAppKey = "ApplicationKey"
CredentialSecretKeyNameClientID = "ClientID"
CredentialSecretKeyNameClientSecret = "ClientSecret"
)

// extractApplicationCredentialType returns the credential type from the product
// setting
func extractApplicationCredentialType(productResource *capabilitiesv1beta1.Product) (string, error) {
credType := productResource.Spec.AuthenticationMode()
if credType == nil {
return "", fmt.Errorf("unable to identify authentication mode from Product CR")
}
return *credType, nil
}

func validateApplicationCrendentialSecret(s *corev1.Secret, authMode string) error {
nn := client.ObjectKeyFromObject(s)

switch authMode {
case ThreescaleCredentialTypeUserKey:
if err := validateSecretForAuthModeUserKey(s); err != nil {
return err
}
case ThreescaleCredentialTypeAppID:
if err := validateSecretForAuthModeAppIDAppKey(s); err != nil {
return err
}
case ThreescaleCredentialTypeOIDC:
if err := validateSecretForAuthModeOIDC(s); err != nil {
return err
}
default:
return fmt.Errorf("secret %s used, but has unsupported type %s", nn, authMode)
}
return nil
}

func validateSecretForAuthModeUserKey(s *corev1.Secret) error {
if _, ok := s.Data[CredentialSecretKeyNameUserKey]; !ok {
return fmt.Errorf("secret %s used as user-key authentication mode, but lacks %s key",
client.ObjectKeyFromObject(s), CredentialSecretKeyNameUserKey,
)
}
return nil
}

func validateSecretForAuthModeAppIDAppKey(s *corev1.Secret) error {
if _, ok := s.Data[CredentialSecretKeyNameAppID]; !ok {
return fmt.Errorf("secret %s used as app-id/app-key authentication mode, but lacks %s key",
client.ObjectKeyFromObject(s), CredentialSecretKeyNameAppID,
)
}
if _, ok := s.Data[CredentialSecretKeyNameAppKey]; !ok {
return fmt.Errorf("secret %s used as app-id/app-key authentication mode, but lacks %s key",
client.ObjectKeyFromObject(s), CredentialSecretKeyNameAppKey,
)
}
return nil
}

func validateSecretForAuthModeOIDC(s *corev1.Secret) error {
if _, ok := s.Data[CredentialSecretKeyNameClientID]; !ok {
return fmt.Errorf("secret %s used as oidc authentication mode, but lacks %s key",
client.ObjectKeyFromObject(s), CredentialSecretKeyNameClientID,
)
}
if _, ok := s.Data[CredentialSecretKeyNameClientSecret]; !ok {
return fmt.Errorf("secret %s used as oidc authentication mode, but lacks %s key",
client.ObjectKeyFromObject(s), CredentialSecretKeyNameClientSecret,
)
}
return nil
}

func handleCredentials(creds *corev1.Secret, authType string) (map[string]string, error) {
authParams := make(map[string]string)
switch authType {
case ThreescaleCredentialTypeUserKey:
authParams[ThreescaleCredentialParamUserKey] = string(creds.Data[CredentialSecretKeyNameUserKey])
case ThreescaleCredentialTypeAppID:
authParams[ThreescaleCredentialParamAppID] = string(creds.Data[CredentialSecretKeyNameAppID])
authParams[ThreescaleCredentialParamAppKey] = string(creds.Data[CredentialSecretKeyNameAppKey])
case ThreescaleCredentialTypeOIDC:
authParams[ThreescaleCredentialParamAppID] = string(creds.Data[CredentialSecretKeyNameClientID])
authParams[ThreescaleCredentialParamAppKey] = string(creds.Data[CredentialSecretKeyNameClientSecret])
default:
return nil, fmt.Errorf("unknown authentication mode")
}
return authParams, nil
}
16 changes: 15 additions & 1 deletion controllers/capabilities/application_threescale_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ type ApplicationThreescaleReconciler struct {
*reconcilers.BaseReconciler
applicationResource *capabilitiesv1beta1.Application
applicationEntity *controllerhelper.ApplicationEntity
authParams map[string]string
accountID int64
productID int64
threescaleAPIClient *threescaleapi.ThreeScaleClient
logger logr.Logger
}

func NewApplicationReconciler(b *reconcilers.BaseReconciler, applicationResource *capabilitiesv1beta1.Application, accountID int64, productID int64, threescaleAPIClient *threescaleapi.ThreeScaleClient) *ApplicationThreescaleReconciler {
func NewApplicationReconciler(b *reconcilers.BaseReconciler, applicationResource *capabilitiesv1beta1.Application, authParams map[string]string, accountID int64, productID int64, threescaleAPIClient *threescaleapi.ThreeScaleClient) *ApplicationThreescaleReconciler {
return &ApplicationThreescaleReconciler{
BaseReconciler: b,
applicationResource: applicationResource,
authParams: authParams,
accountID: accountID,
productID: productID,
threescaleAPIClient: threescaleAPIClient,
Expand Down Expand Up @@ -113,6 +115,12 @@ func (t *ApplicationThreescaleReconciler) syncApplication(_ any) error {
"name": t.applicationResource.Spec.Name,
"description": t.applicationResource.Spec.Description,
}

if t.authParams != nil {
for key, value := range t.authParams {
params.AddParam(key, value)
}
}
// Application doesn't exist yet - create it
a, err := t.threescaleAPIClient.CreateApplication(t.accountID, plan.Element.ID, t.applicationResource.Spec.Name, params)
if err != nil {
Expand All @@ -133,6 +141,12 @@ func (t *ApplicationThreescaleReconciler) syncApplication(_ any) error {
"description": t.applicationResource.Spec.Description,
}

if t.authParams != nil {
for key, value := range t.authParams {
params.AddParam(key, value)
}
}

// Application doesn't exist yet - create it
a, err := t.threescaleAPIClient.CreateApplication(t.accountID, plan.Element.ID, t.applicationResource.Spec.Name, params)
if err != nil {
Expand Down
51 changes: 51 additions & 0 deletions doc/application-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Created by [github-markdown-toc](https://github.com/ekalinin/github-markdown-toc
| ProductCR | `productCR` | object | name of product CR via [v1.LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#localobjectreference-v1-core) | Yes |
| ApplicationPlanName | `applicationPlanName` | string | name of application plan that the application will use | Yes |
| Suspend | `suspend` | bool | suspend application if true suspends application, if false resumes application | No |
| AuthSecretRef | `authSecretRef` | object | [Auth secret reference](#Auth-secret-reference) | No |



Expand All @@ -36,6 +37,56 @@ Created by [github-markdown-toc](https://github.com/ekalinin/github-markdown-toc
Application CR relies on the provider account reference for the [developer account](./developeruser-reference.md#provider-account-reference)
and the [product](./product-reference.md#provider-account-reference) being the same. If not you will see an error in the status.

#### Auth secret reference

Auth secret reference by a [v1.LocalObjectReference](https://v1-15.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#localobjectreference-v1-core) type object.

| **Field** | **Description** | **Required** |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ | ------------ |
| *UserKey* | UserKey field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes |
| *ApplicationID* | ApplicationID field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes |
| *ApplicationKey* | ApplicationKey field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes |
| *ClientID* | ClientID field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes |
| *ClientSecret* | ClientSecret field can be populated with a secret key or left an empty string if you wish to generate the secret | Yes |


NOTE: ApplicationCR relies on ProductCR authentication mode to determine which fields to use from the secret

For example:
* With UserKey authentication mode
```
apiVersion: v1
kind: Secret
metadata:
name: authsecret
type: Opaque
stringData:
UserKey: "testApplicationUserKey"
```

* With AppID/AppKey authentication mode
```
apiVersion: v1
kind: Secret
metadata:
name: authsecret
type: Opaque
stringData:
ApplicationID: "testApplicationID"
ApplicationKey: "testApplicationKey"
```

* With OIDC authentication mode
```
apiVersion: v1
kind: Secret
metadata:
name: authsecret
type: Opaque
stringData:
ClientID: "testApplicationClientID"
ClientSecret: "testApplicationClientSecret"
```

### ApplicationStatus

Expand Down
40 changes: 40 additions & 0 deletions doc/operator-application-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,11 @@ spec:
systemName: hits
backend: backend1
name: product1
deployment:
apicastHosted:
authentication:
appKeyAppID
appID: token
backendUsages:
backend1:
path: /
Expand Down Expand Up @@ -1734,6 +1739,41 @@ status:
providerAccountHost: 'https://3scale-admin.example.com'
state: suspended
```

By default, 3scale automatically generates a random and immutable ID for each application upon creation.

If you require an application to have a predefined ID, you must supply an authentication secret.

```
apiVersion: v1
kind: Secret
metadata:
name: authsecret
type: Opaque
stringData:
ApplicationID: "testApplicationID"
ApplicationKey: "testApplicationKey"
```

Reference the secret with `authSecretRef`

```yaml
apiVersion: capabilities.3scale.net/v1beta1
kind: Application
metadata:
name: example
spec:
accountCR:
name: developeraccount01
applicationPlanName: plan02
productCR:
name: product1-cr
name: testApp12
description: further testing12
suspend: true
authSecretRef: authsecret
```

[Application CRD reference](application-reference.md) for more info about fields.

### Application Custom Resource Status Fields
Expand Down