Skip to content

Commit

Permalink
operator: Add defaulting webhook for Enterprise fields
Browse files Browse the repository at this point in the history
- Add new mutating webhook that defaults fields for Console
- Refactor required keys used for validating/mutating to vars
  • Loading branch information
pvsune committed Sep 8, 2022
1 parent c6af097 commit 2b9ffa1
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 9 deletions.
6 changes: 6 additions & 0 deletions src/go/k8s/apis/redpanda/v1alpha1/console_enterprise_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ type EnterpriseLogin struct {
Google *EnterpriseLoginGoogle `json:"google,omitempty"`
}

// IsGoogleLoginEnabled returns true if Google SSO provider is enabled
func (c *Console) IsGoogleLoginEnabled() bool {
login := c.Spec.Login
return login != nil && login.Google != nil && login.Google.Enabled
}

// EnterpriseLoginGoogle defines configurable fields for Google provider
type EnterpriseLoginGoogle struct {
Enabled bool `json:"enabled"`
Expand Down
20 changes: 20 additions & 0 deletions src/go/k8s/config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ webhooks:
resources:
- clusters
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /mutate-redpanda-vectorized-io-v1alpha1-console
failurePolicy: Fail
name: mconsole.kb.io
rules:
- apiGroups:
- redpanda.vectorized.io
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- consoles
sideEffects: None

---
apiVersion: admissionregistration.k8s.io/v1
Expand Down
2 changes: 1 addition & 1 deletion src/go/k8s/controllers/redpanda/console_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ var _ = Describe("Console controller", func() {
It("Should create Enterprise fields in ConfigMap", func() {
var (
rbacName = fmt.Sprintf("%s-rbac", ConsoleName)
rbacDataKey = "rbac.yaml"
rbacDataKey = consolepkg.EnterpriseRBACDataKey
rbacDataVal = `roleBindings:
- roleName: admin
metadata:
Expand Down
1 change: 1 addition & 0 deletions src/go/k8s/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func main() {
os.Exit(1)
}
hookServer := mgr.GetWebhookServer()
hookServer.Register("/mutate-redpanda-vectorized-io-v1alpha1-console", &webhook.Admission{Handler: &redpandawebhooks.ConsoleDefaulter{Client: mgr.GetClient()}})
hookServer.Register("/validate-redpanda-vectorized-io-v1alpha1-console", &webhook.Admission{Handler: &redpandawebhooks.ConsoleValidator{Client: mgr.GetClient()}})
}

Expand Down
33 changes: 25 additions & 8 deletions src/go/k8s/pkg/console/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,33 @@ func (cm *ConfigMap) genEnterprise() (e Enterprise) {
return Enterprise{
RBAC: EnterpriseRBAC{
Enabled: cm.consoleobj.Spec.Enterprise.RBAC.Enabled,
RoleBindingsFilepath: fmt.Sprintf("%s/%s", enterpriseRBACMountPath, "rbac.yaml"),
RoleBindingsFilepath: fmt.Sprintf("%s/%s", enterpriseRBACMountPath, EnterpriseRBACDataKey),
},
}
}
return e
}

var (
defaultLicenseSecretKey = "license"
defaultJWTSecretKey = "jwt"
// DefaultLicenseSecretKey is the default key required in secret referenced by `SecretKeyRef`.
// The license will be provided to console to allow enterprise features.
DefaultLicenseSecretKey = "license"

// DefaultJWTSecretKey is the default key required in secret referenced by `SecretKeyRef`.
// The secret should consist of JWT used to authenticate into google SSO.
DefaultJWTSecretKey = "jwt"

// EnterpriseRBACDataKey is the required key in Enterprise RBAC
EnterpriseRBACDataKey = "rbac.yaml"

// EnterpriseGoogleSADataKey is the required key in EnterpriseLoginGoogle SA
EnterpriseGoogleSADataKey = "sa.json"

// EnterpriseGoogleClientIDSecretKey is the required key in EnterpriseLoginGoogle Client ID
EnterpriseGoogleClientIDSecretKey = "clientId"

// EnterpriseGoogleClientSecretKey is the required key in EnterpriseLoginGoogle Client secret
EnterpriseGoogleClientSecretKey = "clientSecret"
)

func (cm *ConfigMap) genLogin(ctx context.Context) (e EnterpriseLogin, err error) {
Expand All @@ -180,7 +197,7 @@ func (cm *ConfigMap) genLogin(ctx context.Context) (e EnterpriseLogin, err error
if err != nil {
return e, err
}
jwt, err := provider.JWTSecretRef.GetValue(jwtSecret, defaultJWTSecretKey)
jwt, err := provider.JWTSecretRef.GetValue(jwtSecret, DefaultJWTSecretKey)
if err != nil {
return e, err
}
Expand All @@ -193,11 +210,11 @@ func (cm *ConfigMap) genLogin(ctx context.Context) (e EnterpriseLogin, err error
if err != nil {
return e, err
}
clientID, err := cc.GetValue(ccSecret, "clientId")
clientID, err := cc.GetValue(ccSecret, EnterpriseGoogleClientIDSecretKey)
if err != nil {
return e, err
}
clientSecret, err := cc.GetValue(ccSecret, "clientSecret")
clientSecret, err := cc.GetValue(ccSecret, EnterpriseGoogleClientSecretKey)
if err != nil {
return e, err
}
Expand All @@ -209,7 +226,7 @@ func (cm *ConfigMap) genLogin(ctx context.Context) (e EnterpriseLogin, err error
}
if dir := provider.Google.Directory; dir != nil {
enterpriseLogin.Google.Directory = &EnterpriseLoginGoogleDirectory{
ServiceAccountFilepath: fmt.Sprintf("%s/%s", enterpriseGoogleSAMountPath, "sa.json"),
ServiceAccountFilepath: fmt.Sprintf("%s/%s", enterpriseGoogleSAMountPath, EnterpriseGoogleSADataKey),
TargetPrincipal: provider.Google.Directory.TargetPrincipal,
}
}
Expand All @@ -225,7 +242,7 @@ func (cm *ConfigMap) genLicense(ctx context.Context) (string, error) {
if err != nil {
return "", err
}
licenseValue, err := license.GetValue(licenseSecret, defaultLicenseSecretKey)
licenseValue, err := license.GetValue(licenseSecret, DefaultLicenseSecretKey)
if err != nil {
return "", err
}
Expand Down
80 changes: 80 additions & 0 deletions src/go/k8s/webhooks/redpanda/console_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package redpanda

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"

redpandav1alpha1 "github.com/redpanda-data/redpanda/src/go/k8s/apis/redpanda/v1alpha1"
consolepkg "github.com/redpanda-data/redpanda/src/go/k8s/pkg/console"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
Expand Down Expand Up @@ -43,6 +46,26 @@ func (v *ConsoleValidator) Handle(
return admission.Errored(http.StatusBadRequest, err)
}

if err := ValidateEnterpriseRBAC(ctx, v.Client, console); err != nil {
if errors.Is(err, &ErrKeyNotFound{}) {
return admission.Denied(err.Error())
}
return admission.Errored(http.StatusBadRequest, err)
}

if err := ValidateEnterpriseGoogleClientCredentials(ctx, v.Client, console); err != nil {
if errors.Is(err, &ErrKeyNotFound{}) {
return admission.Denied(err.Error())
}
return admission.Errored(http.StatusBadRequest, err)
}
if err := ValidateEnterpriseGoogleSA(ctx, v.Client, console); err != nil {
if errors.Is(err, &ErrKeyNotFound{}) {
return admission.Denied(err.Error())
}
return admission.Errored(http.StatusBadRequest, err)
}

return admission.Allowed("")
}

Expand All @@ -54,3 +77,60 @@ func (v *ConsoleValidator) InjectDecoder(d *admission.Decoder) error {
v.decoder = d
return nil
}

// +kubebuilder:webhook:path=/mutate-redpanda-vectorized-io-v1alpha1-console,mutating=true,failurePolicy=fail,sideEffects=None,groups="redpanda.vectorized.io",resources=consoles,verbs=create;update,versions=v1alpha1,name=mconsole.kb.io,admissionReviewVersions=v1

// ConsoleDefaulter mutates Consoles
type ConsoleDefaulter struct {
Client client.Client
decoder *admission.Decoder
}

// Handle processes admission for Console
func (m *ConsoleDefaulter) Handle(
ctx context.Context, req admission.Request, // nolint:gocritic // interface not require pointer
) admission.Response {
console := &redpandav1alpha1.Console{}

err := m.decoder.Decode(req, console)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

response, err := m.Default(console)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return *response
}

// Default implements admission defaulting
func (m *ConsoleDefaulter) Default(console *redpandav1alpha1.Console) (*admission.Response, error) {
original, err := json.Marshal(console.DeepCopy())
if err != nil {
return nil, err
}

if login := console.Spec.Login; login != nil && login.JWTSecretRef.Key == "" {
login.JWTSecretRef.Key = consolepkg.DefaultJWTSecretKey
}
if license := console.Spec.LicenseRef; license != nil && license.Key == "" {
license.Key = consolepkg.DefaultLicenseSecretKey
}

current, err := json.Marshal(console)
if err != nil {
return nil, err
}
response := admission.PatchResponseFromRaw(original, current)
return &response, nil
}

// ConsoleDefaulter implements admission.DecoderInjector.
// A decoder will be automatically injected.

// InjectDecoder injects the decoder.
func (m *ConsoleDefaulter) InjectDecoder(d *admission.Decoder) error {
m.decoder = d
return nil
}
69 changes: 69 additions & 0 deletions src/go/k8s/webhooks/redpanda/validate_enterprise.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Package redpanda defines Webhooks for redpanda API group
package redpanda

import (
"context"
"fmt"

redpandav1alpha1 "github.com/redpanda-data/redpanda/src/go/k8s/apis/redpanda/v1alpha1"
consolepkg "github.com/redpanda-data/redpanda/src/go/k8s/pkg/console"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// ErrKeyNotFound is error when getting required key in ConfigMap/Secret
type ErrKeyNotFound struct {
Message string
}

// Error implements error
func (e *ErrKeyNotFound) Error() string {
return e.Message
}

// ValidateEnterpriseRBAC validates the referenced RBAC ConfigMap
func ValidateEnterpriseRBAC(ctx context.Context, cl client.Client, console *redpandav1alpha1.Console) error {
if enterprise := console.Spec.Enterprise; enterprise != nil {
configmap := &corev1.ConfigMap{}
if err := cl.Get(ctx, client.ObjectKey{Namespace: console.GetNamespace(), Name: enterprise.RBAC.RoleBindingsRef.Name}, configmap); err != nil {
return err
}
if _, ok := configmap.Data[consolepkg.EnterpriseRBACDataKey]; !ok {
return &ErrKeyNotFound{fmt.Sprintf("must contain '%s' key", consolepkg.EnterpriseRBACDataKey)}
}
}
return nil
}

// ValidateEnterpriseGoogleSA validates the referenced Google SA ConfigMap
func ValidateEnterpriseGoogleSA(ctx context.Context, cl client.Client, console *redpandav1alpha1.Console) error {
if login := console.Spec.Login; login != nil && login.Google != nil && login.Google.Directory != nil {
configmap := &corev1.ConfigMap{}
if err := cl.Get(ctx, client.ObjectKey{Namespace: console.GetNamespace(), Name: login.Google.Directory.ServiceAccountRef.Name}, configmap); err != nil {
return err
}
if _, ok := configmap.Data[consolepkg.EnterpriseGoogleSADataKey]; !ok {
return &ErrKeyNotFound{fmt.Sprintf("must contain '%s' key", consolepkg.EnterpriseGoogleSADataKey)}
}
}
return nil
}

// ValidateEnterpriseGoogleClientCredentials validates the referenced Google Client Credentials ConfigMap
func ValidateEnterpriseGoogleClientCredentials(ctx context.Context, cl client.Client, console *redpandav1alpha1.Console) error {
if console.IsGoogleLoginEnabled() {
cc := console.Spec.Login.Google.ClientCredentialsRef
key := redpandav1alpha1.SecretKeyRef{Namespace: cc.Namespace, Name: cc.Name}
ccSecret, err := key.GetSecret(ctx, cl)
if err != nil {
return err
}
if _, err := key.GetValue(ccSecret, consolepkg.EnterpriseGoogleClientIDSecretKey); err != nil {
return &ErrKeyNotFound{fmt.Sprintf("must contain '%s' key", consolepkg.EnterpriseGoogleClientIDSecretKey)}
}
if _, err := key.GetValue(ccSecret, consolepkg.EnterpriseGoogleClientSecretKey); err != nil {
return &ErrKeyNotFound{fmt.Sprintf("must contain '%s' key", consolepkg.EnterpriseGoogleClientSecretKey)}
}
}
return nil
}

0 comments on commit 2b9ffa1

Please sign in to comment.