Skip to content

Commit bb851ec

Browse files
feat: Support api key rotation of RC client and server
1 parent 4fe811a commit bb851ec

3 files changed

Lines changed: 441 additions & 71 deletions

File tree

cmd/main.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ func run(opts *options) error {
321321
// If RBAC restricts list and watch permissions, the informer will log errors and may cause crash loops.
322322
// Reader interface as returned from mgr.GetAPIReader() reads directly from API server bypassing cache and informer initialization.
323323
credsManager := config.NewCredentialManagerWithDecryptor(mgr.GetAPIReader(), secrets.NewSecretBackend())
324-
creds, err := credsManager.GetCredentials()
324+
_, err = credsManager.GetCredentials()
325325

326326
if opts.secretRefreshInterval > 0 && opts.secretBackendCommand == "" {
327327
setupLog.Error(nil, "secretRefreshInterval is set but secretBackendCommand is not configured")
@@ -340,10 +340,13 @@ func run(opts *options) error {
340340
// to handle that.
341341
<-mgr.Elected()
342342

343-
if rcErr := rcUpdater.Setup(creds); rcErr != nil {
343+
if rcErr := rcUpdater.Setup(credsManager); rcErr != nil {
344344
setupErrorf(setupLog, rcErr, "Unable to set up Remote Config service")
345345
return
346346
}
347+
if opts.secretBackendCommand != "" && opts.secretRefreshInterval > 0 {
348+
go rcUpdater.StartCredentialWatchRoutine(credsManager, opts.secretRefreshInterval)
349+
}
347350

348351
if opts.remoteUpdatesEnabled {
349352
if rcErr := setupFleetDaemon(setupLog, mgr, rcUpdater.Client(), opts.createControllerRevisions && opts.datadogAgentInternalEnabled); rcErr != nil {

pkg/remoteconfig/updater.go

Lines changed: 205 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,35 @@ const (
4141

4242
type RemoteConfigUpdater struct {
4343
kubeClient kubeclient.Client
44-
rcClient *client.Client
45-
rcService *service.CoreAgentService
44+
rcClient rcRuntimeClient
45+
rcService rcService
4646
serviceConf RcServiceConfiguration
4747
logger logr.Logger
4848
mu sync.RWMutex
49+
50+
lifecycleMu sync.Mutex
51+
activeAPIKey string
52+
subscriptions []rcSubscription
53+
installerState []*pbgo.PackageState
54+
remoteConfigFactory rcRuntimeFactory
55+
}
56+
57+
type rcRuntimeFactory func(conf RcServiceConfiguration) (rcService, rcRuntimeClient, error)
58+
59+
type rcService interface {
60+
Start()
61+
Stop() error
62+
}
63+
64+
type rcRuntimeClient interface {
65+
RCClient
66+
Start()
67+
Close()
68+
}
69+
70+
type rcSubscription struct {
71+
product string
72+
fn func(update map[string]state.RawConfig, applyStateCallback func(string, state.ApplyStatus))
4973
}
5074

5175
type RcServiceConfiguration struct {
@@ -68,7 +92,7 @@ type RCClient interface {
6892

6993
// Client returns the underlying RC client.
7094
func (r *RemoteConfigUpdater) Client() RCClient {
71-
return r.rcClient
95+
return r
7296
}
7397

7498
// DatadogProductRemoteConfig is an interface for Datadog product remote configuration
@@ -135,98 +159,90 @@ type dummyTelemetryReporter struct{}
135159
func (d dummyTelemetryReporter) IncRateLimit() {}
136160
func (d dummyTelemetryReporter) IncTimeout() {}
137161

138-
func (r *RemoteConfigUpdater) Setup(creds config.Creds) error {
139-
apiKey := creds.APIKey
140-
if apiKey == "" {
162+
func (r *RemoteConfigUpdater) Setup(credsManager *config.CredentialManager) error {
163+
creds, err := credsManager.GetCredentials()
164+
if err != nil {
165+
return err
166+
}
167+
if creds.APIKey == "" {
141168
return errors.New("error obtaining API key")
142169
}
143170

171+
r.lifecycleMu.Lock()
172+
defer r.lifecycleMu.Unlock()
173+
if r.rcClient != nil || r.rcService != nil {
174+
return nil
175+
}
176+
177+
if err := r.startRuntime(creds.APIKey); err != nil {
178+
return err
179+
}
180+
r.activeAPIKey = creds.APIKey
181+
return nil
182+
}
183+
184+
// startRuntime will start the remote configuration client and service. This must be called a held lock.
185+
func (r *RemoteConfigUpdater) startRuntime(apiKey string) error {
144186
site := os.Getenv(constants.DDSite) // TODO support DD_URL as well
145187
clusterName := os.Getenv(constants.DDClusterName)
146188
directorRoot := os.Getenv("DD_REMOTE_CONFIGURATION_DIRECTOR_ROOT")
147189
configRoot := os.Getenv("DD_REMOTE_CONFIGURATION_CONFIG_ROOT")
148190
endpoint := os.Getenv("DD_REMOTE_CONFIGURATION_RC_DD_URL")
149191

150-
if r.rcClient == nil && r.rcService == nil {
151-
// Setup rcClient and rcService
152-
err := r.Start(apiKey, site, clusterName, directorRoot, configRoot, endpoint)
153-
if err != nil {
154-
return err
155-
}
156-
}
157-
158-
return nil
159-
192+
return r.startRuntimeWithConfig(apiKey, site, clusterName, directorRoot, configRoot, endpoint)
160193
}
161194

162-
func (r *RemoteConfigUpdater) Start(apiKey string, site string, clusterName string, directorRoot string, configRoot string, endpoint string) error {
163-
195+
func (r *RemoteConfigUpdater) startRuntimeWithConfig(apiKey string, site string, clusterName string, directorRoot string, configRoot string, endpoint string) error {
164196
r.logger.Info("Starting Remote Configuration client and service")
165197

166-
err := r.configureService(apiKey, site, clusterName, directorRoot, configRoot, endpoint)
198+
serviceConf, err := r.newServiceConfiguration(apiKey, site, clusterName, directorRoot, configRoot, endpoint)
167199
if err != nil {
168200
r.logger.Error(err, "Failed to configure Remote Configuration service")
169201
return err
170202
}
171203

172-
rcService, err := service.NewService(
173-
r.serviceConf.cfg,
174-
"",
175-
r.serviceConf.baseRawURL,
176-
r.serviceConf.hostname,
177-
func() []string { return []string{"cluster_name:" + r.serviceConf.clusterName} },
178-
r.serviceConf.telemetryReporter,
179-
r.serviceConf.agentVersion,
180-
service.WithAPIKey(apiKey),
181-
service.WithDatabaseFileName(filepath.Join(r.serviceConf.rcDatabaseDir, fmt.Sprintf("remote-config-%s.db", uuid.New()))),
182-
service.WithDirectorRootOverride(r.serviceConf.cfg.GetString("site"), r.serviceConf.cfg.GetString("remote_configuration.director_root")),
183-
service.WithConfigRootOverride(r.serviceConf.cfg.GetString("site"), r.serviceConf.cfg.GetString("remote_configuration.config_root")),
184-
)
204+
newService, newClient, err := r.remoteConfigFactory(serviceConf)
185205
if err != nil {
186-
r.logger.Error(err, "Failed to create Remote Configuration service")
206+
r.logger.Error(err, "Failed to create Remote Configuration runtime")
187207
return err
188208
}
189-
r.rcService = rcService
190209

191-
updaterTags := []string{"updater_type:datadog-operator"}
192-
if r.serviceConf.clusterName != "" {
193-
updaterTags = append(updaterTags, "cluster_name:"+r.serviceConf.clusterName)
194-
}
195-
rcClient, err := client.NewClient(
196-
rcService,
197-
client.WithUpdater(updaterTags...),
198-
client.WithProducts(state.ProductAgentConfig, state.ProductOrchestratorK8sCRDs),
199-
client.WithDirectorRootOverride(r.serviceConf.cfg.GetString("site"), r.serviceConf.cfg.GetString("remote_configuration.director_root")),
200-
client.WithPollInterval(pollInterval),
201-
)
202-
if err != nil {
203-
r.logger.Error(err, "Failed to create Remote Configuration client")
204-
return err
205-
}
206-
r.rcClient = rcClient
207-
rcClient.SetInstallerState([]*pbgo.PackageState{
208-
{
209-
Package: "datadog-operator",
210-
StableVersion: "0.0.1",
211-
StableConfigVersion: "0.0.1",
212-
},
213-
})
210+
oldSvc := r.rcService
211+
oldClient := r.rcClient
214212

215-
rcService.Start()
213+
newClient.SetInstallerState(r.installerState)
214+
215+
newService.Start()
216216
r.logger.Info("Remote Configuration service started")
217217

218-
rcClient.Start()
219-
r.logger.Info("Remote Configuration client started")
218+
for _, subscription := range r.subscriptions {
219+
newClient.Subscribe(subscription.product, subscription.fn)
220+
}
220221

221-
rcClient.Subscribe(string(state.ProductAgentConfig), r.agentConfigUpdateCallback)
222+
newClient.Start()
223+
r.logger.Info("Remote Configuration client started")
222224

223-
rcClient.Subscribe(string(state.ProductOrchestratorK8sCRDs), r.crdConfigUpdateCallback)
225+
r.serviceConf = serviceConf
226+
r.rcService = newService
227+
r.rcClient = newClient
228+
229+
// Clean up old service/client after the new ones are setup and swapped.
230+
if oldSvc != nil {
231+
if err := oldSvc.Stop(); err != nil {
232+
// The new runtime is already active; returning this error would leave
233+
// activeAPIKey stale and cause repeated restart attempts.
234+
r.logger.Error(err, "Failed to stop previous Remote Configuration service")
235+
}
236+
}
237+
if oldClient != nil {
238+
oldClient.Close()
239+
}
224240

225241
return nil
226242
}
227243

228-
// configureService fills the configuration needed to start the rc service
229-
func (r *RemoteConfigUpdater) configureService(apiKey, site, clusterName, directorRoot, configRoot, endpoint string) error {
244+
// newServiceConfiguration builds the configuration needed to start the rc service.
245+
func (r *RemoteConfigUpdater) newServiceConfiguration(apiKey, site, clusterName, directorRoot, configRoot, endpoint string) (RcServiceConfiguration, error) {
230246
cfg := model.NewConfig("datadog", "DD", strings.NewReplacer(".", "_"))
231247

232248
cfg.SetWithoutSource("api_key", apiKey)
@@ -242,10 +258,10 @@ func (r *RemoteConfigUpdater) configureService(apiKey, site, clusterName, direct
242258
// TODO consider different dir
243259
baseDir := filepath.Join(os.TempDir(), "datadog-operator")
244260
if err := os.MkdirAll(baseDir, 0777); err != nil {
245-
return err
261+
return RcServiceConfiguration{}, err
246262
}
247263

248-
serviceConf := RcServiceConfiguration{
264+
return RcServiceConfiguration{
249265
cfg: cfg,
250266
apiKey: apiKey,
251267
baseRawURL: endpoint,
@@ -255,9 +271,43 @@ func (r *RemoteConfigUpdater) configureService(apiKey, site, clusterName, direct
255271
// TODO fix when other values accepted
256272
agentVersion: "7.50.0",
257273
rcDatabaseDir: baseDir,
274+
}, nil
275+
}
276+
277+
func defaultRCFactory(conf RcServiceConfiguration) (rcService, rcRuntimeClient, error) {
278+
rcService, err := service.NewService(
279+
conf.cfg,
280+
"",
281+
conf.baseRawURL,
282+
conf.hostname,
283+
func() []string { return []string{"cluster_name:" + conf.clusterName} },
284+
conf.telemetryReporter,
285+
conf.agentVersion,
286+
service.WithAPIKey(conf.apiKey),
287+
service.WithDatabaseFileName(filepath.Join(conf.rcDatabaseDir, fmt.Sprintf("remote-config-%s.db", uuid.New()))),
288+
service.WithDirectorRootOverride(conf.cfg.GetString("site"), conf.cfg.GetString("remote_configuration.director_root")),
289+
service.WithConfigRootOverride(conf.cfg.GetString("site"), conf.cfg.GetString("remote_configuration.config_root")),
290+
)
291+
if err != nil {
292+
return nil, nil, err
258293
}
259-
r.serviceConf = serviceConf
260-
return nil
294+
295+
updaterTags := []string{"updater_type:datadog-operator"}
296+
if conf.clusterName != "" {
297+
updaterTags = append(updaterTags, "cluster_name:"+conf.clusterName)
298+
}
299+
rcClient, err := client.NewClient(
300+
rcService,
301+
client.WithUpdater(updaterTags...),
302+
client.WithProducts(state.ProductAgentConfig, state.ProductOrchestratorK8sCRDs),
303+
client.WithDirectorRootOverride(conf.cfg.GetString("site"), conf.cfg.GetString("remote_configuration.director_root")),
304+
client.WithPollInterval(pollInterval),
305+
)
306+
if err != nil {
307+
return nil, nil, err
308+
}
309+
310+
return rcService, rcClient, nil
261311
}
262312

263313
// getEndpoint returns the Remote Config endpoint, based on `site` and the prefix
@@ -268,6 +318,76 @@ func getEndpoint(prefix, site string) string {
268318
return prefix + defaultSite
269319
}
270320

321+
func (r *RemoteConfigUpdater) Subscribe(product string, fn func(update map[string]state.RawConfig, applyStateCallback func(string, state.ApplyStatus))) {
322+
r.lifecycleMu.Lock()
323+
defer r.lifecycleMu.Unlock()
324+
325+
subscription := rcSubscription{product: product, fn: fn}
326+
r.subscriptions = append(r.subscriptions, subscription)
327+
if r.rcClient != nil {
328+
r.rcClient.Subscribe(product, fn)
329+
}
330+
}
331+
332+
func (r *RemoteConfigUpdater) GetInstallerState() []*pbgo.PackageState {
333+
r.lifecycleMu.Lock()
334+
defer r.lifecycleMu.Unlock()
335+
if r.rcClient != nil {
336+
return r.rcClient.GetInstallerState()
337+
}
338+
return r.installerState
339+
}
340+
341+
func (r *RemoteConfigUpdater) SetInstallerState(packages []*pbgo.PackageState) {
342+
r.lifecycleMu.Lock()
343+
defer r.lifecycleMu.Unlock()
344+
345+
// Save the installer state in the updater so that it can be used to restore the state of the installer
346+
// when swapping out remote config clients.
347+
r.installerState = packages
348+
if r.rcClient != nil {
349+
r.rcClient.SetInstallerState(packages)
350+
}
351+
}
352+
353+
func (r *RemoteConfigUpdater) syncCredentials(credsManager *config.CredentialManager) error {
354+
creds, err := credsManager.GetCredentials()
355+
if err != nil {
356+
return err
357+
}
358+
if creds.APIKey == "" {
359+
return errors.New("error obtaining API key")
360+
}
361+
362+
r.lifecycleMu.Lock()
363+
defer r.lifecycleMu.Unlock()
364+
365+
if creds.APIKey == r.activeAPIKey {
366+
return nil
367+
}
368+
369+
if err := r.startRuntime(creds.APIKey); err != nil {
370+
return err
371+
}
372+
r.activeAPIKey = creds.APIKey
373+
r.logger.Info("Remote Configuration credentials changed, runtime restarted")
374+
return nil
375+
}
376+
377+
func (r *RemoteConfigUpdater) StartCredentialWatchRoutine(credsManager *config.CredentialManager, interval time.Duration) {
378+
r.logger.Info("Starting Remote Configuration credential watch routine", "interval", interval)
379+
380+
ticker := time.NewTicker(interval)
381+
defer ticker.Stop()
382+
383+
for {
384+
<-ticker.C
385+
if err := r.syncCredentials(credsManager); err != nil {
386+
r.logger.Error(err, "Failed to sync Remote Configuration credentials")
387+
}
388+
}
389+
}
390+
271391
// getAndUpdateDatadogAgent is used to prevent race conditions when updating the DDA's status
272392
// we do not want to modify the status without using this function or we could have conflicts
273393
func (r *RemoteConfigUpdater) getAndUpdateDatadogAgent(ctx context.Context, cfg DatadogProductRemoteConfig, f func(v2alpha1.DatadogAgent, DatadogProductRemoteConfig) error) error {
@@ -585,6 +705,9 @@ func (r *RemoteConfigUpdater) updateInstanceStatus(dda v2alpha1.DatadogAgent, co
585705
}
586706

587707
func (r *RemoteConfigUpdater) Stop() error {
708+
r.lifecycleMu.Lock()
709+
defer r.lifecycleMu.Unlock()
710+
588711
if r.rcService != nil {
589712
err := r.rcService.Stop()
590713
if err != nil {
@@ -600,8 +723,21 @@ func (r *RemoteConfigUpdater) Stop() error {
600723
}
601724

602725
func NewRemoteConfigUpdater(client kubeclient.Client, logger logr.Logger) *RemoteConfigUpdater {
603-
return &RemoteConfigUpdater{
726+
r := &RemoteConfigUpdater{
604727
kubeClient: client,
605728
logger: logger,
729+
installerState: []*pbgo.PackageState{
730+
{
731+
Package: "datadog-operator",
732+
StableVersion: "0.0.1",
733+
StableConfigVersion: "0.0.1",
734+
},
735+
},
736+
remoteConfigFactory: defaultRCFactory,
737+
}
738+
r.subscriptions = []rcSubscription{
739+
{product: state.ProductAgentConfig, fn: r.agentConfigUpdateCallback},
740+
{product: state.ProductOrchestratorK8sCRDs, fn: r.crdConfigUpdateCallback},
606741
}
742+
return r
607743
}

0 commit comments

Comments
 (0)