Skip to content
Draft
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
1 change: 1 addition & 0 deletions pkg/privateactionrunner/bundles/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions pkg/privateactionrunner/bundles/agent/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
74 changes: 74 additions & 0 deletions pkg/privateactionrunner/bundles/agent/agentapi.go
Original file line number Diff line number Diff line change
@@ -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
}
83 changes: 83 additions & 0 deletions pkg/privateactionrunner/bundles/agent/config.go
Original file line number Diff line number Diff line change
@@ -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": <setting>}; 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
}
86 changes: 86 additions & 0 deletions pkg/privateactionrunner/bundles/agent/diagnose.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 33 additions & 0 deletions pkg/privateactionrunner/bundles/agent/entrypoint.go
Original file line number Diff line number Diff line change
@@ -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]
}
74 changes: 74 additions & 0 deletions pkg/privateactionrunner/bundles/agent/status.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading