Support configuring resource requirements for addon agents (#932)

* Support configuring resource requirements for addon agents

Signed-off-by: zhujian <jiazhu@redhat.com>

* Add unit tests

Signed-off-by: zhujian <jiazhu@redhat.com>

* Add e2e

Signed-off-by: zhujian <jiazhu@redhat.com>

* remove mod replace

Signed-off-by: zhujian <jiazhu@redhat.com>

---------

Signed-off-by: zhujian <jiazhu@redhat.com>
This commit is contained in:
Jian Zhu
2025-04-08 10:26:49 +08:00
committed by GitHub
parent 9024cb0baf
commit 67d9d2a5d3
12 changed files with 426 additions and 81 deletions

2
go.mod
View File

@@ -35,7 +35,7 @@ require (
k8s.io/klog/v2 v2.130.1
k8s.io/kube-aggregator v0.31.4
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6
open-cluster-management.io/addon-framework v0.12.0
open-cluster-management.io/addon-framework v0.12.1-0.20250401143304-75b65b5f45e0
open-cluster-management.io/api v0.16.1
open-cluster-management.io/sdk-go v0.16.1-0.20250327091909-6bd6228a47ad
sigs.k8s.io/cluster-inventory-api v0.0.0-20240730014211-ef0154379848

4
go.sum
View File

@@ -487,8 +487,8 @@ k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7F
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6 h1:MDF6h2H/h4tbzmtIKTuctcwZmY0tY9mD9fNT47QO6HI=
k8s.io/utils v0.0.0-20240921022957-49e7df575cb6/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
open-cluster-management.io/addon-framework v0.12.0 h1:5j7mpyk2ij0SLUZkwWk0KkNTWtsid2w7BIHmhm0Ecok=
open-cluster-management.io/addon-framework v0.12.0/go.mod h1:eReMWXrEHqtilwz5wzEpUrWw9Vfz0HJCH9pi3gOTZns=
open-cluster-management.io/addon-framework v0.12.1-0.20250401143304-75b65b5f45e0 h1:1gK9/3EkjYlXwrGLlF8i/wOAJfU6gzg4b9Zje+umIJY=
open-cluster-management.io/addon-framework v0.12.1-0.20250401143304-75b65b5f45e0/go.mod h1:eReMWXrEHqtilwz5wzEpUrWw9Vfz0HJCH9pi3gOTZns=
open-cluster-management.io/api v0.16.1 h1:mS+4UGxHLPQd7CRM0gdFQdVaz139Lo2bkLfqSE0CDNU=
open-cluster-management.io/api v0.16.1/go.mod h1:9erZEWEn4bEqh0nIX2wA7f/s3KCuFycQdBrPrRzi0QM=
open-cluster-management.io/sdk-go v0.16.1-0.20250327091909-6bd6228a47ad h1:37f9TEwX/U8esBjSJvPleDM3rcFpk9NY5e2ItjO6PcQ=

View File

@@ -208,6 +208,7 @@ func (c *addonTemplateController) runController(ctx context.Context, addonName s
templateagent.ToAddOnRegistriesPrivateValues,
templateagent.ToAddOnInstallNamespacePrivateValues,
templateagent.ToAddOnProxyPrivateValues,
templateagent.ToAddOnResourceRequirementsPrivateValues,
),
)
err = mgr.AddAgent(agentAddon)

View File

@@ -233,7 +233,7 @@ func TestRunController(t *testing.T) {
{
name: "fake kubeconfig",
addonName: "test",
expectedErr: `Get "http://localhost/api": dial tcp [::1]:80: connect: connection refused`,
expectedErr: `connect: connection refused`,
},
}
@@ -261,9 +261,10 @@ func TestRunController(t *testing.T) {
ctx := context.TODO()
err := controller.runController(ctx, c.addonName)
if len(c.expectedErr) == 0 {
assert.NoError(t, err)
if err == nil {
assert.Empty(t, c.expectedErr)
} else {
assert.Contains(t, err.Error(), c.expectedErr, "name : %s, expected error %v, but got %v", c.name, c.expectedErr, err)
}
assert.EqualErrorf(t, err, c.expectedErr, "name : %s, expected error %v, but got %v", c.name, c.expectedErr, err)
}
}

View File

@@ -3,6 +3,7 @@ package templateagent
import (
"fmt"
"path"
"regexp"
"strings"
corev1 "k8s.io/api/core/v1"
@@ -106,6 +107,7 @@ func newDeploymentDecorator(
newNodePlacementDecorator(privateValues),
newImageDecorator(privateValues),
newProxyHandler(logger, addonName, privateValues),
newResourceRequirementsDecorator(logger, supportResourceDeployment, privateValues),
},
}
}
@@ -118,7 +120,7 @@ func (d *deploymentDecorator) decorate(obj *unstructured.Unstructured) (*unstruc
}
for _, decorator := range d.decorators {
err = decorator.decorate(&deployment.Spec.Template)
err = decorator.decorate(deployment.Name, &deployment.Spec.Template)
if err != nil {
return obj, err
}
@@ -152,6 +154,7 @@ func newDaemonSetDecorator(
newNodePlacementDecorator(privateValues),
newImageDecorator(privateValues),
newProxyHandler(logger, addonName, privateValues),
newResourceRequirementsDecorator(logger, supportResourceDaemonset, privateValues),
},
}
}
@@ -164,7 +167,7 @@ func (d *daemonSetDecorator) decorate(obj *unstructured.Unstructured) (*unstruct
}
for _, decorator := range d.decorators {
err = decorator.decorate(&daemonSet.Spec.Template)
err = decorator.decorate(daemonSet.Name, &daemonSet.Spec.Template)
if err != nil {
return obj, err
}
@@ -180,7 +183,8 @@ func (d *daemonSetDecorator) decorate(obj *unstructured.Unstructured) (*unstruct
type podTemplateSpecDecorator interface {
// decorate modifies the pod template in place
decorate(pod *corev1.PodTemplateSpec) error
// resourceName is the name of the resource, could be a deployment name or a daemonset name
decorate(resourceName string, pod *corev1.PodTemplateSpec) error
}
type environmentDecorator struct {
@@ -192,7 +196,7 @@ func newEnvironmentDecorator(orderedValues orderedValues) podTemplateSpecDecorat
orderedValues: orderedValues,
}
}
func (d *environmentDecorator) decorate(pod *corev1.PodTemplateSpec) error {
func (d *environmentDecorator) decorate(_ string, pod *corev1.PodTemplateSpec) error {
envVars := make([]corev1.EnvVar, len(d.orderedValues))
for index, value := range d.orderedValues {
envVars[index] = corev1.EnvVar{
@@ -222,7 +226,7 @@ func newVolumeDecorator(addonName string, template *addonapiv1alpha1.AddOnTempla
}
}
func (d *volumeDecorator) decorate(pod *corev1.PodTemplateSpec) error {
func (d *volumeDecorator) decorate(_ string, pod *corev1.PodTemplateSpec) error {
volumeMounts := []corev1.VolumeMount{}
volumes := []corev1.Volume{}
@@ -289,7 +293,7 @@ func newNodePlacementDecorator(privateValues addonfactory.Values) podTemplateSpe
}
}
func (d *nodePlacementDecorator) decorate(pod *corev1.PodTemplateSpec) error {
func (d *nodePlacementDecorator) decorate(_ string, pod *corev1.PodTemplateSpec) error {
nodePlacement, ok := d.privateValues[NodePlacementPrivateValueKey]
if !ok {
return nil
@@ -321,7 +325,7 @@ func newImageDecorator(privateValues addonfactory.Values) podTemplateSpecDecorat
}
}
func (d *imageDecorator) decorate(pod *corev1.PodTemplateSpec) error {
func (d *imageDecorator) decorate(_ string, pod *corev1.PodTemplateSpec) error {
registries, ok := d.privateValues[RegistriesPrivateValueKey]
if !ok {
return nil
@@ -368,7 +372,7 @@ func newProxyHandler(logger klog.Logger, addonName string, privateValues addonfa
}
}
func (d *proxyHandler) decorate(pod *corev1.PodTemplateSpec) error {
func (d *proxyHandler) decorate(name string, pod *corev1.PodTemplateSpec) error {
pc, ok := d.getProxyConfig()
if !ok {
return nil
@@ -398,7 +402,7 @@ func (d *proxyHandler) decorate(pod *corev1.PodTemplateSpec) error {
return nil
}
err := newEnvironmentDecorator(keyValues).decorate(pod)
err := newEnvironmentDecorator(keyValues).decorate(name, pod)
if err != nil {
return err
}
@@ -407,7 +411,7 @@ func (d *proxyHandler) decorate(pod *corev1.PodTemplateSpec) error {
return nil
}
return newCABundleDecorator(d.addonName, pc.CABundle).decorate(pod)
return newCABundleDecorator(d.addonName, pc.CABundle).decorate(name, pod)
}
func (d *proxyHandler) getProxyConfig() (addonapiv1alpha1.ProxyConfig, bool) {
@@ -474,8 +478,8 @@ func newCABundleDecorator(addonName string, caBundle []byte) podTemplateSpecDeco
}
}
func (d *caBundleDecorator) decorate(pod *corev1.PodTemplateSpec) error {
err := d.envDecorator.decorate(pod)
func (d *caBundleDecorator) decorate(name string, pod *corev1.PodTemplateSpec) error {
err := d.envDecorator.decorate(name, pod)
if err != nil {
return err
}
@@ -508,6 +512,60 @@ func (d *caBundleDecorator) decorate(pod *corev1.PodTemplateSpec) error {
return nil
}
type supportResource string
const (
supportResourceDeployment supportResource = "deployments"
supportResourceDaemonset supportResource = "daemonsets"
)
type resourceRequirementsDecorator struct {
privateValues addonfactory.Values
resource supportResource // only support daemonsets, deployments for now
logger klog.Logger
}
func newResourceRequirementsDecorator(logger klog.Logger, resource supportResource,
privateValues addonfactory.Values) podTemplateSpecDecorator {
return &resourceRequirementsDecorator{
resource: resource,
privateValues: privateValues,
}
}
func (d *resourceRequirementsDecorator) decorate(name string, pod *corev1.PodTemplateSpec) error {
requirements, ok := d.privateValues[ResourceRequirementsPrivateValueKey]
if !ok {
return nil
}
regexRequirements, ok := requirements.([]addonfactory.RegexResourceRequirements)
if !ok {
return fmt.Errorf("resource requirements value is invalid")
}
for i := range pod.Spec.Containers {
containerID := fmt.Sprintf("%s:%s:%s", d.resource, name, pod.Spec.Containers[i].Name)
// revese the requirements array to make the later elements in the array have higher priority
for j := len(regexRequirements) - 1; j >= 0; j-- {
matched, err := regexp.MatchString(regexRequirements[j].ContainerIDRegex, containerID)
if err != nil {
d.logger.Info("regex match container id failed", "pattern",
regexRequirements[j].ContainerIDRegex, "containerID", containerID)
continue
}
if !matched {
continue
}
pod.Spec.Containers[i].Resources = regexRequirements[j].ResourcesRaw
break
}
}
return nil
}
func hubKubeconfigSecretMountPath() string {
return "/managed/hub-kubeconfig"
}

View File

@@ -6,6 +6,7 @@ import (
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -249,7 +250,177 @@ func TestProxyDecorator(t *testing.T) {
ctx := context.TODO()
logger := klog.FromContext(ctx)
d := newProxyHandler(logger, "addon1", values)
err = d.decorate(tc.pod)
err = d.decorate("", tc.pod)
if err != nil {
t.Fatal(err)
}
tc.validateObject(t, tc.pod)
})
}
}
func TestResourceRequirementsDecorator(t *testing.T) {
tests := []struct {
name string
config addonapiv1alpha1.AddOnDeploymentConfig
resourceName string
pod *corev1.PodTemplateSpec
supportResource supportResource
validateObject func(t *testing.T, pod *corev1.PodTemplateSpec)
}{
{
name: "deployment",
pod: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "c1",
Image: "test",
},
{
Name: "c2",
Image: "test",
},
},
},
},
resourceName: "d1",
supportResource: supportResourceDeployment,
config: addonapiv1alpha1.AddOnDeploymentConfig{
Spec: addonapiv1alpha1.AddOnDeploymentConfigSpec{
ResourceRequirements: []addonapiv1alpha1.ContainerResourceRequirements{
{
ContainerID: "deployments:d1:c1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
},
},
},
},
validateObject: func(t *testing.T, pod *corev1.PodTemplateSpec) {
for _, c := range pod.Spec.Containers {
if c.Name == "c1" {
if c.Resources.Requests.Memory() == nil || c.Resources.Requests.Memory().String() != "128Mi" {
t.Errorf("memory request for c1 is not corrent, got %v", c.Resources)
}
} else {
if c.Resources.Requests.Memory() != nil && c.Resources.Requests.Memory().String() != "0" {
t.Errorf("memory request for other containers should not be set, got %v", c.Resources)
}
}
}
},
},
{
name: "daemonset",
pod: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "c1",
Image: "test",
},
{
Name: "c2",
Image: "test",
},
},
},
},
resourceName: "d1",
supportResource: supportResourceDaemonset,
config: addonapiv1alpha1.AddOnDeploymentConfig{
Spec: addonapiv1alpha1.AddOnDeploymentConfigSpec{
ResourceRequirements: []addonapiv1alpha1.ContainerResourceRequirements{
{
ContainerID: "daemonsets:d1:c1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
},
},
},
},
validateObject: func(t *testing.T, pod *corev1.PodTemplateSpec) {
for _, c := range pod.Spec.Containers {
if c.Name == "c1" {
if c.Resources.Requests.Memory() == nil || c.Resources.Requests.Memory().String() != "128Mi" {
t.Errorf("memory request for c1 is not corrent, got %v", c.Resources)
}
} else {
if c.Resources.Requests.Memory() != nil && c.Resources.Requests.Memory().String() != "0" {
t.Errorf("memory request for other containers should not be set, got %v", c.Resources)
}
}
}
},
},
{
name: "regex match",
pod: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "c1",
Image: "test",
},
{
Name: "c2",
Image: "test",
},
},
},
},
resourceName: "d1",
supportResource: supportResourceDeployment,
config: addonapiv1alpha1.AddOnDeploymentConfig{
Spec: addonapiv1alpha1.AddOnDeploymentConfigSpec{
ResourceRequirements: []addonapiv1alpha1.ContainerResourceRequirements{
{
ContainerID: "deployments:d1:c1",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("128Mi"),
},
},
},
{
ContainerID: "*:*:*",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("256Mi"),
},
},
},
},
},
},
validateObject: func(t *testing.T, pod *corev1.PodTemplateSpec) {
for _, c := range pod.Spec.Containers {
if c.Resources.Requests.Memory() == nil || c.Resources.Requests.Memory().String() != "256Mi" {
t.Errorf("memory request for c1 is not corrent, got %v", c.Resources)
}
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
values, err := ToAddOnResourceRequirementsPrivateValues(tc.config)
if err != nil {
t.Fatal(err)
}
ctx := context.TODO()
logger := klog.FromContext(ctx)
d := newResourceRequirementsDecorator(logger, tc.supportResource, values)
err = d.decorate(tc.resourceName, tc.pod)
if err != nil {
t.Fatal(err)
}

View File

@@ -27,12 +27,21 @@ import (
const (
// Private value keys that are used internally by the addon template controller, should not be exposed to users.
// All private value keys should begin with "__"
NodePlacementPrivateValueKey = "__NODE_PLACEMENT"
RegistriesPrivateValueKey = "__REGISTRIES"
InstallNamespacePrivateValueKey = "__INSTALL_NAMESPACE"
ProxyPrivateValueKey = "__PROXY"
NodePlacementPrivateValueKey = "__NODE_PLACEMENT"
RegistriesPrivateValueKey = "__REGISTRIES"
InstallNamespacePrivateValueKey = "__INSTALL_NAMESPACE"
ProxyPrivateValueKey = "__PROXY"
ResourceRequirementsPrivateValueKey = "__RESOURCE_REQUIREMENTS"
)
var PrivateValuesKeys = map[string]struct{}{
NodePlacementPrivateValueKey: {},
RegistriesPrivateValueKey: {},
InstallNamespacePrivateValueKey: {},
ProxyPrivateValueKey: {},
ResourceRequirementsPrivateValueKey: {},
}
// templateBuiltinValues includes the built-in values for crd template agentAddon.
// the values for template config should begin with an uppercase letter, so we need
// to convert it to Values by JsonStructToValues.

View File

@@ -58,6 +58,19 @@ func ToAddOnProxyPrivateValues(config addonapiv1alpha1.AddOnDeploymentConfig) (a
}, nil
}
func ToAddOnResourceRequirementsPrivateValues(config addonapiv1alpha1.AddOnDeploymentConfig) (addonfactory.Values, error) {
if config.Spec.ResourceRequirements == nil {
return nil, nil
}
requirements, err := addonfactory.GetRegexResourceRequirements(config.Spec.ResourceRequirements)
if err != nil {
return nil, err
}
return addonfactory.Values{
ResourceRequirementsPrivateValueKey: requirements,
}, nil
}
type keyValuePair struct {
name string
value string
@@ -84,13 +97,6 @@ func (a *CRDTemplateAgentAddon) getValues(
}
overrideValues = addonfactory.MergeValues(defaultValues, overrideValues)
privateValuesKeys := map[string]struct{}{
NodePlacementPrivateValueKey: {},
RegistriesPrivateValueKey: {},
InstallNamespacePrivateValueKey: {},
ProxyPrivateValueKey: {},
}
for i := 0; i < len(a.getValuesFuncs); i++ {
if a.getValuesFuncs[i] != nil {
userValues, err := a.getValuesFuncs[i](cluster, addon)
@@ -100,7 +106,7 @@ func (a *CRDTemplateAgentAddon) getValues(
publicValues := map[string]interface{}{}
for k, v := range userValues {
if _, ok := privateValuesKeys[k]; ok {
if _, ok := PrivateValuesKeys[k]; ok {
privateValues[k] = v
continue
}

View File

@@ -13,6 +13,7 @@ import (
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -33,13 +34,14 @@ import (
)
const (
nodePlacementDeploymentConfigName = "node-placement-deploy-config"
imageOverrideDeploymentConfigName = "image-override-deploy-config"
namespaceOverrideConfigName = "namespace-override-config"
proxyDeploymentConfigName = "proxy-deploy-config"
originalImageValue = "quay.io/open-cluster-management/addon-examples:latest"
overrideImageValue = "quay.io/ocm/addon-examples:latest"
customSignerName = "example.com/signer-name"
nodePlacementDeploymentConfigName = "node-placement-deploy-config"
imageOverrideDeploymentConfigName = "image-override-deploy-config"
namespaceOverrideConfigName = "namespace-override-config"
proxyDeploymentConfigName = "proxy-deploy-config"
resourceRequirementsDeploymentConfigName = "resource-requirements-deploy-config"
originalImageValue = "quay.io/open-cluster-management/addon-examples:latest"
overrideImageValue = "quay.io/ocm/addon-examples:latest"
customSignerName = "example.com/signer-name"
//#nosec G101
customSignerSecretName = "addon-signer-secret"
)
@@ -59,6 +61,16 @@ var (
NoProxy: "localhost",
CABundle: []byte("test-ca-bundle"),
}
resourceRequirementsConfig = []addonapiv1alpha1.ContainerResourceRequirements{
{
ContainerID: "*:*:helloworld-agent",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("64Mi"),
},
},
},
}
)
var _ = ginkgo.Describe("Enable addon management feature gate", ginkgo.Ordered, ginkgo.Label("addon-manager"), func() {
@@ -744,6 +756,71 @@ var _ = ginkgo.Describe("Enable addon management feature gate", ginkgo.Ordered,
gomega.Expect(cm.Data["ca-bundle.crt"]).To(gomega.Equal(string(proxyConfig.CABundle)))
})
ginkgo.It("Template type addon should be configured by addon deployment config for resource requirement", func() {
ginkgo.By("Prepare a AddOnDeploymentConfig for addon resource requirement config")
gomega.Eventually(func() error {
return prepareResourceRequirementsAddOnDeploymentConfig(universalClusterName, addonInstallNamespace)
}).ShouldNot(gomega.HaveOccurred())
ginkgo.By("Add the configs to ManagedClusterAddOn")
gomega.Eventually(func() error {
addon, err := hub.AddonClient.AddonV1alpha1().ManagedClusterAddOns(universalClusterName).Get(
context.Background(), addOnName, metav1.GetOptions{})
if err != nil {
return err
}
newAddon := addon.DeepCopy()
newAddon.Spec.Configs = []addonapiv1alpha1.AddOnConfig{
{
ConfigGroupResource: addonapiv1alpha1.ConfigGroupResource{
Group: "addon.open-cluster-management.io",
Resource: "addondeploymentconfigs",
},
ConfigReferent: addonapiv1alpha1.ConfigReferent{
Namespace: universalClusterName,
Name: resourceRequirementsDeploymentConfigName,
},
},
}
_, err = hub.AddonClient.AddonV1alpha1().ManagedClusterAddOns(universalClusterName).Update(
context.Background(), newAddon, metav1.UpdateOptions{})
if err != nil {
return err
}
return nil
}).ShouldNot(gomega.HaveOccurred())
ginkgo.By("Make sure addon is configured")
gomega.Eventually(func() error {
agentDeploy, err := spoke.KubeClient.AppsV1().Deployments(addonInstallNamespace).Get(
context.Background(), "hello-template-agent", metav1.GetOptions{})
if err != nil {
return err
}
for _, container := range agentDeploy.Spec.Template.Spec.Containers {
if container.Name == "helloworld-agent" {
if !equality.Semantic.DeepEqual(container.Resources, resourceRequirementsConfig[0].Resources) {
return fmt.Errorf("unexpected resource requirements for deployment: %v", container.Resources)
}
}
}
agentDaemonset, err := spoke.KubeClient.AppsV1().DaemonSets(addonInstallNamespace).Get(
context.Background(), "hello-template-agent-ds", metav1.GetOptions{})
if err != nil {
return err
}
for _, container := range agentDaemonset.Spec.Template.Spec.Containers {
if container.Name == "helloworld-agent" {
if !equality.Semantic.DeepEqual(container.Resources, resourceRequirementsConfig[0].Resources) {
return fmt.Errorf("unexpected resource requirements for daemonset: %v", container.Resources)
}
}
}
return nil
}).ShouldNot(gomega.HaveOccurred())
})
})
func prepareInstallNamespace(namespace, installNamespace string) error {
@@ -860,6 +937,34 @@ func prepareProxyAddOnDeploymentConfig(namespace, installNamespace string) error
return err
}
func prepareResourceRequirementsAddOnDeploymentConfig(namespace, installNamespace string) error {
_, err := hub.AddonClient.AddonV1alpha1().AddOnDeploymentConfigs(namespace).Get(
context.Background(), resourceRequirementsDeploymentConfigName, metav1.GetOptions{})
if errors.IsNotFound(err) {
if _, err := hub.AddonClient.AddonV1alpha1().AddOnDeploymentConfigs(namespace).Create(
context.Background(),
&addonapiv1alpha1.AddOnDeploymentConfig{
ObjectMeta: metav1.ObjectMeta{
Name: resourceRequirementsDeploymentConfigName,
Namespace: namespace,
},
Spec: addonapiv1alpha1.AddOnDeploymentConfigSpec{
AgentInstallNamespace: installNamespace,
ProxyConfig: proxyConfig,
ResourceRequirements: resourceRequirementsConfig,
},
},
metav1.CreateOptions{},
); err != nil {
return err
}
return nil
}
return err
}
func copySignerSecret(ctx context.Context, kubeClient kubernetes.Interface, srcNs, srcName, dstNs, dstName string) error {
src, err := kubeClient.CoreV1().Secrets(srcNs).Get(context.Background(), srcName, metav1.GetOptions{})
if err != nil {

2
vendor/modules.txt vendored
View File

@@ -1692,7 +1692,7 @@ k8s.io/utils/pointer
k8s.io/utils/ptr
k8s.io/utils/strings/slices
k8s.io/utils/trace
# open-cluster-management.io/addon-framework v0.12.0
# open-cluster-management.io/addon-framework v0.12.1-0.20250401143304-75b65b5f45e0
## explicit; go 1.22.0
open-cluster-management.io/addon-framework/pkg/addonfactory
open-cluster-management.io/addon-framework/pkg/addonmanager

View File

@@ -27,13 +27,16 @@ type resourceRequirements struct {
Requests map[string]string `json:"requests,omitempty"`
}
// regexResourceRequirements defines a resource requirement rule for containers. A container is eligible for the
// RegexResourceRequirements defines a resource requirement rule for containers. A container is eligible for the
// specified resource requirements if its container ID matches the regular expression.
type regexResourceRequirements struct {
type RegexResourceRequirements struct {
// ContainerIDRegex is the regular expression used to match container IDs.
ContainerIDRegex string `json:"containerIDRegex"`
// Resources defines the resource requirements for matched containers
// Resources defines the resource requirements for matched containers, its resource value is plain string
Resources resourceRequirements `json:"resources"`
// ResourcesRaw defines the resource requirements for matched containers, its resource value is structure
ResourcesRaw corev1.ResourceRequirements `json:"resourcesRaw"`
}
// ToAddOnNodePlacementValues only transform the AddOnDeploymentConfig NodePlacement part into Values object that has
@@ -139,13 +142,13 @@ func ToAddOnResourceRequirementsValues(config addonapiv1alpha1.AddOnDeploymentCo
return nil, nil
}
resourceRequirements, err := getRegexResourceRequirements(config.Spec.ResourceRequirements)
resourceRequirements, err := GetRegexResourceRequirements(config.Spec.ResourceRequirements)
if err != nil {
return nil, err
}
type global struct {
ResourceRequirements []regexResourceRequirements `json:"resourceRequirements"`
ResourceRequirements []RegexResourceRequirements `json:"resourceRequirements"`
}
jsonStruct := struct {
@@ -221,8 +224,8 @@ func GetAddOnDeploymentConfigValues(
}
}
func getRegexResourceRequirements(requirements []addonapiv1alpha1.ContainerResourceRequirements) ([]regexResourceRequirements, error) {
newRequirements := []regexResourceRequirements{}
func GetRegexResourceRequirements(requirements []addonapiv1alpha1.ContainerResourceRequirements) ([]RegexResourceRequirements, error) {
newRequirements := []RegexResourceRequirements{}
for _, item := range requirements {
// convert container ID to regex
parts := strings.Split(item.ContainerID, ":")
@@ -234,12 +237,13 @@ func getRegexResourceRequirements(requirements []addonapiv1alpha1.ContainerResou
parts[index] = ".+"
}
}
newRequirements = append(newRequirements, regexResourceRequirements{
newRequirements = append(newRequirements, RegexResourceRequirements{
ContainerIDRegex: fmt.Sprintf("^%s:%s:%s$", parts[0], parts[1], parts[2]),
Resources: resourceRequirements{
Requests: toStringResourceList(item.Resources.Requests),
Limits: toStringResourceList(item.Resources.Limits),
},
ResourcesRaw: item.Resources,
})
}
@@ -294,7 +298,7 @@ func ToAddOnDeploymentConfigValues(config addonapiv1alpha1.AddOnDeploymentConfig
}
// load ResourceRequirements settings
resourceRequirements, err := getRegexResourceRequirements(config.Spec.ResourceRequirements)
resourceRequirements, err := GetRegexResourceRequirements(config.Spec.ResourceRequirements)
if err != nil {
return nil, err
}

View File

@@ -229,22 +229,7 @@ func ConvertToDeployment(obj runtime.Object) (*appsv1.Deployment, error) {
return deployment, nil
}
if obj.GetObjectKind().GroupVersionKind().Group != appsv1.GroupName ||
obj.GetObjectKind().GroupVersionKind().Kind != "Deployment" {
return nil, fmt.Errorf("not deployment object, %v", obj.GetObjectKind())
}
deployment := &appsv1.Deployment{}
uobj, ok := obj.(*unstructured.Unstructured)
if !ok {
return deployment, fmt.Errorf("not unstructured object, %v", obj.GetObjectKind())
}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(uobj.Object, deployment)
if err != nil {
return nil, err
}
return deployment, nil
return ConvertTo[appsv1.Deployment](obj, appsv1.GroupName, "Deployment")
}
func DeploymentWellKnowManifestConfig(namespace, name string) workapiv1.ManifestConfigOption {
@@ -284,24 +269,29 @@ func ConvertToDaemonSet(obj runtime.Object) (*appsv1.DaemonSet, error) {
return daemonSet, nil
}
if obj.GetObjectKind().GroupVersionKind().Group != appsv1.GroupName ||
obj.GetObjectKind().GroupVersionKind().Kind != "DaemonSet" {
return nil, fmt.Errorf("not daemonset object, %v", obj.GetObjectKind())
}
daemonSet := &appsv1.DaemonSet{}
uobj, ok := obj.(*unstructured.Unstructured)
if !ok {
return daemonSet, fmt.Errorf("not unstructured object, %v", obj.GetObjectKind())
}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(uobj.Object, daemonSet)
if err != nil {
return nil, err
}
return daemonSet, nil
return ConvertTo[appsv1.DaemonSet](obj, appsv1.GroupName, "DaemonSet")
}
func DaemonSetWellKnowManifestConfig(namespace, name string) workapiv1.ManifestConfigOption {
return WellKnowManifestConfig(appsv1.GroupName, "daemonsets", namespace, name)
}
func ConvertTo[T any](obj runtime.Object, group, kind string) (*T, error) {
// Verify GroupVersionKind matches expected values
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Group != group || gvk.Kind != kind {
return nil, fmt.Errorf("not %s object, %v", kind, obj.GetObjectKind())
}
// Handle unstructured conversion
uobj, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("not unstructured object, %v", obj.GetObjectKind())
}
// Create new instance of target type and convert
target := new(T)
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(uobj.Object, target); err != nil {
return nil, err
}
return target, nil
}