Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(oauth): make OAuth2 Proxy config map mutable #1038

Merged
merged 1 commit into from
Feb 3, 2025
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
79 changes: 48 additions & 31 deletions internal/controllers/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"path"
"text/template"
Expand All @@ -26,7 +27,7 @@ import (
"github.com/cryostatio/cryostat-operator/internal/controllers/constants"
"github.com/cryostatio/cryostat-operator/internal/controllers/model"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
Expand All @@ -38,9 +39,7 @@ func (r *Reconciler) reconcileLockConfigMap(ctx context.Context, cr *model.Cryos
Namespace: cr.InstallNamespace,
},
}
return r.createOrUpdateConfigMap(ctx, cm, cr.Object, func() error {
return nil
})
return r.createOrUpdateConfigMap(ctx, cm, cr.Object, nil)
}

type oauth2ProxyAlphaConfig struct {
Expand All @@ -50,9 +49,9 @@ type oauth2ProxyAlphaConfig struct {
}

type alphaConfigServer struct {
BindAddress string `json:"BindAddress,omitempty"`
SecureBindAddress string `json:"SecureBindAddress,omitempty"`
TLS proxyTLS `json:"TLS,omitempty"`
BindAddress string `json:"BindAddress,omitempty"`
SecureBindAddress string `json:"SecureBindAddress,omitempty"`
TLS *proxyTLS `json:"TLS,omitempty"`
}

type proxyTLS struct {
Expand Down Expand Up @@ -82,13 +81,12 @@ type alphaConfigUpstream struct {
Path string `json:"path,omitempty"`
RewriteTarget string `json:"rewriteTarget,omitempty"`
Uri string `json:"uri,omitempty"`
PassHostHeader bool `json:"passHostHeader,omitempty"`
ProxyWebSockets bool `json:"proxyWebSockets,omitempty"`
PassHostHeader *bool `json:"passHostHeader,omitempty"`
ProxyWebSockets *bool `json:"proxyWebSockets,omitempty"`
}

func (r *Reconciler) reconcileOAuth2ProxyConfig(ctx context.Context, cr *model.CryostatInstance, tls *resources.TLSConfig) error {
bindHost := "0.0.0.0"
immutable := true
cfg := &oauth2ProxyAlphaConfig{
Server: alphaConfigServer{},
UpstreamConfig: alphaConfigUpstreamConfig{ProxyRawPath: true, Upstreams: []alphaConfigUpstream{
Expand All @@ -107,16 +105,16 @@ func (r *Reconciler) reconcileOAuth2ProxyConfig(ctx context.Context, cr *model.C
Path: "^/storage/(.*)$",
RewriteTarget: "/$1",
Uri: fmt.Sprintf("http://localhost:%d", constants.StoragePort),
PassHostHeader: false,
ProxyWebSockets: false,
PassHostHeader: &[]bool{false}[0],
ProxyWebSockets: &[]bool{false}[0],
},
}},
Providers: []alphaConfigProvider{{Id: "dummy", Name: "Unused - Sign In Below", ClientId: "CLIENT_ID", ClientSecret: "CLIENT_SECRET", Provider: "google"}},
}

if tls != nil {
cfg.Server.SecureBindAddress = fmt.Sprintf("https://%s:%d", bindHost, constants.AuthProxyHttpContainerPort)
cfg.Server.TLS = proxyTLS{
cfg.Server.TLS = &proxyTLS{
Key: tlsSecretSource{
FromFile: path.Join(resources.SecretMountPrefix, tls.CryostatSecret, corev1.TLSPrivateKeyKey),
},
Expand All @@ -128,27 +126,25 @@ func (r *Reconciler) reconcileOAuth2ProxyConfig(ctx context.Context, cr *model.C
cfg.Server.BindAddress = fmt.Sprintf("http://%s:%d", bindHost, constants.AuthProxyHttpContainerPort)
}

data := make(map[string]string)
json, err := json.Marshal(cfg)
if err != nil {
return err
}
data[resources.OAuth2ConfigFileName] = string(json)
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name + "-oauth2-proxy-cfg",
Namespace: cr.InstallNamespace,
},
Immutable: &immutable,
Data: data,
}

if r.IsOpenShift {
return r.deleteConfigMap(ctx, cm)
} else {
return r.createOrUpdateConfigMap(ctx, cm, cr.Object, func() error {
return nil
})
json, err := json.Marshal(cfg)
if err != nil {
return err
}
data := map[string]string{
resources.OAuth2ConfigFileName: string(json),
}

return r.createOrUpdateConfigMap(ctx, cm, cr.Object, data)
}
}

Expand Down Expand Up @@ -331,31 +327,52 @@ func (r *Reconciler) reconcileAgentProxyConfig(ctx context.Context, cr *model.Cr
// Add generated nginx.conf to config map
data[constants.AgentProxyConfigFileName] = buf.String()

return r.createOrUpdateConfigMap(ctx, cm, cr.Object, func() error {
cm.Data = data
return nil
})
return r.createOrUpdateConfigMap(ctx, cm, cr.Object, data)
}

var errConfigMapImmutableModified error = errors.New("config map is immutable and should not be")

func (r *Reconciler) createOrUpdateConfigMap(ctx context.Context, cm *corev1.ConfigMap, owner metav1.Object,
delegate controllerutil.MutateFn) error {
data map[string]string) error {
cmCopy := cm.DeepCopy()
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, cm, func() error {
// Check if we need to revert Immutable property
if isImmutable(cm) && !isImmutable(cmCopy) {
return errConfigMapImmutableModified
}
// Set the Cryostat CR as controller
if err := controllerutil.SetControllerReference(owner, cm, r.Scheme); err != nil {
return err
}
return delegate()
cm.Data = data
return nil
})
if err != nil {
if err == errConfigMapImmutableModified {
return r.recreateConfigMap(ctx, cmCopy, owner, data)
}
return err
}
r.Log.Info(fmt.Sprintf("Config Map %s", op), "name", cm.Name, "namespace", cm.Namespace)
return nil
}

func (r *Reconciler) recreateConfigMap(ctx context.Context, cm *corev1.ConfigMap, owner metav1.Object,
data map[string]string) error {
err := r.deleteConfigMap(ctx, cm)
if err != nil {
return err
}
return r.createOrUpdateConfigMap(ctx, cm, owner, data)
}

func isImmutable(cm *corev1.ConfigMap) bool {
return cm.Immutable != nil && *cm.Immutable
}

func (r *Reconciler) deleteConfigMap(ctx context.Context, cm *corev1.ConfigMap) error {
err := r.Client.Delete(ctx, cm)
if err != nil && !errors.IsNotFound(err) {
if err != nil && !kerrors.IsNotFound(err) {
r.Log.Error(err, "Could not delete ConfigMap", "name", cm.Name, "namespace", cm.Namespace)
return err
}
Expand Down
44 changes: 44 additions & 0 deletions internal/controllers/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2350,6 +2350,36 @@ func (c *controllerTest) commonTests() {
Expect(kerrors.IsNotFound(err)).To(BeTrue())
})
})
Context("with OAuth2 proxy", func() {
JustBeforeEach(func() {
t.reconcileCryostatFully()
})
Context("with TLS enabled", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewCryostatWithIngress().Object)
})
It("should create OAuth2 config map", func() {
t.expectOAuth2ConfigMap()
})
})
Context("with TLS disabled", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewCryostatWithIngressCertManagerDisabled().Object)
t.TLS = false
})
It("should create OAuth2 config map", func() {
t.expectOAuth2ConfigMap()
})
})
Context("with existing config map", func() {
BeforeEach(func() {
t.objs = append(t.objs, t.NewCryostatWithIngress().Object, t.NewOAuth2ProxyConfigMapOld())
})
It("should create OAuth2 config map", func() {
t.expectOAuth2ConfigMap()
})
})
})
Context("with report generator service", func() {
BeforeEach(func() {
t.ReportReplicas = 1
Expand Down Expand Up @@ -3045,6 +3075,20 @@ func (t *cryostatTestInput) expectAgentProxyConfigMap() {

t.checkMetadata(cm, expected)
Expect(cm.Data).To(Equal(expected.Data))
Expect(cm.Immutable).To(Equal(expected.Immutable))
}

func (t *cryostatTestInput) expectOAuth2ConfigMap() {
expected := t.NewOAuth2ProxyConfigMap()
cm := &corev1.ConfigMap{}
err := t.Client.Get(context.Background(), types.NamespacedName{Name: expected.Name, Namespace: expected.Namespace}, cm)
Expect(err).ToNot(HaveOccurred())

t.checkMetadata(cm, expected)
Expect(cm.Data).To(HaveLen(1))
Expect(cm.Data).To(HaveKey("alpha_config.json"))
Expect(cm.Data["alpha_config.json"]).To(MatchJSON(expected.Data["alpha_config.json"]))
Expect(cm.Immutable).To(Equal(expected.Immutable))
}

func (t *cryostatTestInput) expectPVC(expectedPVC *corev1.PersistentVolumeClaim) {
Expand Down
106 changes: 106 additions & 0 deletions internal/test/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -4642,6 +4642,112 @@ ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==
}
}

var alphaConfigTLS = `{
"server": {
"SecureBindAddress": "https://0.0.0.0:4180",
"TLS": {
"Key": {
"fromFile": "/var/run/secrets/operator.cryostat.io/%s-tls/tls.key"
},
"Cert": {
"fromFile": "/var/run/secrets/operator.cryostat.io/%s-tls/tls.crt"
}
}
},
"upstreamConfig": {
"proxyRawPath": true,
"upstreams": [
{
"id": "cryostat",
"path": "/",
"uri": "http://localhost:8181"
},
{
"id": "grafana",
"path": "/grafana/",
"uri": "http://localhost:3000"
},
{
"id": "storage",
"path": "^/storage/(.*)$",
"rewriteTarget": "/$1",
"uri": "http://localhost:8333",
"passHostHeader": false,
"proxyWebSockets": false
}
]
},
"providers": [
{
"id": "dummy",
"name": "Unused - Sign In Below",
"clientId": "CLIENT_ID",
"clientSecret": "CLIENT_SECRET",
"provider": "google"
}
]
}`

var alphaConfigNoTLS = `{
"server": {
"BindAddress": "http://0.0.0.0:4180"
},
"upstreamConfig": {
"proxyRawPath": true,
"upstreams": [
{
"id": "cryostat",
"path": "/",
"uri": "http://localhost:8181"
},
{
"id": "grafana",
"path": "/grafana/",
"uri": "http://localhost:3000"
},
{
"id": "storage",
"path": "^/storage/(.*)$",
"rewriteTarget": "/$1",
"uri": "http://localhost:8333",
"passHostHeader": false,
"proxyWebSockets": false
}
]
},
"providers": [
{
"id": "dummy",
"name": "Unused - Sign In Below",
"clientId": "CLIENT_ID",
"clientSecret": "CLIENT_SECRET",
"provider": "google"
}
]
}`

func (r *TestResources) NewOAuth2ProxyConfigMap() *corev1.ConfigMap {
alphaConfig := fmt.Sprintf(alphaConfigTLS, r.Name, r.Name)
if !r.TLS {
alphaConfig = alphaConfigNoTLS
}
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: r.Name + "-oauth2-proxy-cfg",
Namespace: r.Namespace,
},
Data: map[string]string{
"alpha_config.json": alphaConfig,
},
}
}

func (r *TestResources) NewOAuth2ProxyConfigMapOld() *corev1.ConfigMap {
cm := r.NewOAuth2ProxyConfigMap()
cm.Immutable = &[]bool{true}[0]
return cm
}

func (r *TestResources) getClusterUniqueName() string {
return "cryostat-" + r.clusterUniqueSuffix("")
}
Expand Down