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
10 changes: 9 additions & 1 deletion cmd/system-probe/modules/compliance.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

"github.com/DataDog/datadog-agent/pkg/compliance"
"github.com/DataDog/datadog-agent/pkg/compliance/dbconfig"
"github.com/DataDog/datadog-agent/pkg/compliance/statusregistry"
"github.com/DataDog/datadog-agent/pkg/system-probe/api/module"
"github.com/DataDog/datadog-agent/pkg/system-probe/config"
sysconfigtypes "github.com/DataDog/datadog-agent/pkg/system-probe/config/types"
Expand Down Expand Up @@ -51,7 +52,9 @@ func newComplianceModule(_ *sysconfigtypes.Config, deps module.FactoryDependenci
runInSystemProbe := deps.CoreConfig.GetBool("compliance_config.run_in_system_probe")

if enabled && runInSystemProbe {
hostnameDetected, err := deps.Hostname.Get(context.Background())
hostnameCtx, hostnameCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer hostnameCancel()
hostnameDetected, err := deps.Hostname.Get(hostnameCtx)
if err != nil {
return nil, err
}
Expand All @@ -63,6 +66,11 @@ func newComplianceModule(_ *sysconfigtypes.Config, deps module.FactoryDependenci
if err != nil {
return nil, err
}

if complianceAgent != nil {
log.Debug("compliance: registering status renderer for remote agent")
statusregistry.Set(complianceAgent.RenderStatusText)
}
}

return &complianceModule{
Expand Down
22 changes: 22 additions & 0 deletions comp/core/remoteagent/impl-systemprobe/remoteagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/DataDog/datadog-agent/comp/core/remoteagent/helper"
"github.com/DataDog/datadog-agent/comp/core/telemetry/def"
compdef "github.com/DataDog/datadog-agent/comp/def"
"github.com/DataDog/datadog-agent/pkg/compliance/statusregistry"
"github.com/DataDog/datadog-agent/pkg/logs/metrics"
pbcore "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core"
"github.com/DataDog/datadog-agent/pkg/util/flavor"
Expand Down Expand Up @@ -65,6 +66,7 @@ func NewComponent(reqs Requires) (Provides, error) {

// Add your gRPC services implementations here:
pbcore.RegisterTelemetryProviderServer(remoteAgentServer.GetGRPCServer(), remoteagentImpl)
pbcore.RegisterStatusProviderServer(remoteAgentServer.GetGRPCServer(), remoteagentImpl)

provides := Provides{
Comp: remoteagentImpl,
Expand All @@ -80,6 +82,26 @@ type remoteagentImpl struct {

remoteAgentServer *helper.UnimplementedRemoteAgentServer
pbcore.UnimplementedTelemetryProviderServer
pbcore.UnimplementedStatusProviderServer
}

func (r *remoteagentImpl) GetStatusDetails(_ context.Context, _ *pbcore.GetStatusDetailsRequest) (*pbcore.GetStatusDetailsResponse, error) {
text, registered, err := statusregistry.GetTextOrError()
if !registered {
return &pbcore.GetStatusDetailsResponse{}, nil
}
if err != nil {
return &pbcore.GetStatusDetailsResponse{}, nil
}
return &pbcore.GetStatusDetailsResponse{
NamedSections: map[string]*pbcore.StatusSection{
"Compliance": {
Fields: map[string]string{
"": text,
},
},
},
}, nil
}

func (r *remoteagentImpl) GetTelemetry(_ context.Context, _ *pbcore.GetTelemetryRequest) (*pbcore.GetTelemetryResponse, error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ No remote agents registered
{{ $sectionName }}
{{ printDashes $sectionName "-" }}
{{- range $key, $value := $section }}
{{- if $key }}
{{ $key }}: {{ $value }}
{{- else }}
{{ $value }}
{{- end }}
{{- end }}
{{- end }}
{{ end }}
{{- end }}
{{- end }}
{{- end }}
Expand Down
2 changes: 1 addition & 1 deletion pkg/compliance/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ func (a *Agent) runTelemetry(ctx context.Context) {
}
}

func (a *Agent) getChecksStatus() interface{} {
func (a *Agent) getChecksStatus() []*CheckStatus {
a.statusesMu.RLock()
defer a.statusesMu.RUnlock()
statuses := make([]*CheckStatus, 0, len(a.statuses))
Expand Down
120 changes: 92 additions & 28 deletions pkg/compliance/status_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package compliance

import (
"bytes"
"embed"
"encoding/json"
"expvar"
Expand Down Expand Up @@ -38,41 +39,104 @@ func (statusProvider) Section() string {
return "compliance"
}

func (s statusProvider) populateStatus(stats map[string]interface{}) {
complianceStats := map[string]interface{}{}

complianceStats["endpoints"] = s.agent.opts.Reporter.Endpoints().GetStatus()

complianceVar := expvar.Get("compliance")
runnerVar := expvar.Get("runner")
if complianceVar != nil {
complianceStatusJSON := []byte(complianceVar.String())
complianceStatus := make(map[string]interface{})
json.Unmarshal(complianceStatusJSON, &complianceStatus) //nolint:errcheck
complianceStats["complianceChecks"] = complianceStatus["Checks"]

// This is the information from collector provider
// Would be great to find a better pattern
if runnerVar != nil {
runnerStatsJSON := []byte(expvar.Get("runner").String())
runnerStats := make(map[string]interface{})
json.Unmarshal(runnerStatsJSON, &runnerStats) //nolint:errcheck
complianceStats["runnerStats"] = runnerStats
// frameworkSummary holds aggregated check results for one framework.
type frameworkSummary struct {
ID string
Version string
Source string
Total int
Passed int
Failed int
Error int
Skipped int
NotRun int
}

// frameworkSummaries groups CheckStatus slices by framework and returns summaries sorted
// by framework ID.
func frameworkSummaries(checks []*CheckStatus) []frameworkSummary {
byID := map[string]*frameworkSummary{}
order := []string{}
for _, c := range checks {
s, ok := byID[c.Framework]
if !ok {
s = &frameworkSummary{ID: c.Framework, Version: c.Version, Source: c.Source}
byID[c.Framework] = s
order = append(order, c.Framework)
}
} else {
complianceStats["complianceChecks"] = map[string]interface{}{}
complianceStats["runnerStats"] = map[string]interface{}{}
s.Total++
if c.LastEvent == nil {
s.NotRun++
continue
}
switch c.LastEvent.Result {
case CheckPassed:
s.Passed++
case CheckFailed:
s.Failed++
case CheckError:
s.Error++
case CheckSkipped:
s.Skipped++
default:
s.NotRun++
}
}
result := make([]frameworkSummary, 0, len(order))
for _, id := range order {
result = append(result, *byID[id])
}
return result
}

stats["complianceStatus"] = complianceStats
// RenderStatusText renders the compliance status to text using the standard template.
func (a *Agent) RenderStatusText() (string, error) {
var buf bytes.Buffer
if err := statusComp.RenderText(templatesFS, "compliance.tmpl", &buf, a.summaryStatusData()); err != nil {
return "", err
}
return buf.String(), nil
}

func (s statusProvider) getStatus() map[string]interface{} {
stats := make(map[string]interface{})
// summaryStatusData returns status data with per-framework summaries for the remote agent text view.
func (a *Agent) summaryStatusData() map[string]interface{} {
complianceStats := map[string]interface{}{
"endpoints": a.opts.Reporter.Endpoints().GetStatus(),
"frameworkSummaries": frameworkSummaries(a.getChecksStatus()),
}
return map[string]interface{}{
"complianceStatus": complianceStats,
}
}

s.populateStatus(stats)
// StatusData returns the compliance status as a map suitable for JSON serialization.
func (a *Agent) StatusData() map[string]interface{} {
complianceStats := map[string]interface{}{
"endpoints": a.opts.Reporter.Endpoints().GetStatus(),
"complianceChecks": a.getChecksStatus(),
"runnerStats": map[string]interface{}{},
}

return stats
if runnerVar := expvar.Get("runner"); runnerVar != nil {
runnerStats := make(map[string]interface{})
if err := json.Unmarshal([]byte(runnerVar.String()), &runnerStats); err == nil {
complianceStats["runnerStats"] = runnerStats
}
}

return map[string]interface{}{
"complianceStatus": complianceStats,
}
}

func (s statusProvider) populateStatus(stats map[string]interface{}) {
for k, v := range s.agent.StatusData() {
stats[k] = v
}
}

func (s statusProvider) getStatus() map[string]interface{} {
return s.agent.StatusData()
}

// JSON populates the status map
Expand Down
63 changes: 11 additions & 52 deletions pkg/compliance/status_templates/compliance.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,62 +2,21 @@
Not enabled
{{- else}}
{{- with .complianceStatus}}
{{ if .endpoints }}
{{- if .endpoints }}
{{- range $endpoint := .endpoints }}
{{ $endpoint }}
{{- end }}
{{- end }}
{{- end }}
{{- if .frameworkSummaries }}

Checks
======
{{ $runnerStats := .runnerStats }}
{{- range $Check := .complianceChecks }}
{{ $Check.Name }}
{{printDashes $Check.Name "-"}}
Framework: {{ $Check.Framework }} ({{ $Check.Version }})
Source: {{ $Check.Source }}
{{- if $Check.InitError }}
Configuration: [{{ yellowText $Check.InitError }}]
{{- else }}
Configuration: [{{ greenText "OK"}}]
{{- if $Check.LastEvent }}
Frameworks
==========
{{- range $f := .frameworkSummaries }}

Report:
Result: {{ complianceResult $Check.LastEvent.result }}
Data:
{{- range $k, $v := $Check.LastEvent.data }}
{{ $k }}: {{ $v }}
{{- end }}
{{- end }}
{{- if and $runnerStats.Checks (index $runnerStats.Checks $Check.Name) }}
{{ $checkInstances := index $runnerStats.Checks $Check.Name }}
{{- range $checkInstances }}
Total Runs: {{humanize .TotalRuns}}
Average Execution Time : {{humanizeDuration .AverageExecutionTime "ms"}}
Last Execution Date : {{formatUnixTime .UpdateTimestamp}}
Last Successful Execution Date : {{ if .LastSuccessDate }}{{formatUnixTime .LastSuccessDate}}{{ else }}Never{{ end }}
{{- if $.CheckMetadata }}
{{- if index $.CheckMetadata .CheckID }}
metadata:
{{- range $k, $v := index $.CheckMetadata .CheckID }}
{{ $k }}: {{ $v }}
{{- end }}
{{- end }}
{{- end }}
{{- if .LastError }}
Error: {{lastErrorMessage .LastError}}
{{lastErrorTraceback .LastError -}}
{{- end }}
{{- if .LastWarnings }}
{{- range .LastWarnings }}
Warning: {{.}}
{{- end }}
{{- end }}
{{- end }}
{{- else }}
{{ greenText "Check has not run yet" }}
{{- end }}
{{- end }}
{{ end }}
{{ $f.ID }} {{ $f.Version }}
{{ printDashes $f.ID "-" }}
Rules: {{ $f.Total }} total | {{ $f.Passed }} passed | {{ $f.Failed }} failed | {{ $f.Error }} error | {{ $f.Skipped }} skipped | {{ $f.NotRun }} not run yet
{{- end }}
{{- end }}
{{- end }}
{{- end }}
8 changes: 8 additions & 0 deletions pkg/compliance/statusregistry/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "statusregistry",
srcs = ["registry.go"],
importpath = "github.com/DataDog/datadog-agent/pkg/compliance/statusregistry",
visibility = ["//visibility:public"],
)
41 changes: 41 additions & 0 deletions pkg/compliance/statusregistry/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 statusregistry is a process-global bridge that lets the compliance
// module (cmd/system-probe/modules) publish its rendered status text to the
// system-probe remoteagent component without introducing a direct import
// dependency on pkg/compliance from the component layer.
package statusregistry

import "sync"

var (
mu sync.RWMutex
renderer func() (string, error)
)

// Set registers fn as the compliance status renderer. Safe to call once at
// startup; fn must remain safe to call concurrently after registration.
func Set(fn func() (string, error)) {
mu.Lock()
defer mu.Unlock()
renderer = fn
}

// GetTextOrError calls the registered renderer and returns the text plus any
// error from rendering, so callers can log the reason for failure.
func GetTextOrError() (string, bool, error) {
mu.RLock()
fn := renderer
mu.RUnlock()
if fn == nil {
return "", false, nil
}
text, err := fn()
if err != nil {
return "", true, err
}
return text, true, nil
}
Loading