Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/pr-created.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ on:
types: [opened, reopened, synchronize, ready_for_review]
paths-ignore:
- '*.md'
- '*.yaml'
- '.github/workflows/*'

concurrency:
Expand All @@ -22,3 +21,13 @@ jobs:
CGO_ENABLED: 0
GO_VERSION: "1.25"
secrets: inherit

integration-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Build with integration tag
run: go test -tags=integration -run=^$ ./...
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ type NodeAgentAutoscalerConfig struct {
ReconcileInterval time.Duration `json:"reconcileInterval" mapstructure:"reconcileInterval"`
TemplatePath string `json:"templatePath" mapstructure:"templatePath"`
OperatorDeploymentName string `json:"operatorDeploymentName" mapstructure:"operatorDeploymentName"`
GoMemLimitPercentage float64 `json:"goMemLimitPercentage" mapstructure:"goMemLimitPercentage"`
}

type Server struct {
Expand Down Expand Up @@ -316,6 +317,7 @@ func LoadConfig(path string) (Config, error) {
viper.SetDefault("nodeAgentAutoscaler.reconcileInterval", 5*time.Minute)
viper.SetDefault("nodeAgentAutoscaler.templatePath", "/etc/templates/daemonset-template.yaml")
viper.SetDefault("nodeAgentAutoscaler.operatorDeploymentName", "operator")
viper.SetDefault("nodeAgentAutoscaler.goMemLimitPercentage", 0.8)

viper.AutomaticEnv()

Expand Down
1 change: 1 addition & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func TestLoadConfig(t *testing.T) {
ReconcileInterval: 5 * time.Minute,
TemplatePath: "/etc/templates/daemonset-template.yaml",
OperatorDeploymentName: "operator",
GoMemLimitPercentage: 0.8,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion nodeagentautoscaler/autoscaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type Autoscaler struct {

// NewAutoscaler creates a new Autoscaler instance
func NewAutoscaler(client kubernetes.Interface, cfg config.NodeAgentAutoscalerConfig, namespace string, operatorDeploymentName string) (*Autoscaler, error) {
templateRenderer, err := NewTemplateRenderer(cfg.TemplatePath)
templateRenderer, err := NewTemplateRenderer(cfg.TemplatePath, cfg.GoMemLimitPercentage)
if err != nil {
return nil, err
}
Expand Down
10 changes: 5 additions & 5 deletions nodeagentautoscaler/autoscaler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,14 +423,14 @@ func TestGenerateDaemonSetName(t *testing.T) {
func TestNewAutoscaler(t *testing.T) {
client := fake.NewSimpleClientset()
cfg := config.NodeAgentAutoscalerConfig{
Enabled: true,
NodeGroupLabel: "node.kubernetes.io/instance-type",
ReconcileInterval: 5 * time.Minute,
TemplatePath: "/tmp/nonexistent-template.yaml", // Will fail
Enabled: true,
GoMemLimitPercentage: 0.8,
NodeGroupLabel: "node.kubernetes.io/instance-type",
ReconcileInterval: 5 * time.Minute,
TemplatePath: "/tmp/nonexistent-template.yaml", // Will fail
}

// Should fail because template doesn't exist
_, err := NewAutoscaler(client, cfg, "kubescape", "operator")
assert.Error(t, err)
}

12 changes: 6 additions & 6 deletions nodeagentautoscaler/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
// TestIntegration_HelmGeneratedTemplate tests rendering with the actual Helm-generated template
// Run with: go test -tags=integration -v -run TestIntegration_HelmGeneratedTemplate
// Requires the template file to be extracted first from Helm:
// helm template test ../../helm-charts/charts/kubescape-operator \
// --set nodeAgent.autoscaler.enabled=true --set clusterName=test \
// | grep -A 300 "daemonset-template.yaml:" | tail -n +2 | sed 's/^ //' \
// | awk '/^---/{exit} {print}' > /tmp/test-daemonset-template.yaml
//
// helm template test ../../helm-charts/charts/kubescape-operator \
// --set nodeAgent.autoscaler.enabled=true --set clusterName=test \
// | grep -A 300 "daemonset-template.yaml:" | tail -n +2 | sed 's/^ //' \
// | awk '/^---/{exit} {print}' > /tmp/test-daemonset-template.yaml
func TestIntegration_HelmGeneratedTemplate(t *testing.T) {
templatePath := "/tmp/test-daemonset-template.yaml"

Expand All @@ -27,7 +28,7 @@ func TestIntegration_HelmGeneratedTemplate(t *testing.T) {
}

// Create renderer
renderer, err := NewTemplateRenderer(templatePath)
renderer, err := NewTemplateRenderer(templatePath, 0.8)
require.NoError(t, err)

// Test data simulating a node group
Expand Down Expand Up @@ -76,4 +77,3 @@ func TestIntegration_HelmGeneratedTemplate(t *testing.T) {
container.Resources.Limits.Cpu().String(),
container.Resources.Limits.Memory().String())
}

29 changes: 21 additions & 8 deletions nodeagentautoscaler/templaterenderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type TemplateData struct {
NodeGroupLabel string
// Resources contains the calculated resource requests and limits
Resources TemplateResources
// GoMemLimit is the GOMEMLIMIT value derived from the memory limit and percentage (e.g., "360MiB")
GoMemLimit string
}

// TemplateResources holds the resource values for template rendering
Expand All @@ -41,17 +43,23 @@ type TemplateResourcePair struct {

// TemplateRenderer loads and renders DaemonSet templates
type TemplateRenderer struct {
templatePath string
template *template.Template
mu sync.RWMutex // Protects template during reload
watcher *fsnotify.Watcher
stopCh chan struct{}
templatePath string
template *template.Template
mu sync.RWMutex // Protects template during reload
watcher *fsnotify.Watcher
stopCh chan struct{}
goMemLimitPercentage float64
}

// NewTemplateRenderer creates a new TemplateRenderer
func NewTemplateRenderer(templatePath string) (*TemplateRenderer, error) {
func NewTemplateRenderer(templatePath string, goMemLimitPercentage float64) (*TemplateRenderer, error) {
if goMemLimitPercentage <= 0 || goMemLimitPercentage > 1.0 {
return nil, fmt.Errorf("goMemLimitPercentage %v is out of valid range (0, 1.0]", goMemLimitPercentage)
}

tr := &TemplateRenderer{
templatePath: templatePath,
templatePath: templatePath,
goMemLimitPercentage: goMemLimitPercentage,
}

if err := tr.loadTemplate(); err != nil {
Expand Down Expand Up @@ -200,6 +208,9 @@ func formatMemory(q resource.Quantity) string {

// RenderDaemonSet renders a DaemonSet for the given node group and resources
func (tr *TemplateRenderer) RenderDaemonSet(group NodeGroup, resources CalculatedResources) (*appsv1.DaemonSet, error) {
limitBytes := resources.Limits.Memory.Value()
goMemLimitMiB := int64(float64(limitBytes) * tr.goMemLimitPercentage / (1024 * 1024))

data := TemplateData{
Name: fmt.Sprintf("node-agent-%s", group.SanitizedName),
NodeGroupLabel: group.LabelValue,
Expand All @@ -213,6 +224,7 @@ func (tr *TemplateRenderer) RenderDaemonSet(group NodeGroup, resources Calculate
Memory: formatMemory(resources.Limits.Memory),
},
},
GoMemLimit: fmt.Sprintf("%dMiB", goMemLimitMiB),
}

tr.mu.RLock()
Expand All @@ -236,7 +248,8 @@ func (tr *TemplateRenderer) RenderDaemonSet(group NodeGroup, resources Calculate
helpers.String("requestCPU", data.Resources.Requests.CPU),
helpers.String("requestMemory", data.Resources.Requests.Memory),
helpers.String("limitCPU", data.Resources.Limits.CPU),
helpers.String("limitMemory", data.Resources.Limits.Memory))
helpers.String("limitMemory", data.Resources.Limits.Memory),
helpers.String("goMemLimit", data.GoMemLimit))

return ds, nil
}
Expand Down
131 changes: 128 additions & 3 deletions nodeagentautoscaler/templaterenderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ spec:
require.NoError(t, err)

// Create renderer
renderer, err := NewTemplateRenderer(templatePath)
renderer, err := NewTemplateRenderer(templatePath, 0.8)
require.NoError(t, err)

// Test data
Expand Down Expand Up @@ -160,11 +160,39 @@ func TestTemplateRenderer_RenderDaemonSet_InvalidTemplate(t *testing.T) {
require.NoError(t, err)

// Should fail to create renderer
_, err = NewTemplateRenderer(templatePath)
_, err = NewTemplateRenderer(templatePath, 0.8)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse template")
}

func TestTemplateRenderer_NewTemplateRenderer_InvalidPercentage(t *testing.T) {
tmpDir := t.TempDir()
templatePath := filepath.Join(tmpDir, "daemonset-template.yaml")
err := os.WriteFile(templatePath, []byte("apiVersion: apps/v1\nkind: DaemonSet\nmetadata:\n name: test\nspec:\n selector:\n matchLabels:\n app: test\n template:\n metadata:\n labels:\n app: test\n spec:\n containers:\n - name: test\n image: test\n"), 0644)
require.NoError(t, err)

tests := []struct {
name string
percentage float64
}{
{name: "zero", percentage: 0},
{name: "negative", percentage: -0.5},
{name: "greater than 1.0", percentage: 1.5},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewTemplateRenderer(templatePath, tt.percentage)
assert.Error(t, err)
assert.Contains(t, err.Error(), "out of valid range")
})
}

// Boundary: exactly 1.0 should be valid
_, err = NewTemplateRenderer(templatePath, 1.0)
assert.NoError(t, err)
}

func TestTemplateRenderer_ReloadTemplate(t *testing.T) {
templateContent1 := `apiVersion: apps/v1
kind: DaemonSet
Expand Down Expand Up @@ -225,7 +253,7 @@ spec:
require.NoError(t, err)

// Create renderer
renderer, err := NewTemplateRenderer(templatePath)
renderer, err := NewTemplateRenderer(templatePath, 0.8)
require.NoError(t, err)

group := NodeGroup{
Expand Down Expand Up @@ -262,4 +290,101 @@ spec:
assert.Equal(t, "node-agent-test-v2", ds2.Name)
}

func TestTemplateRenderer_RenderDaemonSet_GoMemLimit(t *testing.T) {
templateContent := `apiVersion: apps/v1
kind: DaemonSet
metadata:
name: "{{ .Name }}"
namespace: kubescape
spec:
selector:
matchLabels:
app: node-agent
template:
metadata:
labels:
app: node-agent
spec:
containers:
- name: node-agent
image: "quay.io/kubescape/node-agent:v0.3.3"
resources:
requests:
cpu: "{{ .Resources.Requests.CPU }}"
memory: "{{ .Resources.Requests.Memory }}"
limits:
cpu: "{{ .Resources.Limits.CPU }}"
memory: "{{ .Resources.Limits.Memory }}"
env:
- name: GOMEMLIMIT
value: "{{ .GoMemLimit }}"
`

tmpDir := t.TempDir()
templatePath := filepath.Join(tmpDir, "daemonset-template.yaml")
err := os.WriteFile(templatePath, []byte(templateContent), 0644)
require.NoError(t, err)

tests := []struct {
name string
limitMemory string
percentage float64
expectedGoMemLimit string
}{
{
name: "450Mi at 80% = 360MiB",
limitMemory: "450Mi",
percentage: 0.8,
expectedGoMemLimit: "360MiB",
},
{
name: "1Gi at 80% = 819MiB",
limitMemory: "1Gi",
percentage: 0.8,
expectedGoMemLimit: "819MiB",
},
{
name: "1800Mi at 80% = 1440MiB",
limitMemory: "1800Mi",
percentage: 0.8,
expectedGoMemLimit: "1440MiB",
},
{
name: "900Mi at 80% = 720MiB",
limitMemory: "900Mi",
percentage: 0.8,
expectedGoMemLimit: "720MiB",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
renderer, err := NewTemplateRenderer(templatePath, tt.percentage)
require.NoError(t, err)

group := NodeGroup{LabelValue: "m5.large", SanitizedName: "m5-large"}
resources := CalculatedResources{
Requests: ResourcePair{
CPU: resource.MustParse("100m"),
Memory: resource.MustParse("200Mi"),
},
Limits: ResourcePair{
CPU: resource.MustParse("500m"),
Memory: resource.MustParse(tt.limitMemory),
},
}

ds, err := renderer.RenderDaemonSet(group, resources)
require.NoError(t, err)

var goMemLimitValue string
for _, env := range ds.Spec.Template.Spec.Containers[0].Env {
if env.Name == "GOMEMLIMIT" {
goMemLimitValue = env.Value
break
}
}
assert.Equal(t, tt.expectedGoMemLimit, goMemLimitValue)
})
}
}
Loading