Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
160b3f2
add image resolver config type
erikayasuda Nov 20, 2025
9e11533
update image_resolver_test
erikayasuda Nov 20, 2025
88fd768
update injector_test
erikayasuda Nov 20, 2025
a060afe
remove duplicate remoteconfigclient interface
erikayasuda Nov 20, 2025
9e53643
update start command to use an ImageResolverConfig to init an ImageRe…
erikayasuda Nov 20, 2025
74dbd9b
Merge branch 'main' into erikayasuda/imageresolver_config
erikayasuda Nov 20, 2025
9296434
update tests
erikayasuda Nov 20, 2025
61db2a2
add comments
erikayasuda Nov 20, 2025
f4a874e
remove stutter
erikayasuda Nov 20, 2025
59ee119
pass config by value not pointer
erikayasuda Nov 20, 2025
479b0db
pkg comment
erikayasuda Nov 20, 2025
be61aec
Merge branch 'main' into erikayasuda/imageresolver_config
erikayasuda Dec 3, 2025
4026dbb
update imageresolver.config to have a string slice for the dd registries
erikayasuda Dec 3, 2025
91fd8e8
Merge branch 'main' into erikayasuda/imageresolver_config
erikayasuda Dec 16, 2025
b749f15
Merge branch 'erikayasuda/imageresolver_config' of github.com:DataDog…
erikayasuda Dec 18, 2025
8bb35b6
Merge branch 'main' into erikayasuda/imageresolver_config
erikayasuda Dec 18, 2025
8e501a7
update tests to reflect changes
erikayasuda Dec 18, 2025
d887719
clean up
erikayasuda Dec 18, 2025
825728a
clean up
erikayasuda Dec 18, 2025
6f33f6f
Merge branch 'main' into erikayasuda/imageresolver_config
erikayasuda Dec 18, 2025
28c8c78
Update pkg/clusteragent/admission/mutate/autoinstrumentation/image_re…
erikayasuda Dec 18, 2025
d05ae4b
Merge branch 'main' into erikayasuda/imageresolver_config
erikayasuda Dec 19, 2025
57878bd
added simple tests to validate config
erikayasuda Dec 19, 2025
d250361
remove NewTestConfig()
erikayasuda Dec 22, 2025
90244eb
Merge branch 'main' into erikayasuda/imageresolver_config
erikayasuda Dec 22, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/DataDog/datadog-agent/comp/core/config"
workloadmeta "github.com/DataDog/datadog-agent/comp/core/workloadmeta/def"
"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoinstrumentation/imageresolver"
mutatecommon "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common"
configWebhook "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/config"
"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/tagsfromlabels"
Expand All @@ -28,7 +29,7 @@ func NewAutoInstrumentation(datadogConfig config.Component, wmeta workloadmeta.C
return nil, fmt.Errorf("failed to create auto instrumentation config: %v", err)
}

imageResolver := NewImageResolver(rcClient, datadogConfig)
imageResolver := NewImageResolver(imageresolver.NewConfig(datadogConfig, rcClient))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this nice that you only had to touch this here 👀

apm, err := NewTargetMutator(config, wmeta, imageResolver)
if err != nil {
return nil, fmt.Errorf("failed to create auto instrumentation namespace mutator: %v", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,12 @@ import (

"github.com/opencontainers/go-digest"

"github.com/DataDog/datadog-agent/comp/core/config"
"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics"
"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoinstrumentation/imageresolver"
"github.com/DataDog/datadog-agent/pkg/remoteconfig/state"
"github.com/DataDog/datadog-agent/pkg/util/log"
)

// RemoteConfigClient defines the interface we need for remote config operations
type RemoteConfigClient interface {
GetConfigs(product string) map[string]state.RawConfig
Subscribe(product string, callback func(map[string]state.RawConfig, func(string, state.ApplyStatus)))
}

// ImageResolver resolves container image references from tag-based to digest-based.
type ImageResolver interface {
// Resolve takes a registry, repository, and tag string (e.g., "gcr.io/datadoghq", "dd-lib-python-init", "v3")
Expand Down Expand Up @@ -56,7 +50,7 @@ func (r *noOpImageResolver) Resolve(registry string, repository string, tag stri
// remoteConfigImageResolver resolves image references using remote configuration data.
// It maintains a cache of image mappings received from the remote config service.
type remoteConfigImageResolver struct {
rcClient RemoteConfigClient
rcClient imageresolver.RemoteConfigClient

mu sync.RWMutex
imageMappings map[string]map[string]ImageInfo // repository name -> tag -> resolved image
Expand All @@ -67,24 +61,16 @@ type remoteConfigImageResolver struct {
retryDelay time.Duration
}

// newRemoteConfigImageResolver creates a new remoteConfigImageResolver.
// Assumes rcClient is non-nil.
func newRemoteConfigImageResolver(rcClient RemoteConfigClient, datadoghqRegistries map[string]struct{}) ImageResolver {
return newRemoteConfigImageResolverWithRetryConfig(rcClient, 5, 1*time.Second, datadoghqRegistries)
}

// newRemoteConfigImageResolverWithRetryConfig creates a resolver with configurable retry behavior.
// Useful for testing with faster retry settings.
func newRemoteConfigImageResolverWithRetryConfig(rcClient RemoteConfigClient, maxRetries int, retryDelay time.Duration, datadoghqRegistries map[string]struct{}) ImageResolver {
func newRcImageResolver(cfg imageresolver.Config) ImageResolver {
resolver := &remoteConfigImageResolver{
rcClient: rcClient,
rcClient: cfg.RCClient,
imageMappings: make(map[string]map[string]ImageInfo),
maxRetries: maxRetries,
retryDelay: retryDelay,
datadoghqRegistries: datadoghqRegistries,
maxRetries: cfg.MaxInitRetries,
retryDelay: cfg.InitRetryDelay,
datadoghqRegistries: cfg.DDRegistries,
}

rcClient.Subscribe(state.ProductGradualRollout, resolver.processUpdate)
resolver.rcClient.Subscribe(state.ProductGradualRollout, resolver.processUpdate)
log.Debugf("Subscribed to %s", state.ProductGradualRollout)

go func() {
Expand Down Expand Up @@ -286,25 +272,16 @@ func newResolvedImage(registry string, repositoryName string, imageInfo ImageInf

// NewImageResolver creates the appropriate ImageResolver based on whether
// a remote config client is available.
func NewImageResolver(rcClient RemoteConfigClient, cfg config.Component) ImageResolver {

if rcClient == nil || reflect.ValueOf(rcClient).IsNil() {
func NewImageResolver(cfg imageresolver.Config) ImageResolver {
if cfg.RCClient == nil || reflect.ValueOf(cfg.RCClient).IsNil() {
log.Debugf("No remote config client available")
return newNoOpImageResolver()
}

datadogRegistriesList := cfg.GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries")
datadogRegistries := newDatadoghqRegistries(datadogRegistriesList)

return newRemoteConfigImageResolverWithDefaultDatadoghqRegistries(rcClient, datadogRegistries)
}

func newRemoteConfigImageResolverWithDefaultDatadoghqRegistries(rcClient RemoteConfigClient, datadoghqRegistries map[string]struct{}) ImageResolver {
resolver := newRemoteConfigImageResolver(rcClient, datadoghqRegistries)
resolver.(*remoteConfigImageResolver).datadoghqRegistries = datadoghqRegistries
return resolver
return newRcImageResolver(cfg)
}

// DEV: Delete this in favor of the imageresolver package one after the refactor is complete
func newDatadoghqRegistries(datadogRegistriesList []string) map[string]struct{} {
datadoghqRegistries := make(map[string]struct{})
for _, registry := range datadogRegistriesList {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/DataDog/datadog-agent/comp/core/config"
"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoinstrumentation/imageresolver"
"github.com/DataDog/datadog-agent/pkg/remoteconfig/state"
"github.com/DataDog/datadog-agent/pkg/util/log"
)
Expand Down Expand Up @@ -107,24 +108,24 @@ func (m *mockRCClient) setBlocking(block bool) {
func TestNewImageResolver(t *testing.T) {
t.Run("with_remote_config_client", func(t *testing.T) {
mockClient := newMockRCClient("image_resolver_multi_repo.json")
mockConfig := config.NewMock(t)
resolver := NewImageResolver(mockClient, mockConfig)
mockConfig := imageresolver.NewConfig(config.NewMock(t), mockClient)
resolver := NewImageResolver(mockConfig)

_, ok := resolver.(*remoteConfigImageResolver)
assert.True(t, ok, "Should return remoteConfigImageResolver when rcClient is not nil")
})

t.Run("without_remote_config_client__typed_nil", func(t *testing.T) {
mockConfig := config.NewMock(t)
resolver := NewImageResolver((*mockRCClient)(nil), mockConfig)
mockConfig := imageresolver.NewConfig(config.NewMock(t), (*mockRCClient)(nil))
resolver := NewImageResolver(mockConfig)

_, ok := resolver.(*noOpImageResolver)
assert.True(t, ok, "Should return noOpImageResolver when rcClient is nil")
})

t.Run("without_remote_config_client__untyped_nil", func(t *testing.T) {
mockConfig := config.NewMock(t)
resolver := NewImageResolver(nil, mockConfig)
mockConfig := imageresolver.NewConfig(config.NewMock(t), nil)
resolver := NewImageResolver(mockConfig)

_, ok := resolver.(*noOpImageResolver)
assert.True(t, ok, "Should return noOpImageResolver when rcClient is nil")
Expand Down Expand Up @@ -228,8 +229,7 @@ func TestImageResolverEmptyConfig(t *testing.T) {

func TestRemoteConfigImageResolver_Resolve(t *testing.T) {
mockRCClient := newMockRCClient("image_resolver_multi_repo.json")
datadoghqRegistries := newDatadoghqRegistries(config.NewMock(t).GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries"))
resolver := newRemoteConfigImageResolver(mockRCClient, datadoghqRegistries)
resolver := newRcImageResolver(imageresolver.NewConfig(config.NewMock(t), mockRCClient))

testCases := []struct {
name string
Expand Down Expand Up @@ -414,8 +414,7 @@ func TestRemoteConfigImageResolver_InvalidDigestValidation(t *testing.T) {
}

func TestRemoteConfigImageResolver_ConcurrentAccess(t *testing.T) {
datadoghqRegistries := newDatadoghqRegistries(config.NewMock(t).GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries"))
resolver := newRemoteConfigImageResolver(newMockRCClient("image_resolver_multi_repo.json"), datadoghqRegistries).(*remoteConfigImageResolver)
resolver := newRcImageResolver(imageresolver.NewConfig(config.NewMock(t), newMockRCClient("image_resolver_multi_repo.json")))

t.Run("concurrent_read_write", func(_ *testing.T) {
var wg sync.WaitGroup
Expand All @@ -437,7 +436,7 @@ func TestRemoteConfigImageResolver_ConcurrentAccess(t *testing.T) {
go func() {
defer wg.Done()
for j := 0; j < 10; j++ {
resolver.processUpdate(map[string]state.RawConfig{}, func(string, state.ApplyStatus) {})
resolver.(*remoteConfigImageResolver).processUpdate(map[string]state.RawConfig{}, func(string, state.ApplyStatus) {})
time.Sleep(10 * time.Millisecond)
}
}()
Expand Down Expand Up @@ -495,9 +494,8 @@ func TestAsyncInitialization(t *testing.T) {
t.Run("noop_during_initialization", func(t *testing.T) {
mockClient := newMockRCClient("image_resolver_multi_repo.json")
mockClient.setBlocking(true) // Block initialization
datadoghqRegistries := newDatadoghqRegistries(config.NewMock(t).GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries"))

resolver := newRemoteConfigImageResolverWithRetryConfig(mockClient, 2, 10*time.Millisecond, datadoghqRegistries)
resolver := newRcImageResolver(imageresolver.NewConfig(config.NewMock(t), mockClient))

resolved, ok := resolver.Resolve("gcr.io/datadoghq", "dd-lib-python-init", "latest")
assert.False(t, ok, "Should not complete image resolution during initialization")
Expand All @@ -508,8 +506,7 @@ func TestAsyncInitialization(t *testing.T) {
mockClient := newMockRCClient("image_resolver_multi_repo.json")
mockClient.setBlocking(true)

datadoghqRegistries := newDatadoghqRegistries(config.NewMock(t).GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries"))
resolver := newRemoteConfigImageResolverWithRetryConfig(mockClient, 2, 10*time.Millisecond, datadoghqRegistries)
resolver := newRcImageResolver(imageresolver.NewConfig(config.NewMock(t), mockClient))

resolved, ok := resolver.Resolve("gcr.io/datadoghq", "dd-lib-python-init", "latest")
assert.False(t, ok, "Should not complete image resolution during initialization")
Expand All @@ -533,8 +530,7 @@ func TestAsyncInitialization(t *testing.T) {
}
close(mockClient.configsReady)

datadoghqRegistries := newDatadoghqRegistries(config.NewMock(t).GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries"))
resolver := newRemoteConfigImageResolverWithRetryConfig(mockClient, 2, 10*time.Millisecond, datadoghqRegistries)
resolver := newRcImageResolver(imageresolver.NewConfig(config.NewMock(t), mockClient))
time.Sleep(50 * time.Millisecond)

resolved, ok := resolver.Resolve("gcr.io/datadoghq", "dd-lib-python-init", "latest")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 imageresolver provides configuration and utilities for resolving
// container image references from mutable tags to digests.
package imageresolver

import (
"time"

"github.com/DataDog/datadog-agent/comp/core/config"
"github.com/DataDog/datadog-agent/pkg/remoteconfig/state"
)

// RemoteConfigClient defines the interface we need for remote config operations
type RemoteConfigClient interface {
GetConfigs(product string) map[string]state.RawConfig
Subscribe(product string, callback func(map[string]state.RawConfig, func(string, state.ApplyStatus)))
}

// Config contains information needed to create an ImageResolver
type Config struct {
Site string
DDRegistries map[string]struct{}
RCClient RemoteConfigClient
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client ultimately shouldn't be here. The config struct should be purely config. Dependencies should be passed in when you make an image resolver

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense. My intent here was to add it in the imageresolver.Config since we know we want to remove it when we pivot to tag-based, so I wanted the changes to be in less places 😅

MaxInitRetries int
InitRetryDelay time.Duration
}

// NewConfig creates a new Config
func NewConfig(cfg config.Component, rcClient RemoteConfigClient) Config {
return Config{
Site: cfg.GetString("site"),
DDRegistries: newDatadoghqRegistries(cfg.GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries")),
RCClient: rcClient,
MaxInitRetries: 5,
InitRetryDelay: 1 * time.Second,
Comment on lines +40 to +41
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could possibly be configured via config/env var, but this is already static in current state, so retaining that for now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love to see all of these configurable by ddConfig!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first two already are, and the last 2 will be removed with the pivot to tag-based, so I might not add them to ddConfig (unless you think it could still be useful in the meantime, then I'm open to adding them)

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 imageresolver

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/DataDog/datadog-agent/comp/core/config"
)

func TestNewConfig(t *testing.T) {
tests := []struct {
name string
configFactory func(*testing.T) config.Component
expectedState Config
}{
{
name: "default_config",
configFactory: func(t *testing.T) config.Component {
mockConfig := config.NewMock(t)
mockConfig.SetWithoutSource("site", "datadoghq.com")
return mockConfig
},
expectedState: Config{
Site: "datadoghq.com",
DDRegistries: map[string]struct{}{"gcr.io/datadoghq": {}, "docker.io/datadog": {}, "public.ecr.aws/datadog": {}},
RCClient: nil,
MaxInitRetries: 5,
InitRetryDelay: 1 * time.Second,
},
},
{
name: "custom_dd_registries",
configFactory: func(t *testing.T) config.Component {
mockConfig := config.NewMock(t)
mockConfig.SetWithoutSource("site", "datadoghq.com")
mockConfig.SetWithoutSource("admission_controller.auto_instrumentation.default_dd_registries", []string{"helloworld.io/datadog"})
return mockConfig
},
expectedState: Config{
Site: "datadoghq.com",
DDRegistries: map[string]struct{}{"helloworld.io/datadog": {}},
RCClient: nil,
MaxInitRetries: 5,
InitRetryDelay: 1 * time.Second,
},
},
{
name: "configured_site",
configFactory: func(t *testing.T) config.Component {
mockConfig := config.NewMock(t)
mockConfig.SetWithoutSource("site", "datad0g.com")
return mockConfig
},
expectedState: Config{
Site: "datad0g.com",
DDRegistries: map[string]struct{}{"gcr.io/datadoghq": {}, "docker.io/datadog": {}, "public.ecr.aws/datadog": {}},
RCClient: nil,
MaxInitRetries: 5,
InitRetryDelay: 1 * time.Second,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockConfig := tt.configFactory(t)
result := NewConfig(mockConfig, nil)

require.Equal(t, tt.expectedState, result)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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 imageresolver provides configuration and utilities for resolving
// container image references from mutable tags to digests.
package imageresolver

func newDatadoghqRegistries(datadogRegistriesList []string) map[string]struct{} {
Copy link
Contributor Author

@erikayasuda erikayasuda Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This is a duplicate of the one that already exists in the image_resolver.go file, but maintaining both until we move all relevant components under the imageresolver package

datadoghqRegistries := make(map[string]struct{})
for _, registry := range datadogRegistriesList {
datadoghqRegistries[registry] = struct{}{}
}
return datadoghqRegistries
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
corev1 "k8s.io/api/core/v1"

"github.com/DataDog/datadog-agent/comp/core/config"
"github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoinstrumentation/imageresolver"
"github.com/DataDog/datadog-agent/pkg/util/pointer"
)

Expand Down Expand Up @@ -97,13 +98,7 @@ func TestInjectorWithRemoteConfigImageResolver(t *testing.T) {
var resolver ImageResolver
if tc.hasRemoteData {
mockClient := newMockRCClient("image_resolver_multi_repo.json")
datadoghqRegistries := newDatadoghqRegistries(config.NewMock(t).GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries"))
resolver = newRemoteConfigImageResolverWithRetryConfig(
mockClient,
2,
1*time.Millisecond,
datadoghqRegistries,
)
resolver = newRcImageResolver(imageresolver.NewConfig(config.NewMock(t), mockClient))
} else {
resolver = newNoOpImageResolver()
}
Expand All @@ -119,13 +114,7 @@ func TestInjectorWithRemoteConfigImageResolver(t *testing.T) {

func TestInjectorWithRemoteConfigImageResolverAfterInit(t *testing.T) {
mockClient := newMockRCClient("image_resolver_multi_repo.json")
datadoghqRegistries := newDatadoghqRegistries(config.NewMock(t).GetStringSlice("admission_controller.auto_instrumentation.default_dd_registries"))
resolver := newRemoteConfigImageResolverWithRetryConfig(
mockClient,
2,
1*time.Millisecond,
datadoghqRegistries,
)
resolver := newRcImageResolver(imageresolver.NewConfig(config.NewMock(t), mockClient))

assert.Eventually(t, func() bool {
_, ok := resolver.Resolve("gcr.io/datadoghq", "apm-inject", "0")
Expand Down