mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-02-14 18:09:57 +00:00
✨ 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:
2
go.mod
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
2
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
54
vendor/open-cluster-management.io/addon-framework/pkg/utils/probe_helper.go
generated
vendored
54
vendor/open-cluster-management.io/addon-framework/pkg/utils/probe_helper.go
generated
vendored
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user