Skip to content

Commit 76d27d1

Browse files
chrissetoandrewstucki
authored andcommitted
[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 4e3357d commit 76d27d1

17 files changed

+2600
-425
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

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

129140
type AutoScaling struct {
@@ -236,3 +247,85 @@ type RedpandaAdminAPISecrets struct {
236247
TLSCert *string `json:"tlsCert,omitempty"`
237248
TLSKey *string `json:"tlsKey,omitempty"`
238249
}
250+
251+
// ProbeApplyConfiguration is a wrapper type that allows including a partial
252+
// [corev1.Probe] in a CRD.
253+
type ProbeApplyConfiguration struct {
254+
*applycorev1.ProbeApplyConfiguration `json:",inline"`
255+
}
256+
257+
func (ac *ProbeApplyConfiguration) DeepCopy() *ProbeApplyConfiguration {
258+
// For some inexplicable reason, apply configs don't have deepcopy
259+
// generated for them.
260+
//
261+
// DeepCopyInto can be generated with just DeepCopy implemented. Sadly, the
262+
// easiest way to implement DeepCopy is to run this type through JSON. It's
263+
// highly unlikely that we'll hit a panic but it is possible to do so with
264+
// invalid values for resource.Quantity and the like.
265+
out := new(ProbeApplyConfiguration)
266+
data, err := json.Marshal(ac)
267+
if err != nil {
268+
panic(err)
269+
}
270+
if err := json.Unmarshal(data, out); err != nil {
271+
panic(err)
272+
}
273+
return out
274+
}
275+
276+
// ConvertConsoleSubchartToConsoleValues "migrates" the Console field
277+
// ([RedpandaConsole]) of a [Redpanda] into a Console v3 compliant
278+
// [ConsoleValues].
279+
func ConvertConsoleSubchartToConsoleValues(src *RedpandaConsole) (*ConsoleValues, error) {
280+
// By the redpanda chart's default values, console is enabled by default
281+
// and must be explicitly opted out of.
282+
if src == nil {
283+
// Empty values is valid.
284+
return &ConsoleValues{}, nil
285+
}
286+
287+
// If the console integration is opted out of, return nil.
288+
if !ptr.Deref(src.Enabled, true) {
289+
return nil, nil
290+
}
291+
292+
out, err := autoconv_RedpandaConsole_To_ConsoleValues(src)
293+
if err != nil {
294+
return nil, err
295+
}
296+
297+
// Extract out .Console and .Config. .Console will be migrated and then
298+
// merged into .Config as Config is meant to house V3 configurations.
299+
var v2Config map[string]any
300+
if src.Console != nil && len(src.Console.Raw) > 0 {
301+
if err := json.Unmarshal(src.Console.Raw, &v2Config); err != nil {
302+
return nil, errors.WithStack(err)
303+
}
304+
}
305+
306+
var v3Config map[string]any
307+
if src.Config != nil && len(src.Config.Raw) > 0 {
308+
if err := json.Unmarshal(src.Config.Raw, &v3Config); err != nil {
309+
return nil, errors.WithStack(err)
310+
}
311+
}
312+
313+
migrated, warnings, err := console.ConfigFromV2(v2Config)
314+
if err != nil {
315+
return nil, errors.WithStack(err)
316+
}
317+
318+
merged := functional.MergeMaps(migrated, v3Config)
319+
320+
marshalled, err := json.Marshal(merged)
321+
if err != nil {
322+
return nil, errors.WithStack(err)
323+
}
324+
325+
out.Config = &runtime.RawExtension{Raw: marshalled}
326+
// Unlike the docs migrate, warnings get their own field. We can't set
327+
// comments of a Kubernetes resource.
328+
out.Warnings = warnings
329+
330+
return out, nil
331+
}
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

@@ -49,7 +50,13 @@ var (
4950
ConvertSchemaRegistrySpecToIR func(namespace string, src *SchemaRegistrySpec) *ir.SchemaRegistrySpec
5051

5152
// Private conversions for tuning / customizing conversions.
52-
// Naming conversion: `autoconv_<Type>_To_<pkg>_<Type>`
53+
// Naming convention: `autoconv_<Type>_To_<pkg>_<Type>`
54+
55+
// goverter:map . LicenseSecretRef | convertConsoleLicenseSecretRef
56+
// goverter:ignore Config
57+
// goverter:ignore Warnings
58+
// goverter:ignore ExtraContainerPorts
59+
autoconv_RedpandaConsole_To_ConsoleValues func(*RedpandaConsole) (*ConsoleValues, error)
5360

5461
// goverter:ignore Create
5562
// Ability to disable creation of Deployment is not exposed through the Console CRD.
@@ -71,6 +78,33 @@ func getNamespace(namespace string) string {
7178
return namespace
7279
}
7380

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

@@ -231,22 +265,46 @@ func conv_KafkaSASLGSSAPI_To_ir_KafkaSASLGSSAPI(gssAPI *KafkaSASLGSSAPI, namespa
231265
return irGSSAPI
232266
}
233267

234-
func conv_runtime_RawExtension_To_mapany(ext *runtime.RawExtension) (map[string]any, error) {
235-
if ext == nil {
236-
return nil, nil
237-
}
238-
239-
var out map[string]any
240-
if err := json.Unmarshal(ext.Raw, &out); err != nil {
241-
return nil, errors.WithStack(err)
242-
}
243-
return out, nil
244-
}
245-
246268
var (
247269
conv_corev1_Volume_To_corev1_Volume = convertDeepCopier[corev1.Volume]
248270
conv_corev1_EnvVar_To_corev1EnvVar = convertDeepCopier[corev1.EnvVar]
249271
conv_corev1_ResourceRequirements_To_corev1_ResourceRequirements = convertDeepCopier[corev1.ResourceRequirements]
272+
273+
// RawExtension -> Custom type (RedpandaConsole -> Console)
274+
275+
conv_runtime_RawExtension_To_mapstringany = convertRuntimeRawExtension[map[string]any]
276+
conv_runtime_RawExtension_To_mapstringstring = convertRuntimeRawExtension[map[string]string]
277+
conv_runtime_RawExtension_To_Image = convertRuntimeRawExtension[*Image]
278+
conv_runtime_RawExtension_To_ServiceAccountConfig = convertRuntimeRawExtension[*ServiceAccountConfig]
279+
conv_runtime_RawExtension_To_Service = convertRuntimeRawExtension[*ServiceConfig]
280+
conv_runtime_RawExtension_To_Ingress = convertRuntimeRawExtension[*IngressConfig]
281+
conv_runtime_RawExtension_To_Autoscaling = convertRuntimeRawExtension[*AutoScaling]
282+
conv_runtime_RawExtension_To_SecretMounts = convertRuntimeRawExtension[SecretMount]
283+
conv_runtime_RawExtension_To_Secret = convertRuntimeRawExtension[SecretConfig]
284+
conv_runtime_RawExtension_To_Deployment = convertRuntimeRawExtension[*DeploymentConfig]
285+
286+
// RawExtension -> built in types (RedpandaConsole -> Console)
287+
288+
conv_runtime_RawExtension_To_corev1_Affinity = convertRuntimeRawExtension[*corev1.Affinity]
289+
conv_runtime_RawExtension_To_corev1_Container = convertRuntimeRawExtension[corev1.Container]
290+
conv_runtime_RawExtension_To_corev1_EnvFromSource = convertRuntimeRawExtension[corev1.EnvFromSource]
291+
conv_runtime_RawExtension_To_corev1_EnvVar = convertRuntimeRawExtension[corev1.EnvVar]
292+
conv_runtime_RawExtension_To_corev1_LocalObjectReference = convertRuntimeRawExtension[corev1.LocalObjectReference]
293+
conv_runtime_RawExtension_To_corev1_PodSecurityContext = convertRuntimeRawExtension[*corev1.PodSecurityContext]
294+
conv_runtime_RawExtension_To_corev1_Resources = convertRuntimeRawExtension[*corev1.ResourceRequirements]
295+
conv_runtime_RawExtension_To_corev1_SecurityContext = convertRuntimeRawExtension[*corev1.SecurityContext]
296+
conv_runtime_RawExtension_To_corev1_Strategy = convertRuntimeRawExtension[*appsv1.DeploymentStrategy]
297+
conv_runtime_RawExtension_To_corev1_Tolerations = convertRuntimeRawExtension[corev1.Toleration]
298+
conv_runtime_RawExtension_To_corev1_TopologySpreadConstraints = convertRuntimeRawExtension[[]corev1.TopologySpreadConstraint]
299+
conv_runtime_RawExtension_To_corev1_Volume = convertRuntimeRawExtension[corev1.Volume]
300+
conv_runtime_RawExtension_To_corev1_VolumeMount = convertRuntimeRawExtension[corev1.VolumeMount]
301+
302+
// TODO THIS IS BAD AND BROKEN (Will write 0s for unspecified fields and generate invalid options).
303+
// ConsolePartialValues really needs to have ApplyConfigs for most k8s types.
304+
// Upgrade gen partial to pull an overridden type from a comment or field tag?
305+
conv_LivenessProbe_To_ProbeApplyConfiguration = convertViaMarshaling[*LivenessProbe, *ProbeApplyConfiguration]
306+
conv_ProbeApplyConfiguration_To_corev1_Probe = convertViaMarshaling[ProbeApplyConfiguration, corev1.Probe]
307+
conv_ReadinessProbe_To_ProbeApplyConfiguration = convertViaMarshaling[*ReadinessProbe, *ProbeApplyConfiguration]
250308
)
251309

252310
type deepCopier[T any] interface {
@@ -274,3 +332,33 @@ func conv_SecretKeyRefPtr_To_ir_ValueSourcePtr(skr *SecretKeyRef, namespace stri
274332
},
275333
}
276334
}
335+
336+
func convertRuntimeRawExtension[T any](ext *runtime.RawExtension) (T, error) {
337+
if ext == nil {
338+
var zero T
339+
return zero, nil
340+
}
341+
342+
var out T
343+
if err := json.Unmarshal(ext.Raw, &out); err != nil {
344+
var zero T
345+
return zero, errors.Wrapf(err, "unmarshalling %T into %T", ext, zero)
346+
}
347+
return out, nil
348+
}
349+
350+
func convertViaMarshaling[From any, To any](src From) (To, error) {
351+
marshalled, err := json.Marshal(src)
352+
if err != nil {
353+
var zero To
354+
return zero, errors.Wrapf(err, "marshalling: %T", src)
355+
}
356+
357+
var out To
358+
if err := json.Unmarshal(marshalled, &out); err != nil {
359+
var zero To
360+
return zero, errors.Wrapf(err, "unmarshalling %T into %T", src, zero)
361+
}
362+
363+
return out, nil
364+
}

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)