From 28db604c5c4c995d962d50029b122a1833c202dc Mon Sep 17 00:00:00 2001 From: Alexandre Lavigne Date: Tue, 14 Apr 2026 18:11:28 +0200 Subject: [PATCH 1/2] CONTINT-5286 - add new action in remote config k8s actions This is a temporary action implementation. Add the new action, it won't pass validation due to missing type in the proto definition. This is un purpose until it's ready. Signed-off-by: Alexandre Lavigne --- .../kubeactions/config_retriever.go | 3 +- .../kubeactions/executors/adapter.go | 6 ++ .../kubeactions/executors/get_resource.go | 68 +++++++++++++++++++ pkg/clusteragent/kubeactions/setup.go | 8 ++- pkg/clusteragent/kubeactions/types.go | 4 ++ ...ion-k8s-get-resource-b6cbed5ae1f48410.yaml | 12 ++++ 6 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 pkg/clusteragent/kubeactions/executors/get_resource.go create mode 100644 releasenotes/notes/add-new-remote-config-action-k8s-get-resource-b6cbed5ae1f48410.yaml diff --git a/pkg/clusteragent/kubeactions/config_retriever.go b/pkg/clusteragent/kubeactions/config_retriever.go index b3b98bd6017a..bed5deec3383 100644 --- a/pkg/clusteragent/kubeactions/config_retriever.go +++ b/pkg/clusteragent/kubeactions/config_retriever.go @@ -8,6 +8,7 @@ package kubeactions import ( + rcclient "github.com/DataDog/datadog-agent/pkg/config/remote/client" "github.com/DataDog/datadog-agent/pkg/remoteconfig/state" "github.com/DataDog/datadog-agent/pkg/util/log" ) @@ -24,7 +25,7 @@ type ConfigRetriever struct { } // NewConfigRetriever creates a new ConfigRetriever and subscribes to K8S_ACTIONS -func NewConfigRetriever(processor *ActionProcessor, isLeader func() bool, rcClient RcClient) *ConfigRetriever { +func NewConfigRetriever(processor *ActionProcessor, isLeader func() bool, rcClient *rcclient.Client) *ConfigRetriever { cr := &ConfigRetriever{ processor: processor, isLeader: isLeader, diff --git a/pkg/clusteragent/kubeactions/executors/adapter.go b/pkg/clusteragent/kubeactions/executors/adapter.go index fb3dc03a557b..28782995c09b 100644 --- a/pkg/clusteragent/kubeactions/executors/adapter.go +++ b/pkg/clusteragent/kubeactions/executors/adapter.go @@ -10,10 +10,16 @@ package executors import ( "context" + "time" kubeactions "github.com/DataDog/agent-payload/v5/kubeactions" ) +const ( + // default timeout for all executors, can be overridden by individual executors if needed + defaultExecutorTimeout = 10 * time.Second +) + // Executor is the interface that all executors in this package implement type Executor interface { Execute(ctx context.Context, action *kubeactions.KubeAction) ExecutionResult diff --git a/pkg/clusteragent/kubeactions/executors/get_resource.go b/pkg/clusteragent/kubeactions/executors/get_resource.go new file mode 100644 index 000000000000..02435923e43b --- /dev/null +++ b/pkg/clusteragent/kubeactions/executors/get_resource.go @@ -0,0 +1,68 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build kubeapiserver + +package executors + +import ( + "context" + "fmt" + + "k8s.io/client-go/kubernetes" + + "github.com/DataDog/datadog-agent/pkg/util/log" + + kubeactions "github.com/DataDog/agent-payload/v5/kubeactions" +) + +type GetResourceExecutor struct { + clientset kubernetes.Interface +} + +// NewGetResourceExecutor creates a new GetResourceExecutor +func NewGetResourceExecutor(clientset kubernetes.Interface) *GetResourceExecutor { + return &GetResourceExecutor{ + clientset: clientset, + } +} + +// Execute retrieves the specified Kubernetes resource and returns it as JSON string in the message field of ExecutionResult +func (e *GetResourceExecutor) Execute(ctx context.Context, action *kubeactions.KubeAction) ExecutionResult { + resource := action.Resource + namespace := resource.GetNamespace() + name := resource.GetName() + + log.Infof("Getting resource %s/%s of type %s", namespace, name, resource.Kind) + + // build the raw REST request to get the resource as unstructured JSON + // resource.GetApiVersion() returns group/version, it will automagically handle adding the group prefix if needed + // or not adding it for core resources + var path string + if namespace == "" { + path = fmt.Sprintf("/apis/%s/%s/%s", resource.GetApiVersion(), resource.GetKind(), name) + } else { + path = fmt.Sprintf("/apis/%s/namespaces/%s/%s/%s", resource.GetApiVersion(), namespace, resource.GetKind(), name) + } + + ctx, cancel := context.WithTimeout(ctx, defaultExecutorTimeout) + defer cancel() + + data, err := e.clientset.CoreV1().RESTClient().Get().AbsPath(path).Do(ctx).Raw() + if err != nil { + log.Errorf("Failed to get resource %s/%s: %v", namespace, name, err) + return ExecutionResult{ + Status: StatusFailed, + Message: fmt.Sprintf("failed to get resource: %v", err), + } + } + + log.Infof("received: '%#v'", data) + + return ExecutionResult{ + Status: StatusSuccess, + Message: string(data), + } +} diff --git a/pkg/clusteragent/kubeactions/setup.go b/pkg/clusteragent/kubeactions/setup.go index 63aa53b74a61..af993a3063b1 100644 --- a/pkg/clusteragent/kubeactions/setup.go +++ b/pkg/clusteragent/kubeactions/setup.go @@ -15,10 +15,12 @@ import ( "github.com/DataDog/datadog-agent/pkg/clusteragent/kubeactions/executors" "github.com/DataDog/datadog-agent/pkg/util/log" "k8s.io/client-go/kubernetes" + + rcclient "github.com/DataDog/datadog-agent/pkg/config/remote/client" ) // Setup initializes the kubeactions subsystem with all executors registered -func Setup(ctx context.Context, clientset kubernetes.Interface, clusterName, clusterID string, isLeader func() bool, rcClient RcClient, epForwarderComp eventplatform.Component) (*ConfigRetriever, error) { +func Setup(ctx context.Context, clientset kubernetes.Interface, clusterName, clusterID string, isLeader func() bool, rcClient *rcclient.Client, epForwarderComp eventplatform.Component) (*ConfigRetriever, error) { log.Infof("Setting up Kubernetes actions subsystem") // Create the executor registry @@ -76,6 +78,10 @@ func registerExecutors(registry *ExecutorRegistry, clientset kubernetes.Interfac registry.Register("patch_deployment", &executorAdapter{exec: executors.NewPatchDeploymentExecutor(clientset)}) log.Infof("Registered executor for action type: patch_deployment") + // Register get_resource executor + registry.Register("get_resource", &executorAdapter{exec: executors.NewGetResourceExecutor(clientset)}) + log.Infof("Registered executor for action type: get_resource") + // TODO: Add more executors here as they are implemented: // registry.Register("drain_node", &executorAdapter{exec: executors.NewDrainNodeExecutor(clientset)}) // registry.Register("cordon_node", &executorAdapter{exec: executors.NewCordonNodeExecutor(clientset)}) diff --git a/pkg/clusteragent/kubeactions/types.go b/pkg/clusteragent/kubeactions/types.go index db9c9e1b804a..d1b17c9262bf 100644 --- a/pkg/clusteragent/kubeactions/types.go +++ b/pkg/clusteragent/kubeactions/types.go @@ -18,6 +18,7 @@ const ( ActionTypeDeletePod = "delete_pod" ActionTypeRestartDeployment = "restart_deployment" ActionTypePatchDeployment = "patch_deployment" + ActionTypeGetResource = "get_resource" ActionTypeUnknown = "unknown" ) @@ -55,6 +56,9 @@ func GetActionType(action *kubeactions.KubeAction) string { return ActionTypeRestartDeployment case *kubeactions.KubeAction_PatchDeployment: return ActionTypePatchDeployment + // to add when PR https://github.com/DataDog/agent-payload/pull/472 is merged + // case *kubeactions.KubeAction_GetResource: + // return ActionTypeGetResource default: return ActionTypeUnknown } diff --git a/releasenotes/notes/add-new-remote-config-action-k8s-get-resource-b6cbed5ae1f48410.yaml b/releasenotes/notes/add-new-remote-config-action-k8s-get-resource-b6cbed5ae1f48410.yaml new file mode 100644 index 000000000000..59cf993bd0e3 --- /dev/null +++ b/releasenotes/notes/add-new-remote-config-action-k8s-get-resource-b6cbed5ae1f48410.yaml @@ -0,0 +1,12 @@ +# Each section from every release note are combined when the +# CHANGELOG.rst is rendered. So the text needs to be worded so that +# it does not depend on any information only available in another +# section. This may mean repeating some details, but each section +# must be readable independently of the other. +# +# Each section note must be formatted as reStructuredText. +--- +features: + - | + Add a new action get-resource in kubeactions + From d05f3610cfa78d9f4592d4c095c8accdaa01a7e1 Mon Sep 17 00:00:00 2001 From: Alexandre Lavigne Date: Wed, 15 Apr 2026 15:15:51 +0200 Subject: [PATCH 2/2] fixup! CONTINT-5286 - add new action in remote config k8s actions --- .../kubeactions/executors/get_resource.go | 55 ++++++++++++++++--- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/pkg/clusteragent/kubeactions/executors/get_resource.go b/pkg/clusteragent/kubeactions/executors/get_resource.go index 02435923e43b..02e7ddefe95c 100644 --- a/pkg/clusteragent/kubeactions/executors/get_resource.go +++ b/pkg/clusteragent/kubeactions/executors/get_resource.go @@ -10,9 +10,12 @@ package executors import ( "context" "fmt" + "strings" "k8s.io/client-go/kubernetes" + "sigs.k8s.io/yaml" + "github.com/DataDog/datadog-agent/pkg/util/log" kubeactions "github.com/DataDog/agent-payload/v5/kubeactions" @@ -32,8 +35,17 @@ func NewGetResourceExecutor(clientset kubernetes.Interface) *GetResourceExecutor // Execute retrieves the specified Kubernetes resource and returns it as JSON string in the message field of ExecutionResult func (e *GetResourceExecutor) Execute(ctx context.Context, action *kubeactions.KubeAction) ExecutionResult { resource := action.Resource - namespace := resource.GetNamespace() - name := resource.GetName() + namespace := strings.ToLower(resource.GetNamespace()) + name := strings.ToLower(resource.GetName()) + apiVersion := strings.ToLower(resource.GetApiVersion()) + kind := strings.ToLower(resource.GetKind()) + + if apiVersion == "" { + return ExecutionResult{ + Status: StatusFailed, + Message: "apiVersion is required to get resource", + } + } log.Infof("Getting resource %s/%s of type %s", namespace, name, resource.Kind) @@ -42,27 +54,54 @@ func (e *GetResourceExecutor) Execute(ctx context.Context, action *kubeactions.K // or not adding it for core resources var path string if namespace == "" { - path = fmt.Sprintf("/apis/%s/%s/%s", resource.GetApiVersion(), resource.GetKind(), name) + path = fmt.Sprintf("/apis/%s/%s/%s", apiVersion, kind, name) } else { - path = fmt.Sprintf("/apis/%s/namespaces/%s/%s/%s", resource.GetApiVersion(), namespace, resource.GetKind(), name) + path = fmt.Sprintf("/apis/%s/namespaces/%s/%s/%s", apiVersion, namespace, kind, name) } ctx, cancel := context.WithTimeout(ctx, defaultExecutorTimeout) defer cancel() + log.Debugf("get_resource using path '%s'", path) data, err := e.clientset.CoreV1().RESTClient().Get().AbsPath(path).Do(ctx).Raw() if err != nil { - log.Errorf("Failed to get resource %s/%s: %v", namespace, name, err) return ExecutionResult{ Status: StatusFailed, - Message: fmt.Sprintf("failed to get resource: %v", err), + Message: fmt.Sprintf("failed to get resource: %v -- raw response body: %s", err, string(data)), } } - log.Infof("received: '%#v'", data) + outputFormat := "json" + // if output := action.GetGetResource_().GetOutputFormat(); output != "" { + // outputFormat = strings.ToLower(output) + // } + + output, err := formatOutput(data, outputFormat) + if err != nil { + return ExecutionResult{ + Status: StatusFailed, + Message: fmt.Sprintf("failed to format output to %s: %v", outputFormat, err), + } + } return ExecutionResult{ Status: StatusSuccess, - Message: string(data), + Message: output, + } +} + +func formatOutput(data []byte, format string) (string, error) { + switch format { + case "json": + return string(data), nil + case "yaml": + jsonData := data + yamlData, err := yaml.JSONToYAML(jsonData) + if err != nil { + return "", fmt.Errorf("failed to convert resource JSON to YAML: %v", err) + } + return string(yamlData), nil + default: + return "", fmt.Errorf("unsupported output format: %s", format) } }