From 88c9d21f4a6c48dd09a3f57fb8ba9265e96a8896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Gronowski?= Date: Mon, 26 Oct 2026 21:16:39 +0100 Subject: [PATCH] daemon: Move name-generator provider resolution out of extpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- daemon/names.go | 12 +- daemon/names_extension.go | 144 ++++++++++++ daemon/names_extension_test.go | 211 ++++++++++++++++++ .../v0/containernamegenerator.go | 25 --- .../v0/containernamegenerator_test.go | 42 ---- .../internal/namesgenerator/namesgenerator.go | 99 -------- .../namesgenerator/namesgenerator_test.go | 154 ------------- .../v0/servicenamegenerator.go | 25 --- .../v0/servicenamegenerator_test.go | 40 ---- 9 files changed, 357 insertions(+), 395 deletions(-) create mode 100644 daemon/names_extension.go create mode 100644 daemon/names_extension_test.go delete mode 100644 extpoints/containernamegenerator/v0/containernamegenerator_test.go delete mode 100644 extpoints/internal/namesgenerator/namesgenerator.go delete mode 100644 extpoints/internal/namesgenerator/namesgenerator_test.go delete mode 100644 extpoints/servicenamegenerator/v0/servicenamegenerator_test.go diff --git a/daemon/names.go b/daemon/names.go index 9bbac7896c..8985224225 100644 --- a/daemon/names.go +++ b/daemon/names.go @@ -97,14 +97,10 @@ func (daemon *Daemon) GenerateServiceName(ctx context.Context, retry int, image return "", errors.New("name generator extension host is not configured") } - reply, err := servicenamegeneratorv0.GenerateServiceName(ctx, daemon.extensionHost, &servicenamegeneratorv0.GenerateServiceNameRequest{ + name, err := resolveServiceName(ctx, daemon.extensionHost, &servicenamegeneratorv0.GenerateServiceNameRequest{ Retry: int64(retry), Image: image, }) - var name string - if reply != nil { - name = reply.Name - } return generatedName(ctx, name, err) } @@ -113,11 +109,7 @@ func (daemon *Daemon) generateContainerName(ctx context.Context, req *containern return "", errors.New("name generator extension host is not configured") } - reply, err := containernamegeneratorv0.GenerateContainerName(ctx, daemon.extensionHost, req) - var name string - if reply != nil { - name = reply.Name - } + name, err := resolveContainerName(ctx, daemon.extensionHost, req) return generatedName(ctx, name, err) } diff --git a/daemon/names_extension.go b/daemon/names_extension.go new file mode 100644 index 0000000000..a47a5545b8 --- /dev/null +++ b/daemon/names_extension.go @@ -0,0 +1,144 @@ +package daemon + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/moby/extensions" + containernamegeneratorv0 "github.com/moby/moby/v2/extpoints/containernamegenerator/v0" + servicenamegeneratorv0 "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0" +) + +const nameProviderTimeout = 5 * time.Second + +// resolveContainerName calls the effective container name provider. +func resolveContainerName(ctx context.Context, resolver extensions.Resolver, req *containernamegeneratorv0.GenerateContainerNameRequest) (string, error) { + return callSingleWithFallback(ctx, containernamegeneratorv0.Point, resolver, nameProviderTimeout, func(ctx context.Context, generator containernamegeneratorv0.ContainerNameGenerator) (string, error) { + reply, err := generator.GenerateContainerName(ctx, req) + if err != nil { + return "", err + } + if reply == nil { + return "", errors.New("provider returned a nil reply") + } + return reply.Name, validateGeneratedName(reply.Name) + }) +} + +// resolveServiceName calls the effective service name provider. +func resolveServiceName(ctx context.Context, resolver extensions.Resolver, req *servicenamegeneratorv0.GenerateServiceNameRequest) (string, error) { + return callSingleWithFallback(ctx, servicenamegeneratorv0.Point, resolver, nameProviderTimeout, func(ctx context.Context, generator servicenamegeneratorv0.ServiceNameGenerator) (string, error) { + reply, err := generator.GenerateServiceName(ctx, req) + if err != nil { + return "", err + } + if reply == nil { + return "", errors.New("provider returned a nil reply") + } + return reply.Name, validateGeneratedName(reply.Name) + }) +} + +func validateGeneratedName(name string) error { + if name == "" { + return errors.New("provider returned an empty name") + } + if !isValidGeneratedName(name) { + return errors.New("provider returned an invalid name: expected 2-63 ASCII letters, digits, hyphens, or underscores, starting and ending with a letter or digit") + } + return nil +} + +func isValidGeneratedName(name string) bool { + if len(name) < 2 || len(name) > 63 { + return false + } + for i := range len(name) { + ch := name[i] + if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') { + continue + } + if i > 0 && i < len(name)-1 && (ch == '-' || ch == '_') { + continue + } + return false + } + return true +} + +// callSingleWithFallback calls the effective provider of a single point. +// An executable provider hides the built-in during normal resolution, but the +// full provider list retains it as a fallback. +// If the executable provider fails and the built-in succeeds, it returns the +// built-in result together with the executable provider's error so callers can +// report the extension failure without blocking the operation. +// If the built-in also fails, the returned error contains both failures. +// Each provider call gets its own timeout. +// +// It is the single-point counterpart of [extensions.Each] and +// [extensions.Fold]; move it to github.com/moby/extensions once points outside +// name generation need it. +func callSingleWithFallback[T, R any](ctx context.Context, point extensions.Point[T], resolver extensions.Resolver, timeout time.Duration, fn func(context.Context, T) (R, error)) (R, error) { + providers := resolver.Providers(point.ID()) + primary, err := effectiveSingleProvider(point.ID(), providers) + if err != nil { + var zero R + return zero, err + } + + result, primaryErr := callTypedProvider(ctx, point, primary, timeout, fn) + if primaryErr == nil || primary.Identity.Origin.Kind == extensions.ExtensionOriginBuiltin { + return result, primaryErr + } + + fallback, ok := findBuiltinFallback(providers, primary) + if !ok { + return result, primaryErr + } + + result, fallbackErr := callTypedProvider(ctx, point, fallback, timeout, fn) + return result, errors.Join(primaryErr, fallbackErr) +} + +func effectiveSingleProvider(point extensions.PointID, providers []extensions.ResolvedProvider) (extensions.ResolvedProvider, error) { + effective := extensions.EffectiveProviders(providers) + switch len(effective) { + case 0: + return extensions.ResolvedProvider{}, fmt.Errorf("point %q has no providers", point) + case 1: + return effective[0], nil + default: + return extensions.ResolvedProvider{}, fmt.Errorf("point %q has multiple providers", point) + } +} + +func findBuiltinFallback(providers []extensions.ResolvedProvider, primary extensions.ResolvedProvider) (extensions.ResolvedProvider, bool) { + for _, provider := range providers { + if provider.Identity.Origin.Kind == extensions.ExtensionOriginBuiltin && provider.Identity.ID != primary.Identity.ID { + return provider, true + } + } + return extensions.ResolvedProvider{}, false +} + +func callTypedProvider[T, R any](ctx context.Context, point extensions.Point[T], provider extensions.ResolvedProvider, timeout time.Duration, fn func(context.Context, T) (R, error)) (R, error) { + var zero R + impl, ok := provider.Impl.(T) + if !ok { + return zero, providerError(point.ID(), provider.Identity.ID, fmt.Errorf("provider has type %T", provider.Impl)) + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + result, err := fn(ctx, impl) + if err != nil { + return zero, providerError(point.ID(), provider.Identity.ID, err) + } + return result, nil +} + +func providerError(point extensions.PointID, extension extensions.ExtensionID, err error) error { + return fmt.Errorf("%s provider %q: %w", point.Name(), extension, err) +} diff --git a/daemon/names_extension_test.go b/daemon/names_extension_test.go new file mode 100644 index 0000000000..0fe5a35b4c --- /dev/null +++ b/daemon/names_extension_test.go @@ -0,0 +1,211 @@ +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)) +} diff --git a/extpoints/containernamegenerator/v0/containernamegenerator.go b/extpoints/containernamegenerator/v0/containernamegenerator.go index 206c072d6e..6429bf55a0 100644 --- a/extpoints/containernamegenerator/v0/containernamegenerator.go +++ b/extpoints/containernamegenerator/v0/containernamegenerator.go @@ -6,11 +6,8 @@ package containernamegeneratorv0 import ( "context" - "errors" - "fmt" "github.com/moby/extensions" - "github.com/moby/moby/v2/extpoints/internal/namesgenerator" ) // ContainerNameGenerator generates a default container name. @@ -37,25 +34,3 @@ type GenerateContainerNameReply struct { // Point is the single-provider container name-generator point. var Point = extensions.DefineSinglePoint[ContainerNameGenerator]("org.mobyproject.extension.containernamegenerator.v0") - -// GenerateContainerName calls the effective container name provider. -func GenerateContainerName(ctx context.Context, resolver extensions.Resolver, req *GenerateContainerNameRequest) (*GenerateContainerNameReply, error) { - name, err := namesgenerator.Generate(ctx, resolver, Point.ID(), func(ctx context.Context, impl any) (string, error) { - generator, ok := impl.(ContainerNameGenerator) - if !ok { - return "", fmt.Errorf("provider has type %T, not ContainerNameGenerator", impl) - } - reply, err := generator.GenerateContainerName(ctx, req) - if err != nil { - return "", err - } - if reply == nil { - return "", errors.New("provider returned a nil reply") - } - return reply.Name, nil - }) - if name == "" { - return nil, err - } - return &GenerateContainerNameReply{Name: name}, err -} diff --git a/extpoints/containernamegenerator/v0/containernamegenerator_test.go b/extpoints/containernamegenerator/v0/containernamegenerator_test.go deleted file mode 100644 index ebe22c4163..0000000000 --- a/extpoints/containernamegenerator/v0/containernamegenerator_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package containernamegeneratorv0 - -import ( - "context" - "testing" - - "github.com/moby/extensions" - "github.com/moby/moby/v2/internal/testutil/staticresolver" - "gotest.tools/v3/assert" -) - -type generatorFunc func(context.Context, *GenerateContainerNameRequest) (*GenerateContainerNameReply, error) - -func (f generatorFunc) GenerateContainerName(ctx context.Context, req *GenerateContainerNameRequest) (*GenerateContainerNameReply, error) { - return f(ctx, req) -} - -func TestGenerateContainerName(t *testing.T) { - t.Parallel() - - provider := generatorFunc(func(_ context.Context, req *GenerateContainerNameRequest) (*GenerateContainerNameReply, error) { - assert.Equal(t, req.Retry, int64(3)) - assert.Equal(t, req.ContainerID, "container-id") - assert.Equal(t, req.Image, "image:latest") - return &GenerateContainerNameReply{Name: "container-name"}, nil - }) - resolver := staticresolver.New(extensions.ResolvedProvider{ - Identity: extensions.ExtensionIdentity{ - ID: "org.example.containernamegenerator.v1", - Origin: extensions.ExtensionOrigin{Kind: extensions.ExtensionOriginBuiltin}, - }, - Impl: provider, - }) - - reply, err := GenerateContainerName(t.Context(), resolver, &GenerateContainerNameRequest{ - Retry: 3, - ContainerID: "container-id", - Image: "image:latest", - }) - assert.NilError(t, err) - assert.Equal(t, reply.Name, "container-name") -} diff --git a/extpoints/internal/namesgenerator/namesgenerator.go b/extpoints/internal/namesgenerator/namesgenerator.go deleted file mode 100644 index ab7a5dcc00..0000000000 --- a/extpoints/internal/namesgenerator/namesgenerator.go +++ /dev/null @@ -1,99 +0,0 @@ -// Package namesgenerator implements behavior shared by name-generator points. -package namesgenerator - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/moby/extensions" -) - -// Generate calls the effective provider for a name-generator point. -// A replacement hides the built-in during normal resolution, but the full -// provider list retains it as a fallback. -// If the replacement fails and the fallback succeeds, Generate returns the -// fallback name together with the replacement error so callers can report the -// extension failure without blocking object creation. -// If the fallback also fails, the returned error contains both failures. -func Generate(ctx context.Context, resolver extensions.Resolver, point extensions.PointID, invoke func(context.Context, any) (string, error)) (string, error) { - providers := resolver.Providers(point) - primary, err := effectiveProvider(point, providers) - if err != nil { - return "", err - } - - name, primaryErr := generate(ctx, point, primary, invoke) - if primaryErr == nil || primary.Identity.Origin.Kind == extensions.ExtensionOriginBuiltin { - return name, primaryErr - } - - fallback, ok := findBuiltinFallback(providers, primary) - if !ok { - return "", primaryErr - } - - name, fallbackErr := generate(ctx, point, fallback, invoke) - return name, errors.Join(primaryErr, fallbackErr) -} - -func effectiveProvider(point extensions.PointID, providers []extensions.ResolvedProvider) (extensions.ResolvedProvider, error) { - effective := extensions.EffectiveProviders(providers) - switch len(effective) { - case 0: - return extensions.ResolvedProvider{}, fmt.Errorf("point %q has no providers", point) - case 1: - return effective[0], nil - default: - return extensions.ResolvedProvider{}, fmt.Errorf("point %q has multiple providers", point) - } -} - -func findBuiltinFallback(providers []extensions.ResolvedProvider, primary extensions.ResolvedProvider) (extensions.ResolvedProvider, bool) { - for _, provider := range providers { - if provider.Identity.Origin.Kind == extensions.ExtensionOriginBuiltin && provider.Identity.ID != primary.Identity.ID { - return provider, true - } - } - return extensions.ResolvedProvider{}, false -} - -func generate(ctx context.Context, point extensions.PointID, provider extensions.ResolvedProvider, invoke func(context.Context, any) (string, error)) (string, error) { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - name, err := invoke(ctx, provider.Impl) - if err != nil { - return "", providerError(point, provider.Identity.ID, err) - } - if name == "" { - return "", providerError(point, provider.Identity.ID, errors.New("provider returned an empty name")) - } - if !isValidGeneratedName(name) { - return "", providerError(point, provider.Identity.ID, errors.New( - "provider returned an invalid name: expected 2-63 ASCII letters, digits, hyphens, or underscores, starting and ending with a letter or digit", - )) - } - return name, nil -} - -func isValidGeneratedName(name string) bool { - if len(name) < 2 || len(name) > 63 { - return false - } - for i := range len(name) { - ch := name[i] - if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') { - continue - } - if i > 0 && i < len(name)-1 && (ch == '-' || ch == '_') { - continue - } - return false - } - return true -} - -func providerError(point extensions.PointID, extension extensions.ExtensionID, err error) error { - return fmt.Errorf("%s provider %q: %w", point.Name(), extension, err) -} diff --git a/extpoints/internal/namesgenerator/namesgenerator_test.go b/extpoints/internal/namesgenerator/namesgenerator_test.go deleted file mode 100644 index 24ad8d8a23..0000000000 --- a/extpoints/internal/namesgenerator/namesgenerator_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package namesgenerator - -import ( - "context" - "errors" - "strings" - "testing" - "time" - - "github.com/moby/extensions" - "github.com/moby/moby/v2/internal/testutil/staticresolver" - "gotest.tools/v3/assert" -) - -var testPoint = extensions.DefineSinglePoint[any]("org.example.namesgenerator.v0") - -func resolvedProvider(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, - } -} - -func TestGenerateUsesReplacementProvider(t *testing.T) { - t.Parallel() - - builtinCalled := false - resolver := staticresolver.New( - resolvedProvider("org.example.replacement.v1", "replacement", false), - resolvedProvider("org.example.builtin.v1", "builtin", true), - ) - name, err := Generate(t.Context(), resolver, testPoint.ID(), func(ctx context.Context, impl any) (string, error) { - deadline, ok := ctx.Deadline() - assert.Assert(t, ok) - assert.Assert(t, time.Until(deadline) <= 5*time.Second) - if impl == "builtin" { - builtinCalled = true - return "builtin-name", nil - } - return "replacement-name", nil - }) - assert.NilError(t, err) - assert.Equal(t, name, "replacement-name") - assert.Assert(t, !builtinCalled) -} - -func TestGenerateFallsBackToBuiltin(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 - resolver := staticresolver.New( - resolvedProvider("org.example.replacement.v1", "replacement", false), - resolvedProvider("org.example.builtin.v1", "builtin", true), - ) - name, err := Generate(t.Context(), resolver, testPoint.ID(), func(ctx context.Context, impl any) (string, error) { - if impl == "builtin" { - _, ok := ctx.Deadline() - assert.Assert(t, ok, "the fallback attempt must have its own deadline") - builtinCalls++ - return "builtin-name", nil - } - return tc.replacementName, tc.replacementErr - }) - assert.ErrorContains(t, err, `namesgenerator provider "org.example.replacement.v1"`) - assert.ErrorContains(t, err, tc.wantErr) - assert.Equal(t, name, "builtin-name") - assert.Equal(t, builtinCalls, 1) - }) - } -} - -func TestGenerateDoesNotRetryFailingBuiltin(t *testing.T) { - t.Parallel() - - calls := 0 - name, err := Generate(t.Context(), staticresolver.New( - resolvedProvider("org.example.builtin.v1", "builtin", true), - ), testPoint.ID(), func(context.Context, any) (string, error) { - calls++ - return "", errors.New("builtin failed") - }) - assert.Equal(t, name, "") - assert.ErrorContains(t, err, `namesgenerator provider "org.example.builtin.v1": builtin failed`) - assert.Equal(t, calls, 1) -} - -func TestGenerateJoinsReplacementAndBuiltinErrors(t *testing.T) { - t.Parallel() - - replacementErr := errors.New("replacement failed") - builtinErr := errors.New("builtin failed") - name, err := Generate(t.Context(), staticresolver.New( - resolvedProvider("org.example.replacement.v1", replacementErr, false), - resolvedProvider("org.example.builtin.v1", builtinErr, true), - ), testPoint.ID(), func(_ context.Context, impl any) (string, error) { - return "", impl.(error) - }) - assert.Equal(t, name, "") - assert.Assert(t, errors.Is(err, replacementErr)) - assert.Assert(t, errors.Is(err, builtinErr)) -} - -func TestGenerateRequiresExactlyOneEffectiveProvider(t *testing.T) { - t.Parallel() - - invoke := func(context.Context, any) (string, error) { return "name", nil } - _, err := Generate(t.Context(), staticresolver.New(), testPoint.ID(), invoke) - assert.ErrorContains(t, err, "has no providers") - - _, err = Generate(t.Context(), staticresolver.New( - resolvedProvider("org.example.one.v1", "one", false), - resolvedProvider("org.example.two.v1", "two", false), - ), testPoint.ID(), invoke) - assert.ErrorContains(t, err, "has multiple providers") -} - -func TestGeneratePropagatesContextTimeout(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond) - defer cancel() - _, err := Generate(ctx, staticresolver.New( - resolvedProvider("org.example.slow.v1", "slow", false), - ), testPoint.ID(), func(ctx context.Context, _ any) (string, error) { - <-ctx.Done() - return "", ctx.Err() - }) - assert.Assert(t, errors.Is(err, context.DeadlineExceeded)) -} diff --git a/extpoints/servicenamegenerator/v0/servicenamegenerator.go b/extpoints/servicenamegenerator/v0/servicenamegenerator.go index 743917fc63..9c2712fca3 100644 --- a/extpoints/servicenamegenerator/v0/servicenamegenerator.go +++ b/extpoints/servicenamegenerator/v0/servicenamegenerator.go @@ -6,11 +6,8 @@ package servicenamegeneratorv0 import ( "context" - "errors" - "fmt" "github.com/moby/extensions" - "github.com/moby/moby/v2/extpoints/internal/namesgenerator" ) // ServiceNameGenerator generates a default Swarm service name. @@ -36,25 +33,3 @@ type GenerateServiceNameReply struct { // Point is the single-provider service name-generator point. var Point = extensions.DefineSinglePoint[ServiceNameGenerator]("org.mobyproject.extension.servicenamegenerator.v0") - -// GenerateServiceName calls the effective service name provider. -func GenerateServiceName(ctx context.Context, resolver extensions.Resolver, req *GenerateServiceNameRequest) (*GenerateServiceNameReply, error) { - name, err := namesgenerator.Generate(ctx, resolver, Point.ID(), func(ctx context.Context, impl any) (string, error) { - generator, ok := impl.(ServiceNameGenerator) - if !ok { - return "", fmt.Errorf("provider has type %T, not ServiceNameGenerator", impl) - } - reply, err := generator.GenerateServiceName(ctx, req) - if err != nil { - return "", err - } - if reply == nil { - return "", errors.New("provider returned a nil reply") - } - return reply.Name, nil - }) - if name == "" { - return nil, err - } - return &GenerateServiceNameReply{Name: name}, err -} diff --git a/extpoints/servicenamegenerator/v0/servicenamegenerator_test.go b/extpoints/servicenamegenerator/v0/servicenamegenerator_test.go deleted file mode 100644 index 9f14c9a2d1..0000000000 --- a/extpoints/servicenamegenerator/v0/servicenamegenerator_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package servicenamegeneratorv0 - -import ( - "context" - "testing" - - "github.com/moby/extensions" - "github.com/moby/moby/v2/internal/testutil/staticresolver" - "gotest.tools/v3/assert" -) - -type generatorFunc func(context.Context, *GenerateServiceNameRequest) (*GenerateServiceNameReply, error) - -func (f generatorFunc) GenerateServiceName(ctx context.Context, req *GenerateServiceNameRequest) (*GenerateServiceNameReply, error) { - return f(ctx, req) -} - -func TestGenerateServiceName(t *testing.T) { - t.Parallel() - - provider := generatorFunc(func(_ context.Context, req *GenerateServiceNameRequest) (*GenerateServiceNameReply, error) { - assert.Equal(t, req.Retry, int64(3)) - assert.Equal(t, req.Image, "image:latest") - return &GenerateServiceNameReply{Name: "service-name"}, nil - }) - resolver := staticresolver.New(extensions.ResolvedProvider{ - Identity: extensions.ExtensionIdentity{ - ID: "org.example.servicenamegenerator.v1", - Origin: extensions.ExtensionOrigin{Kind: extensions.ExtensionOriginBuiltin}, - }, - Impl: provider, - }) - - reply, err := GenerateServiceName(t.Context(), resolver, &GenerateServiceNameRequest{ - Retry: 3, - Image: "image:latest", - }) - assert.NilError(t, err) - assert.Equal(t, reply.Name, "service-name") -}