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
6 changes: 6 additions & 0 deletions pkg/clusteragent/kubeactions/executors/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions pkg/clusteragent/kubeactions/executors/delete_pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (
"fmt"

kubeactions "github.com/DataDog/agent-payload/v5/kubeactions"
"github.com/DataDog/datadog-agent/pkg/util/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

"github.com/DataDog/datadog-agent/pkg/util/log"
)

// Execution status constants
Expand All @@ -25,15 +26,18 @@ const (

// ExecutionResult represents the result of executing an action
type ExecutionResult struct {
Status string
Message string
Status string
Message string
Payloads map[string][]byte
}

// DeletePodExecutor executes delete pod actions
type DeletePodExecutor struct {
clientset kubernetes.Interface
}

var _ Executor = (*DeletePodExecutor)(nil)

// NewDeletePodExecutor creates a new DeletePodExecutor
func NewDeletePodExecutor(clientset kubernetes.Interface) *DeletePodExecutor {
return &DeletePodExecutor{
Expand Down
150 changes: 150 additions & 0 deletions pkg/clusteragent/kubeactions/executors/get_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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 (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"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"
)

const (
maxResourceOutputSize = 4 * 1024 // 4KB
)

type GetResourceExecutor struct {
clientset kubernetes.Interface
}

// Ensure interface compliance at compile time
var _ Executor = (*GetResourceExecutor)(nil)

var (
// ErrUnsupportedFormat is returned when the requested output format is not supported
ErrUnsupportedFormat = errors.New("unsupported output format")
)

// 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 := strings.ToLower(resource.GetNamespace())
Comment thread
frank-spano marked this conversation as resolved.
name := strings.ToLower(resource.GetName())
Comment thread
frank-spano marked this conversation as resolved.
apiVersion := strings.ToLower(resource.GetApiVersion())
kind := strings.ToLower(resource.GetKind())
Comment thread
lavigne958 marked this conversation as resolved.
Comment thread
frank-spano marked this conversation as resolved.

if apiVersion == "" {
return ExecutionResult{
Status: StatusFailed,
Message: "apiVersion is required to get resource",
}
}

// prevent the executor from being used to get secrets for security reasons, even if the user has permissions to do so, we don't want to allow that
if strings.Contains(kind, "secret") {
return ExecutionResult{
Status: StatusFailed,
Message: "getting secrets is not allowed for security reasons",
}
}

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
var path string

// the api version for core resources does not contain a '/'.
// and the path for core resources is /api/....
// as for all other resources the path is /apis/...
var apiPrefix string
if !strings.Contains(apiVersion, "/") {
apiPrefix = "/api"
} else {
apiPrefix = "/apis"
}

// resource.GetApiVersion() returns group/version, it will automagically handle adding the group prefix if needed
// or not adding it for core resources
if namespace == "" {
path, _ = url.JoinPath(apiPrefix, apiVersion, kind, name)
} else {
path, _ = url.JoinPath(apiPrefix, apiVersion, "namespaces", 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 {
return ExecutionResult{
Status: StatusFailed,
Message: fmt.Sprintf("failed to get resource: %v -- raw response body: %s", err, string(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),
}
}

output = bytes.TrimSpace(output)
if len(output) > maxResourceOutputSize {
log.Warnf("output for resource %s/%s of type %s is too large (%d bytes), truncating to %d bytes", namespace, name, kind, len(output), maxResourceOutputSize)
output = output[:maxResourceOutputSize]
}

return ExecutionResult{
Status: StatusSuccess,
Message: fmt.Sprintf("get resource %s/%s success", kind, name),
Payloads: map[string][]byte{
"resource": []byte(output),
},
}
}

func formatOutput(data []byte, format string) ([]byte, error) {
switch format {
case "json":
return data, nil
case "yaml":
jsonData := data
yamlData, err := yaml.JSONToYAML(jsonData)
if err != nil {
return nil, fmt.Errorf("failed to convert resource JSON to YAML: %v", err)
}
return yamlData, nil
default:
return nil, ErrUnsupportedFormat
}
}
79 changes: 79 additions & 0 deletions pkg/clusteragent/kubeactions/executors/get_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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 (
"errors"
"testing"
)

var testPodJSON = []byte(`{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test-pod",
"namespace": "default"
},
"spec": {
"containers": [
{
"name": "test-container",
"image": "busybox",
"command": ["sleep", "3600"]
}
]
}
}`)

func TestOutputFormat(t *testing.T) {
tests := []struct {
name string
input []byte
outputFormat string
expectErr bool
}{
{
name: "from json to json",
input: testPodJSON,
outputFormat: "json",
expectErr: false,
},
{
name: "from json to yaml",
input: testPodJSON,
outputFormat: "yaml",
expectErr: false,
},
{
name: "unsupported format",
input: testPodJSON,
outputFormat: "xml",
expectErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test the output format conversion
_, err := formatOutput(tt.input, tt.outputFormat)

// we don't compare the output because the yaml output can have unordered fields.
// we only ensure it does not fail
if err != nil {
if !tt.expectErr {
t.Errorf("exccpected a result, unexpected error: %v", err)
}

// with unsuported format we expect an error
if !errors.Is(err, ErrUnsupportedFormat) {
t.Errorf("expected ErrUnsupportedFormat, got: %v", err)
}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@ import (
"fmt"

kubeactions "github.com/DataDog/agent-payload/v5/kubeactions"
"github.com/DataDog/datadog-agent/pkg/util/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"

"github.com/DataDog/datadog-agent/pkg/util/log"
)

// PatchDeploymentExecutor executes patch deployment actions
type PatchDeploymentExecutor struct {
clientset kubernetes.Interface
}

var _ Executor = (*PatchDeploymentExecutor)(nil)

// NewPatchDeploymentExecutor creates a new PatchDeploymentExecutor
func NewPatchDeploymentExecutor(clientset kubernetes.Interface) *PatchDeploymentExecutor {
return &PatchDeploymentExecutor{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ import (
"time"

kubeactions "github.com/DataDog/agent-payload/v5/kubeactions"
"github.com/DataDog/datadog-agent/pkg/util/log"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

"github.com/DataDog/datadog-agent/pkg/util/log"
)

// RestartDeploymentExecutor executes restart deployment actions
type RestartDeploymentExecutor struct {
clientset kubernetes.Interface
}

var _ Executor = (*RestartDeploymentExecutor)(nil)

// NewRestartDeploymentExecutor creates a new RestartDeploymentExecutor
func NewRestartDeploymentExecutor(clientset kubernetes.Interface) *RestartDeploymentExecutor {
return &RestartDeploymentExecutor{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ type RollbackDeploymentExecutor struct {
clientset kubernetes.Interface
}

var _ Executor = (*RollbackDeploymentExecutor)(nil)

// NewRollbackDeploymentExecutor creates a new RollbackDeploymentExecutor
func NewRollbackDeploymentExecutor(clientset kubernetes.Interface) *RollbackDeploymentExecutor {
return &RollbackDeploymentExecutor{
Expand Down
Loading
Loading