diff --git a/internal/controllers/constants/constants.go b/internal/controllers/constants/constants.go index 071bbfb6..9970c401 100644 --- a/internal/controllers/constants/constants.go +++ b/internal/controllers/constants/constants.go @@ -59,13 +59,16 @@ const ( TargetNamespaceCRNamespaceLabel = targetNamespaceCRLabelPrefix + "namespace" // Labels for agent auto-configuration - agentLabelPrefix = "cryostat.io/" - AgentLabelCryostatName = agentLabelPrefix + "name" - AgentLabelCryostatNamespace = agentLabelPrefix + "namespace" - AgentLabelCallbackPort = agentLabelPrefix + "callback-port" - AgentLabelContainer = agentLabelPrefix + "container" - AgentLabelReadOnly = agentLabelPrefix + "read-only" - AgentLabelJavaOptionsVar = agentLabelPrefix + "java-options-var" + agentLabelPrefix = "cryostat.io/" + AgentLabelCryostatName = agentLabelPrefix + "name" + AgentLabelCryostatNamespace = agentLabelPrefix + "namespace" + AgentLabelCallbackPort = agentLabelPrefix + "callback-port" + AgentLabelContainer = agentLabelPrefix + "container" + AgentLabelReadOnly = agentLabelPrefix + "read-only" + AgentLabelJavaOptionsVar = agentLabelPrefix + "java-options-var" + AgentLabelHarvesterTemplate = agentLabelPrefix + "harvester-template" + AgentLabelHarvesterExitMaxAge = agentLabelPrefix + "harvester-exit-max-age" + AgentLabelHarvesterExitMaxSize = agentLabelPrefix + "harvester-exit-max-size" CryostatCATLSCommonName = "cryostat-ca-cert-manager" CryostatTLSCommonName = "cryostat" diff --git a/internal/webhooks/agent/pod_defaulter.go b/internal/webhooks/agent/pod_defaulter.go index ecd0a65b..b729690a 100644 --- a/internal/webhooks/agent/pod_defaulter.go +++ b/internal/webhooks/agent/pod_defaulter.go @@ -20,6 +20,7 @@ import ( "fmt" "slices" "strconv" + "time" operatorv1beta2 "github.com/cryostatio/cryostat-operator/api/v1beta2" "github.com/cryostatio/cryostat-operator/internal/controllers/common" @@ -47,13 +48,17 @@ type podMutator struct { var _ admission.CustomDefaulter = &podMutator{} const ( - agentArg = "-javaagent:/tmp/cryostat-agent/cryostat-agent-shaded.jar" - podNameEnvVar = "CRYOSTAT_AGENT_POD_NAME" - podIPEnvVar = "CRYOSTAT_AGENT_POD_IP" - agentMaxSizeBytes = "50Mi" - agentInitCpuRequest = "10m" - agentInitMemoryRequest = "32Mi" - defaultJavaOptsVar = "JAVA_TOOL_OPTIONS" + agentArg = "-javaagent:/tmp/cryostat-agent/cryostat-agent-shaded.jar" + podNameEnvVar = "CRYOSTAT_AGENT_POD_NAME" + podIPEnvVar = "CRYOSTAT_AGENT_POD_IP" + agentMaxSizeBytes = "50Mi" + agentInitCpuRequest = "10m" + agentInitMemoryRequest = "32Mi" + defaultJavaOptsVar = "JAVA_TOOL_OPTIONS" + defaultHarvesterExitMaxAge = int32(30000) + kib = int32(1024) + mib = 1024 * kib + defaultHarvesterExitMaxSize = 20 * mib ) // Default optionally mutates a pod to inject the Cryostat agent @@ -108,6 +113,16 @@ func (r *podMutator) Default(ctx context.Context, obj runtime.Object) error { return err } + harvesterTemplate := getHarvesterTemplate(pod.Labels) + harvesterExitMaxAge, err := getHarvesterExitMaxAge(pod.Labels) + if err != nil { + return err + } + harvesterExitMaxSize, err := getHarvesterExitMaxSize(pod.Labels) + if err != nil { + return err + } + // Add init container nonRoot := true imageTag := r.getImageTag() @@ -185,6 +200,23 @@ func (r *podMutator) Default(ctx context.Context, obj runtime.Object) error { }, ) + if len(harvesterTemplate) > 0 { + container.Env = append(container.Env, + corev1.EnvVar{ + Name: "CRYOSTAT_AGENT_HARVESTER_TEMPLATE", + Value: harvesterTemplate, + }, + corev1.EnvVar{ + Name: "CRYOSTAT_AGENT_HARVESTER_EXIT_MAX_AGE_MS", + Value: strconv.Itoa(int(*harvesterExitMaxAge)), + }, + corev1.EnvVar{ + Name: "CRYOSTAT_AGENT_HARVESTER_EXIT_MAX_SIZE_B", + Value: strconv.Itoa(int(*harvesterExitMaxSize)), + }, + ) + } + // Append a port for the callback server container.Ports = append(container.Ports, corev1.ContainerPort{ Name: constants.AgentCallbackPortName, @@ -344,6 +376,42 @@ func getJavaOptionsVar(labels map[string]string) string { return result } +func getHarvesterTemplate(labels map[string]string) string { + result := "" + value, pres := labels[constants.AgentLabelHarvesterTemplate] + if pres { + result = value + } + return result +} + +func getHarvesterExitMaxAge(labels map[string]string) (*int32, error) { + value := defaultHarvesterExitMaxAge + age, pres := labels[constants.AgentLabelHarvesterExitMaxAge] + if pres { + // Parse the label value into an int32 and return an error if invalid + parsed, err := time.ParseDuration(age) + if err != nil { + return nil, fmt.Errorf("invalid label value for \"%s\": %s", constants.AgentLabelHarvesterExitMaxAge, err.Error()) + } + value = int32(parsed.Milliseconds()) + } + return &value, nil +} + +func getHarvesterExitMaxSize(labels map[string]string) (*int32, error) { + value := defaultHarvesterExitMaxSize + size, pres := labels[constants.AgentLabelHarvesterExitMaxSize] + if pres { + parsed, err := resource.ParseQuantity(size) + if err != nil { + return nil, fmt.Errorf("invalid label value for \"%s\": %s", constants.AgentLabelHarvesterExitMaxSize, err.Error()) + } + value = int32(parsed.Value()) + } + return &value, nil +} + func getResourceRequirements(cr *model.CryostatInstance) *corev1.ResourceRequirements { resources := &corev1.ResourceRequirements{} if cr.Spec.AgentOptions != nil { diff --git a/internal/webhooks/agent/pod_defaulter_test.go b/internal/webhooks/agent/pod_defaulter_test.go index 89fe974d..181476b8 100644 --- a/internal/webhooks/agent/pod_defaulter_test.go +++ b/internal/webhooks/agent/pod_defaulter_test.go @@ -421,6 +421,64 @@ var _ = Describe("PodDefaulter", func() { ExpectPod() }) + Context("with harvester enabled", func() { + Context("with default exit settings", func() { + BeforeEach(func() { + t.objs = append(t.objs, t.NewCryostat().Object) + originalPod = t.NewPodHarvesterTemplate() + expectedPod = t.NewMutatedPodHarvesterTemplate() + }) + + ExpectPod() + }) + + Context("with exit age setting", func() { + Context("that is valid", func() { + BeforeEach(func() { + t.objs = append(t.objs, t.NewCryostat().Object) + originalPod = t.NewPodHarvesterTemplateAge() + expectedPod = t.NewMutatedPodHarvesterTemplateAge() + }) + + ExpectPod() + }) + + Context("that is invalid", func() { + BeforeEach(func() { + t.objs = append(t.objs, t.NewCryostat().Object) + originalPod = t.NewPodHarvesterTemplateInvalidAge() + // Should fail + expectedPod = originalPod + }) + + ExpectPod() + }) + }) + + Context("with exit size setting", func() { + Context("that is valid", func() { + BeforeEach(func() { + t.objs = append(t.objs, t.NewCryostat().Object) + originalPod = t.NewPodHarvesterTemplateSize() + expectedPod = t.NewMutatedPodHarvesterTemplateSize() + }) + + ExpectPod() + }) + + Context("that is invalid", func() { + BeforeEach(func() { + t.objs = append(t.objs, t.NewCryostat().Object) + originalPod = t.NewPodHarvesterTemplateInvalidSize() + // Should fail + expectedPod = originalPod + }) + + ExpectPod() + }) + }) + }) + Context("with a custom resource requirements", func() { Context("that are valid", func() { BeforeEach(func() { diff --git a/internal/webhooks/agent/test/resources.go b/internal/webhooks/agent/test/resources.go index 9eda0e61..72de3535 100644 --- a/internal/webhooks/agent/test/resources.go +++ b/internal/webhooks/agent/test/resources.go @@ -188,17 +188,50 @@ func (r *AgentWebhookTestResources) NewPodJavaOptsVar() *corev1.Pod { return pod } +func (r *AgentWebhookTestResources) NewPodHarvesterTemplate() *corev1.Pod { + pod := r.NewPod() + pod.Labels["cryostat.io/harvester-template"] = "default.jfc" + return pod +} + +func (r *AgentWebhookTestResources) NewPodHarvesterTemplateAge() *corev1.Pod { + pod := r.NewPod() + pod.Labels["cryostat.io/harvester-exit-max-age"] = "10s" + return pod +} + +func (r *AgentWebhookTestResources) NewPodHarvesterTemplateInvalidAge() *corev1.Pod { + pod := r.NewPod() + pod.Labels["cryostat.io/harvester-exit-max-age"] = "tenseconds" + return pod +} + +func (r *AgentWebhookTestResources) NewPodHarvesterTemplateSize() *corev1.Pod { + pod := r.NewPod() + pod.Labels["cryostat.io/harvester-exit-max-size"] = "10Mi" + return pod +} + +func (r *AgentWebhookTestResources) NewPodHarvesterTemplateInvalidSize() *corev1.Pod { + pod := r.NewPod() + pod.Labels["cryostat.io/harvester-exit-max-size"] = "tenmib" + return pod +} + type mutatedPodOptions struct { - javaOptionsName string - javaOptionsValue string - namespace string - image string - pullPolicy corev1.PullPolicy - gatewayPort int32 - callbackPort int32 - writeAccess *bool - scheme string - resources *corev1.ResourceRequirements + javaOptionsName string + javaOptionsValue string + namespace string + image string + pullPolicy corev1.PullPolicy + gatewayPort int32 + callbackPort int32 + writeAccess *bool + harvesterTemplate string + harvesterExitAge int32 + harvesterExitSize int32 + scheme string + resources *corev1.ResourceRequirements // Function to produce mutated container array containersFunc func(*AgentWebhookTestResources, *mutatedPodOptions) []corev1.Container } @@ -219,6 +252,12 @@ func (r *AgentWebhookTestResources) setDefaultMutatedPodOptions(options *mutated if options.callbackPort == 0 { options.callbackPort = 9977 } + if options.harvesterExitAge == 0 { + options.harvesterExitAge = 30000 + } + if options.harvesterExitSize == 0 { + options.harvesterExitSize = 20971520 + } if options.writeAccess == nil { options.writeAccess = &[]bool{true}[0] } @@ -308,6 +347,24 @@ func (r *AgentWebhookTestResources) NewMutatedPodJavaOptsVarLabel() *corev1.Pod }) } +func (r *AgentWebhookTestResources) NewMutatedPodHarvesterTemplate() *corev1.Pod { + return r.newMutatedPod(&mutatedPodOptions{ + harvesterTemplate: "default.jfc", + }) +} + +func (r *AgentWebhookTestResources) NewMutatedPodHarvesterTemplateAge() *corev1.Pod { + return r.newMutatedPod(&mutatedPodOptions{ + harvesterExitAge: 10000, + }) +} + +func (r *AgentWebhookTestResources) NewMutatedPodHarvesterTemplateSize() *corev1.Pod { + return r.newMutatedPod(&mutatedPodOptions{ + harvesterExitSize: 123456, + }) +} + func (r *AgentWebhookTestResources) NewMutatedPodResources() *corev1.Pod { return r.newMutatedPod(&mutatedPodOptions{ resources: &corev1.ResourceRequirements{ @@ -581,6 +638,23 @@ func (r *AgentWebhookTestResources) newMutatedContainer(original *corev1.Contain } container.Env = append(container.Env, callbackEnvs...) + if len(options.harvesterTemplate) > 0 { + container.Env = append(container.Env, + corev1.EnvVar{ + Name: "CRYOSTAT_AGENT_HARVESTER_TEMPLATE", + Value: options.harvesterTemplate, + }, + corev1.EnvVar{ + Name: "CRYOSTAT_AGENT_HARVESTER_EXIT_MAX_AGE_MS", + Value: strconv.Itoa(int(options.harvesterExitAge)), + }, + corev1.EnvVar{ + Name: "CRYOSTAT_AGENT_HARVESTER_EXIT_MAX_SIZE_B", + Value: strconv.Itoa(int(options.harvesterExitSize)), + }, + ) + } + return container }