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:
22 files changed
+1404
-35
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
+40
-8
@@ -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 */ {
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user