Skip to content

Commit 233b340

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 9024bc7 commit 233b340

Some content is hidden

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

65 files changed

+17094
-528
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
@@ -69,6 +69,14 @@ var setupSuite = sync.OnceValues(func() (*framework.Suite, error) {
6969
CreateNamespace: true,
7070
Values: map[string]any{
7171
"installCRDs": true,
72+
"global": map[string]any{
73+
// Make leader election more aggressive as cert-manager appears to
74+
// not release it when uninstalled.
75+
"leaderElection": map[string]any{
76+
"renewDeadline": "10s",
77+
"retryPeriod": "5s",
78+
},
79+
},
7280
},
7381
}).
7482
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, t.Namespace(), client.MatchingLabelsSelector{Selector: selector})
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: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ package steps
1212
import (
1313
"context"
1414
"os"
15+
"regexp"
1516
"strings"
1617

1718
"github.com/cucumber/godog"
@@ -21,11 +22,26 @@ import (
2122
framework "github.com/redpanda-data/redpanda-operator/harpoon"
2223
)
2324

25+
// substRE is a regular expression used to match placeholders in YAML manifests in the form of: ${KEY_NAME}
26+
// os.Expand is not used as it's matching is too permissive.
27+
var substRE = regexp.MustCompile(`\$\{[A-Z_]+\}`)
28+
2429
func iApplyKubernetesManifest(ctx context.Context, t framework.TestingT, manifest *godog.DocString) {
2530
file, err := os.CreateTemp("", "manifest-*.yaml")
2631
require.NoError(t, err)
2732

28-
_, err = file.Write(normalizeContent(t, manifest.Content))
33+
content := substRE.ReplaceAllStringFunc(manifest.Content, func(match string) string {
34+
key := match[2 : len(match)-1] // ${FOO} -> FOO
35+
switch key {
36+
case "NAMESPACE":
37+
return t.Namespace()
38+
}
39+
40+
t.Fatalf("unhandled expansion: %s", key)
41+
return "UNREACHABLE"
42+
})
43+
44+
_, err = file.Write(normalizeContent(t, content))
2945
require.NoError(t, err)
3046
require.NoError(t, file.Close())
3147

acceptance/steps/register.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ func init() {
1515
// General scenario steps
1616
framework.RegisterStep(`^(vectorized )?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)
19+
1820
framework.RegisterStep(`^I store "([^"]*)" of Kubernetes object with type "([^"]*)" and name "([^"]*)" as "([^"]*)"$`, recordVariable)
1921
framework.RegisterStep(`^the recorded value "([^"]*)" has the same value as "([^"]*)" of the Kubernetes object with type "([^"]*)" and name "([^"]*)"$`, assertVariableValue)
2022
framework.RegisterStep(`^the recorded value "([^"]*)" is one less than "([^"]*)" of the Kubernetes object with type "([^"]*)" and name "([^"]*)"$`, assertVariableValueIncremented)
@@ -92,4 +94,7 @@ func init() {
9294
framework.RegisterStep(`^I can upgrade to the latest operator with the values:$`, iCanUpgradeToTheLatestOperatorWithTheValues)
9395
framework.RegisterStep(`^I install redpanda helm chart version "([^"]*)" with the values:$`, iInstallRedpandaHelmChartVersionWithTheValues)
9496
framework.RegisterStep(`^I install local CRDs from "([^"]*)"`, iInstallLocalCRDs)
97+
98+
// Console scenario steps
99+
framework.RegisterStep(`^Console "([^"]+)" will be healthy`, consoleIsHealthy)
95100
}

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)