Skip to content

Commit 8100e07

Browse files
committed
[WIP] operator: add console stanza -> CRD migration
Reconciling a Redpanda will now produce a Console CR with a clusterRef pointing back at the cluster itself. TODO LIST: - [ ] Conversions are fallible, figure out how to log errors without flood the log (rate.Limit?) - As everything is wrapped in a runtime.Extension, it's pretty easy to make an invalid config. - [ ] Acceptance tests that upgrade the operator and show a functioning console CRD and one with migration warnings. - [ ] Figure out how to remove the console Stanza w/o removing the Console CRD (ideally an opt in method?) - [ ] Clean up naming of CRD conversion?
1 parent 8e08ada commit 8100e07

17 files changed

+2585
-427
lines changed

operator/api/redpanda/v1alpha2/console_types.go

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,18 @@
1010
package v1alpha2
1111

1212
import (
13+
"encoding/json"
14+
15+
"github.com/cockroachdb/errors"
1316
appsv1 "k8s.io/api/apps/v1"
1417
corev1 "k8s.io/api/core/v1"
1518
networkingv1 "k8s.io/api/networking/v1"
1619
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1720
"k8s.io/apimachinery/pkg/runtime"
21+
applycorev1 "k8s.io/client-go/applyconfigurations/core/v1"
1822
"k8s.io/utils/ptr"
1923

24+
"github.com/redpanda-data/redpanda-operator/charts/console/v3"
2025
"github.com/redpanda-data/redpanda-operator/operator/pkg/functional"
2126
)
2227

@@ -121,10 +126,16 @@ type ConsoleValues struct {
121126
SecretMounts []SecretMount `json:"secretMounts,omitempty"`
122127
Secret SecretConfig `json:"secret,omitempty"`
123128
LicenseSecretRef *corev1.SecretKeySelector `json:"licenseSecretRef,omitempty"`
124-
LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty"`
125-
ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty"`
129+
LivenessProbe *ProbeApplyConfiguration `json:"livenessProbe,omitempty"`
130+
ReadinessProbe *ProbeApplyConfiguration `json:"readinessProbe,omitempty"`
126131
Deployment *DeploymentConfig `json:"deployment,omitempty"`
127132
Strategy *appsv1.DeploymentStrategy `json:"strategy,omitempty"`
133+
// Warnings is a slice of human readable warnings generated by the automatic
134+
// migration of a Console V2 config to a Console V3 config. If warnings are
135+
// present, they will describe which fields from the original config have
136+
// been dropped and why.
137+
// Setting this field has no effect.
138+
Warnings []string `json:"warnings,omitempty"`
128139
}
129140

130141
type AutoScaling struct {
@@ -237,3 +248,85 @@ type RedpandaAdminAPISecrets struct {
237248
TLSCert *string `json:"tlsCert,omitempty"`
238249
TLSKey *string `json:"tlsKey,omitempty"`
239250
}
251+
252+
// ProbeApplyConfiguration is a wrapper type that allows including a partial
253+
// [corev1.Probe] in a CRD.
254+
type ProbeApplyConfiguration struct {
255+
*applycorev1.ProbeApplyConfiguration `json:",inline"`
256+
}
257+
258+
func (ac *ProbeApplyConfiguration) DeepCopy() *ProbeApplyConfiguration {
259+
// For some inexplicable reason, apply configs don't have deepcopy
260+
// generated for them.
261+
//
262+
// DeepCopyInto can be generated with just DeepCopy implemented. Sadly, the
263+
// easiest way to implement DeepCopy is to run this type through JSON. It's
264+
// highly unlikely that we'll hit a panic but it is possible to do so with
265+
// invalid values for resource.Quantity and the like.
266+
out := new(ProbeApplyConfiguration)
267+
data, err := json.Marshal(ac)
268+
if err != nil {
269+
panic(err)
270+
}
271+
if err := json.Unmarshal(data, out); err != nil {
272+
panic(err)
273+
}
274+
return out
275+
}
276+
277+
// ConvertConsoleSubchartToConsoleValues "migrates" the Console field
278+
// ([RedpandaConsole]) of a [Redpanda] into a Console v3 compliant
279+
// [ConsoleValues].
280+
func ConvertConsoleSubchartToConsoleValues(src *RedpandaConsole) (*ConsoleValues, error) {
281+
// By the redpanda chart's default values, console is enabled by default
282+
// and must be explicitly opted out of.
283+
if src == nil {
284+
// Empty values is valid.
285+
return &ConsoleValues{}, nil
286+
}
287+
288+
// If the console integration is opted out of, return nil.
289+
if !ptr.Deref(src.Enabled, true) {
290+
return nil, nil
291+
}
292+
293+
out, err := autoconv_RedpandaConsole_To_ConsoleValues(src)
294+
if err != nil {
295+
return nil, err
296+
}
297+
298+
// Extract out .Console and .Config. .Console will be migrated and then
299+
// merged into .Config as Config is meant to house V3 configurations.
300+
var v2Config map[string]any
301+
if src.Console != nil && len(src.Console.Raw) > 0 {
302+
if err := json.Unmarshal(src.Console.Raw, &v2Config); err != nil {
303+
return nil, errors.WithStack(err)
304+
}
305+
}
306+
307+
var v3Config map[string]any
308+
if src.Config != nil && len(src.Config.Raw) > 0 {
309+
if err := json.Unmarshal(src.Config.Raw, &v3Config); err != nil {
310+
return nil, errors.WithStack(err)
311+
}
312+
}
313+
314+
migrated, warnings, err := console.ConfigFromV2(v2Config)
315+
if err != nil {
316+
return nil, errors.WithStack(err)
317+
}
318+
319+
merged := functional.MergeMaps(migrated, v3Config)
320+
321+
marshalled, err := json.Marshal(merged)
322+
if err != nil {
323+
return nil, errors.WithStack(err)
324+
}
325+
326+
out.Config = &runtime.RawExtension{Raw: marshalled}
327+
// Unlike the docs migrate, warnings get their own field. We can't set
328+
// comments of a Kubernetes resource.
329+
out.Warnings = warnings
330+
331+
return out, nil
332+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 v1alpha2
11+
12+
import (
13+
"fmt"
14+
"testing"
15+
16+
"github.com/stretchr/testify/require"
17+
"golang.org/x/tools/txtar"
18+
"sigs.k8s.io/yaml"
19+
20+
"github.com/redpanda-data/redpanda-operator/pkg/testutil"
21+
)
22+
23+
func TestConvertConsoleSubchartToConsoleValues(t *testing.T) {
24+
cases, err := txtar.ParseFile("testdata/console-migration-cases.txtar")
25+
require.NoError(t, err)
26+
27+
goldens := testutil.NewTxTar(t, "testdata/console-migration-cases.golden.txtar")
28+
29+
for i, tc := range cases.Files {
30+
t.Run(tc.Name, func(t *testing.T) {
31+
var in RedpandaConsole
32+
require.NoError(t, yaml.Unmarshal(tc.Data, &in))
33+
34+
out, err := ConvertConsoleSubchartToConsoleValues(&in)
35+
require.NoError(t, err)
36+
37+
actual, err := yaml.Marshal(out)
38+
require.NoError(t, err)
39+
40+
// Add a bit of extra padding to make it easier to navigate the golden file.
41+
actual = append(actual, '\n')
42+
43+
goldens.AssertGolden(t, testutil.YAML, fmt.Sprintf("%02d-%s", i, tc.Name), actual)
44+
})
45+
}
46+
}

operator/api/redpanda/v1alpha2/conversion.go

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"encoding/json"
1414

1515
"github.com/cockroachdb/errors"
16+
appsv1 "k8s.io/api/apps/v1"
1617
corev1 "k8s.io/api/core/v1"
1718
"k8s.io/apimachinery/pkg/runtime"
1819

@@ -39,7 +40,13 @@ var (
3940
ConvertStaticConfigToIR func(namespace string, src *StaticConfigurationSource) *ir.StaticConfigurationSource
4041

4142
// Private conversions for tuning / customizing conversions.
42-
// Naming conversion: `autoconv_<Type>_To_<pkg>_<Type>`
43+
// Naming convention: `autoconv_<Type>_To_<pkg>_<Type>`
44+
45+
// goverter:map . LicenseSecretRef | convertConsoleLicenseSecretRef
46+
// goverter:ignore Config
47+
// goverter:ignore Warnings
48+
// goverter:ignore ExtraContainerPorts
49+
autoconv_RedpandaConsole_To_ConsoleValues func(*RedpandaConsole) (*ConsoleValues, error)
4350

4451
// goverter:ignore Create
4552
// Ability to disable creation of Deployment is not exposed through the Console CRD.
@@ -69,6 +76,33 @@ func getNamespace(namespace string) string {
6976
return namespace
7077
}
7178

79+
// convertConsoleLicenseSecretRef extracts either the LicenseSecretRef or
80+
// Enterprise.LicenseSecret from a [RedpandaConsole] into a
81+
// [corev1.SecretKeySelector].
82+
func convertConsoleLicenseSecretRef(src *RedpandaConsole) (*corev1.SecretKeySelector, error) {
83+
// If LicenseSecreRef is set, accept that.
84+
if src.LicenseSecretRef != nil {
85+
return src.LicenseSecretRef, nil
86+
}
87+
88+
// Short circuit if Enterprise isn't specified.
89+
if src.Enterprise == nil || len(src.Enterprise.Raw) != 0 {
90+
return nil, nil
91+
}
92+
93+
// Otherwise attempt to extract a secret reference from the Enterprise block.
94+
type ConsoleEnterprise struct {
95+
LicenseSecret *corev1.SecretKeySelector
96+
}
97+
98+
enterprise, err := convertRuntimeRawExtension[ConsoleEnterprise](src.Enterprise)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
return enterprise.LicenseSecret, nil
104+
}
105+
72106
// Manually implemented conversion routines
73107
// Naming conversion: `conv_<Type>_To_<pkg>_<Type>`
74108

@@ -90,22 +124,46 @@ func conv_SecretKeyRef_To_ir_ObjectKeyRef(skr *SecretKeyRef, namespace string) *
90124
}
91125
}
92126

93-
func conv_runtime_RawExtension_To_mapany(ext *runtime.RawExtension) (map[string]any, error) {
94-
if ext == nil {
95-
return nil, nil
96-
}
97-
98-
var out map[string]any
99-
if err := json.Unmarshal(ext.Raw, &out); err != nil {
100-
return nil, errors.WithStack(err)
101-
}
102-
return out, nil
103-
}
104-
105127
var (
106128
conv_corev1_Volume_To_corev1_Volume = convertDeepCopier[corev1.Volume]
107129
conv_corev1_EnvVar_To_corev1EnvVar = convertDeepCopier[corev1.EnvVar]
108130
conv_corev1_ResourceRequirements_To_corev1_ResourceRequirements = convertDeepCopier[corev1.ResourceRequirements]
131+
132+
// RawExtension -> Custom type (RedpandaConsole -> Console)
133+
134+
conv_runtime_RawExtension_To_mapstringany = convertRuntimeRawExtension[map[string]any]
135+
conv_runtime_RawExtension_To_mapstringstring = convertRuntimeRawExtension[map[string]string]
136+
conv_runtime_RawExtension_To_Image = convertRuntimeRawExtension[*Image]
137+
conv_runtime_RawExtension_To_ServiceAccountConfig = convertRuntimeRawExtension[*ServiceAccountConfig]
138+
conv_runtime_RawExtension_To_Service = convertRuntimeRawExtension[*ServiceConfig]
139+
conv_runtime_RawExtension_To_Ingress = convertRuntimeRawExtension[*IngressConfig]
140+
conv_runtime_RawExtension_To_Autoscaling = convertRuntimeRawExtension[*AutoScaling]
141+
conv_runtime_RawExtension_To_SecretMounts = convertRuntimeRawExtension[SecretMount]
142+
conv_runtime_RawExtension_To_Secret = convertRuntimeRawExtension[SecretConfig]
143+
conv_runtime_RawExtension_To_Deployment = convertRuntimeRawExtension[*DeploymentConfig]
144+
145+
// RawExtension -> built in types (RedpandaConsole -> Console)
146+
147+
conv_runtime_RawExtension_To_corev1_Affinity = convertRuntimeRawExtension[*corev1.Affinity]
148+
conv_runtime_RawExtension_To_corev1_Container = convertRuntimeRawExtension[corev1.Container]
149+
conv_runtime_RawExtension_To_corev1_EnvFromSource = convertRuntimeRawExtension[corev1.EnvFromSource]
150+
conv_runtime_RawExtension_To_corev1_EnvVar = convertRuntimeRawExtension[corev1.EnvVar]
151+
conv_runtime_RawExtension_To_corev1_LocalObjectReference = convertRuntimeRawExtension[corev1.LocalObjectReference]
152+
conv_runtime_RawExtension_To_corev1_PodSecurityContext = convertRuntimeRawExtension[*corev1.PodSecurityContext]
153+
conv_runtime_RawExtension_To_corev1_Resources = convertRuntimeRawExtension[*corev1.ResourceRequirements]
154+
conv_runtime_RawExtension_To_corev1_SecurityContext = convertRuntimeRawExtension[*corev1.SecurityContext]
155+
conv_runtime_RawExtension_To_corev1_Strategy = convertRuntimeRawExtension[*appsv1.DeploymentStrategy]
156+
conv_runtime_RawExtension_To_corev1_Tolerations = convertRuntimeRawExtension[corev1.Toleration]
157+
conv_runtime_RawExtension_To_corev1_TopologySpreadConstraints = convertRuntimeRawExtension[[]corev1.TopologySpreadConstraint]
158+
conv_runtime_RawExtension_To_corev1_Volume = convertRuntimeRawExtension[corev1.Volume]
159+
conv_runtime_RawExtension_To_corev1_VolumeMount = convertRuntimeRawExtension[corev1.VolumeMount]
160+
161+
// TODO THIS IS BAD AND BROKEN (Will write 0s for unspecified fields and generate invalid options).
162+
// ConsolePartialValues really needs to have ApplyConfigs for most k8s types.
163+
// Upgrade gen partial to pull an overridden type from a comment or field tag?
164+
conv_LivenessProbe_To_ProbeApplyConfiguration = convertViaMarshaling[*LivenessProbe, *ProbeApplyConfiguration]
165+
conv_ProbeApplyConfiguration_To_corev1_Probe = convertViaMarshaling[ProbeApplyConfiguration, corev1.Probe]
166+
conv_ReadinessProbe_To_ProbeApplyConfiguration = convertViaMarshaling[*ReadinessProbe, *ProbeApplyConfiguration]
109167
)
110168

111169
type deepCopier[T any] interface {
@@ -116,3 +174,33 @@ type deepCopier[T any] interface {
116174
func convertDeepCopier[T any, P deepCopier[T]](in T) T {
117175
return *P(&in).DeepCopy()
118176
}
177+
178+
func convertRuntimeRawExtension[T any](ext *runtime.RawExtension) (T, error) {
179+
if ext == nil {
180+
var zero T
181+
return zero, nil
182+
}
183+
184+
var out T
185+
if err := json.Unmarshal(ext.Raw, &out); err != nil {
186+
var zero T
187+
return zero, errors.Wrapf(err, "unmarshalling %T into %T", ext, zero)
188+
}
189+
return out, nil
190+
}
191+
192+
func convertViaMarshaling[From any, To any](src From) (To, error) {
193+
marshalled, err := json.Marshal(src)
194+
if err != nil {
195+
var zero To
196+
return zero, errors.Wrapf(err, "marshalling: %T", src)
197+
}
198+
199+
var out To
200+
if err := json.Unmarshal(marshalled, &out); err != nil {
201+
var zero To
202+
return zero, errors.Wrapf(err, "unmarshalling %T into %T", src, zero)
203+
}
204+
205+
return out, nil
206+
}

operator/api/redpanda/v1alpha2/redpanda_clusterspec_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ type RedpandaConsole struct {
196196
// Specifies whether the Redpanda Console subchart should be deployed.
197197
Enabled *bool `json:"enabled,omitempty"`
198198
// Sets the number of replicas for the Redpanda Console Deployment resource.
199-
ReplicaCount *int `json:"replicaCount,omitempty"`
199+
ReplicaCount *int32 `json:"replicaCount,omitempty"`
200200
// Specifies a custom name for the Redpanda Console resources, overriding the default naming convention.
201201
NameOverride *string `json:"nameOverride,omitempty"`
202202
// Specifies a full custom name, which overrides the entire naming convention including release name and chart name.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
-- 00-empty --
2+
config:
3+
kafka:
4+
sasl:
5+
enabled: true
6+
impersonateUser: true
7+
secret: {}
8+
9+
-- 01-disabled --
10+
null
11+
12+
-- 02-configured --
13+
config:
14+
authentication:
15+
jwtSigningKey: secret123
16+
useSecureCookies: true
17+
authorization:
18+
roleBindings:
19+
- roleName: admin
20+
users:
21+
- loginType: oidc
22+
name: devs
23+
kafka:
24+
sasl:
25+
enabled: true
26+
impersonateUser: true
27+
secret: {}
28+
29+
-- 03-config-and-console --
30+
config:
31+
authentication:
32+
someOtherSetting:
33+
- absolutely
34+
kafka:
35+
sasl:
36+
enabled: true
37+
impersonateUser: true
38+
secret: {}
39+
40+
-- 04-enterprise-and-license-ref --
41+
config:
42+
kafka:
43+
sasl:
44+
enabled: true
45+
impersonateUser: true
46+
licenseSecretRef:
47+
key: license
48+
name: license
49+
secret: {}
50+
51+
-- 05-migration-warnings --
52+
config:
53+
authorization:
54+
roleBindings:
55+
- roleName: admin
56+
users: []
57+
kafka:
58+
sasl:
59+
enabled: true
60+
impersonateUser: true
61+
secret: {}
62+
warnings:
63+
- Removed group subject from role binding 'admin'. Groups are not supported in v3.
64+

0 commit comments

Comments
 (0)