Skip to content

Commit e2b91c1

Browse files
committed
address review comments
- Move to new `Convert` method in clusterctl client interface from the proposed migrate. - Create new conversion package with parsing and conversion utilities - Remove deprecated `migrate` command and related internal migration code Signed-off-by: Satyam Bhardwaj <[email protected]>
1 parent f0dd120 commit e2b91c1

File tree

21 files changed

+1355
-1325
lines changed

21 files changed

+1355
-1325
lines changed

cmd/clusterctl/client/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ type Client interface {
7575
// DescribeCluster returns the object tree representing the status of a Cluster API cluster.
7676
DescribeCluster(ctx context.Context, options DescribeClusterOptions) (*tree.ObjectTree, error)
7777

78+
// Convert converts CAPI core resources between API versions.
79+
// EXPERIMENTAL: This method is experimental and may be removed in a future release.
80+
Convert(ctx context.Context, options ConvertOptions) (ConvertResult, error)
81+
7882
// AlphaClient is an Interface for alpha features in clusterctl
7983
AlphaClient
8084
}

cmd/clusterctl/client/client_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ func (f fakeClient) RolloutResume(ctx context.Context, options RolloutResumeOpti
145145
return f.internalClient.RolloutResume(ctx, options)
146146
}
147147

148+
func (f fakeClient) Convert(ctx context.Context, options ConvertOptions) (ConvertResult, error) {
149+
return f.internalClient.Convert(ctx, options)
150+
}
151+
148152
// newFakeClient returns a clusterctl client that allows to execute tests on a set of fake config, fake repositories and fake clusters.
149153
// you can use WithCluster and WithRepository to prepare for the test case.
150154
func newFakeClient(ctx context.Context, configClient config.Client) *fakeClient {

cmd/clusterctl/client/convert.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package client
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
24+
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
25+
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/convert"
26+
)
27+
28+
var (
29+
// sourceGroupVersions defines the source GroupVersions that should be converted.
30+
sourceGroupVersions = []schema.GroupVersion{
31+
clusterv1.GroupVersion,
32+
}
33+
34+
// knownAPIGroups defines all known API groups for resource classification.
35+
knownAPIGroups = []string{
36+
clusterv1.GroupVersion.Group,
37+
}
38+
)
39+
40+
// ConvertOptions carries the options supported by Convert.
41+
type ConvertOptions struct {
42+
// Input is the YAML content to convert.
43+
Input []byte
44+
45+
// ToVersion is the target API version to convert to (e.g., "v1beta2").
46+
ToVersion string
47+
}
48+
49+
// ConvertResult contains the result of a conversion operation.
50+
type ConvertResult struct {
51+
// Output is the converted YAML content.
52+
Output []byte
53+
54+
// Messages contains informational messages from the conversion.
55+
Messages []string
56+
}
57+
58+
// Convert converts CAPI core resources between API versions.
59+
func (c *clusterctlClient) Convert(_ context.Context, options ConvertOptions) (ConvertResult, error) {
60+
converter := convert.NewConverter(
61+
clusterv1.GroupVersion.Group, // targetAPIGroup: "cluster.x-k8s.io"
62+
clusterv1.GroupVersion, // targetGV: schema.GroupVersion{Group: "cluster.x-k8s.io", Version: "v1beta2"}
63+
sourceGroupVersions, // sourceGroupVersions
64+
knownAPIGroups, // knownAPIGroups
65+
)
66+
67+
output, msgs, err := converter.Convert(options.Input, options.ToVersion)
68+
if err != nil {
69+
return ConvertResult{}, err
70+
}
71+
72+
return ConvertResult{
73+
Output: output,
74+
Messages: msgs,
75+
}, nil
76+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package convert provides a converter for CAPI core resources between API versions.
18+
package convert
19+
20+
import (
21+
"github.com/pkg/errors"
22+
"k8s.io/apimachinery/pkg/runtime"
23+
"k8s.io/apimachinery/pkg/runtime/schema"
24+
25+
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
26+
"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
27+
)
28+
29+
// SupportedTargetVersions defines all supported target API versions for conversion.
30+
var SupportedTargetVersions = []string{
31+
clusterv1.GroupVersion.Version,
32+
}
33+
34+
// Converter handles the conversion of CAPI core resources between API versions.
35+
type Converter struct {
36+
scheme *runtime.Scheme
37+
targetAPIGroup string
38+
targetGV schema.GroupVersion
39+
sourceGroupVersions []schema.GroupVersion
40+
knownAPIGroups []string
41+
}
42+
43+
// NewConverter creates a new Converter instance.
44+
func NewConverter(targetAPIGroup string, targetGV schema.GroupVersion, sourceGroupVersions []schema.GroupVersion, knownAPIGroups []string) *Converter {
45+
return &Converter{
46+
scheme: scheme.Scheme,
47+
targetAPIGroup: targetAPIGroup,
48+
targetGV: targetGV,
49+
sourceGroupVersions: sourceGroupVersions,
50+
knownAPIGroups: knownAPIGroups,
51+
}
52+
}
53+
54+
// Convert processes multi-document YAML streams and converts resources to the target version.
55+
func (c *Converter) Convert(input []byte, toVersion string) (output []byte, messages []string, err error) {
56+
messages = make([]string, 0)
57+
58+
targetGV := schema.GroupVersion{
59+
Group: c.targetAPIGroup,
60+
Version: toVersion,
61+
}
62+
63+
// Create GVK matcher for resource classification.
64+
matcher := newGVKMatcher(c.sourceGroupVersions, c.knownAPIGroups)
65+
66+
// Parse input YAML stream.
67+
docs, err := parseYAMLStream(input, c.scheme, matcher)
68+
if err != nil {
69+
return nil, nil, errors.Wrap(err, "failed to parse YAML stream")
70+
}
71+
72+
for i := range docs {
73+
doc := &docs[i]
74+
75+
switch doc.typ {
76+
case resourceTypeConvertible:
77+
convertedObj, wasConverted, convErr := convertResource(doc.object, targetGV, c.scheme, c.targetAPIGroup)
78+
if convErr != nil {
79+
return nil, nil, errors.Wrapf(convErr, "failed to convert resource %s at index %d", doc.gvk.String(), doc.index)
80+
}
81+
82+
if wasConverted {
83+
doc.object = convertedObj
84+
} else {
85+
// Resource that are already at target version.
86+
if msg := getInfoMessage(doc.gvk, toVersion, c.targetAPIGroup); msg != "" {
87+
messages = append(messages, msg)
88+
}
89+
}
90+
91+
case resourceTypeKnown:
92+
// Pass through unchanged with info message.
93+
if msg := getInfoMessage(doc.gvk, toVersion, c.targetAPIGroup); msg != "" {
94+
messages = append(messages, msg)
95+
}
96+
97+
case resourceTypePassThrough:
98+
// Non-target API group resource - pass through unchanged with info message.
99+
if msg := getInfoMessage(doc.gvk, toVersion, c.targetAPIGroup); msg != "" {
100+
messages = append(messages, msg)
101+
}
102+
}
103+
}
104+
105+
// Serialize documents back to YAML.
106+
output, err = serializeYAMLStream(docs, c.scheme)
107+
if err != nil {
108+
return nil, nil, errors.Wrap(err, "failed to serialize output")
109+
}
110+
111+
return output, messages, nil
112+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package convert
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
"k8s.io/apimachinery/pkg/runtime/schema"
24+
25+
clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1"
26+
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
27+
)
28+
29+
func TestConverter_Convert(t *testing.T) {
30+
tests := []struct {
31+
name string
32+
input string
33+
toVersion string
34+
wantErr bool
35+
wantConverted bool
36+
}{
37+
{
38+
name: "convert v1beta1 cluster to v1beta2",
39+
input: `apiVersion: cluster.x-k8s.io/v1beta1
40+
kind: Cluster
41+
metadata:
42+
name: test-cluster
43+
namespace: default
44+
spec:
45+
clusterNetwork:
46+
pods:
47+
cidrBlocks:
48+
- 192.168.0.0/16
49+
`,
50+
toVersion: "v1beta2",
51+
wantErr: false,
52+
wantConverted: true,
53+
},
54+
{
55+
name: "pass through v1beta2 cluster unchanged",
56+
input: `apiVersion: cluster.x-k8s.io/v1beta2
57+
kind: Cluster
58+
metadata:
59+
name: test-cluster
60+
namespace: default
61+
spec:
62+
clusterNetwork:
63+
pods:
64+
cidrBlocks:
65+
- 192.168.0.0/16
66+
`,
67+
toVersion: "v1beta2",
68+
wantErr: false,
69+
wantConverted: false,
70+
},
71+
{
72+
name: "pass through non-CAPI resource",
73+
input: `apiVersion: v1
74+
kind: ConfigMap
75+
metadata:
76+
name: test-config
77+
namespace: default
78+
data:
79+
key: value
80+
`,
81+
toVersion: "v1beta2",
82+
wantErr: false,
83+
wantConverted: false,
84+
},
85+
{
86+
name: "convert multi-document YAML",
87+
input: `apiVersion: cluster.x-k8s.io/v1beta1
88+
kind: Cluster
89+
metadata:
90+
name: test-cluster
91+
namespace: default
92+
---
93+
apiVersion: cluster.x-k8s.io/v1beta1
94+
kind: Machine
95+
metadata:
96+
name: test-machine
97+
namespace: default
98+
`,
99+
toVersion: "v1beta2",
100+
wantErr: false,
101+
wantConverted: true,
102+
},
103+
{
104+
name: "invalid YAML",
105+
input: `this is not valid yaml
106+
kind: Cluster
107+
`,
108+
toVersion: "v1beta2",
109+
wantErr: true,
110+
},
111+
}
112+
113+
for _, tt := range tests {
114+
t.Run(tt.name, func(t *testing.T) {
115+
sourceGroupVersions := []schema.GroupVersion{clusterv1beta1.GroupVersion}
116+
knownAPIGroups := []string{clusterv1.GroupVersion.Group}
117+
converter := NewConverter("cluster.x-k8s.io", clusterv1.GroupVersion, sourceGroupVersions, knownAPIGroups)
118+
output, messages, err := converter.Convert([]byte(tt.input), tt.toVersion)
119+
120+
if (err != nil) != tt.wantErr {
121+
t.Errorf("Convert() error = %v, wantErr %v", err, tt.wantErr)
122+
return
123+
}
124+
125+
if tt.wantErr {
126+
return
127+
}
128+
129+
if len(output) == 0 {
130+
t.Error("Convert() returned empty output")
131+
}
132+
133+
// Verify output contains expected version if conversion happened.
134+
if tt.wantConverted {
135+
outputStr := string(output)
136+
if !strings.Contains(outputStr, "cluster.x-k8s.io/v1beta2") {
137+
t.Errorf("Convert() output does not contain v1beta2 version: %s", outputStr)
138+
}
139+
}
140+
141+
// Messages should be non-nil (even if empty).
142+
if messages == nil {
143+
t.Error("Convert() returned nil messages slice")
144+
}
145+
})
146+
}
147+
}

0 commit comments

Comments
 (0)