daemon/extensions: Expose bootstrap configuration to extensions

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski committed 2026-09-30 20:02:39 +02:00
1 parent 069efe3441
commit 0c71b3f8ce
8 files changed
+438 -17

No files matched your search

+1 -1
View File
@@ -956,7 +956,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
// Build the host after installing the cleanup defer: this starts extension
// processes and must be covered if initialization fails.
d.extensionHost, err = newExtensionHost(ctx, config)
d.extensionHost, err = d.newExtensionHost(ctx)
if err != nil {
return nil, err
}
+6 -10
View File
@@ -10,7 +10,6 @@ import (
"github.com/moby/extensions"
"github.com/moby/extensions/grpcproxy"
"github.com/moby/extensions/host"
"github.com/moby/extensions/serverpoint"
"github.com/moby/moby/v2/daemon/config"
"github.com/moby/moby/v2/daemon/internal/rootless"
servicegrpcv0 "github.com/moby/moby/v2/extpoints/servicegrpc/v0"
@@ -19,7 +18,8 @@ import (
)
// newExtensionHost builds the daemon's extension host.
func newExtensionHost(ctx context.Context, cfg *config.Config) (*host.Host, error) {
func (daemon *Daemon) newExtensionHost(ctx context.Context) (*host.Host, error) {
cfg := daemon.Config()
runtimeDir := filepath.Join(cfg.ExecRoot, "extensions")
if err := os.MkdirAll(runtimeDir, 0o700); err != nil {
return nil, fmt.Errorf("create extension runtime directory: %w", err)
@@ -29,22 +29,18 @@ func newExtensionHost(ctx context.Context, cfg *config.Config) (*host.Host, erro
}
return host.New(ctx,
host.WithRuntimeDir(runtimeDir),
host.WithExtensions(builtinExtensions(cfg)...),
host.WithDirs(extensionDirs(cfg)...),
host.WithExtensions(builtinExtensions...),
host.WithExtensions(daemon.daemonExtensions()...),
host.WithDirs(extensionDirs(&cfg)...),
host.WithClientProviders(clientProviders()...),
host.WithProviderPolicy(host.PointPolicyFunc(func(extensions.ExtensionIdentity, extensions.PointID) host.PointPolicyResult {
return host.Allow()
})),
host.WithDependencyProviders(dependencyProviders()...),
host.WithExtensionConfig(extensionConfig(cfg)),
host.WithExtensionConfig(extensionConfig(&cfg)),
)
}
// dependencyProviders lists points that launched extensions may call back into.
func dependencyProviders() []serverpoint.Registration {
return nil
}
// extensionConfig converts daemon.json's extension-config entries to host form.
func extensionConfig(cfg *config.Config) map[extensions.ExtensionID]extensions.Config {
if len(cfg.ExtensionConfig) == 0 {
+30 -6
View File
@@ -3,14 +3,36 @@ package daemon
import (
"github.com/moby/extensions"
"github.com/moby/extensions/clientpoint"
"github.com/moby/moby/v2/daemon/config"
"github.com/moby/extensions/serverpoint"
"github.com/moby/moby/v2/daemon/internal/extensionconfig"
containernamegeneratorpb "github.com/moby/moby/v2/extpoints/containernamegenerator/v0/protogen"
daemonconfigpb "github.com/moby/moby/v2/extpoints/daemonconfig/v0/protogen"
servicenamegeneratorpb "github.com/moby/moby/v2/extpoints/servicenamegenerator/v0/protogen"
namesgeneratorlegacy "github.com/moby/moby/v2/internal/namesgenerator/legacy"
)
// builtinExtensions lists in-tree extensions that should be available in the
// engine by default.
var builtinExtensions = []extensions.Extension{
namesgeneratorlegacy.Extension,
}
// daemonExtensions declares extensions that need daemon-owned state.
// Ordinary builtin definitions obtain their bootstrap settings through points.
func (daemon *Daemon) daemonExtensions() []extensions.Extension {
cfg := daemon.Config()
return []extensions.Extension{
extensionconfig.New(&cfg),
}
}
// clientProviders lists generated client wiring for points that launched
// extensions may provide. Socket exposure is resolved locally and is not listed.
// extensions may provide.
//
// A point that's simply exposed as a service by an extension DOES NOT need to
// be in this list.
// This list only contains points that the daemon calls directly.
func clientProviders() []clientpoint.Registration {
return []clientpoint.Registration{
containernamegeneratorpb.ClientPoint,
@@ -18,9 +40,11 @@ func clientProviders() []clientpoint.Registration {
}
}
// builtinExtensions returns the in-process extensions selected by daemon config.
func builtinExtensions(*config.Config) []extensions.Extension {
return []extensions.Extension{
namesgeneratorlegacy.Extension,
// dependencyProviders lists points that external extensions may call back
// into.
// Points not in this list will not be able to used as dependencies.
func dependencyProviders() []serverpoint.Registration {
return []serverpoint.Registration{
daemonconfigpb.ServerPoint,
}
}
+28
View File
@@ -0,0 +1,28 @@
// Package extensionconfig provides the daemon configuration snapshot to extensions.
package extensionconfig
import (
"context"
"github.com/moby/extensions"
"github.com/moby/moby/v2/daemon/config"
daemonconfigv0 "github.com/moby/moby/v2/extpoints/daemonconfig/v0"
)
// New declares a config provider with an immutable persistent-root snapshot.
func New(cfg *config.Config) extensions.Extension {
return extensions.New(extensions.Declaration{
ID: "org.mobyproject.daemon.config.v0",
Providers: []extensions.Provider{
daemonconfigv0.Point.Provide(snapshot{rootDir: cfg.Root}),
},
})
}
type snapshot struct {
rootDir string
}
func (s snapshot) Get(context.Context, *daemonconfigv0.GetRequest) (*daemonconfigv0.GetResponse, error) {
return &daemonconfigv0.GetResponse{RootDir: s.rootDir}, nil
}
@@ -0,0 +1,18 @@
// Code generated by mobyextgen. DO NOT EDIT.
syntax = "proto3";
package org.mobyproject.extension.daemon.config.v0;
option go_package = "github.com/moby/moby/v2/extpoints/daemonconfig/v0/protogen";
service DaemonConfig {
rpc Get(GetRequest) returns (GetResponse);
}
message GetRequest {
}
message GetResponse {
string root_dir = 1;
}
+34
View File
@@ -0,0 +1,34 @@
//go:generate go tool mobyextgen
// Package daemonconfigv0 defines the read-only daemon configuration snapshot
// available to Moby extensions.
package daemonconfigv0
import (
"context"
"github.com/moby/extensions"
)
// DaemonConfig provides an immutable snapshot of extension bootstrap settings.
// It does not expose the daemon's full configuration or a generic lookup API.
type DaemonConfig interface {
// Get returns a fresh copy of the snapshot.
Get(ctx context.Context, req *GetRequest) (*GetResponse, error)
}
// Point is single-cardinality for the daemon configuration snapshot.
// The daemon registers its builtin provider for extension dependencies,
// including out-of-process extensions, but does not publish it on the public
// daemon socket.
var Point = extensions.DefineSinglePoint[DaemonConfig]("org.mobyproject.extension.daemon.config.v0")
// GetRequest requests the daemon configuration snapshot.
type GetRequest struct{}
// GetResponse contains the immutable extension bootstrap settings.
type GetResponse struct {
// RootDir is the absolute path to the daemon's persistent data root,
// not its execution root.
RootDir string `pb:"1"`
}
@@ -0,0 +1,165 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.12
// protoc (unknown)
// source: extpoints/daemonconfig/v0/daemon_config.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 GetRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetRequest) Reset() {
*x = GetRequest{}
mi := &file_extpoints_daemonconfig_v0_daemon_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetRequest) ProtoMessage() {}
func (x *GetRequest) ProtoReflect() protoreflect.Message {
mi := &file_extpoints_daemonconfig_v0_daemon_config_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 GetRequest.ProtoReflect.Descriptor instead.
func (*GetRequest) Descriptor() ([]byte, []int) {
return file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescGZIP(), []int{0}
}
type GetResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
RootDir string `protobuf:"bytes,1,opt,name=root_dir,json=rootDir,proto3" json:"root_dir,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetResponse) Reset() {
*x = GetResponse{}
mi := &file_extpoints_daemonconfig_v0_daemon_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetResponse) ProtoMessage() {}
func (x *GetResponse) ProtoReflect() protoreflect.Message {
mi := &file_extpoints_daemonconfig_v0_daemon_config_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 GetResponse.ProtoReflect.Descriptor instead.
func (*GetResponse) Descriptor() ([]byte, []int) {
return file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescGZIP(), []int{1}
}
func (x *GetResponse) GetRootDir() string {
if x != nil {
return x.RootDir
}
return ""
}
var File_extpoints_daemonconfig_v0_daemon_config_proto protoreflect.FileDescriptor
const file_extpoints_daemonconfig_v0_daemon_config_proto_rawDesc = "" +
"\n" +
"-extpoints/daemonconfig/v0/daemon_config.proto\x12*org.mobyproject.extension.daemon.config.v0\"\f\n" +
"\n" +
"GetRequest\"(\n" +
"\vGetResponse\x12\x19\n" +
"\broot_dir\x18\x01 \x01(\tR\arootDir2\x86\x01\n" +
"\fDaemonConfig\x12v\n" +
"\x03Get\x126.org.mobyproject.extension.daemon.config.v0.GetRequest\x1a7.org.mobyproject.extension.daemon.config.v0.GetResponseB<Z:github.com/moby/moby/v2/extpoints/daemonconfig/v0/protogenb\x06proto3"
var (
file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescOnce sync.Once
file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescData []byte
)
func file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescGZIP() []byte {
file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescOnce.Do(func() {
file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_extpoints_daemonconfig_v0_daemon_config_proto_rawDesc), len(file_extpoints_daemonconfig_v0_daemon_config_proto_rawDesc)))
})
return file_extpoints_daemonconfig_v0_daemon_config_proto_rawDescData
}
var file_extpoints_daemonconfig_v0_daemon_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_extpoints_daemonconfig_v0_daemon_config_proto_goTypes = []any{
(*GetRequest)(nil), // 0: org.mobyproject.extension.daemon.config.v0.GetRequest
(*GetResponse)(nil), // 1: org.mobyproject.extension.daemon.config.v0.GetResponse
}
var file_extpoints_daemonconfig_v0_daemon_config_proto_depIdxs = []int32{
0, // 0: org.mobyproject.extension.daemon.config.v0.DaemonConfig.Get:input_type -> org.mobyproject.extension.daemon.config.v0.GetRequest
1, // 1: org.mobyproject.extension.daemon.config.v0.DaemonConfig.Get:output_type -> org.mobyproject.extension.daemon.config.v0.GetResponse
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_daemonconfig_v0_daemon_config_proto_init() }
func file_extpoints_daemonconfig_v0_daemon_config_proto_init() {
if File_extpoints_daemonconfig_v0_daemon_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeFor[x]().PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_extpoints_daemonconfig_v0_daemon_config_proto_rawDesc), len(file_extpoints_daemonconfig_v0_daemon_config_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_extpoints_daemonconfig_v0_daemon_config_proto_goTypes,
DependencyIndexes: file_extpoints_daemonconfig_v0_daemon_config_proto_depIdxs,
MessageInfos: file_extpoints_daemonconfig_v0_daemon_config_proto_msgTypes,
}.Build()
File_extpoints_daemonconfig_v0_daemon_config_proto = out.File
file_extpoints_daemonconfig_v0_daemon_config_proto_goTypes = nil
file_extpoints_daemonconfig_v0_daemon_config_proto_depIdxs = nil
}
@@ -0,0 +1,156 @@
// 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"
daemonconfigv0 "github.com/moby/moby/v2/extpoints/daemonconfig/v0"
grpc "google.golang.org/grpc"
)
// serviceName is the point's fully-qualified gRPC service name.
const serviceName = "org.mobyproject.extension.daemon.config.v0.DaemonConfig"
const (
methodGet = "/" + serviceName + "/Get"
)
// DaemonConfigServer 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 DaemonConfigServer interface {
Get(context.Context, *GetRequest) (*GetResponse, 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: (*DaemonConfigServer)(nil),
Metadata: "extpoints/daemonconfig/v0/daemon_config.proto",
Methods: []grpc.MethodDesc{
{MethodName: "Get", Handler: handleGet},
},
}
func handleGet(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(GetRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonConfigServer).Get(ctx, in)
}
info := &grpc.UnaryServerInfo{Server: srv, FullMethod: methodGet}
return interceptor(ctx, in, info, func(ctx context.Context, req any) (any, error) {
return srv.(DaemonConfigServer).Get(ctx, req.(*GetRequest))
})
}
// DaemonConfigClient 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 DaemonConfigClient interface {
Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error)
}
// NewDaemonConfigClient returns a client for the point's gRPC service on cc.
func NewDaemonConfigClient(cc grpc.ClientConnInterface) DaemonConfigClient {
return &serviceClient{cc: cc}
}
type serviceClient struct{ cc grpc.ClientConnInterface }
func (c *serviceClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) {
out := new(GetResponse)
if err := c.cc.Invoke(ctx, methodGet, in, out, append([]grpc.CallOption{grpc.StaticMethod()}, opts...)...); err != nil {
return nil, err
}
return out, nil
}
// ServerPoint serves the DaemonConfig 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: daemonconfigv0.Point.ID(),
Register: func(r grpc.ServiceRegistrar, impl any) {
r.RegisterService(&serviceDesc, &grpcServer{impl: impl.(daemonconfigv0.DaemonConfig)})
},
}
// ClientProvider builds a broker provider for the DaemonConfig point from an
// out-of-process gRPC connection.
func ClientProvider(conn grpc.ClientConnInterface) extensions.Provider {
return daemonconfigv0.Point.Provide(NewClient(conn))
}
// ClientPoint registers ClientProvider for the DaemonConfig point with a host.
// Single carries the contract's cardinality: the point admits one provider.
var ClientPoint = clientpoint.Registration{Point: daemonconfigv0.Point.ID(), Provider: ClientProvider, Single: true}
// NewClient returns a daemonconfigv0.DaemonConfig that calls the DaemonConfig point over conn.
func NewClient(conn grpc.ClientConnInterface) daemonconfigv0.DaemonConfig {
return &grpcClient{client: NewDaemonConfigClient(conn)}
}
// grpcServer serves an implementation of the contract's Go interface.
type grpcServer struct {
impl daemonconfigv0.DaemonConfig
}
func (s *grpcServer) Get(ctx context.Context, req *GetRequest) (*GetResponse, error) {
resp, err := s.impl.Get(ctx, getRequestFromProto(req))
if err != nil {
return nil, err
}
return getResponseToProto(resp), nil
}
type grpcClient struct {
client DaemonConfigClient
}
func (c *grpcClient) Get(ctx context.Context, req *daemonconfigv0.GetRequest) (*daemonconfigv0.GetResponse, error) {
resp, err := c.client.Get(ctx, getRequestToProto(req))
if err != nil {
return nil, err
}
return getResponseFromProto(resp), nil
}
func getRequestToProto(in *daemonconfigv0.GetRequest) *GetRequest {
if in == nil {
return nil
}
out := &GetRequest{}
return out
}
func getRequestFromProto(in *GetRequest) *daemonconfigv0.GetRequest {
if in == nil {
return nil
}
out := &daemonconfigv0.GetRequest{}
return out
}
func getResponseToProto(in *daemonconfigv0.GetResponse) *GetResponse {
if in == nil {
return nil
}
out := &GetResponse{}
out.RootDir = in.RootDir
return out
}
func getResponseFromProto(in *GetResponse) *daemonconfigv0.GetResponse {
if in == nil {
return nil
}
out := &daemonconfigv0.GetResponse{}
out.RootDir = in.GetRootDir()
return out
}