diff --git a/pkg/privateactionrunner/bundles/BUILD.bazel b/pkg/privateactionrunner/bundles/BUILD.bazel index b87b0c6eaa9f..4200fecda0a3 100644 --- a/pkg/privateactionrunner/bundles/BUILD.bazel +++ b/pkg/privateactionrunner/bundles/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "//comp/forwarder/eventplatform/def", "//comp/networkpath/traceroute/def", "//pkg/privateactionrunner/adapters/config", + "//pkg/privateactionrunner/bundles/agent", "//pkg/privateactionrunner/bundles/gitlab/branches", "//pkg/privateactionrunner/bundles/gitlab/commits", "//pkg/privateactionrunner/bundles/gitlab/customattributes", diff --git a/pkg/privateactionrunner/bundles/agent/BUILD.bazel b/pkg/privateactionrunner/bundles/agent/BUILD.bazel new file mode 100644 index 000000000000..45f3c549828e --- /dev/null +++ b/pkg/privateactionrunner/bundles/agent/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "agent", + srcs = [ + "agentapi.go", + "config.go", + "diagnose.go", + "entrypoint.go", + "status.go", + ], + importpath = "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/agent", + visibility = ["//visibility:public"], + deps = [ + "//comp/core/diagnose/def", + "//comp/core/ipc/def", + "//comp/core/ipc/httphelpers", + "//pkg/config/setup", + "//pkg/privateactionrunner/libs/privateconnection", + "//pkg/privateactionrunner/types", + "@in_yaml_go_yaml_v3//:yaml", + ], +) diff --git a/pkg/privateactionrunner/bundles/agent/agentapi.go b/pkg/privateactionrunner/bundles/agent/agentapi.go new file mode 100644 index 000000000000..87770b7fb29b --- /dev/null +++ b/pkg/privateactionrunner/bundles/agent/agentapi.go @@ -0,0 +1,74 @@ +// 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 2026-present Datadog, Inc. + +// Package com_datadoghq_agent provides Private Action Runner actions that +// operate on the local datadog-agent through its authenticated IPC HTTP API. +// +// The actions in this bundle run inside the Private Action Runner process, +// which is co-located with the core agent, and reach the agent the same way the +// agent's own CLI subcommands do: over the local HTTPS command API, reusing the +// agent's IPC auth token and certificate. They deliberately ignore any Action +// Platform connection credential — the target is always the local agent. +package com_datadoghq_agent + +import ( + "encoding/json" + "fmt" + "net" + "strconv" + + "go.yaml.in/yaml/v3" + + pkgconfigsetup "github.com/DataDog/datadog-agent/pkg/config/setup" +) + +// agentBaseURL returns the base URL (scheme + host + port) of the local agent's +// IPC command API, e.g. "https://127.0.0.1:5001". +func agentBaseURL() (string, error) { + ipcAddress, err := pkgconfigsetup.GetIPCAddress(pkgconfigsetup.Datadog()) + if err != nil { + return "", fmt.Errorf("failed to get agent IPC address: %w", err) + } + port := pkgconfigsetup.Datadog().GetInt("cmd_port") + return "https://" + net.JoinHostPort(ipcAddress, strconv.Itoa(port)), nil +} + +// decodeAgentObject decodes an agent JSON response into a map. Action outputs +// are surfaced to callers as a protobuf struct, which must be a JSON object at +// the top level, so responses that decode to a non-object (or that are not JSON +// at all) are wrapped so the action always returns an object. +func decodeAgentObject(b []byte) map[string]interface{} { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return map[string]interface{}{"raw": string(b)} + } + if obj, ok := v.(map[string]interface{}); ok { + return obj + } + return map[string]interface{}{"value": v} +} + +// decodeAgentValue decodes an agent JSON response into an arbitrary value, +// falling back to the raw string when the body is not valid JSON. +func decodeAgentValue(b []byte) interface{} { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return string(b) + } + return v +} + +// decodeAgentYAML decodes a YAML agent response (such as the full agent +// configuration returned by /agent/config) into a JSON-serializable object. It +// falls back to returning the raw text under a "raw" key when the body is not a +// YAML mapping. yaml/v3 decodes mappings with string keys, so the result +// marshals cleanly to JSON. +func decodeAgentYAML(b []byte) map[string]interface{} { + var v map[string]interface{} + if err := yaml.Unmarshal(b, &v); err != nil || v == nil { + return map[string]interface{}{"raw": string(b)} + } + return v +} diff --git a/pkg/privateactionrunner/bundles/agent/config.go b/pkg/privateactionrunner/bundles/agent/config.go new file mode 100644 index 000000000000..691cbd0cd56a --- /dev/null +++ b/pkg/privateactionrunner/bundles/agent/config.go @@ -0,0 +1,83 @@ +// 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 2026-present Datadog, Inc. + +package com_datadoghq_agent + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + + ipc "github.com/DataDog/datadog-agent/comp/core/ipc/def" + ipchttp "github.com/DataDog/datadog-agent/comp/core/ipc/httphelpers" + "github.com/DataDog/datadog-agent/pkg/privateactionrunner/libs/privateconnection" + "github.com/DataDog/datadog-agent/pkg/privateactionrunner/types" +) + +// GetConfigHandler handles the getConfig action, returning either the full +// resolved agent configuration or a single configuration value as JSON. +type GetConfigHandler struct { + ipcClient ipc.HTTPClient +} + +// NewGetConfigHandler creates a new GetConfigHandler. +func NewGetConfigHandler(client ipc.HTTPClient) *GetConfigHandler { + return &GetConfigHandler{ipcClient: client} +} + +// GetConfigInputs defines the inputs for the getConfig action. +type GetConfigInputs struct { + // Key optionally selects a single configuration setting (for example + // "log_level"). When empty the full resolved configuration is returned. + Key string `json:"key"` +} + +// Run executes the getConfig action. +func (h *GetConfigHandler) Run( + ctx context.Context, + task *types.Task, + _ *privateconnection.PrivateCredentials, +) (interface{}, error) { + if h.ipcClient == nil { + return nil, errors.New("getConfig: IPC client is not available") + } + + inputs, err := types.ExtractInputs[GetConfigInputs](task) + if err != nil { + return nil, fmt.Errorf("getConfig: failed to parse inputs: %w", err) + } + + base, err := agentBaseURL() + if err != nil { + return nil, fmt.Errorf("getConfig: %w", err) + } + + if inputs.Key != "" { + configURL := base + "/agent/config/" + url.PathEscape(inputs.Key) + resp, err := h.ipcClient.Get(configURL, ipchttp.WithContext(ctx)) + if err != nil { + return nil, fmt.Errorf("getConfig: request to agent failed: %w", err) + } + // The per-key endpoint responds with {"value": }; surface that + // value directly rather than nesting it under a second "value" field. + var wrapper map[string]interface{} + if jsonErr := json.Unmarshal(resp, &wrapper); jsonErr == nil { + if v, ok := wrapper["value"]; ok { + return map[string]interface{}{"key": inputs.Key, "value": v}, nil + } + } + return map[string]interface{}{"key": inputs.Key, "value": decodeAgentValue(resp)}, nil + } + + // The full-config endpoint returns scrubbed YAML, not JSON. + configURL := base + "/agent/config" + resp, err := h.ipcClient.Get(configURL, ipchttp.WithContext(ctx)) + if err != nil { + return nil, fmt.Errorf("getConfig: request to agent failed: %w", err) + } + return decodeAgentYAML(resp), nil +} diff --git a/pkg/privateactionrunner/bundles/agent/diagnose.go b/pkg/privateactionrunner/bundles/agent/diagnose.go new file mode 100644 index 000000000000..08ba4d36a438 --- /dev/null +++ b/pkg/privateactionrunner/bundles/agent/diagnose.go @@ -0,0 +1,86 @@ +// 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 2026-present Datadog, Inc. + +package com_datadoghq_agent + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + diagnose "github.com/DataDog/datadog-agent/comp/core/diagnose/def" + ipc "github.com/DataDog/datadog-agent/comp/core/ipc/def" + ipchttp "github.com/DataDog/datadog-agent/comp/core/ipc/httphelpers" + "github.com/DataDog/datadog-agent/pkg/privateactionrunner/libs/privateconnection" + "github.com/DataDog/datadog-agent/pkg/privateactionrunner/types" +) + +// GetDiagnoseHandler handles the getDiagnose action, running the agent's +// in-process diagnose suites and returning the result as JSON. +type GetDiagnoseHandler struct { + ipcClient ipc.HTTPClient +} + +// NewGetDiagnoseHandler creates a new GetDiagnoseHandler. +func NewGetDiagnoseHandler(client ipc.HTTPClient) *GetDiagnoseHandler { + return &GetDiagnoseHandler{ipcClient: client} +} + +// GetDiagnoseInputs defines the inputs for the getDiagnose action. +type GetDiagnoseInputs struct { + // Verbose enables verbose diagnosis output. + Verbose bool `json:"verbose"` + // Include optionally restricts the run to the named suites. When empty all + // suites run. Unknown suite names are rejected by the agent. + Include []string `json:"include"` + // Exclude optionally skips the named suites. + Exclude []string `json:"exclude"` +} + +// Run executes the getDiagnose action. +func (h *GetDiagnoseHandler) Run( + ctx context.Context, + task *types.Task, + _ *privateconnection.PrivateCredentials, +) (interface{}, error) { + if h.ipcClient == nil { + return nil, errors.New("getDiagnose: IPC client is not available") + } + + inputs, err := types.ExtractInputs[GetDiagnoseInputs](task) + if err != nil { + return nil, fmt.Errorf("getDiagnose: failed to parse inputs: %w", err) + } + + cfg := diagnose.Config{ + Verbose: inputs.Verbose, + Include: inputs.Include, + Exclude: inputs.Exclude, + } + body, err := json.Marshal(cfg) + if err != nil { + return nil, fmt.Errorf("getDiagnose: failed to encode diagnose config: %w", err) + } + + base, err := agentBaseURL() + if err != nil { + return nil, fmt.Errorf("getDiagnose: %w", err) + } + diagnoseURL := base + "/agent/diagnose" + + resp, err := h.ipcClient.Post(diagnoseURL, "application/json", bytes.NewBuffer(body), ipchttp.WithContext(ctx)) + if err != nil { + msg := strings.TrimSpace(string(resp)) + if msg == "" { + msg = err.Error() + } + return nil, fmt.Errorf("getDiagnose: request to agent failed: %s", msg) + } + + return decodeAgentObject(resp), nil +} diff --git a/pkg/privateactionrunner/bundles/agent/entrypoint.go b/pkg/privateactionrunner/bundles/agent/entrypoint.go new file mode 100644 index 000000000000..1207057dd897 --- /dev/null +++ b/pkg/privateactionrunner/bundles/agent/entrypoint.go @@ -0,0 +1,33 @@ +// 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 2026-present Datadog, Inc. + +package com_datadoghq_agent + +import ( + ipc "github.com/DataDog/datadog-agent/comp/core/ipc/def" + "github.com/DataDog/datadog-agent/pkg/privateactionrunner/types" +) + +// AgentBundle holds the actions that operate on the local datadog-agent. +type AgentBundle struct { + actions map[string]types.Action +} + +// NewAgent creates a new agent bundle. The provided IPC HTTP client is used to +// reach the local agent's authenticated command API. +func NewAgent(client ipc.HTTPClient) types.Bundle { + return &AgentBundle{ + actions: map[string]types.Action{ + "getStatus": NewGetStatusHandler(client), + "getDiagnose": NewGetDiagnoseHandler(client), + "getConfig": NewGetConfigHandler(client), + }, + } +} + +// GetAction returns the action with the given name. +func (b *AgentBundle) GetAction(name string) types.Action { + return b.actions[name] +} diff --git a/pkg/privateactionrunner/bundles/agent/status.go b/pkg/privateactionrunner/bundles/agent/status.go new file mode 100644 index 000000000000..08c0b0c1ba28 --- /dev/null +++ b/pkg/privateactionrunner/bundles/agent/status.go @@ -0,0 +1,74 @@ +// 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 2026-present Datadog, Inc. + +package com_datadoghq_agent + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + ipc "github.com/DataDog/datadog-agent/comp/core/ipc/def" + ipchttp "github.com/DataDog/datadog-agent/comp/core/ipc/httphelpers" + "github.com/DataDog/datadog-agent/pkg/privateactionrunner/libs/privateconnection" + "github.com/DataDog/datadog-agent/pkg/privateactionrunner/types" +) + +// GetStatusHandler handles the getStatus action, returning the local agent's +// status as JSON. +type GetStatusHandler struct { + ipcClient ipc.HTTPClient +} + +// NewGetStatusHandler creates a new GetStatusHandler. +func NewGetStatusHandler(client ipc.HTTPClient) *GetStatusHandler { + return &GetStatusHandler{ipcClient: client} +} + +// GetStatusInputs defines the inputs for the getStatus action. +type GetStatusInputs struct { + // Section optionally restricts the status to a single named section (for + // example "collector"). When empty the full status is returned. + Section string `json:"section"` +} + +// Run executes the getStatus action. +func (h *GetStatusHandler) Run( + ctx context.Context, + task *types.Task, + _ *privateconnection.PrivateCredentials, +) (interface{}, error) { + if h.ipcClient == nil { + return nil, errors.New("getStatus: IPC client is not available") + } + + inputs, err := types.ExtractInputs[GetStatusInputs](task) + if err != nil { + return nil, fmt.Errorf("getStatus: failed to parse inputs: %w", err) + } + + base, err := agentBaseURL() + if err != nil { + return nil, fmt.Errorf("getStatus: %w", err) + } + + path := "/agent/status" + if inputs.Section != "" { + path = "/agent/status/section/" + url.PathEscape(inputs.Section) + } + statusURL := base + path + "?format=json" + + resp, err := h.ipcClient.Get(statusURL, ipchttp.WithContext(ctx)) + if err != nil { + if msg := strings.TrimSpace(string(resp)); msg != "" { + return nil, fmt.Errorf("getStatus: request to agent failed: %s", msg) + } + return nil, fmt.Errorf("getStatus: request to agent failed: %w", err) + } + + return decodeAgentObject(resp), nil +} diff --git a/pkg/privateactionrunner/bundles/registry.go b/pkg/privateactionrunner/bundles/registry.go index 7c71cd6778ab..7c2a1b613c66 100644 --- a/pkg/privateactionrunner/bundles/registry.go +++ b/pkg/privateactionrunner/bundles/registry.go @@ -12,6 +12,7 @@ import ( eventplatform "github.com/DataDog/datadog-agent/comp/forwarder/eventplatform/def" traceroute "github.com/DataDog/datadog-agent/comp/networkpath/traceroute/def" "github.com/DataDog/datadog-agent/pkg/privateactionrunner/adapters/config" + com_datadoghq_agent "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/agent" com_datadoghq_gitlab_branches "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/gitlab/branches" com_datadoghq_gitlab_commits "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/gitlab/commits" com_datadoghq_gitlab_customattributes "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/gitlab/customattributes" @@ -59,6 +60,7 @@ type Registry struct { func NewRegistry(configuration *config.Config, traceroute traceroute.Component, eventPlatform eventplatform.Component, ipcClient ipc.HTTPClient, encryptionStore *encryptioncontext.Store) *Registry { return &Registry{ Bundles: map[string]types.Bundle{ + "com.datadoghq.agent": com_datadoghq_agent.NewAgent(ipcClient), "com.datadoghq.gitlab.branches": com_datadoghq_gitlab_branches.NewGitlabBranches(), "com.datadoghq.gitlab.commits": com_datadoghq_gitlab_commits.NewGitlabCommits(), "com.datadoghq.gitlab.customattributes": com_datadoghq_gitlab_customattributes.NewGitlabCustomAttributes(), diff --git a/pkg/privateactionrunner/bundles/registry_kubeapiserver.go b/pkg/privateactionrunner/bundles/registry_kubeapiserver.go index 86f7108d8528..a9af34dc156e 100644 --- a/pkg/privateactionrunner/bundles/registry_kubeapiserver.go +++ b/pkg/privateactionrunner/bundles/registry_kubeapiserver.go @@ -13,6 +13,7 @@ import ( eventplatform "github.com/DataDog/datadog-agent/comp/forwarder/eventplatform/def" traceroute "github.com/DataDog/datadog-agent/comp/networkpath/traceroute/def" "github.com/DataDog/datadog-agent/pkg/privateactionrunner/adapters/config" + com_datadoghq_agent "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/agent" com_datadoghq_gitlab_branches "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/gitlab/branches" com_datadoghq_gitlab_commits "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/gitlab/commits" com_datadoghq_gitlab_customattributes "github.com/DataDog/datadog-agent/pkg/privateactionrunner/bundles/gitlab/customattributes" @@ -59,6 +60,7 @@ type Registry struct { func NewRegistry(configuration *config.Config, traceroute traceroute.Component, eventPlatform eventplatform.Component, ipcClient ipc.HTTPClient, encryptionStore *encryptioncontext.Store) *Registry { return &Registry{ Bundles: map[string]types.Bundle{ + "com.datadoghq.agent": com_datadoghq_agent.NewAgent(ipcClient), "com.datadoghq.remoteaction": com_datadoghq_remoteaction.NewRemoteAction(configuration), "com.datadoghq.remoteaction.internal": com_datadoghq_remoteaction_internal.NewInternal(encryptionStore), "com.datadoghq.remoteaction.networks": com_datadoghq_remoteaction_networks.NewNetworks(traceroute, eventPlatform),