Skip to content

Commit 0075a35

Browse files
committed
[ACTP] add ensure-enrollment command
1 parent 18205fd commit 0075a35

14 files changed

Lines changed: 644 additions & 31 deletions

File tree

cmd/privateactionrunner/subcommands/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ go_library(
77
visibility = ["//visibility:public"],
88
deps = [
99
"//cmd/privateactionrunner/command",
10+
"//cmd/privateactionrunner/subcommands/ensureenrollment",
1011
"//cmd/privateactionrunner/subcommands/rotateidentity",
1112
"//cmd/privateactionrunner/subcommands/run",
1213
"//cmd/privateactionrunner/subcommands/runexecutor",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
load("//bazel/rules/go:dd_agent_go_test.bzl", "dd_agent_go_test")
3+
4+
go_library(
5+
name = "ensureenrollment",
6+
srcs = ["command.go"],
7+
importpath = "github.com/DataDog/datadog-agent/cmd/privateactionrunner/subcommands/ensureenrollment",
8+
visibility = ["//visibility:public"],
9+
deps = [
10+
"//cmd/privateactionrunner/command",
11+
"//cmd/privateactionrunner/subcommands/identity",
12+
"//comp/core",
13+
"//comp/core/config",
14+
"//comp/core/hostname",
15+
"//comp/core/hostname/hostnameimpl",
16+
"//comp/core/log/def",
17+
"//comp/privateactionrunner/def",
18+
"//pkg/privateactionrunner/enrollment",
19+
"//pkg/privateactionrunner/util",
20+
"//pkg/util/fxutil",
21+
"@com_github_spf13_cobra//:cobra",
22+
"@org_uber_go_fx//:fx",
23+
],
24+
)
25+
26+
dd_agent_go_test(
27+
name = "ensureenrollment_test",
28+
srcs = ["command_test.go"],
29+
embed = [":ensureenrollment"],
30+
deps = [
31+
"//cmd/privateactionrunner/command",
32+
"//comp/core/config",
33+
"//comp/core/hostname/hostnameinterface/mock",
34+
"//comp/core/log/def",
35+
"//comp/core/log/mock",
36+
"//pkg/privateactionrunner/enrollment",
37+
"//pkg/privateactionrunner/util",
38+
"//pkg/util/fxutil",
39+
"@com_github_stretchr_testify//assert",
40+
"@com_github_stretchr_testify//require",
41+
],
42+
)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2025-present Datadog, Inc.
5+
6+
// Package ensureenrollment implements the 'ensure-enrollment' subcommand for the private-action-runner.
7+
package ensureenrollment
8+
9+
import (
10+
"context"
11+
"crypto/ecdsa"
12+
"errors"
13+
"fmt"
14+
15+
"github.com/spf13/cobra"
16+
"go.uber.org/fx"
17+
18+
"github.com/DataDog/datadog-agent/cmd/privateactionrunner/command"
19+
identitycmd "github.com/DataDog/datadog-agent/cmd/privateactionrunner/subcommands/identity"
20+
"github.com/DataDog/datadog-agent/comp/core"
21+
"github.com/DataDog/datadog-agent/comp/core/config"
22+
"github.com/DataDog/datadog-agent/comp/core/hostname"
23+
"github.com/DataDog/datadog-agent/comp/core/hostname/hostnameimpl"
24+
log "github.com/DataDog/datadog-agent/comp/core/log/def"
25+
par "github.com/DataDog/datadog-agent/comp/privateactionrunner/def"
26+
"github.com/DataDog/datadog-agent/pkg/privateactionrunner/enrollment"
27+
parutil "github.com/DataDog/datadog-agent/pkg/privateactionrunner/util"
28+
"github.com/DataDog/datadog-agent/pkg/util/fxutil"
29+
)
30+
31+
type enrollAndPersistFunc func(context.Context, log.Component, config.Component, *enrollment.AgentIdentifier) (*enrollment.Result, error)
32+
33+
// Commands returns the ensure-enrollment subcommand.
34+
func Commands(globalParams *command.GlobalParams) []*cobra.Command {
35+
cmd := &cobra.Command{
36+
Use: "ensure-enrollment",
37+
Short: "Ensure that the Private Action Runner has a valid identity",
38+
Long: `Reuses a persisted identity when it belongs to the current Agent host.
39+
If no usable persisted or configured identity exists, self-enrollment is performed
40+
when private_action_runner.self_enroll is enabled.`,
41+
RunE: func(_ *cobra.Command, _ []string) error {
42+
return fxutil.OneShot(run,
43+
fx.Supply(core.BundleParams{
44+
ConfigParams: config.NewAgentParams(globalParams.ConfFilePath, config.WithExtraConfFiles(globalParams.ExtraConfFilePath)),
45+
LogParams: log.ForOneShot(command.LoggerName, "info", true),
46+
}),
47+
core.Bundle(core.WithSecrets()),
48+
hostnameimpl.Module(),
49+
)
50+
},
51+
}
52+
return []*cobra.Command{cmd}
53+
}
54+
55+
func run(logger log.Component, cfg config.Component, hostnameComp hostname.Component) error {
56+
return ensureEnrollment(context.Background(), logger, cfg, hostnameComp, identitycmd.EnrollAndPersist)
57+
}
58+
59+
func ensureEnrollment(ctx context.Context, logger log.Component, cfg config.Component, hostnameComp hostname.Component, enrollAndPersist enrollAndPersistFunc) error {
60+
if !cfg.GetBool(par.PAREnabled) {
61+
return errors.New("private_action_runner.enabled is false - set it to true before ensuring enrollment")
62+
}
63+
64+
agentIdentifier, err := enrollment.GetAgentIdentifier(ctx, hostnameComp)
65+
if err != nil {
66+
return fmt.Errorf("failed to get agent identifier: %w", err)
67+
}
68+
69+
discardPersisted := false
70+
71+
persisted, err := enrollment.GetIdentityFromPreviousEnrollment(ctx, cfg)
72+
switch {
73+
case err != nil && !errors.Is(err, enrollment.ErrIdentityCorrupt):
74+
// May be transient; re-enrolling would register a second runner.
75+
return fmt.Errorf("failed to load persisted identity: %w", err)
76+
case err != nil:
77+
logger.Warnf("Discarding unusable persisted identity: %v", err)
78+
discardPersisted = true
79+
case persisted != nil:
80+
if err := validateIdentity(persisted.URN, persisted.PrivateKey); err != nil {
81+
logger.Warnf("Discarding invalid persisted identity: %v", err)
82+
discardPersisted = true
83+
} else if !enrollment.ShouldReenroll(agentIdentifier, persisted) {
84+
logger.Info("Persisted identity is valid; enrollment is not required")
85+
return nil
86+
} else {
87+
discardPersisted = true
88+
}
89+
}
90+
91+
configuredURN := cfg.GetString(par.PARUrn)
92+
configuredPrivateKey := cfg.GetString(par.PARPrivateKey)
93+
if err := validateConfiguredIdentity(configuredURN, configuredPrivateKey); err != nil {
94+
return err
95+
}
96+
if configuredURN != "" && configuredPrivateKey != "" {
97+
// Rust prefers a persisted file over inline configuration and repeats none of
98+
// the checks above, so the unusable file has to go.
99+
if discardPersisted {
100+
if err := enrollment.RemoveIdentityFile(cfg); err != nil {
101+
return err
102+
}
103+
}
104+
logger.Info("Configured identity is complete; enrollment is not required")
105+
return nil
106+
}
107+
108+
if !cfg.GetBool(par.PARSelfEnroll) {
109+
return errors.New("no valid Private Action Runner identity is available and private_action_runner.self_enroll is false; configure a URN and private key or enable self-enrollment")
110+
}
111+
112+
result, err := enrollAndPersist(ctx, logger, cfg, agentIdentifier)
113+
if err != nil {
114+
return err
115+
}
116+
logger.Infof("Identity successfully enrolled. New URN: %s", result.URN)
117+
return nil
118+
}
119+
120+
func validateConfiguredIdentity(urn, privateKey string) error {
121+
if urn != "" {
122+
if _, err := parutil.ParseRunnerURN(urn); err != nil {
123+
return fmt.Errorf("configured private_action_runner.urn is invalid: %w", err)
124+
}
125+
}
126+
if privateKey != "" {
127+
if err := validatePrivateKey(privateKey); err != nil {
128+
return fmt.Errorf("configured private_action_runner.private_key is invalid: %w", err)
129+
}
130+
}
131+
return nil
132+
}
133+
134+
func validateIdentity(urn, privateKey string) error {
135+
if _, err := parutil.ParseRunnerURN(urn); err != nil {
136+
return fmt.Errorf("invalid URN: %w", err)
137+
}
138+
if err := validatePrivateKey(privateKey); err != nil {
139+
return fmt.Errorf("invalid private key: %w", err)
140+
}
141+
return nil
142+
}
143+
144+
func validatePrivateKey(encoded string) error {
145+
jwk, err := parutil.Base64ToJWK(encoded)
146+
if err != nil {
147+
return err
148+
}
149+
if _, ok := jwk.Key.(*ecdsa.PrivateKey); !ok {
150+
return errors.New("JWK does not contain an ECDSA private key")
151+
}
152+
return nil
153+
}

0 commit comments

Comments
 (0)