Files
moby/daemon/names_extension_test.go
Paweł Gronowski 88c9d21f4a daemon: Move name-generator provider resolution out of extpoints
The extpoints packages define the extension contract.
Provider selection, timeouts, name validation, and the built-in
fallback are daemon policy, and the daemon is the only caller.

The fallback helper is can be moved to github.com/moby/extensions once
another point needs it.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-10-26 21:35:07 +01:00

212 lines
8.4 KiB
Go

package daemon
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/moby/extensions"
containernamegeneratorv0 "github.com/moby/moby/v2/extpoints/containernamegenerator/v0"
servicenamegeneratorv0 "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0"
"github.com/moby/moby/v2/internal/testutil/staticresolver"
"gotest.tools/v3/assert"
)
var testSinglePoint = extensions.DefineSinglePoint[any]("org.example.single.v0")
func resolvedTestProvider(id extensions.ExtensionID, impl any, builtin bool) extensions.ResolvedProvider {
origin := extensions.ExtensionOrigin{
Kind: extensions.ExtensionOriginExecutable,
Executable: &extensions.ExecutableOrigin{Path: string(id)},
}
if builtin {
origin = extensions.ExtensionOrigin{Kind: extensions.ExtensionOriginBuiltin}
}
return extensions.ResolvedProvider{
Identity: extensions.ExtensionIdentity{ID: id, Origin: origin},
Impl: impl,
}
}
type containerNameGeneratorFunc func(context.Context, *containernamegeneratorv0.GenerateContainerNameRequest) (*containernamegeneratorv0.GenerateContainerNameReply, error)
func (f containerNameGeneratorFunc) GenerateContainerName(ctx context.Context, req *containernamegeneratorv0.GenerateContainerNameRequest) (*containernamegeneratorv0.GenerateContainerNameReply, error) {
return f(ctx, req)
}
func TestResolveContainerName(t *testing.T) {
t.Parallel()
provider := containerNameGeneratorFunc(func(_ context.Context, req *containernamegeneratorv0.GenerateContainerNameRequest) (*containernamegeneratorv0.GenerateContainerNameReply, error) {
assert.Equal(t, req.Retry, int64(3))
assert.Equal(t, req.ContainerID, "container-id")
assert.Equal(t, req.Image, "image:latest")
return &containernamegeneratorv0.GenerateContainerNameReply{Name: "container-name"}, nil
})
resolver := staticresolver.New(resolvedTestProvider("org.example.containernamegenerator.v1", provider, true))
name, err := resolveContainerName(t.Context(), resolver, &containernamegeneratorv0.GenerateContainerNameRequest{
Retry: 3,
ContainerID: "container-id",
Image: "image:latest",
})
assert.NilError(t, err)
assert.Equal(t, name, "container-name")
}
func TestResolveContainerNameFallsBackToBuiltin(t *testing.T) {
t.Parallel()
tests := []struct {
name string
replacementName string
replacementErr error
wantErr string
}{
{name: "error", replacementErr: errors.New("replacement failed"), wantErr: "replacement failed"},
{name: "empty name", wantErr: "provider returned an empty name"},
{name: "invalid character", replacementName: "invalid.name", wantErr: "provider returned an invalid name"},
{name: "too short", replacementName: "x", wantErr: "provider returned an invalid name"},
{name: "too long", replacementName: strings.Repeat("x", 64), wantErr: "provider returned an invalid name"},
{name: "ending with separator", replacementName: "invalid_name_", wantErr: "provider returned an invalid name"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
builtinCalls := 0
replacement := containerNameGeneratorFunc(func(context.Context, *containernamegeneratorv0.GenerateContainerNameRequest) (*containernamegeneratorv0.GenerateContainerNameReply, error) {
if tc.replacementErr != nil {
return nil, tc.replacementErr
}
return &containernamegeneratorv0.GenerateContainerNameReply{Name: tc.replacementName}, nil
})
builtin := containerNameGeneratorFunc(func(ctx context.Context, _ *containernamegeneratorv0.GenerateContainerNameRequest) (*containernamegeneratorv0.GenerateContainerNameReply, error) {
_, ok := ctx.Deadline()
assert.Assert(t, ok, "the fallback attempt must have its own deadline")
builtinCalls++
return &containernamegeneratorv0.GenerateContainerNameReply{Name: "builtin-name"}, nil
})
resolver := staticresolver.New(
resolvedTestProvider("org.example.replacement.v1", replacement, false),
resolvedTestProvider("org.example.builtin.v1", builtin, true),
)
name, err := resolveContainerName(t.Context(), resolver, &containernamegeneratorv0.GenerateContainerNameRequest{})
assert.ErrorContains(t, err, `containernamegenerator provider "org.example.replacement.v1"`)
assert.ErrorContains(t, err, tc.wantErr)
assert.Equal(t, name, "builtin-name")
assert.Equal(t, builtinCalls, 1)
})
}
}
type serviceNameGeneratorFunc func(context.Context, *servicenamegeneratorv0.GenerateServiceNameRequest) (*servicenamegeneratorv0.GenerateServiceNameReply, error)
func (f serviceNameGeneratorFunc) GenerateServiceName(ctx context.Context, req *servicenamegeneratorv0.GenerateServiceNameRequest) (*servicenamegeneratorv0.GenerateServiceNameReply, error) {
return f(ctx, req)
}
func TestResolveServiceName(t *testing.T) {
t.Parallel()
provider := serviceNameGeneratorFunc(func(_ context.Context, req *servicenamegeneratorv0.GenerateServiceNameRequest) (*servicenamegeneratorv0.GenerateServiceNameReply, error) {
assert.Equal(t, req.Retry, int64(3))
assert.Equal(t, req.Image, "image:latest")
return &servicenamegeneratorv0.GenerateServiceNameReply{Name: "service-name"}, nil
})
resolver := staticresolver.New(resolvedTestProvider("org.example.servicenamegenerator.v1", provider, true))
name, err := resolveServiceName(t.Context(), resolver, &servicenamegeneratorv0.GenerateServiceNameRequest{
Retry: 3,
Image: "image:latest",
})
assert.NilError(t, err)
assert.Equal(t, name, "service-name")
}
func TestCallSingleWithFallbackUsesReplacementProvider(t *testing.T) {
t.Parallel()
builtinCalled := false
resolver := staticresolver.New(
resolvedTestProvider("org.example.replacement.v1", "replacement", false),
resolvedTestProvider("org.example.builtin.v1", "builtin", true),
)
result, err := callSingleWithFallback(t.Context(), testSinglePoint, resolver, time.Second, func(ctx context.Context, impl any) (string, error) {
deadline, ok := ctx.Deadline()
assert.Assert(t, ok)
assert.Assert(t, time.Until(deadline) <= time.Second)
if impl == "builtin" {
builtinCalled = true
return "builtin-result", nil
}
return "replacement-result", nil
})
assert.NilError(t, err)
assert.Equal(t, result, "replacement-result")
assert.Assert(t, !builtinCalled)
}
func TestCallSingleWithFallbackDoesNotRetryFailingBuiltin(t *testing.T) {
t.Parallel()
calls := 0
result, err := callSingleWithFallback(t.Context(), testSinglePoint, staticresolver.New(
resolvedTestProvider("org.example.builtin.v1", "builtin", true),
), time.Second, func(context.Context, any) (string, error) {
calls++
return "partial", errors.New("builtin failed")
})
assert.Equal(t, result, "")
assert.ErrorContains(t, err, `single provider "org.example.builtin.v1": builtin failed`)
assert.Equal(t, calls, 1)
}
func TestCallSingleWithFallbackJoinsReplacementAndBuiltinErrors(t *testing.T) {
t.Parallel()
replacementErr := errors.New("replacement failed")
builtinErr := errors.New("builtin failed")
result, err := callSingleWithFallback(t.Context(), testSinglePoint, staticresolver.New(
resolvedTestProvider("org.example.replacement.v1", replacementErr, false),
resolvedTestProvider("org.example.builtin.v1", builtinErr, true),
), time.Second, func(_ context.Context, impl any) (string, error) {
return "", impl.(error)
})
assert.Equal(t, result, "")
assert.Assert(t, errors.Is(err, replacementErr))
assert.Assert(t, errors.Is(err, builtinErr))
}
func TestCallSingleWithFallbackRequiresExactlyOneEffectiveProvider(t *testing.T) {
t.Parallel()
fn := func(context.Context, any) (string, error) { return "result", nil }
_, err := callSingleWithFallback(t.Context(), testSinglePoint, staticresolver.New(), time.Second, fn)
assert.ErrorContains(t, err, "has no providers")
_, err = callSingleWithFallback(t.Context(), testSinglePoint, staticresolver.New(
resolvedTestProvider("org.example.one.v1", "one", false),
resolvedTestProvider("org.example.two.v1", "two", false),
), time.Second, fn)
assert.ErrorContains(t, err, "has multiple providers")
}
func TestCallSingleWithFallbackPropagatesContextTimeout(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
defer cancel()
_, err := callSingleWithFallback(ctx, testSinglePoint, staticresolver.New(
resolvedTestProvider("org.example.slow.v1", "slow", false),
), time.Second, func(ctx context.Context, _ any) (string, error) {
<-ctx.Done()
return "", ctx.Err()
})
assert.Assert(t, errors.Is(err, context.DeadlineExceeded))
}