Skip to content

Commit edc0b2a

Browse files
qiujian16claude
andauthored
Add --config-file flag to addon enable command (#512)
This commit implements the feature requested in issue #501 by adding a --config-file flag to the addon enable command. The flag allows users to specify addon configurations that will be applied to the cluster and referenced in the ManagedClusterAddOn resource. Changes: - Added ConfigFile field to Options struct - Added --config-file flag to the enable command - Implemented applyConfigFileAndBuildReferences() function that: - Reads configuration resources from a YAML file - Applies the resources to the cluster using ResourceReader - Uses REST mapper to discover GVR (Group/Version/Resource) - Builds AddOnConfig references with proper group, resource, namespace, and name - Updated NewClusterAddonInfo() to accept configs as a parameter - Updated runWithClient() to apply config file before creating addons - Updated ApplyAddon() to handle configs when updating existing addons - Added comprehensive integration tests for the new functionality - Added TestClientGetter helper for test setup The config file should contain actual Kubernetes resources (e.g., AddOnDeploymentConfig) in YAML format. Multiple resources can be separated by '---'. The implementation automatically discovers the resource type and builds proper references that are set in the ManagedClusterAddOn.Spec.Configs field. Example usage: clusteradm addon enable --names my-addon --clusters cluster1 \ --config-file addon-config.yaml Fixes #501 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Jian Qiu <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent ea3fc6d commit edc0b2a

File tree

6 files changed

+379
-3
lines changed

6 files changed

+379
-3
lines changed

pkg/cmd/addon/disable/exec_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ var _ = ginkgo.Describe("addon disable", func() {
6060
for _, clus := range clusters {
6161
ginkgo.By(fmt.Sprintf("Enabling %s addon on %s cluster in %s namespace", addon, clus, ns))
6262

63-
cai, err := enable.NewClusterAddonInfo(clus, o, addon)
63+
cai, err := enable.NewClusterAddonInfo(clus, o, addon, nil)
6464
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "enable addon error")
6565
err = enable.ApplyAddon(addonClient, cai)
6666
gomega.Expect(err).ToNot(gomega.HaveOccurred(), "enable addon error")

pkg/cmd/addon/enable/cmd.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ var example = `
3232
%[1]s addon enable --names config-policy-controller --namespace namespace --clusters cluster1,cluster2
3333
# Enable config-policy-controller addon for specified clusters
3434
%[1]s addon enable --names config-policy-controller --clusters cluster1,cluster2
35+
36+
## With Configuration File
37+
38+
# Enable addon with configurations from a file
39+
%[1]s addon enable --names my-addon --clusters cluster1 --config-file addon-config.yaml
3540
`
3641

3742
// NewCmd...
@@ -70,6 +75,7 @@ func NewCmd(clusteradmFlags *genericclioptionsclusteradm.ClusteradmFlags, stream
7075
cmd.Flags().StringVar(&o.OutputFile, "output-file", "", "The generated resources will be copied in the specified file")
7176
cmd.Flags().StringSliceVar(&o.Annotate, "annotate", []string{}, "Annotations to add to the ManagedClusterAddon (eg. key1=value1,key2=value2)")
7277
cmd.Flags().StringSliceVar(&o.Labels, "labels", []string{}, "Labels to add to the ManagedClusterAddon (eg. key1=value1,key2=value2)")
78+
cmd.Flags().StringVar(&o.ConfigFile, "config-file", "", "Path to the configuration file containing addon configs (YAML format with group, resource, namespace, and name)")
7379

7480
return cmd
7581
}

pkg/cmd/addon/enable/exec.go

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,21 @@
22
package enable
33

44
import (
5+
"bufio"
6+
"bytes"
57
"context"
68
"fmt"
9+
"io"
10+
"open-cluster-management.io/clusteradm/pkg/helpers/reader"
11+
"os"
712
"strings"
813

914
"github.com/spf13/cobra"
1015
"k8s.io/apimachinery/pkg/api/errors"
1116
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1218
"k8s.io/apimachinery/pkg/util/sets"
19+
"k8s.io/apimachinery/pkg/util/yaml"
1320
"k8s.io/klog/v2"
1421
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
1522
addonclientset "open-cluster-management.io/api/client/addon/clientset/versioned"
@@ -24,7 +31,95 @@ type ClusterAddonInfo struct {
2431
Annotations map[string]string
2532
}
2633

27-
func NewClusterAddonInfo(cn string, o *Options, an string) (*addonv1alpha1.ManagedClusterAddOn, error) {
34+
// applyConfigFileAndBuildReferences reads the config file, applies the resources to the cluster,
35+
// and builds AddOnConfig references from the applied resources
36+
func applyConfigFileAndBuildReferences(o *Options) ([]addonv1alpha1.AddOnConfig, error) {
37+
if o.ConfigFile == "" {
38+
return nil, nil
39+
}
40+
41+
// Read the config file
42+
data, err := os.ReadFile(o.ConfigFile)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to read config file %s: %w", o.ConfigFile, err)
45+
}
46+
47+
yamlReader := yaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(data)))
48+
var rawResources [][]byte
49+
50+
for {
51+
doc, err := yamlReader.Read()
52+
if err != nil {
53+
if err == io.EOF {
54+
break
55+
}
56+
return nil, fmt.Errorf("failed to parse config file %s: %w", o.ConfigFile, err)
57+
}
58+
if len(bytes.TrimSpace(doc)) == 0 {
59+
continue
60+
}
61+
rawResources = append(rawResources, doc)
62+
}
63+
64+
if len(rawResources) == 0 {
65+
return nil, nil
66+
}
67+
68+
// Apply the resources to the cluster using ResourceReader
69+
r := reader.NewResourceReader(o.ClusteradmFlags.KubectlFactory, o.ClusteradmFlags.DryRun, o.Streams)
70+
if err := r.ApplyRaw(rawResources); err != nil {
71+
return nil, fmt.Errorf("failed to apply config resources: %w", err)
72+
}
73+
74+
// Parse each applied resource to build AddOnConfig references
75+
var configs []addonv1alpha1.AddOnConfig
76+
restMapper, err := o.ClusteradmFlags.KubectlFactory.ToRESTMapper()
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to get REST mapper: %w", err)
79+
}
80+
81+
for _, rawResource := range rawResources {
82+
// Decode the YAML to unstructured object
83+
obj := &unstructured.Unstructured{}
84+
decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(string(rawResource)), 4096)
85+
if err := decoder.Decode(obj); err != nil {
86+
klog.Warningf("failed to decode resource: %v", err)
87+
continue
88+
}
89+
90+
if obj.GetKind() == "" {
91+
continue
92+
}
93+
94+
// Get the GVK from the object
95+
gvk := obj.GroupVersionKind()
96+
97+
// Use REST mapper to get the resource name (plural form)
98+
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
99+
if err != nil {
100+
klog.Warningf("failed to get REST mapping for %s: %v", gvk.String(), err)
101+
continue
102+
}
103+
104+
// Build AddOnConfig
105+
config := addonv1alpha1.AddOnConfig{
106+
ConfigGroupResource: addonv1alpha1.ConfigGroupResource{
107+
Group: gvk.Group,
108+
Resource: mapping.Resource.Resource,
109+
},
110+
ConfigReferent: addonv1alpha1.ConfigReferent{
111+
Namespace: obj.GetNamespace(),
112+
Name: obj.GetName(),
113+
},
114+
}
115+
116+
configs = append(configs, config)
117+
}
118+
119+
return configs, nil
120+
}
121+
122+
func NewClusterAddonInfo(cn string, o *Options, an string, configs []addonv1alpha1.AddOnConfig) (*addonv1alpha1.ManagedClusterAddOn, error) {
28123
// Parse provided annotations
29124
annos := map[string]string{}
30125
for _, annoString := range o.Annotate {
@@ -51,6 +146,7 @@ func NewClusterAddonInfo(cn string, o *Options, an string) (*addonv1alpha1.Manag
51146
},
52147
Spec: addonv1alpha1.ManagedClusterAddOnSpec{
53148
InstallNamespace: o.Namespace,
149+
Configs: configs,
54150
},
55151
}, nil
56152
}
@@ -115,6 +211,12 @@ func (o *Options) runWithClient(clusterClient clusterclientset.Interface,
115211
}
116212
}
117213

214+
// Apply config file and build AddOnConfig references once for all addons
215+
configs, err := applyConfigFileAndBuildReferences(o)
216+
if err != nil {
217+
return err
218+
}
219+
118220
for _, addon := range addons {
119221
_, err := addonClient.AddonV1alpha1().ClusterManagementAddOns().Get(context.TODO(), addon, metav1.GetOptions{})
120222
if err != nil {
@@ -125,7 +227,7 @@ func (o *Options) runWithClient(clusterClient clusterclientset.Interface,
125227
}
126228

127229
for _, clusterName := range clusters {
128-
cai, err := NewClusterAddonInfo(clusterName, o, addon)
230+
cai, err := NewClusterAddonInfo(clusterName, o, addon, configs)
129231
if err != nil {
130232
return err
131233
}
@@ -155,6 +257,9 @@ func ApplyAddon(addonClient addonclientset.Interface, addon *addonv1alpha1.Manag
155257
originalAddon.Annotations = addon.Annotations
156258
originalAddon.Labels = addon.Labels
157259
originalAddon.Spec.InstallNamespace = addon.Spec.InstallNamespace
260+
if addon.Spec.Configs != nil {
261+
originalAddon.Spec.Configs = addon.Spec.Configs
262+
}
158263
_, err = addonClient.AddonV1alpha1().ManagedClusterAddOns(addon.Namespace).Update(context.TODO(), originalAddon, metav1.UpdateOptions{})
159264
return err
160265
}

0 commit comments

Comments
 (0)