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
18 changes: 18 additions & 0 deletions pkg/validate/testdata/valid_datadogagent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
metadata:
name: datadog-demo
spec:
global:
clusterName: demo-cluster
credentials:
apiKey: "0123456789abcdef0123456789abcdef"
appKey: "0123456789abcdef0123456789abcdef01234567"
commonLabels:
team: platform
environment: staging
features:
apm:
enabled: true
logCollection:
enabled: true
50 changes: 50 additions & 0 deletions pkg/validate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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.

// Package validate offers a reusable entry point for parsing a DatadogAgent
// manifest and running the operator's own semantic validation against it,
// without needing a running cluster or reconcile loop.
package validate

import (
"fmt"
"os"

"sigs.k8s.io/yaml"

datadoghqv2alpha1 "github.com/DataDog/datadog-operator/api/datadoghq/v2alpha1"
)

// ParseAndValidateBytes strictly decodes a DatadogAgent YAML/JSON document and
// runs the operator's semantic validation (the same ValidateDatadogAgent check
// the reconcile loop performs). Strict decoding rejects unknown/duplicate
// fields, so structural mistakes surface here rather than being silently
// dropped. The parsed DatadogAgent is returned so callers can inspect it.
func ParseAndValidateBytes(data []byte) (*datadoghqv2alpha1.DatadogAgent, error) {
dda := &datadoghqv2alpha1.DatadogAgent{}
if err := yaml.UnmarshalStrict(data, dda); err != nil {
return nil, fmt.Errorf("parsing DatadogAgent: %w", err)
}

if dda.Name == "" {
return nil, fmt.Errorf("DatadogAgent has no metadata.name")
}

if err := datadoghqv2alpha1.ValidateDatadogAgent(dda); err != nil {
return dda, fmt.Errorf("invalid DatadogAgent %q: %w", dda.Name, err)
}

return dda, nil
}

// ParseAndValidateFile reads a DatadogAgent manifest from disk and delegates to
// ParseAndValidateBytes.
func ParseAndValidateFile(path string) (*datadoghqv2alpha1.DatadogAgent, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
return ParseAndValidateBytes(data)
}
134 changes: 134 additions & 0 deletions pkg/validate/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// 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.

package validate

import (
"strings"
"testing"
)

func TestParseAndValidateBytes(t *testing.T) {
tests := []struct {
name string
manifest string
wantErr bool
errContains string
}{
{
name: "valid manifest",
manifest: `apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
metadata:
name: datadog-demo
spec:
global:
credentials:
apiKey: key
appKey: appkey
commonLabels:
team: platform
`,
wantErr: false,
},
{
name: "missing name",
manifest: `apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
spec:
global:
credentials:
apiKey: key
`,
wantErr: true,
errContains: "no metadata.name",
},
{
name: "missing credentials fails semantic validation",
manifest: `apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
metadata:
name: datadog-demo
spec:
global:
clusterName: demo
`,
wantErr: true,
errContains: "credentials not configured",
},
{
name: "reserved commonLabels prefix rejected",
manifest: `apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
metadata:
name: datadog-demo
spec:
global:
credentials:
apiKey: key
commonLabels:
agent.datadoghq.com/foo: bar
`,
wantErr: true,
errContains: "reserved key",
},
{
name: "unknown field rejected by strict decoding",
manifest: `apiVersion: datadoghq.com/v2alpha1
kind: DatadogAgent
metadata:
name: datadog-demo
spec:
global:
credentials:
apiKey: key
thisFieldDoesNotExist: true
`,
wantErr: true,
errContains: "parsing DatadogAgent",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dda, err := ParseAndValidateBytes([]byte(tt.manifest))
if tt.wantErr {
if err == nil {
t.Fatalf("expected error, got nil")
}
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Fatalf("expected error containing %q, got %q", tt.errContains, err.Error())
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if dda == nil {
t.Fatalf("expected non-nil DatadogAgent")
}
if dda.Name != "datadog-demo" {
t.Fatalf("expected name datadog-demo, got %q", dda.Name)
}
})
}
}

func TestParseAndValidateFile(t *testing.T) {
_, err := ParseAndValidateFile("testdata/valid_datadogagent.yaml")
if err != nil {
t.Fatalf("unexpected error validating demo manifest: %v", err)
}
}

func TestParseAndValidateFile_NotFound(t *testing.T) {
_, err := ParseAndValidateFile("testdata/does_not_exist.yaml")
if err == nil {
t.Fatalf("expected error for missing file")
}
if !strings.Contains(err.Error(), "reading") {
t.Fatalf("expected reading error, got %q", err.Error())
}
}
Loading