Skip to content

Commit e243f95

Browse files
committed
operator: implement Console controller
This commit adds a standalone `Console` CR and its controller. Unlike the console stanza in the redpanda chart, this CR deploys console V3. This commit does NOT include a migration from the subchart to the new CR. That will be implemented later.
1 parent 5619ffb commit e243f95

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+16958
-158
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
project: operator
2+
kind: Added
3+
body: Added a new `Console` CRD for managing a [Redpanda Console](https://github.com/redpanda-data/console/) deployments. For examples, see [`acceptance/features/console.feature`](../acceptance/features/console.feature).
4+
time: 2025-10-01T16:52:06.679323-04:00
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
project: operator
2+
kind: Deprecated
3+
body: The Redpanda console stanza (`.spec.clusterSpec.console`) is now deprecated in favor of the stand-alone Console CRD.
4+
time: 2025-10-01T17:09:58.327552-04:00
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
@cluster:basic
2+
Feature: Console CRDs
3+
Background: Cluster available
4+
Given cluster "basic" is available
5+
6+
Scenario: Using clusterRef
7+
When I apply Kubernetes manifest:
8+
```yaml
9+
---
10+
apiVersion: cluster.redpanda.com/v1alpha2
11+
kind: Console
12+
metadata:
13+
name: console
14+
spec:
15+
cluster:
16+
clusterRef:
17+
name: basic
18+
```
19+
Then Console "console" will be healthy
20+
# These steps demonstrate that console is correctly connected to Redpanda (Kafka, Schema Registry, and Admin API).
21+
And I exec "curl localhost:8080/api/schema-registry/mode" in a Pod matching "app.kubernetes.io/instance=console", it will output:
22+
```
23+
{"mode":"READWRITE"}
24+
```
25+
And I exec "curl localhost:8080/api/topics" in a Pod matching "app.kubernetes.io/instance=console", it will output:
26+
```
27+
{"topics":[{"topicName":"_schemas","isInternal":false,"partitionCount":1,"replicationFactor":1,"cleanupPolicy":"compact","documentation":"NOT_CONFIGURED","logDirSummary":{"totalSizeBytes":117}}]}
28+
```
29+
And I exec "curl localhost:8080/api/console/endpoints | grep -o '{[^{}]*DebugBundleService[^{}]*}'" in a Pod matching "app.kubernetes.io/instance=console", it will output:
30+
```
31+
{"endpoint":"redpanda.api.console.v1alpha1.DebugBundleService","method":"POST","isSupported":true}
32+
```
33+
34+
Scenario: Using staticConfig
35+
When I apply Kubernetes manifest:
36+
```yaml
37+
---
38+
apiVersion: cluster.redpanda.com/v1alpha2
39+
kind: Console
40+
metadata:
41+
name: console
42+
spec:
43+
cluster:
44+
staticConfiguration:
45+
kafka:
46+
brokers:
47+
- basic-0.basic.${NAMESPACE}.svc.cluster.local.:9093
48+
tls:
49+
caCertSecretRef:
50+
name: "basic-default-cert"
51+
key: "ca.crt"
52+
admin:
53+
urls:
54+
- https://basic-0.basic.${NAMESPACE}.svc.cluster.local.:9644
55+
tls:
56+
caCertSecretRef:
57+
name: "basic-default-cert"
58+
key: "ca.crt"
59+
schemaRegistry:
60+
urls:
61+
- https://basic-0.basic.${NAMESPACE}.svc.cluster.local.:8081
62+
tls:
63+
caCertSecretRef:
64+
name: "basic-default-cert"
65+
key: "ca.crt"
66+
```
67+
Then Console "console" will be healthy
68+
# These steps demonstrate that console is correctly connected to Redpanda (Kafka, Schema Registry, and Admin API).
69+
And I exec "curl localhost:8080/api/schema-registry/mode" in a Pod matching "app.kubernetes.io/instance=console", it will output:
70+
```
71+
{"mode":"READWRITE"}
72+
```
73+
And I exec "curl localhost:8080/api/topics" in a Pod matching "app.kubernetes.io/instance=console", it will output:
74+
```
75+
{"topics":[{"topicName":"_schemas","isInternal":false,"partitionCount":1,"replicationFactor":1,"cleanupPolicy":"compact","documentation":"NOT_CONFIGURED","logDirSummary":{"totalSizeBytes":117}}]}
76+
```
77+
And I exec "curl localhost:8080/api/console/endpoints | grep -o '{[^{}]*DebugBundleService[^{}]*}'" in a Pod matching "app.kubernetes.io/instance=console", it will output:
78+
```
79+
{"endpoint":"redpanda.api.console.v1alpha1.DebugBundleService","method":"POST","isSupported":true}
80+
```

acceptance/main_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ var setupSuite = sync.OnceValues(func() (*framework.Suite, error) {
6868
CreateNamespace: true,
6969
Values: map[string]any{
7070
"installCRDs": true,
71+
"global": map[string]any{
72+
// Make leader election more aggressive as cert-manager appears to
73+
// not release it when uninstalled.
74+
"leaderElection": map[string]any{
75+
"renewDeadline": "10s",
76+
"retryPeriod": "5s",
77+
},
78+
},
7179
},
7280
}).
7381
OnFeature(func(ctx context.Context, t framework.TestingT, tags ...framework.ParsedTag) {

acceptance/steps/console.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package steps
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
framework "github.com/redpanda-data/redpanda-operator/harpoon"
10+
redpandav1alpha2 "github.com/redpanda-data/redpanda-operator/operator/api/redpanda/v1alpha2"
11+
)
12+
13+
func consoleIsHealthy(ctx context.Context, t framework.TestingT, name string) {
14+
key := t.ResourceKey(name)
15+
16+
t.Logf("Checking console %q is healthy", name)
17+
require.Eventually(t, func() bool {
18+
var console redpandav1alpha2.Console
19+
require.NoError(t, t.Get(ctx, key, &console))
20+
21+
upToDate := console.Generation == console.Status.ObservedGeneration
22+
hasHealthyReplicas := console.Status.ReadyReplicas == console.Status.Replicas
23+
24+
return upToDate && hasHealthyReplicas
25+
}, time.Minute, 10*time.Second)
26+
}

acceptance/steps/k8s.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
package steps
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"reflect"
78
"strings"
89
"time"
910

11+
"github.com/cucumber/godog"
12+
"github.com/stretchr/testify/assert"
1013
"github.com/stretchr/testify/require"
14+
corev1 "k8s.io/api/core/v1"
15+
"k8s.io/apimachinery/pkg/labels"
1116
"k8s.io/apimachinery/pkg/runtime/schema"
1217
"k8s.io/client-go/util/jsonpath"
1318
"sigs.k8s.io/controller-runtime/pkg/client"
1419

1520
framework "github.com/redpanda-data/redpanda-operator/harpoon"
1621
redpandav1alpha2 "github.com/redpanda-data/redpanda-operator/operator/api/redpanda/v1alpha2"
22+
"github.com/redpanda-data/redpanda-operator/pkg/kube"
1723
)
1824

1925
// this is a nasty hack due to the fact that we can't disable the linter for typecheck
@@ -147,3 +153,30 @@ func execJSONPath(ctx context.Context, t framework.TestingT, jsonPath, groupVers
147153
}
148154
return nil
149155
}
156+
157+
func iExecInPodMatching(
158+
ctx context.Context,
159+
t framework.TestingT,
160+
cmd,
161+
selectorStr string,
162+
expected *godog.DocString,
163+
) {
164+
selector, err := labels.Parse(selectorStr)
165+
require.NoError(t, err)
166+
167+
ctl, err := kube.FromRESTConfig(t.RestConfig())
168+
require.NoError(t, err)
169+
170+
pods, err := kube.List[corev1.PodList](ctx, ctl, client.MatchingLabelsSelector{Selector: selector}, client.InNamespace(t.Namespace()))
171+
require.NoError(t, err)
172+
173+
require.True(t, len(pods.Items) > 0, "selector %q found no Pods", selector.String())
174+
175+
var stdout bytes.Buffer
176+
require.NoError(t, ctl.Exec(ctx, &pods.Items[0], kube.ExecOptions{
177+
Command: []string{"sh", "-c", cmd},
178+
Stdout: &stdout,
179+
}))
180+
181+
assert.Equal(t, strings.TrimSpace(expected.Content), strings.TrimSpace(stdout.String()))
182+
}

acceptance/steps/manifest.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,17 @@ func iApplyKubernetesManifest(ctx context.Context, t framework.TestingT, manifes
2323
file, err := os.CreateTemp("", "manifest-*.yaml")
2424
require.NoError(t, err)
2525

26-
_, err = file.Write([]byte(manifest.Content))
26+
content := os.Expand(manifest.Content, func(key string) string {
27+
switch key {
28+
case "NAMESPACE":
29+
return t.Namespace()
30+
}
31+
32+
t.Fatalf("unhandled expansion: %s", key)
33+
return "UNREACHABLE"
34+
})
35+
36+
_, err = file.Write([]byte(content))
2737
require.NoError(t, err)
2838
require.NoError(t, file.Close())
2939

acceptance/steps/register.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func init() {
1515
// General scenario steps
1616
framework.RegisterStep(`^cluster "([^"]*)" is available$`, checkClusterAvailability)
1717
framework.RegisterStep(`^I apply Kubernetes manifest:$`, iApplyKubernetesManifest)
18+
framework.RegisterStep(`^I exec "([^"]+)" in a Pod matching "([^"]+)", it will output:$`, iExecInPodMatching)
1819

1920
framework.RegisterStep(`^I store "([^"]*)" of Kubernetes object with type "([^"]*)" and name "([^"]*)" as "([^"]*)"$`, recordVariable)
2021
framework.RegisterStep(`^the recorded value "([^"]*)" has the same value as "([^"]*)" of the Kubernetes object with type "([^"]*)" and name "([^"]*)"$`, assertVariableValue)
@@ -93,4 +94,7 @@ func init() {
9394
framework.RegisterStep(`^I can upgrade to the latest operator with the values:$`, iCanUpgradeToTheLatestOperatorWithTheValues)
9495
framework.RegisterStep(`^I install redpanda helm chart version "([^"]*)" with the values:$`, iInstallRedpandaHelmChartVersionWithTheValues)
9596
framework.RegisterStep(`^I install local CRDs from "([^"]*)"`, iInstallLocalCRDs)
97+
98+
// Console scenario steps
99+
framework.RegisterStep(`^Console "([^"]+)" will be healthy`, consoleIsHealthy)
96100
}

charts/console/chart/chart.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ func DotToState(dot *helmette.Dot) *console.RenderState {
5656
values := helmette.Unwrap[Values](dot.Values)
5757
templater := &templater{Dot: dot, FauxDot: newFauxDot(dot)}
5858

59+
if values.RenderValues.Secret.Authentication.JWTSigningKey == "" {
60+
values.RenderValues.Secret.Authentication.JWTSigningKey = helmette.RandAlphaNum(32)
61+
}
62+
5963
return &console.RenderState{
6064
ReleaseName: dot.Release.Name,
6165
Namespace: dot.Release.Namespace,

charts/console/chart/templates/_chart.chart.tpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
{{- $_is_returning := false -}}
1919
{{- $values := $dot.Values.AsMap -}}
2020
{{- $templater := (mustMergeOverwrite (dict "Dot" (coalesce nil) "FauxDot" (coalesce nil)) (dict "Dot" $dot "FauxDot" (get (fromJson (include "chart.newFauxDot" (dict "a" (list $dot)))) "r"))) -}}
21+
{{- if (eq $values.secret.authentication.jwtSigningKey "") -}}
22+
{{- $_ := (set $values.secret.authentication "jwtSigningKey" (randAlphaNum (32 | int))) -}}
23+
{{- end -}}
2124
{{- $_is_returning = true -}}
2225
{{- (dict "r" (mustMergeOverwrite (dict "ReleaseName" "" "Namespace" "" "Template" (coalesce nil) "CommonLabels" (coalesce nil) "Values" (dict "replicaCount" 0 "nameOverride" "" "commonLabels" (coalesce nil) "fullnameOverride" "" "image" (dict "registry" "" "repository" "" "pullPolicy" "" "tag" "") "imagePullSecrets" (coalesce nil) "automountServiceAccountToken" false "serviceAccount" (dict "create" false "automountServiceAccountToken" false "annotations" (coalesce nil) "name" "") "annotations" (coalesce nil) "podAnnotations" (coalesce nil) "podLabels" (coalesce nil) "podSecurityContext" (dict) "securityContext" (dict) "service" (dict "type" "" "port" 0 "annotations" (coalesce nil)) "ingress" (dict "enabled" false "annotations" (coalesce nil) "hosts" (coalesce nil) "tls" (coalesce nil)) "resources" (dict) "autoscaling" (dict "enabled" false "minReplicas" 0 "maxReplicas" 0 "targetCPUUtilizationPercentage" (coalesce nil)) "nodeSelector" (coalesce nil) "tolerations" (coalesce nil) "affinity" (dict) "topologySpreadConstraints" (coalesce nil) "priorityClassName" "" "config" (coalesce nil) "extraEnv" (coalesce nil) "extraEnvFrom" (coalesce nil) "extraVolumes" (coalesce nil) "extraVolumeMounts" (coalesce nil) "extraContainers" (coalesce nil) "initContainers" (dict "extraInitContainers" (coalesce nil)) "secretMounts" (coalesce nil) "secret" (dict "create" false "kafka" (dict) "authentication" (dict "jwtSigningKey" "" "oidc" (dict)) "license" "" "redpanda" (dict "adminApi" (dict)) "serde" (dict) "schemaRegistry" (dict)) "livenessProbe" (dict) "readinessProbe" (dict) "configmap" (dict "create" false) "deployment" (dict "create" false) "strategy" (dict))) (dict "ReleaseName" $dot.Release.Name "Namespace" $dot.Release.Namespace "Values" $values "Template" (list "chart.templater.Template" $templater) "CommonLabels" (dict "helm.sh/chart" (get (fromJson (include "chart.ChartLabel" (dict "a" (list $dot)))) "r") "app.kubernetes.io/managed-by" $dot.Release.Service "app.kubernetes.io/version" $dot.Chart.AppVersion)))) | toJson -}}
2326
{{- break -}}

0 commit comments

Comments
 (0)