Skip to content

Commit dab9772

Browse files
authored
support cluster import config secret (#1170)
Signed-off-by: Zhiwei Yin <zyin@redhat.com>
1 parent cca5ba8 commit dab9772

File tree

14 files changed

+417
-29
lines changed

14 files changed

+417
-29
lines changed

deploy/cluster-manager/chart/cluster-manager/templates/cluster_role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ rules:
3232
- "work-driver-config"
3333
- "open-cluster-management-image-pull-credentials"
3434
- "grpc-server-serving-cert"
35+
- "cluster-import-config"
3536
- apiGroups: [""]
3637
resources: ["secrets"]
3738
verbs: ["create"]

deploy/cluster-manager/config/rbac/cluster_role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ rules:
3434
- "work-driver-config"
3535
- "open-cluster-management-image-pull-credentials"
3636
- "grpc-server-serving-cert"
37+
- "cluster-import-config"
3738
- apiGroups: [""]
3839
resources: ["secrets"]
3940
verbs: ["create"]

deploy/cluster-manager/olm-catalog/latest/manifests/cluster-manager.clusterserviceversion.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ metadata:
5959
categories: Integration & Delivery,OpenShift Optional
6060
certified: "false"
6161
containerImage: quay.io/open-cluster-management/registration-operator:latest
62-
createdAt: "2025-09-03T08:04:12Z"
62+
createdAt: "2025-09-08T08:15:29Z"
6363
description: Manages the installation and upgrade of the ClusterManager.
6464
operators.operatorframework.io/builder: operator-sdk-v1.32.0
6565
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3
@@ -158,6 +158,7 @@ spec:
158158
- work-driver-config
159159
- open-cluster-management-image-pull-credentials
160160
- grpc-server-serving-cert
161+
- cluster-import-config
161162
resources:
162163
- secrets
163164
verbs:

deploy/klusterlet/olm-catalog/latest/manifests/klusterlet.clusterserviceversion.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ metadata:
3131
categories: Integration & Delivery,OpenShift Optional
3232
certified: "false"
3333
containerImage: quay.io/open-cluster-management/registration-operator:latest
34-
createdAt: "2025-09-03T08:04:12Z"
34+
createdAt: "2025-09-08T08:15:29Z"
3535
description: Manages the installation and upgrade of the Klusterlet.
3636
operators.operatorframework.io/builder: operator-sdk-v1.32.0
3737
operators.operatorframework.io/project_layout: go.kubebuilder.io/v3

manifests/cluster-manager/hub/registration/clusterrole.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,14 @@ rules:
115115
- apiGroups: ["cluster.x-k8s.io"]
116116
resources: ["clusters"]
117117
verbs: ["get", "list", "watch"]
118+
# Allow registration to render the klusterlet manifests from the cluster-import-config
119+
# or the iimage-pull-credentials secret when import a CAPI cluster.
120+
- apiGroups: [""]
121+
resources: ["secrets"]
122+
verbs: ["get"]
123+
resourceNames:
124+
- "open-cluster-management-image-pull-credentials"
125+
- "cluster-import-config"
118126
{{end}}
119127
{{if .ClusterProfileEnabled}}
120128
# Allow hub to manage clusterprofile

pkg/registration/hub/importer/importer.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ import (
3939
)
4040

4141
const (
42-
kluterletNamespace = "open-cluster-management-agent"
42+
klusterletNamespace = "open-cluster-management-agent"
4343
ManagedClusterConditionImported = "ManagedClusterImportSucceeded"
44+
45+
// clusterImportConfigSecret is the name of the secret containing cluster import configuration
46+
clusterImportConfigSecret = "cluster-import-config"
47+
// valuesYamlKey is the key for the values.yaml data in the cluster import config secret
48+
valuesYamlKey = "values.yaml"
4449
)
4550

4651
var (
@@ -55,9 +60,13 @@ func init() {
5560
utilruntime.Must(operatorv1.Install(genericScheme))
5661
}
5762

58-
// KlusterletConfigRenderer renders the config for klusterlet chart.
63+
// KlusterletConfigRenderer renders config for the klusterlet chart.
64+
// Contract:
65+
// - Overlay onto the provided config and return it; do not replace it with a fresh struct.
66+
// - Preserve fields already populated by the caller unless explicitly overridden.
67+
// - Must return a non-nil config if err is nil.
5968
type KlusterletConfigRenderer func(
60-
ctx context.Context, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error)
69+
ctx context.Context, cluster *v1.ManagedCluster, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error)
6170

6271
type Importer struct {
6372
providers []cloudproviders.Interface
@@ -186,7 +195,7 @@ func (i *Importer) reconcile(
186195
},
187196
}
188197
for _, renderer := range i.renders {
189-
klusterletChartConfig, err = renderer(ctx, klusterletChartConfig)
198+
klusterletChartConfig, err = renderer(ctx, cluster, klusterletChartConfig)
190199
if err != nil {
191200
meta.SetStatusCondition(&cluster.Status.Conditions, metav1.Condition{
192201
Type: ManagedClusterConditionImported,
@@ -198,7 +207,7 @@ func (i *Importer) reconcile(
198207
return cluster, err
199208
}
200209
}
201-
crdObjs, rawObjs, err := chart.RenderKlusterletChart(klusterletChartConfig, kluterletNamespace)
210+
crdObjs, rawObjs, err := chart.RenderKlusterletChart(klusterletChartConfig, klusterletNamespace)
202211
if err != nil {
203212
return cluster, err
204213
}
Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
package options
22

3-
import "github.com/spf13/pflag"
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/pflag"
7+
"k8s.io/client-go/kubernetes"
8+
9+
"open-cluster-management.io/ocm/pkg/registration/hub/importer"
10+
)
411

512
type Options struct {
6-
APIServerURL string
7-
AgentImage string
8-
BootstrapSA string
13+
APIServerURL string
14+
AgentImage string
15+
BootstrapSA string
16+
ImporterRenderers []string
917
}
1018

19+
const (
20+
// RenderFromConfigSecret renders klusterlet manifests using configuration from a config secret named cluster-import-config
21+
RenderFromConfigSecret string = "render-from-config-secret"
22+
// RenderAuto renders klusterlet manifests using automatic configuration including bootstrap kubeconfig, agent image, and image pull secret
23+
RenderAuto string = "render-auto"
24+
)
25+
1126
func New() *Options {
1227
return &Options{
13-
BootstrapSA: "open-cluster-management/agent-registration-bootstrap",
28+
BootstrapSA: "open-cluster-management/agent-registration-bootstrap",
29+
ImporterRenderers: []string{RenderFromConfigSecret},
1430
}
1531
}
1632

@@ -23,4 +39,38 @@ func (m *Options) AddFlags(fs *pflag.FlagSet) {
2339
"image is needed.")
2440
fs.StringVar(&m.BootstrapSA, "bootstrap-serviceaccount", m.BootstrapSA,
2541
"Service account used to bootstrap the agent.")
42+
fs.StringSliceVar(&m.ImporterRenderers, "import-renderers", m.ImporterRenderers,
43+
"Ordered list of import renderers applied sequentially to render klusterlet manifests. "+
44+
"Allowed: render-auto, render-from-config-secret. Later renderers may override earlier values.")
45+
}
46+
47+
func GetImporterRenderers(options *Options, kubeClient kubernetes.Interface,
48+
operatorNamespace string) ([]importer.KlusterletConfigRenderer, error) {
49+
var renderers []importer.KlusterletConfigRenderer
50+
if len(options.ImporterRenderers) == 0 {
51+
renderers = append(renderers,
52+
importer.RenderBootstrapHubKubeConfig(kubeClient, options.APIServerURL, options.BootstrapSA),
53+
importer.RenderImage(options.AgentImage),
54+
importer.RenderImagePullSecret(kubeClient, operatorNamespace),
55+
)
56+
return renderers, nil
57+
}
58+
59+
for _, renderer := range options.ImporterRenderers {
60+
switch renderer {
61+
case RenderAuto:
62+
renderers = append(renderers,
63+
importer.RenderBootstrapHubKubeConfig(kubeClient, options.APIServerURL, options.BootstrapSA),
64+
importer.RenderImage(options.AgentImage),
65+
importer.RenderImagePullSecret(kubeClient, operatorNamespace),
66+
)
67+
case RenderFromConfigSecret:
68+
renderers = append(renderers,
69+
importer.RenderFromConfigSecret(kubeClient),
70+
)
71+
default:
72+
return renderers, fmt.Errorf("unknown importer renderer %s", renderer)
73+
}
74+
}
75+
return renderers, nil
2676
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package options
2+
3+
import (
4+
"testing"
5+
6+
"k8s.io/client-go/kubernetes"
7+
)
8+
9+
func TestGetImporterRenderers(t *testing.T) {
10+
type args struct {
11+
options *Options
12+
operatorNamespace string
13+
}
14+
tests := []struct {
15+
name string
16+
args args
17+
wantNum int
18+
wantErr bool
19+
wantErrMsg string
20+
}{
21+
{
22+
name: "empty ImporterRenderers returns default renderers",
23+
args: args{
24+
options: &Options{
25+
APIServerURL: "https://hub.example.com",
26+
AgentImage: "test-image",
27+
BootstrapSA: "test-sa",
28+
ImporterRenderers: []string{},
29+
},
30+
operatorNamespace: "test-ns",
31+
},
32+
wantNum: 3,
33+
wantErr: false,
34+
},
35+
{
36+
name: "ImporterRenderers contains RenderAuto",
37+
args: args{
38+
options: &Options{
39+
APIServerURL: "https://hub.example.com",
40+
AgentImage: "test-image",
41+
BootstrapSA: "test-sa",
42+
ImporterRenderers: []string{RenderAuto},
43+
},
44+
operatorNamespace: "test-ns",
45+
},
46+
wantNum: 3,
47+
wantErr: false,
48+
},
49+
{
50+
name: "ImporterRenderers contains RenderFromConfigSecret",
51+
args: args{
52+
options: &Options{
53+
ImporterRenderers: []string{RenderFromConfigSecret},
54+
},
55+
operatorNamespace: "test-ns",
56+
},
57+
wantNum: 1,
58+
wantErr: false,
59+
},
60+
{
61+
name: "ImporterRenderers contains unknown renderer",
62+
args: args{
63+
options: &Options{
64+
ImporterRenderers: []string{"unknown-renderer"},
65+
},
66+
operatorNamespace: "test-ns",
67+
},
68+
wantNum: 0,
69+
wantErr: true,
70+
wantErrMsg: "unknown importer renderer unknown-renderer",
71+
},
72+
}
73+
74+
// Use a nil kubeClient since the renderer functions do not call methods in these tests
75+
var kubeClient kubernetes.Interface = nil
76+
77+
for _, tt := range tests {
78+
t.Run(tt.name, func(t *testing.T) {
79+
renderers, err := GetImporterRenderers(tt.args.options, kubeClient, tt.args.operatorNamespace)
80+
if tt.wantErr {
81+
if err == nil {
82+
t.Errorf("expected error, got nil")
83+
} else if tt.wantErrMsg != "" && err.Error() != tt.wantErrMsg {
84+
t.Errorf("expected error message %q, got %q", tt.wantErrMsg, err.Error())
85+
}
86+
if len(renderers) != tt.wantNum {
87+
t.Errorf("expected %d renderers, got %d", tt.wantNum, len(renderers))
88+
}
89+
} else {
90+
if err != nil {
91+
t.Errorf("unexpected error: %v", err)
92+
}
93+
if len(renderers) != tt.wantNum {
94+
t.Errorf("expected %d renderers, got %d", tt.wantNum, len(renderers))
95+
}
96+
}
97+
})
98+
}
99+
}

pkg/registration/hub/importer/renderers.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
clientcmdapiv1 "k8s.io/client-go/tools/clientcmd/api/v1"
1515
"k8s.io/utils/ptr"
1616

17+
v1 "open-cluster-management.io/api/cluster/v1"
1718
sdkhelpers "open-cluster-management.io/sdk-go/pkg/helpers"
1819

1920
"open-cluster-management.io/ocm/pkg/operator/helpers/chart"
@@ -23,7 +24,7 @@ const imagePullSecretName = "open-cluster-management-image-pull-credentials"
2324

2425
func RenderBootstrapHubKubeConfig(
2526
kubeClient kubernetes.Interface, apiServerURL, bootstrapSA string) KlusterletConfigRenderer {
26-
return func(ctx context.Context, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error) {
27+
return func(ctx context.Context, _ *v1.ManagedCluster, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error) {
2728
// get bootstrap token
2829
bootstrapSANamespace, bootstrapSAName, err := cache.SplitMetaNamespaceKey(bootstrapSA)
2930
if err != nil {
@@ -39,7 +40,7 @@ func RenderBootstrapHubKubeConfig(
3940
}, metav1.CreateOptions{})
4041
if err != nil {
4142
return config, fmt.Errorf(
42-
"failed to get token from sa %s/%s: %v", bootstrapSANamespace, bootstrapSA, err)
43+
"failed to get token from sa %s/%s: %v", bootstrapSANamespace, bootstrapSAName, err)
4344
}
4445

4546
// get apisever url
@@ -102,7 +103,7 @@ func RenderBootstrapHubKubeConfig(
102103
}
103104

104105
func RenderImage(image string) KlusterletConfigRenderer {
105-
return func(ctx context.Context, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error) {
106+
return func(ctx context.Context, _ *v1.ManagedCluster, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error) {
106107
if len(image) == 0 {
107108
return config, nil
108109
}
@@ -114,7 +115,7 @@ func RenderImage(image string) KlusterletConfigRenderer {
114115
}
115116

116117
func RenderImagePullSecret(kubeClient kubernetes.Interface, namespace string) KlusterletConfigRenderer {
117-
return func(ctx context.Context, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error) {
118+
return func(ctx context.Context, _ *v1.ManagedCluster, config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error) {
118119
secret, err := kubeClient.CoreV1().Secrets(namespace).Get(ctx, imagePullSecretName, metav1.GetOptions{})
119120
switch {
120121
case errors.IsNotFound(err):
@@ -132,3 +133,30 @@ func RenderImagePullSecret(kubeClient kubernetes.Interface, namespace string) Kl
132133
return config, nil
133134
}
134135
}
136+
137+
func RenderFromConfigSecret(kubeClient kubernetes.Interface) KlusterletConfigRenderer {
138+
return func(ctx context.Context, cluster *v1.ManagedCluster,
139+
config *chart.KlusterletChartConfig) (*chart.KlusterletChartConfig, error) {
140+
if cluster == nil {
141+
return nil, fmt.Errorf("cluster must not be nil")
142+
}
143+
144+
configSecret, err := kubeClient.CoreV1().Secrets(cluster.Name).
145+
Get(ctx, clusterImportConfigSecret, metav1.GetOptions{})
146+
if err != nil {
147+
return nil, err
148+
}
149+
valuesRaw := configSecret.Data[valuesYamlKey]
150+
if len(valuesRaw) == 0 {
151+
return nil, fmt.Errorf("no values found in secret %s/%s in namespace %s",
152+
clusterImportConfigSecret, valuesYamlKey, cluster.Name)
153+
}
154+
155+
err = yaml.Unmarshal(valuesRaw, config)
156+
if err != nil {
157+
return nil, fmt.Errorf("the values in the cluster import config secret is invalid: %v", err)
158+
}
159+
160+
return config, nil
161+
}
162+
}

0 commit comments

Comments
 (0)