Skip to content

Commit 9b7faa7

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 335c0e1 commit 9b7faa7

File tree

71 files changed

+17379
-672
lines changed

Some content is hidden

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

71 files changed

+17379
-672
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
@@ -75,6 +75,14 @@ var setupSuite = sync.OnceValues(func() (*framework.Suite, error) {
7575
CreateNamespace: true,
7676
Values: map[string]any{
7777
"installCRDs": true,
78+
"global": map[string]any{
79+
// Make leader election more aggressive as cert-manager appears to
80+
// not release it when uninstalled.
81+
"leaderElection": map[string]any{
82+
"renewDeadline": "10s",
83+
"retryPeriod": "5s",
84+
},
85+
},
7886
},
7987
}).
8088
OnFeature(func(ctx context.Context, t framework.TestingT, tags ...framework.ParsedTag) {

acceptance/steps/console.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2025 Redpanda Data, Inc.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.md
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0
9+
10+
package steps
11+
12+
import (
13+
"context"
14+
"time"
15+
16+
"github.com/stretchr/testify/require"
17+
18+
framework "github.com/redpanda-data/redpanda-operator/harpoon"
19+
redpandav1alpha2 "github.com/redpanda-data/redpanda-operator/operator/api/redpanda/v1alpha2"
20+
)
21+
22+
func consoleIsHealthy(ctx context.Context, t framework.TestingT, name string) {
23+
key := t.ResourceKey(name)
24+
25+
t.Logf("Checking console %q is healthy", name)
26+
require.Eventually(t, func() bool {
27+
var console redpandav1alpha2.Console
28+
require.NoError(t, t.Get(ctx, key, &console))
29+
30+
upToDate := console.Generation == console.Status.ObservedGeneration
31+
hasHealthyReplicas := console.Status.ReadyReplicas == console.Status.Replicas
32+
33+
return upToDate && hasHealthyReplicas
34+
}, time.Minute, 10*time.Second)
35+
}

acceptance/steps/k8s.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,25 @@
1010
package steps
1111

1212
import (
13+
"bytes"
1314
"context"
1415
"fmt"
1516
"reflect"
1617
"strings"
1718
"time"
1819

20+
"github.com/cucumber/godog"
21+
"github.com/stretchr/testify/assert"
1922
"github.com/stretchr/testify/require"
23+
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/labels"
2025
"k8s.io/apimachinery/pkg/runtime/schema"
2126
"k8s.io/client-go/util/jsonpath"
2227
"sigs.k8s.io/controller-runtime/pkg/client"
2328

2429
framework "github.com/redpanda-data/redpanda-operator/harpoon"
2530
redpandav1alpha2 "github.com/redpanda-data/redpanda-operator/operator/api/redpanda/v1alpha2"
31+
"github.com/redpanda-data/redpanda-operator/pkg/kube"
2632
)
2733

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

acceptance/steps/manifest.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ func iApplyKubernetesManifest(ctx context.Context, t framework.TestingT, manifes
2626
file, err := os.CreateTemp("", "manifest-*.yaml")
2727
require.NoError(t, err)
2828

29-
_, err = file.Write(normalizeContent(t, manifest.Content))
29+
content := PatchManifest(t, manifest.Content)
30+
31+
_, err = file.Write(normalizeContent(t, content))
3032
require.NoError(t, err)
3133
require.NoError(t, file.Close())
3234

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)
@@ -98,4 +100,7 @@ func init() {
98100
framework.RegisterStep(`^I can upgrade to the latest operator with the values:$`, iCanUpgradeToTheLatestOperatorWithTheValues)
99101
framework.RegisterStep(`^I install redpanda helm chart version "([^"]*)" with the values:$`, iInstallRedpandaHelmChartVersionWithTheValues)
100102
framework.RegisterStep(`^I install local CRDs from "([^"]*)"`, iInstallLocalCRDs)
103+
104+
// Console scenario steps
105+
framework.RegisterStep(`^Console "([^"]+)" will be healthy`, consoleIsHealthy)
101106
}

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)