Skip to content

Commit 6ebb956

Browse files
qiujian16claude
andcommitted
Add --config-file flag to addon enable command
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) Co-Authored-By: Claude <[email protected]> Signed-off-by: Jian Qiu <[email protected]>
1 parent ea3fc6d commit 6ebb956

File tree

6 files changed

+369
-3
lines changed

6 files changed

+369
-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: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ package enable
44
import (
55
"context"
66
"fmt"
7+
"os"
78
"strings"
89

910
"github.com/spf13/cobra"
1011
"k8s.io/apimachinery/pkg/api/errors"
1112
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1214
"k8s.io/apimachinery/pkg/util/sets"
15+
"k8s.io/apimachinery/pkg/util/yaml"
1316
"k8s.io/klog/v2"
1417
addonv1alpha1 "open-cluster-management.io/api/addon/v1alpha1"
1518
addonclientset "open-cluster-management.io/api/client/addon/clientset/versioned"
1619
clusterclientset "open-cluster-management.io/api/client/cluster/clientset/versioned"
1720
"open-cluster-management.io/clusteradm/pkg/helpers/parse"
21+
"open-cluster-management.io/clusteradm/pkg/helpers/reader"
1822
)
1923

2024
type ClusterAddonInfo struct {
@@ -24,7 +28,90 @@ type ClusterAddonInfo struct {
2428
Annotations map[string]string
2529
}
2630

27-
func NewClusterAddonInfo(cn string, o *Options, an string) (*addonv1alpha1.ManagedClusterAddOn, error) {
31+
// applyConfigFileAndBuildReferences reads the config file, applies the resources to the cluster,
32+
// and builds AddOnConfig references from the applied resources
33+
func applyConfigFileAndBuildReferences(o *Options) ([]addonv1alpha1.AddOnConfig, error) {
34+
if o.ConfigFile == "" {
35+
return nil, nil
36+
}
37+
38+
// Read the config file
39+
data, err := os.ReadFile(o.ConfigFile)
40+
if err != nil {
41+
return nil, fmt.Errorf("failed to read config file %s: %w", o.ConfigFile, err)
42+
}
43+
44+
// Split the file by YAML document separator to handle multiple sections
45+
yamlSections := strings.Split(string(data), "\n---")
46+
var rawResources [][]byte
47+
48+
for _, section := range yamlSections {
49+
section = strings.TrimSpace(section)
50+
if section == "" || section == "---" {
51+
continue
52+
}
53+
rawResources = append(rawResources, []byte(section))
54+
}
55+
56+
if len(rawResources) == 0 {
57+
return nil, nil
58+
}
59+
60+
// Apply the resources to the cluster using ResourceReader
61+
r := reader.NewResourceReader(o.ClusteradmFlags.KubectlFactory, o.ClusteradmFlags.DryRun, o.Streams)
62+
if err := r.ApplyRaw(rawResources); err != nil {
63+
return nil, fmt.Errorf("failed to apply config resources: %w", err)
64+
}
65+
66+
// Parse each applied resource to build AddOnConfig references
67+
var configs []addonv1alpha1.AddOnConfig
68+
restMapper, err := o.ClusteradmFlags.KubectlFactory.ToRESTMapper()
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to get REST mapper: %w", err)
71+
}
72+
73+
for _, rawResource := range rawResources {
74+
// Decode the YAML to unstructured object
75+
obj := &unstructured.Unstructured{}
76+
decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(string(rawResource)), 4096)
77+
if err := decoder.Decode(obj); err != nil {
78+
klog.Warningf("failed to decode resource: %v", err)
79+
continue
80+
}
81+
82+
if obj.GetKind() == "" {
83+
continue
84+
}
85+
86+
// Get the GVK from the object
87+
gvk := obj.GroupVersionKind()
88+
89+
// Use REST mapper to get the resource name (plural form)
90+
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
91+
if err != nil {
92+
klog.Warningf("failed to get REST mapping for %s: %v", gvk.String(), err)
93+
continue
94+
}
95+
96+
// Build AddOnConfig
97+
config := addonv1alpha1.AddOnConfig{
98+
ConfigGroupResource: addonv1alpha1.ConfigGroupResource{
99+
Group: gvk.Group,
100+
Resource: mapping.Resource.Resource,
101+
},
102+
ConfigReferent: addonv1alpha1.ConfigReferent{
103+
Namespace: obj.GetNamespace(),
104+
Name: obj.GetName(),
105+
},
106+
}
107+
108+
configs = append(configs, config)
109+
}
110+
111+
return configs, nil
112+
}
113+
114+
func NewClusterAddonInfo(cn string, o *Options, an string, configs []addonv1alpha1.AddOnConfig) (*addonv1alpha1.ManagedClusterAddOn, error) {
28115
// Parse provided annotations
29116
annos := map[string]string{}
30117
for _, annoString := range o.Annotate {
@@ -51,6 +138,7 @@ func NewClusterAddonInfo(cn string, o *Options, an string) (*addonv1alpha1.Manag
51138
},
52139
Spec: addonv1alpha1.ManagedClusterAddOnSpec{
53140
InstallNamespace: o.Namespace,
141+
Configs: configs,
54142
},
55143
}, nil
56144
}
@@ -115,6 +203,12 @@ func (o *Options) runWithClient(clusterClient clusterclientset.Interface,
115203
}
116204
}
117205

206+
// Apply config file and build AddOnConfig references once for all addons
207+
configs, err := applyConfigFileAndBuildReferences(o)
208+
if err != nil {
209+
return err
210+
}
211+
118212
for _, addon := range addons {
119213
_, err := addonClient.AddonV1alpha1().ClusterManagementAddOns().Get(context.TODO(), addon, metav1.GetOptions{})
120214
if err != nil {
@@ -125,7 +219,7 @@ func (o *Options) runWithClient(clusterClient clusterclientset.Interface,
125219
}
126220

127221
for _, clusterName := range clusters {
128-
cai, err := NewClusterAddonInfo(clusterName, o, addon)
222+
cai, err := NewClusterAddonInfo(clusterName, o, addon, configs)
129223
if err != nil {
130224
return err
131225
}
@@ -155,6 +249,7 @@ func ApplyAddon(addonClient addonclientset.Interface, addon *addonv1alpha1.Manag
155249
originalAddon.Annotations = addon.Annotations
156250
originalAddon.Labels = addon.Labels
157251
originalAddon.Spec.InstallNamespace = addon.Spec.InstallNamespace
252+
originalAddon.Spec.Configs = addon.Spec.Configs
158253
_, err = addonClient.AddonV1alpha1().ManagedClusterAddOns(addon.Namespace).Update(context.TODO(), originalAddon, metav1.UpdateOptions{})
159254
return err
160255
}

0 commit comments

Comments
 (0)