daemon: Refactor namegenerator into extension

Add a single-provider namesgenerator point with the existing random
generator as its built-in implementation.

Installed extensions can now supply container and Swarm service names,
while failures fall back to the built-in generator so create operations
remain available.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski committed 2026-09-03 11:06:16 +02:00
1 parent 252bd664ba
commit f8b81d9066
22 files changed
+1404 -35

No files matched your search

+1
View File
@@ -91,6 +91,7 @@ type Config struct {
PluginBackend plugin.Backend
VolumeBackend executorpkg.VolumeBackend
NetworkSubnetsProvider NetworkSubnetsProvider
GenerateServiceName func(context.Context, int, string) (string, error)
// DefaultAdvertiseAddr is the default host/IP or network interface to use
// if no AdvertiseAddr value is specified.
+42 -4
View File
@@ -17,7 +17,6 @@ import (
"github.com/moby/moby/v2/daemon/server/backend"
"github.com/moby/moby/v2/daemon/server/swarmbackend"
"github.com/moby/moby/v2/errdefs"
"github.com/moby/moby/v2/internal/namesgenerator"
swarmapi "github.com/moby/swarmkit/v2/api"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
@@ -175,6 +174,36 @@ func (c *Cluster) GetService(input string, insertDefaults bool) (swarm.Service,
return svc, nil
}
func (c *Cluster) generateServiceName(ctx context.Context, spec *swarm.ServiceSpec, retry int) (string, error) {
if c.config.GenerateServiceName == nil {
return "", errors.New("service name generator is not configured")
}
var image string
if spec.TaskTemplate.ContainerSpec != nil {
image = spec.TaskTemplate.ContainerSpec.Image
}
name, err := c.config.GenerateServiceName(ctx, retry, image)
if err != nil {
return "", errors.Wrap(err, "generate service name")
}
if name == "" {
return "", errors.New("service name generator returned no name")
}
return name, nil
}
func (c *Cluster) defaultServiceName(ctx context.Context, spec *swarm.ServiceSpec) error {
if spec.Name != "" {
return nil
}
name, err := c.generateServiceName(ctx, spec, 0)
if err != nil {
return err
}
spec.Name = name
return nil
}
// CreateService creates a new service in a managed swarm cluster.
func (c *Cluster) CreateService(s swarm.ServiceSpec, encodedAuth string, queryRegistry bool) (*swarm.ServiceCreateResponse, error) {
var resp *swarm.ServiceCreateResponse
@@ -183,6 +212,10 @@ func (c *Cluster) CreateService(s swarm.ServiceSpec, encodedAuth string, queryRe
if err != nil {
return err
}
generatedName := s.Name == ""
if err := c.defaultServiceName(ctx, &s); err != nil {
return err
}
serviceSpec, err := convert.ServiceSpecToGRPC(s)
if err != nil {
@@ -257,12 +290,14 @@ func (c *Cluster) CreateService(s swarm.ServiceSpec, encodedAuth string, queryRe
}
}
generatedName := serviceSpec.Annotations.Name == ""
var r *swarmapi.CreateServiceResponse
// Use the same six-attempt limit as automatic container names.
for retry := range 6 {
if generatedName {
serviceSpec.Annotations.Name = namesgenerator.GetRandomName(retry)
if generatedName && retry > 0 {
serviceSpec.Annotations.Name, err = c.generateServiceName(ctx, &s, retry)
if err != nil {
return err
}
}
r, err = state.controlClient.CreateService(ctx, &swarmapi.CreateServiceRequest{Spec: &serviceSpec})
if !generatedName || status.Code(err) != codes.AlreadyExists {
@@ -288,6 +323,9 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec swa
if err != nil {
return err
}
if err := c.defaultServiceName(ctx, &spec); err != nil {
return err
}
serviceSpec, err := convert.ServiceSpecToGRPC(spec)
if err != nil {
+23 -10
View File
@@ -2,6 +2,7 @@ package cluster
import (
"context"
"fmt"
"testing"
"github.com/moby/moby/api/types/swarm"
@@ -39,39 +40,46 @@ func TestCreateServiceNameConflict(t *testing.T) {
name string
conflicts int
err error
expCalls int
expNames []string
expErr codes.Code
}{
{
doc: "generated name is redrawn on conflict",
conflicts: 1,
expCalls: 2,
expNames: []string{"generated-0", "generated-1"},
},
{
doc: "gives up after six draws",
conflicts: 100,
expCalls: 6,
expNames: []string{"generated-0", "generated-1", "generated-2", "generated-3", "generated-4", "generated-5"},
expErr: codes.AlreadyExists,
},
{
doc: "caller-chosen name is not redrawn",
name: "bold_lichterman",
conflicts: 1,
expCalls: 1,
expNames: []string{"bold_lichterman"},
expErr: codes.AlreadyExists,
},
{
doc: "other errors are not retried",
conflicts: 1,
err: status.Error(codes.InvalidArgument, "invalid spec"),
expCalls: 1,
expNames: []string{"generated-0"},
expErr: codes.InvalidArgument,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
client := &fakeServiceCreator{conflicts: tc.conflicts, err: tc.err}
cluster := &Cluster{nr: &nodeRunner{nodeState: nodeState{controlClient: client}}}
var generatedImages []string
cluster := &Cluster{
config: Config{GenerateServiceName: func(_ context.Context, retry int, image string) (string, error) {
generatedImages = append(generatedImages, image)
return fmt.Sprintf("generated-%d", retry), nil
}},
nr: &nodeRunner{nodeState: nodeState{controlClient: client}},
}
resp, err := cluster.CreateService(swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: tc.name},
TaskTemplate: swarm.TaskSpec{
@@ -79,14 +87,19 @@ func TestCreateServiceNameConflict(t *testing.T) {
},
}, "", false)
assert.Check(t, is.Len(client.names, tc.expCalls))
assert.Check(t, is.DeepEqual(client.names, tc.expNames))
if tc.name == "" {
assert.Check(t, is.Len(generatedImages, len(tc.expNames)))
for _, image := range generatedImages {
assert.Check(t, is.Equal(image, "image"))
}
} else {
assert.Check(t, is.Len(generatedImages, 0))
}
assert.Check(t, is.Equal(status.Code(err), tc.expErr))
if tc.expErr == codes.OK {
assert.Check(t, is.Equal(resp.ID, "serviceid"))
}
if len(client.names) > 1 {
assert.Check(t, client.names[1] != client.names[0])
}
})
}
}
+1
View File
@@ -1060,6 +1060,7 @@ func createAndStartCluster(d *daemon.Daemon, cfg *config.Config) (*cluster.Clust
ImageBackend: d.ImageBackend(),
PluginBackend: d.PluginManager(),
NetworkSubnetsProvider: d,
GenerateServiceName: d.GenerateServiceName,
DefaultAdvertiseAddr: cfg.SwarmDefaultAdvertiseAddr,
RaftHeartbeatTick: cfg.SwarmRaftHeartbeatTick,
RaftElectionTick: cfg.SwarmRaftElectionTick,
+1 -1
View File
@@ -115,7 +115,7 @@ func (daemon *Daemon) newContainer(name string, platform ocispec.Platform, confi
id string
err error
)
id, name, err = daemon.generateIDAndName(name)
id, name, err = daemon.generateIDAndName(name, config.Image)
if err != nil {
return nil, err
}
+10 -2
View File
@@ -4,15 +4,23 @@ import (
"github.com/moby/extensions"
"github.com/moby/extensions/clientpoint"
"github.com/moby/moby/v2/daemon/config"
containernamegeneratorpb "github.com/moby/moby/v2/extpoints/containernamegenerator/v0/protogen"
servicenamegeneratorpb "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0/protogen"
namesgeneratorlegacy "github.com/moby/moby/v2/internal/namesgenerator/legacy"
)
// clientProviders lists generated client wiring for points that launched
// extensions may provide. Socket exposure is resolved locally and is not listed.
func clientProviders() []clientpoint.Registration {
return nil
return []clientpoint.Registration{
containernamegeneratorpb.ClientPoint,
servicenamegeneratorpb.ClientPoint,
}
}
// builtinExtensions returns the in-process extensions selected by daemon config.
func builtinExtensions(*config.Config) []extensions.Extension {
return nil
return []extensions.Extension{
namesgeneratorlegacy.Extension,
}
}
+58 -6
View File
@@ -10,7 +10,8 @@ import (
"github.com/moby/moby/v2/daemon/internal/stringid"
"github.com/moby/moby/v2/daemon/names"
"github.com/moby/moby/v2/errdefs"
"github.com/moby/moby/v2/internal/namesgenerator"
containernamegeneratorv0 "github.com/moby/moby/v2/extpoints/containernamegenerator/v0"
servicenamegeneratorv0 "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0"
"github.com/pkg/errors"
)
@@ -29,7 +30,7 @@ func (daemon *Daemon) registerName(container *container.Container) error {
return errors.New("container is already loaded")
}
if container.Name == "" {
name, err := daemon.generateAndReserveName(container.ID)
name, err := daemon.generateAndReserveName(container.ID, container.Config.Image)
if err != nil {
return err
}
@@ -39,14 +40,14 @@ func (daemon *Daemon) registerName(container *container.Container) error {
return daemon.containersReplica.ReserveName(container.Name, container.ID)
}
func (daemon *Daemon) generateIDAndName(name string) (string, string, error) {
func (daemon *Daemon) generateIDAndName(name, image string) (string, string, error) {
var (
err error
id = stringid.GenerateRandomID()
)
if name == "" {
if name, err = daemon.generateAndReserveName(id); err != nil {
if name, err = daemon.generateAndReserveName(id, image); err != nil {
return "", "", err
}
return id, name, nil
@@ -85,10 +86,61 @@ func (daemon *Daemon) releaseName(name string) {
daemon.containersReplica.ReleaseName(name)
}
func (daemon *Daemon) generateAndReserveName(id string) (string, error) {
// GenerateServiceName resolves the configured service name-generator extension.
func (daemon *Daemon) GenerateServiceName(ctx context.Context, retry int, image string) (string, error) {
if daemon.extensionHost == nil {
return "", errors.New("name generator extension host is not configured")
}
reply, err := servicenamegeneratorv0.GenerateServiceName(ctx, daemon.extensionHost, &servicenamegeneratorv0.GenerateServiceNameRequest{
Retry: int64(retry),
Image: image,
})
var name string
if reply != nil {
name = reply.Name
}
return generatedName(ctx, name, err)
}
func (daemon *Daemon) generateContainerName(ctx context.Context, req *containernamegeneratorv0.GenerateContainerNameRequest) (string, error) {
if daemon.extensionHost == nil {
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
}
return generatedName(ctx, name, err)
}
func generatedName(ctx context.Context, name string, err error) (string, error) {
if name != "" {
if err != nil {
log.G(ctx).WithError(err).Warn("name generator extension failed; using built-in fallback")
}
return name, nil
}
if err != nil {
return "", err
}
return "", errors.New("name generator returned no name")
}
func (daemon *Daemon) generateAndReserveName(id, image string) (string, error) {
var name string
for i := range 6 {
name = namesgenerator.GetRandomName(i)
var err error
name, err = daemon.generateContainerName(context.TODO(), &containernamegeneratorv0.GenerateContainerNameRequest{
Retry: int64(i),
ContainerID: id,
Image: image,
})
if err != nil {
return "", err
}
if name[0] != '/' {
name = "/" + name
}
@@ -0,0 +1,21 @@
// Code generated by mobyextgen. DO NOT EDIT.
syntax = "proto3";
package org.mobyproject.extension.containernamegenerator.v0;
option go_package = "github.com/moby/moby/v2/extpoints/containernamegenerator/v0/protogen";
service ContainerNameGenerator {
rpc GenerateContainerName(GenerateContainerNameRequest) returns (GenerateContainerNameReply);
}
message GenerateContainerNameReply {
string name = 1;
}
message GenerateContainerNameRequest {
int64 retry = 1;
string container_id = 2;
string image = 3;
}
@@ -0,0 +1,63 @@
//go:generate go tool mobyextgen
// Package containernamegeneratorv0 defines the experimental point for
// generating default container names.
package containernamegeneratorv0
import (
"context"
"errors"
"fmt"
"github.com/moby/extensions"
"github.com/moby/moby/v2/extpoints/internal/namesgenerator"
)
// ContainerNameGenerator generates a default container name.
type ContainerNameGenerator interface {
GenerateContainerName(ctx context.Context, req *GenerateContainerNameRequest) (*GenerateContainerNameReply, error)
}
// GenerateContainerNameRequest describes a container-name generation attempt.
type GenerateContainerNameRequest struct {
// Retry is the number of prior generated names that collided.
Retry int64 `pb:"1"`
// ContainerID is the ID assigned to the container.
ContainerID string `pb:"2"`
// Image is the image reference used to create the container.
Image string `pb:"3"`
}
// GenerateContainerNameReply contains a generated container name.
type GenerateContainerNameReply struct {
// Name must contain 2 to 63 ASCII letters, digits, hyphens, or underscores.
// It must start and end with a letter or digit.
Name string `pb:"1"`
}
// Point is the single-provider container name-generator point.
//
//mobyextgen:service=ContainerNameGenerator
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
}
@@ -0,0 +1,42 @@
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(context.Background(), resolver, &GenerateContainerNameRequest{
Retry: 3,
ContainerID: "container-id",
Image: "image:latest",
})
assert.NilError(t, err)
assert.Equal(t, reply.Name, "container-name")
}
@@ -0,0 +1,191 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.12
// protoc (unknown)
// source: extpoints/containernamegenerator/v0/container_name_generator.proto
package protogen
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GenerateContainerNameReply struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GenerateContainerNameReply) Reset() {
*x = GenerateContainerNameReply{}
mi := &file_extpoints_containernamegenerator_v0_container_name_generator_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GenerateContainerNameReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GenerateContainerNameReply) ProtoMessage() {}
func (x *GenerateContainerNameReply) ProtoReflect() protoreflect.Message {
mi := &file_extpoints_containernamegenerator_v0_container_name_generator_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GenerateContainerNameReply.ProtoReflect.Descriptor instead.
func (*GenerateContainerNameReply) Descriptor() ([]byte, []int) {
return file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescGZIP(), []int{0}
}
func (x *GenerateContainerNameReply) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type GenerateContainerNameRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Retry int64 `protobuf:"varint,1,opt,name=retry,proto3" json:"retry,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GenerateContainerNameRequest) Reset() {
*x = GenerateContainerNameRequest{}
mi := &file_extpoints_containernamegenerator_v0_container_name_generator_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GenerateContainerNameRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GenerateContainerNameRequest) ProtoMessage() {}
func (x *GenerateContainerNameRequest) ProtoReflect() protoreflect.Message {
mi := &file_extpoints_containernamegenerator_v0_container_name_generator_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GenerateContainerNameRequest.ProtoReflect.Descriptor instead.
func (*GenerateContainerNameRequest) Descriptor() ([]byte, []int) {
return file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescGZIP(), []int{1}
}
func (x *GenerateContainerNameRequest) GetRetry() int64 {
if x != nil {
return x.Retry
}
return 0
}
func (x *GenerateContainerNameRequest) GetContainerId() string {
if x != nil {
return x.ContainerId
}
return ""
}
func (x *GenerateContainerNameRequest) GetImage() string {
if x != nil {
return x.Image
}
return ""
}
var File_extpoints_containernamegenerator_v0_container_name_generator_proto protoreflect.FileDescriptor
const file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDesc = "" +
"\n" +
"Bextpoints/containernamegenerator/v0/container_name_generator.proto\x123org.mobyproject.extension.containernamegenerator.v0\"0\n" +
"\x1aGenerateContainerNameReply\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"m\n" +
"\x1cGenerateContainerNameRequest\x12\x14\n" +
"\x05retry\x18\x01 \x01(\x03R\x05retry\x12!\n" +
"\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\x12\x14\n" +
"\x05image\x18\x03 \x01(\tR\x05image2\xd6\x01\n" +
"\x16ContainerNameGenerator\x12\xbb\x01\n" +
"\x15GenerateContainerName\x12Q.org.mobyproject.extension.containernamegenerator.v0.GenerateContainerNameRequest\x1aO.org.mobyproject.extension.containernamegenerator.v0.GenerateContainerNameReplyBFZDgithub.com/moby/moby/v2/extpoints/containernamegenerator/v0/protogenb\x06proto3"
var (
file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescOnce sync.Once
file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescData []byte
)
func file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescGZIP() []byte {
file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescOnce.Do(func() {
file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDesc), len(file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDesc)))
})
return file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDescData
}
var file_extpoints_containernamegenerator_v0_container_name_generator_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_extpoints_containernamegenerator_v0_container_name_generator_proto_goTypes = []any{
(*GenerateContainerNameReply)(nil), // 0: org.mobyproject.extension.containernamegenerator.v0.GenerateContainerNameReply
(*GenerateContainerNameRequest)(nil), // 1: org.mobyproject.extension.containernamegenerator.v0.GenerateContainerNameRequest
}
var file_extpoints_containernamegenerator_v0_container_name_generator_proto_depIdxs = []int32{
1, // 0: org.mobyproject.extension.containernamegenerator.v0.ContainerNameGenerator.GenerateContainerName:input_type -> org.mobyproject.extension.containernamegenerator.v0.GenerateContainerNameRequest
0, // 1: org.mobyproject.extension.containernamegenerator.v0.ContainerNameGenerator.GenerateContainerName:output_type -> org.mobyproject.extension.containernamegenerator.v0.GenerateContainerNameReply
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_extpoints_containernamegenerator_v0_container_name_generator_proto_init() }
func file_extpoints_containernamegenerator_v0_container_name_generator_proto_init() {
if File_extpoints_containernamegenerator_v0_container_name_generator_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeFor[x]().PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDesc), len(file_extpoints_containernamegenerator_v0_container_name_generator_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_extpoints_containernamegenerator_v0_container_name_generator_proto_goTypes,
DependencyIndexes: file_extpoints_containernamegenerator_v0_container_name_generator_proto_depIdxs,
MessageInfos: file_extpoints_containernamegenerator_v0_container_name_generator_proto_msgTypes,
}.Build()
File_extpoints_containernamegenerator_v0_container_name_generator_proto = out.File
file_extpoints_containernamegenerator_v0_container_name_generator_proto_goTypes = nil
file_extpoints_containernamegenerator_v0_container_name_generator_proto_depIdxs = nil
}
@@ -0,0 +1,162 @@
// Code generated by mobyextgen. DO NOT EDIT.
package protogen
import (
context "context"
extensions "github.com/moby/extensions"
clientpoint "github.com/moby/extensions/clientpoint"
serverpoint "github.com/moby/extensions/serverpoint"
containernamegeneratorv0 "github.com/moby/moby/v2/extpoints/containernamegenerator/v0"
grpc "google.golang.org/grpc"
)
// serviceName is the point's fully-qualified gRPC service name.
const serviceName = "org.mobyproject.extension.containernamegenerator.v0.ContainerNameGenerator"
const (
methodGenerateContainerName = "/" + serviceName + "/GenerateContainerName"
)
// ContainerNameGeneratorServer is the server side of the point's gRPC service. It is the
// proto-level shape of the point, not the point's Go interface: a contract
// method returning a bare error returns an empty response message here.
type ContainerNameGeneratorServer interface {
GenerateContainerName(context.Context, *GenerateContainerNameRequest) (*GenerateContainerNameReply, error)
}
// serviceDesc describes the point's gRPC service to a server. HandlerType is
// what a registrar type-checks an implementation against, so registering the
// wrong provider for this point is caught at registration.
var serviceDesc = grpc.ServiceDesc{
ServiceName: serviceName,
HandlerType: (*ContainerNameGeneratorServer)(nil),
Metadata: "extpoints/containernamegenerator/v0/container_name_generator.proto",
Methods: []grpc.MethodDesc{
{MethodName: "GenerateContainerName", Handler: handleGenerateContainerName},
},
}
func handleGenerateContainerName(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(GenerateContainerNameRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ContainerNameGeneratorServer).GenerateContainerName(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: methodGenerateContainerName}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(ContainerNameGeneratorServer).GenerateContainerName(ctx, req.(*GenerateContainerNameRequest))
})
}
// ContainerNameGeneratorClient calls the point's gRPC service. It is exported so a client
// outside the framework - one calling a service an extension publishes on the
// API socket - can reach it with a plain gRPC client.
type ContainerNameGeneratorClient interface {
GenerateContainerName(ctx context.Context, in *GenerateContainerNameRequest, opts ...grpc.CallOption) (*GenerateContainerNameReply, error)
}
// NewContainerNameGeneratorClient returns a client for the point's gRPC service on cc.
func NewContainerNameGeneratorClient(cc grpc.ClientConnInterface) ContainerNameGeneratorClient {
return &serviceClient{cc: cc}
}
type serviceClient struct{ cc grpc.ClientConnInterface }
func (c *serviceClient) GenerateContainerName(ctx context.Context, in *GenerateContainerNameRequest, opts ...grpc.CallOption) (*GenerateContainerNameReply, error) {
out := new(GenerateContainerNameReply)
if err := c.cc.Invoke(ctx, methodGenerateContainerName, in, out, append([]grpc.CallOption{grpc.StaticMethod()}, opts...)...); err != nil {
return nil, err
}
return out, nil
}
// ServerPoint serves the ContainerNameGenerator point: it registers the point's gRPC service for
// a provider with an SDK server. A binary passes it to (*sdk.Server).Register.
var ServerPoint = serverpoint.Registration{
Point: containernamegeneratorv0.Point.ID(),
Register: func(r grpc.ServiceRegistrar, impl any) {
r.RegisterService(&serviceDesc, &grpcServer{impl: impl.(containernamegeneratorv0.ContainerNameGenerator)})
},
}
// ClientProvider builds a broker provider for the ContainerNameGenerator point from an
// out-of-process gRPC connection.
func ClientProvider(conn grpc.ClientConnInterface) extensions.Provider {
return containernamegeneratorv0.Point.Provide(NewClient(conn))
}
// ClientPoint registers ClientProvider for the ContainerNameGenerator point with a host.
// Single carries the contract's cardinality: the point admits one provider.
var ClientPoint = clientpoint.Registration{Point: containernamegeneratorv0.Point.ID(), Provider: ClientProvider, Single: true}
// NewClient returns a containernamegeneratorv0.ContainerNameGenerator that calls the ContainerNameGenerator point over conn.
func NewClient(conn grpc.ClientConnInterface) containernamegeneratorv0.ContainerNameGenerator {
return &grpcClient{client: NewContainerNameGeneratorClient(conn)}
}
// grpcServer serves an implementation of the contract's Go interface.
type grpcServer struct {
impl containernamegeneratorv0.ContainerNameGenerator
}
func (s *grpcServer) GenerateContainerName(ctx context.Context, req *GenerateContainerNameRequest) (*GenerateContainerNameReply, error) {
resp, err := s.impl.GenerateContainerName(ctx, generateContainerNameRequestFromProto(req))
if err != nil {
return nil, err
}
return generateContainerNameReplyToProto(resp), nil
}
type grpcClient struct {
client ContainerNameGeneratorClient
}
func (c *grpcClient) GenerateContainerName(ctx context.Context, req *containernamegeneratorv0.GenerateContainerNameRequest) (*containernamegeneratorv0.GenerateContainerNameReply, error) {
resp, err := c.client.GenerateContainerName(ctx, generateContainerNameRequestToProto(req))
if err != nil {
return nil, err
}
return generateContainerNameReplyFromProto(resp), nil
}
func generateContainerNameReplyToProto(in *containernamegeneratorv0.GenerateContainerNameReply) *GenerateContainerNameReply {
if in == nil {
return nil
}
out := &GenerateContainerNameReply{}
out.Name = in.Name
return out
}
func generateContainerNameReplyFromProto(in *GenerateContainerNameReply) *containernamegeneratorv0.GenerateContainerNameReply {
if in == nil {
return nil
}
out := &containernamegeneratorv0.GenerateContainerNameReply{}
out.Name = in.GetName()
return out
}
func generateContainerNameRequestToProto(in *containernamegeneratorv0.GenerateContainerNameRequest) *GenerateContainerNameRequest {
if in == nil {
return nil
}
out := &GenerateContainerNameRequest{}
out.Retry = in.Retry
out.ContainerId = in.ContainerID
out.Image = in.Image
return out
}
func generateContainerNameRequestFromProto(in *GenerateContainerNameRequest) *containernamegeneratorv0.GenerateContainerNameRequest {
if in == nil {
return nil
}
out := &containernamegeneratorv0.GenerateContainerNameRequest{}
out.Retry = in.GetRetry()
out.ContainerID = in.GetContainerId()
out.Image = in.GetImage()
return out
}
@@ -0,0 +1,99 @@
// 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)
}
@@ -0,0 +1,154 @@
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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), staticresolver.New(), testPoint.ID(), invoke)
assert.ErrorContains(t, err, "has no providers")
_, err = Generate(context.Background(), 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(context.Background(), 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))
}
@@ -0,0 +1,182 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.12
// protoc (unknown)
// source: extpoints/servicenamegenerator/v0/service_name_generator.proto
package protogen
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GenerateServiceNameReply struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GenerateServiceNameReply) Reset() {
*x = GenerateServiceNameReply{}
mi := &file_extpoints_servicenamegenerator_v0_service_name_generator_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GenerateServiceNameReply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GenerateServiceNameReply) ProtoMessage() {}
func (x *GenerateServiceNameReply) ProtoReflect() protoreflect.Message {
mi := &file_extpoints_servicenamegenerator_v0_service_name_generator_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GenerateServiceNameReply.ProtoReflect.Descriptor instead.
func (*GenerateServiceNameReply) Descriptor() ([]byte, []int) {
return file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescGZIP(), []int{0}
}
func (x *GenerateServiceNameReply) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type GenerateServiceNameRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Retry int64 `protobuf:"varint,1,opt,name=retry,proto3" json:"retry,omitempty"`
Image string `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GenerateServiceNameRequest) Reset() {
*x = GenerateServiceNameRequest{}
mi := &file_extpoints_servicenamegenerator_v0_service_name_generator_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GenerateServiceNameRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GenerateServiceNameRequest) ProtoMessage() {}
func (x *GenerateServiceNameRequest) ProtoReflect() protoreflect.Message {
mi := &file_extpoints_servicenamegenerator_v0_service_name_generator_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GenerateServiceNameRequest.ProtoReflect.Descriptor instead.
func (*GenerateServiceNameRequest) Descriptor() ([]byte, []int) {
return file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescGZIP(), []int{1}
}
func (x *GenerateServiceNameRequest) GetRetry() int64 {
if x != nil {
return x.Retry
}
return 0
}
func (x *GenerateServiceNameRequest) GetImage() string {
if x != nil {
return x.Image
}
return ""
}
var File_extpoints_servicenamegenerator_v0_service_name_generator_proto protoreflect.FileDescriptor
const file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDesc = "" +
"\n" +
">extpoints/servicenamegenerator/v0/service_name_generator.proto\x121org.mobyproject.extension.servicenamegenerator.v0\".\n" +
"\x18GenerateServiceNameReply\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"H\n" +
"\x1aGenerateServiceNameRequest\x12\x14\n" +
"\x05retry\x18\x01 \x01(\x03R\x05retry\x12\x14\n" +
"\x05image\x18\x02 \x01(\tR\x05image2\xca\x01\n" +
"\x14ServiceNameGenerator\x12\xb1\x01\n" +
"\x13GenerateServiceName\x12M.org.mobyproject.extension.servicenamegenerator.v0.GenerateServiceNameRequest\x1aK.org.mobyproject.extension.servicenamegenerator.v0.GenerateServiceNameReplyBDZBgithub.com/moby/moby/v2/extpoints/servicenamegenerator/v0/protogenb\x06proto3"
var (
file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescOnce sync.Once
file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescData []byte
)
func file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescGZIP() []byte {
file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescOnce.Do(func() {
file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDesc), len(file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDesc)))
})
return file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDescData
}
var file_extpoints_servicenamegenerator_v0_service_name_generator_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_extpoints_servicenamegenerator_v0_service_name_generator_proto_goTypes = []any{
(*GenerateServiceNameReply)(nil), // 0: org.mobyproject.extension.servicenamegenerator.v0.GenerateServiceNameReply
(*GenerateServiceNameRequest)(nil), // 1: org.mobyproject.extension.servicenamegenerator.v0.GenerateServiceNameRequest
}
var file_extpoints_servicenamegenerator_v0_service_name_generator_proto_depIdxs = []int32{
1, // 0: org.mobyproject.extension.servicenamegenerator.v0.ServiceNameGenerator.GenerateServiceName:input_type -> org.mobyproject.extension.servicenamegenerator.v0.GenerateServiceNameRequest
0, // 1: org.mobyproject.extension.servicenamegenerator.v0.ServiceNameGenerator.GenerateServiceName:output_type -> org.mobyproject.extension.servicenamegenerator.v0.GenerateServiceNameReply
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_extpoints_servicenamegenerator_v0_service_name_generator_proto_init() }
func file_extpoints_servicenamegenerator_v0_service_name_generator_proto_init() {
if File_extpoints_servicenamegenerator_v0_service_name_generator_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeFor[x]().PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDesc), len(file_extpoints_servicenamegenerator_v0_service_name_generator_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_extpoints_servicenamegenerator_v0_service_name_generator_proto_goTypes,
DependencyIndexes: file_extpoints_servicenamegenerator_v0_service_name_generator_proto_depIdxs,
MessageInfos: file_extpoints_servicenamegenerator_v0_service_name_generator_proto_msgTypes,
}.Build()
File_extpoints_servicenamegenerator_v0_service_name_generator_proto = out.File
file_extpoints_servicenamegenerator_v0_service_name_generator_proto_goTypes = nil
file_extpoints_servicenamegenerator_v0_service_name_generator_proto_depIdxs = nil
}
@@ -0,0 +1,160 @@
// Code generated by mobyextgen. DO NOT EDIT.
package protogen
import (
context "context"
extensions "github.com/moby/extensions"
clientpoint "github.com/moby/extensions/clientpoint"
serverpoint "github.com/moby/extensions/serverpoint"
servicenamegeneratorv0 "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0"
grpc "google.golang.org/grpc"
)
// serviceName is the point's fully-qualified gRPC service name.
const serviceName = "org.mobyproject.extension.servicenamegenerator.v0.ServiceNameGenerator"
const (
methodGenerateServiceName = "/" + serviceName + "/GenerateServiceName"
)
// ServiceNameGeneratorServer is the server side of the point's gRPC service. It is the
// proto-level shape of the point, not the point's Go interface: a contract
// method returning a bare error returns an empty response message here.
type ServiceNameGeneratorServer interface {
GenerateServiceName(context.Context, *GenerateServiceNameRequest) (*GenerateServiceNameReply, error)
}
// serviceDesc describes the point's gRPC service to a server. HandlerType is
// what a registrar type-checks an implementation against, so registering the
// wrong provider for this point is caught at registration.
var serviceDesc = grpc.ServiceDesc{
ServiceName: serviceName,
HandlerType: (*ServiceNameGeneratorServer)(nil),
Metadata: "extpoints/servicenamegenerator/v0/service_name_generator.proto",
Methods: []grpc.MethodDesc{
{MethodName: "GenerateServiceName", Handler: handleGenerateServiceName},
},
}
func handleGenerateServiceName(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(GenerateServiceNameRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ServiceNameGeneratorServer).GenerateServiceName(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: methodGenerateServiceName}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(ServiceNameGeneratorServer).GenerateServiceName(ctx, req.(*GenerateServiceNameRequest))
})
}
// ServiceNameGeneratorClient calls the point's gRPC service. It is exported so a client
// outside the framework - one calling a service an extension publishes on the
// API socket - can reach it with a plain gRPC client.
type ServiceNameGeneratorClient interface {
GenerateServiceName(ctx context.Context, in *GenerateServiceNameRequest, opts ...grpc.CallOption) (*GenerateServiceNameReply, error)
}
// NewServiceNameGeneratorClient returns a client for the point's gRPC service on cc.
func NewServiceNameGeneratorClient(cc grpc.ClientConnInterface) ServiceNameGeneratorClient {
return &serviceClient{cc: cc}
}
type serviceClient struct{ cc grpc.ClientConnInterface }
func (c *serviceClient) GenerateServiceName(ctx context.Context, in *GenerateServiceNameRequest, opts ...grpc.CallOption) (*GenerateServiceNameReply, error) {
out := new(GenerateServiceNameReply)
if err := c.cc.Invoke(ctx, methodGenerateServiceName, in, out, append([]grpc.CallOption{grpc.StaticMethod()}, opts...)...); err != nil {
return nil, err
}
return out, nil
}
// ServerPoint serves the ServiceNameGenerator point: it registers the point's gRPC service for
// a provider with an SDK server. A binary passes it to (*sdk.Server).Register.
var ServerPoint = serverpoint.Registration{
Point: servicenamegeneratorv0.Point.ID(),
Register: func(r grpc.ServiceRegistrar, impl any) {
r.RegisterService(&serviceDesc, &grpcServer{impl: impl.(servicenamegeneratorv0.ServiceNameGenerator)})
},
}
// ClientProvider builds a broker provider for the ServiceNameGenerator point from an
// out-of-process gRPC connection.
func ClientProvider(conn grpc.ClientConnInterface) extensions.Provider {
return servicenamegeneratorv0.Point.Provide(NewClient(conn))
}
// ClientPoint registers ClientProvider for the ServiceNameGenerator point with a host.
// Single carries the contract's cardinality: the point admits one provider.
var ClientPoint = clientpoint.Registration{Point: servicenamegeneratorv0.Point.ID(), Provider: ClientProvider, Single: true}
// NewClient returns a servicenamegeneratorv0.ServiceNameGenerator that calls the ServiceNameGenerator point over conn.
func NewClient(conn grpc.ClientConnInterface) servicenamegeneratorv0.ServiceNameGenerator {
return &grpcClient{client: NewServiceNameGeneratorClient(conn)}
}
// grpcServer serves an implementation of the contract's Go interface.
type grpcServer struct {
impl servicenamegeneratorv0.ServiceNameGenerator
}
func (s *grpcServer) GenerateServiceName(ctx context.Context, req *GenerateServiceNameRequest) (*GenerateServiceNameReply, error) {
resp, err := s.impl.GenerateServiceName(ctx, generateServiceNameRequestFromProto(req))
if err != nil {
return nil, err
}
return generateServiceNameReplyToProto(resp), nil
}
type grpcClient struct {
client ServiceNameGeneratorClient
}
func (c *grpcClient) GenerateServiceName(ctx context.Context, req *servicenamegeneratorv0.GenerateServiceNameRequest) (*servicenamegeneratorv0.GenerateServiceNameReply, error) {
resp, err := c.client.GenerateServiceName(ctx, generateServiceNameRequestToProto(req))
if err != nil {
return nil, err
}
return generateServiceNameReplyFromProto(resp), nil
}
func generateServiceNameReplyToProto(in *servicenamegeneratorv0.GenerateServiceNameReply) *GenerateServiceNameReply {
if in == nil {
return nil
}
out := &GenerateServiceNameReply{}
out.Name = in.Name
return out
}
func generateServiceNameReplyFromProto(in *GenerateServiceNameReply) *servicenamegeneratorv0.GenerateServiceNameReply {
if in == nil {
return nil
}
out := &servicenamegeneratorv0.GenerateServiceNameReply{}
out.Name = in.GetName()
return out
}
func generateServiceNameRequestToProto(in *servicenamegeneratorv0.GenerateServiceNameRequest) *GenerateServiceNameRequest {
if in == nil {
return nil
}
out := &GenerateServiceNameRequest{}
out.Retry = in.Retry
out.Image = in.Image
return out
}
func generateServiceNameRequestFromProto(in *GenerateServiceNameRequest) *servicenamegeneratorv0.GenerateServiceNameRequest {
if in == nil {
return nil
}
out := &servicenamegeneratorv0.GenerateServiceNameRequest{}
out.Retry = in.GetRetry()
out.Image = in.GetImage()
return out
}
@@ -0,0 +1,20 @@
// Code generated by mobyextgen. DO NOT EDIT.
syntax = "proto3";
package org.mobyproject.extension.servicenamegenerator.v0;
option go_package = "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0/protogen";
service ServiceNameGenerator {
rpc GenerateServiceName(GenerateServiceNameRequest) returns (GenerateServiceNameReply);
}
message GenerateServiceNameReply {
string name = 1;
}
message GenerateServiceNameRequest {
int64 retry = 1;
string image = 2;
}
@@ -0,0 +1,62 @@
//go:generate go tool mobyextgen
// Package servicenamegeneratorv0 defines the experimental point for generating
// default Swarm service names.
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.
type ServiceNameGenerator interface {
GenerateServiceName(ctx context.Context, req *GenerateServiceNameRequest) (*GenerateServiceNameReply, error)
}
// GenerateServiceNameRequest describes a service-name generation attempt.
type GenerateServiceNameRequest struct {
// Retry is the number of prior generated names that collided.
Retry int64 `pb:"1"`
// Image is the container image used by the service.
// It is empty for services that use another runtime.
Image string `pb:"2"`
}
// GenerateServiceNameReply contains a generated Swarm service name.
type GenerateServiceNameReply struct {
// Name must contain 2 to 63 ASCII letters, digits, hyphens, or underscores.
// It must start and end with a letter or digit.
Name string `pb:"1"`
}
// Point is the single-provider service name-generator point.
//
//mobyextgen:service=ServiceNameGenerator
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
}
@@ -0,0 +1,40 @@
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(context.Background(), resolver, &GenerateServiceNameRequest{
Retry: 3,
Image: "image:latest",
})
assert.NilError(t, err)
assert.Equal(t, reply.Name, "service-name")
}
@@ -1,21 +1,54 @@
// Package namesgenerator generates random names.
// Package legacy generates random names using Moby's original name lists.
//
// This package is officially "frozen" - no new additions will be accepted.
//
// For a long time, this package provided a lot of joy within the project, but
// at some point the conflicts of opinion became greater than the added joy.
//
// At some future time, this may be replaced with something that sparks less
// controversy, but for now it will remain as-is.
// Alternative generators can replace this built-in through the container or
// service name-generator extension points.
//
// See also https://github.com/moby/moby/pull/43210#issuecomment-1029934277
package namesgenerator
package legacy
import (
"context"
"errors"
"math/rand/v2"
"strconv"
"github.com/moby/extensions"
containernamegeneratorv0 "github.com/moby/moby/v2/extpoints/containernamegenerator/v0"
servicenamegeneratorv0 "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0"
)
const extensionID extensions.ExtensionID = "org.mobyproject.namesgenerator.legacy.v1"
// Extension provides Moby's built-in random name generator.
var Extension = extensions.New(extensions.Declaration{
ID: extensionID,
Providers: []extensions.Provider{
containernamegeneratorv0.Point.Provide(provider{}),
servicenamegeneratorv0.Point.Provide(provider{}),
},
})
type provider struct{}
func (provider) GenerateContainerName(_ context.Context, req *containernamegeneratorv0.GenerateContainerNameRequest) (*containernamegeneratorv0.GenerateContainerNameReply, error) {
if req == nil {
return nil, errors.New("generate container name request is required")
}
return &containernamegeneratorv0.GenerateContainerNameReply{Name: generateName(req.Retry)}, nil
}
func (provider) GenerateServiceName(_ context.Context, req *servicenamegeneratorv0.GenerateServiceNameRequest) (*servicenamegeneratorv0.GenerateServiceNameReply, error) {
if req == nil {
return nil, errors.New("generate service name request is required")
}
return &servicenamegeneratorv0.GenerateServiceNameReply{Name: generateName(req.Retry)}, nil
}
var (
left = [...]string{
"admiring",
@@ -843,10 +876,9 @@ var (
}
)
// GetRandomName generates a random name from the list of adjectives and surnames in this package
// formatted as "adjective_surname". For example 'focused_turing'. If retry is non-zero, a random
// integer between 0 and 10 will be added to the end of the name, e.g `focused_turing3`
func GetRandomName(retry int) string {
// generateName generates a random name formatted as "adjective_surname".
// If retry is non-zero, one random decimal digit is appended.
func generateName(retry int64) string {
begin:
name := left[rand.IntN(len(left))] + "_" + right[rand.IntN(len(right))] // #nosec G404 -- Use of weak random number generator (math/rand instead of crypto/rand)
if name == "boring_wozniak" /* Steve Wozniak is not boring */ {
@@ -1,4 +1,4 @@
package namesgenerator
package legacy
import (
"strings"
@@ -6,7 +6,7 @@ import (
)
func TestNameFormat(t *testing.T) {
name := GetRandomName(0)
name := generateName(0)
if !strings.Contains(name, "_") {
t.Fatalf("Generated name does not contain an underscore")
}
@@ -16,7 +16,7 @@ func TestNameFormat(t *testing.T) {
}
func TestNameRetries(t *testing.T) {
name := GetRandomName(1)
name := generateName(1)
if !strings.Contains(name, "_") {
t.Fatalf("Generated name does not contain an underscore")
}
@@ -29,7 +29,7 @@ func BenchmarkGetRandomName(b *testing.B) {
b.ReportAllocs()
var out string
for b.Loop() {
out = GetRandomName(5)
out = generateName(5)
}
b.Log("Last result:", out)
}
@@ -0,0 +1,28 @@
// Package staticresolver provides a fixed implementation of extensions.Resolver.
package staticresolver
import (
"fmt"
"github.com/moby/extensions"
)
// New returns a resolver backed by providers.
func New(providers ...extensions.ResolvedProvider) extensions.Resolver {
return resolver(providers)
}
type resolver []extensions.ResolvedProvider
func (r resolver) Provider(_ extensions.PointID, id extensions.ExtensionID) (any, error) {
for _, provider := range r {
if provider.Identity.ID == id {
return provider.Impl, nil
}
}
return nil, fmt.Errorf("no provider for extension %q", id)
}
func (r resolver) Providers(extensions.PointID) []extensions.ResolvedProvider {
return r
}