Merge pull request #54003 from vvoland/update-ext

vendor: github.com/moby/extensions d2819d88bcbe
This commit is contained in:
Sebastiaan van Stijn authored and GitHub committed 2026-10-22 11:41:14 +02:00
commit b711d21792
15 files changed
+964 -499

No files matched your search

+1 -1
View File
@@ -66,7 +66,7 @@ require (
github.com/mitchellh/copystructure v1.2.0
github.com/moby/buildkit v0.34.1
github.com/moby/docker-image-spec v1.3.1
github.com/moby/extensions v0.0.0-20260918151408-e4c23d866037
github.com/moby/extensions v0.0.0-20261020100352-d2819d88bcbe
github.com/moby/go-archive v0.3.3
github.com/moby/ipvs v1.1.0
github.com/moby/locker v1.0.1
+2 -2
View File
@@ -471,8 +471,8 @@ github.com/moby/buildkit v0.34.1 h1:jVdxsXX0UpU0q1Et+9lFezPmh9sGE6vF0TrKZaGlfy4=
github.com/moby/buildkit v0.34.1/go.mod h1:e+Za70y2a3nL1U2Y0P8TvhPyKp2Ok2NS+Q4siSArDiM=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/extensions v0.0.0-20260918151408-e4c23d866037 h1:i1M45d/grN16YYOstJ1aGWgo4QOnItgAAa/X4z2+Y9Q=
github.com/moby/extensions v0.0.0-20260918151408-e4c23d866037/go.mod h1:4craSfD3oQ+965ywyz5AzG/Eq3j0lflhEB+GTdRmyyU=
github.com/moby/extensions v0.0.0-20261020100352-d2819d88bcbe h1:wHRo58MA+vgX2sXtL4uT/NcSYPiyJarqHZ4aDS8P0ys=
github.com/moby/extensions v0.0.0-20261020100352-d2819d88bcbe/go.mod h1:5OTGReLaEAAQ0XoeIQzL6Ep+J3kwlFVTRSP86RwVoso=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/ipvs v1.1.0 h1:ONN4pGaZQgAx+1Scz5RvWV4Q7Gb+mvfRh3NsPS+1XQQ=
+112 -328
View File
@@ -1,378 +1,162 @@
# Extensions
> [!WARNING]
> ⚠️ This project is a work in progress.
> This project is still a work in progress.
Moby extensions are one model for adding engine behavior.
They replace the older per-feature plugin systems for network, volume, and log drivers.
Moby extensions provide a common model for extending engine behavior.
They are intended to replace the separate plugin systems currently used by network, volume, and logging drivers.
## Mental model
An extension can run inside the Host process or as a separate process.
In both cases, it implements the same Go interface.
- A **Point** is a versioned, namespaced Go interface and its message types.
- A daemon- or Host-owned capability is an ordinary Point too. Its in-process
implementation is registered through an extension `Declaration`, rather than
through a separate hook or plugin mechanism.
- An extension declares the Points it provides and the typed dependencies it
needs. A consumer declares `Point.Dependency()` and resolves the typed Go
interface during `Declaration.Init`.
- A Point contract is transport-neutral. The same interface and messages work
for a direct in-process call or for a launched process over gRPC.
- Placement is assembled with generated wiring: `ServerPoint` serves an
ordinary Point and `ClientPoint` presents the same typed interface over a
launched-provider or dependency-callback connection.
- The Host registers the fixed extension set, initializes it in dependency order,
and shuts it down in reverse dependency order. Importing a package does not
activate an extension.
See the [authoring guide](./docs/AUTHORING.md) for examples and setup instructions.
For details about the architecture, wire protocol, current limitations, and discovery security, see the [design guide](./docs/DESIGN.md).
## Principles
## How it works
- **Everything is a Point.**
Host capabilities, engine hooks, and extension-to-extension calls use
ordinary Point contracts and the same provider and resolution model.
- **Dependencies are typed and explicit.**
`Point.Dependency()` controls presence and initialization order;
`Point.Single`, `Point.All`, and `Point.ByExtension` perform typed resolution.
A dependency declaration does not inject a field automatically.
- **Contracts do not choose placement.**
Direct Go and gRPC are two ways to place the same Point. Generated
`ServerPoint` and `ClientPoint` wiring makes that choice without changing the
contract.
- **The Host owns lifecycle.**
Provider extensions must finish their `Init` work before returning. The Host
then initializes dependents and later shuts them down in reverse dependency
order.
- **No private activation paths.**
Consumers keep Point interfaces, not concrete Host or daemon pointers, and
the Host lifecycle replaces later manual `Activate` calls.
- **Publication is opt-in.**
An ordinary Point is externally reachable only when it is offered, Host
policy allows publication, and generated server wiring is supplied.
Published services enforce their own access control.
- **Fan-out order is not a contract.**
A Point that needs ordering defines its own phases or ordering rules.
- **Legacy plugins are replaced.**
Network, volume, and log drivers become ordinary extension Points rather than
separate plugin systems.
Extensions are built around Points.
A Point is an interface that describes a capability, such as running a workload.
Providers implement Points, and consumers call them.
The Host registers these components, connects consumers to providers, and manages their lifecycle.
## Authoring a host capability
For example, a daemon can expose its workload runtime as a Point.
An extension can then use that capability without depending on the daemon's internal runtime implementation.
The same mechanism is used for daemon-owned capabilities and communication between extensions.
A Host capability is an ordinary Point owned by the daemon or application. The
contract, daemon-owned provider, and consumer can live in separate packages.
Imports are omitted from these examples. `workloadv0` is the handwritten
contract package. After generation, import its generated `protogen` package as
`workloadpb`; it provides `workloadpb.ServerPoint` and
`workloadpb.ClientPoint`.
To add an extension:
The contract package defines one deciding Point:
1. **Define the contract.**
```go
// Package workloadv0 defines the daemon workload runtime Point.
package workloadv0
A Point assigns a namespaced, versioned identity to a Go interface and its message types.
type Runtime interface {
Run(context.Context, *Request) (*Response, error)
}
2. **Declare what the extension provides and depends on.**
type Request struct {
Name string `pb:"1"`
}
Providers implement Points.
Consumers declare their dependencies using `Point.Dependency()`.
type Response struct {
Message string `pb:"1"`
}
3. **Register the extension with the Host.**
var Point = extensions.DefineSinglePoint[Runtime](
"org.example.daemon.workload.v0",
)
```
The daemon or application decides which extensions to load and supplies any generated wiring required for cross-process calls.
Merely importing an extension package does not activate it.
Keep the contract expressible by the generated wire representation. Do not put
concrete Host types, internal backend structs, function callbacks, or channels
in a Point interface or its messages.
4. **Resolve dependencies during initialization.**
The daemon package owns the provider and registers it through the contract
Point:
In `Declaration.Init`, consumers resolve their declared dependencies using `Point.Single`, `Point.All`, or `Point.ByExtension`.
The resolved implementation is not automatically assigned to a field.
```go
type runtime struct{}
5. **Call the Point interface.**
func (runtime) Run(
_ context.Context,
req *workloadv0.Request,
) (*workloadv0.Response, error) {
return &workloadv0.Response{Message: "ran " + req.Name}, nil
}
Consumers retain and call the resolved interface rather than a concrete Host or daemon type.
Calls go directly to an in-process provider or through generated gRPC code when the provider runs in another process.
var runtimeExtension extensions.Extension = extensions.New(extensions.Declaration{
ID: "org.example.daemon-runtime.v1",
Providers: []extensions.Provider{
workloadv0.Point.Provide(runtime{}),
},
})
```
The Host initializes providers before their consumers and shuts them down in the opposite order.
A provider must be ready for use when its `Init` method returns.
The consumer package keeps the resolved interface on its stateful extension:
Keep the Host returned by `host.New` and call `Shutdown` when the application exits so that lifecycle resources are released.
```go
type consumer struct {
runtime workloadv0.Runtime
}
Breaking changes require a new Point version.
The `.v0` suffix identifies an experimental contract.
For fan-out calls, provider order is unspecified unless the Point defines its own ordering rules.
func (c *consumer) Declaration() extensions.Declaration {
return extensions.Declaration{
ID: "org.example.workload-consumer.v1",
Dependencies: []extensions.Dependency{
workloadv0.Point.Dependency(),
},
Init: func(
_ context.Context,
_ extensions.Config,
resolver extensions.Resolver,
) error {
runtime, err := workloadv0.Point.Single(resolver)
if err != nil {
return err
}
c.runtime = runtime
return nil
},
}
}
```
## Running extensions
In-process composition uses both extensions. Retain the returned Host and call
`Shutdown` so it owns and releases lifecycle resources:
A Point defines an interface, not where its implementation runs.
In-process consumers call the Go implementation directly.
Cross-process consumers use generated gRPC wiring.
```go
h, err := host.New(ctx, host.WithExtensions(runtimeExtension, &consumer{}))
if err != nil {
return err
}
defer h.Shutdown(context.Background())
```
Because the same contract may cross a process boundary, Point interfaces and messages should not contain Host types, internal backend structures, callbacks, or channels.
For a launched consumer, the Host registers the daemon-owned provider and
serves it on the dependency callback:
The `mobyextgen` generator creates the required wire code:
```go
h, err := host.New(ctx,
host.WithRuntimeDir(runtimeDir),
host.WithDirs(extensionDir),
host.WithExtensions(runtimeExtension),
host.WithDependencyProviders(workloadpb.ServerPoint),
)
if err != nil {
return err
}
defer h.Shutdown(context.Background())
```
* `ServerPoint` exposes a Point implementation over gRPC.
* `ClientPoint` presents the remote implementation through the same typed Go interface.
The consumer process has no ordinary Point of its own, so it registers without
a `ServerPoint`, installs the generated client wiring, and listens:
The exact setup depends on where the consumer and provider run:
```go
func run(ctx context.Context) error {
srv := sdk.NewServer()
if err := srv.Register(&consumer{}); err != nil {
return err
}
srv.Depends(workloadpb.ClientPoint)
return srv.Listen(ctx)
}
```
* Register in-process extensions with `host.WithExtensions`.
* When the Host calls a provider in a launched process, configure the Host with `host.WithClientProviders` and the generated `ClientPoint`.
The provider registers its generated `ServerPoint` with the SDK.
* When a launched consumer calls a Host provider, configure the Host with `host.WithDependencyProviders` and the generated `ServerPoint`.
In the consumer, pass the generated `ClientPoint` to `Server.Depends`.
`Declaration.Dependencies`—here populated by `workloadv0.Point.Dependency()`—is
the logical dependency. `WithDependencyProviders` does not declare a dependency
or create a provider; it exposes an already registered effective provider over
the callback socket. `Depends` does not declare a dependency or create a
provider either; it installs the generated client wiring used to resolve that
declared dependency. A process that also provides ordinary Points passes every
corresponding generated `ServerPoint` to `Register`.
The dependency callback exposes one effective provider for each Point. The SDK
closes its callback connection before the launched extension's `Shutdown`, so
do not use a retained callback dependency during shutdown.
The last case exposes an existing Host provider to the launched process.
The consumer must still declare the dependency itself.
### Initialization and readiness
A dependency callback currently exposes one effective provider for each Point.
Do not retain and call that dependency during `Shutdown`, because the SDK closes the connection before shutdown begins.
The extension providing a capability must establish the readiness it promises
before its `Init` returns. Because the consumer declares
`workloadv0.Point.Dependency()`, dependency ordering makes the provider's
`Init` finish before the consumer's `Init` begins. A successful `host.New`
means that all extension initializers returned successfully.
For launched processes, the `ready\n` handshake only confirms that the listener is ready.
It does not mean that the extension's `Init` method has completed.
For a launched process, the `ready\n` line means only process
listener/runtime-handshake readiness: the SDK has bound its extension socket
and registered its runtime service. It does not mean that the extension's
`Init` has completed. Do not retain concrete Host or daemon pointers as
capability dependencies, and do not manually call `Activate` after Host
startup; declare a Point dependency and perform setup in `Init` instead.
See the [authoring guide](./docs/AUTHORING.md) for complete examples covering code generation, SDK registration, dependency resolution, and deployment.
## Ordinary Point publication
## Publishing services
Publication uses the same ordinary Point contract; there is no separate service
contract. This example defines a `greeterv0` contract:
Implementing a Point makes it available within the extension system, but does not automatically expose it to external callers.
```go
// Package greeterv0 defines the published Greeter Point contract.
package greeterv0
Publishing a Point requires three things:
type Greeter interface {
Greet(context.Context, *HelloRequest) (*HelloReply, error)
}
* The extension offers it using `servicev0.Offer`.
* Host policy permits it through `host.WithProviderPolicy`.
* The generated server wiring is registered.
type HelloRequest struct {
Name string `pb:"1"`
}
For an in-process provider, supply the server wiring through `host.WithPointServers`.
A provider running in a separate process registers its `ServerPoint` with the SDK.
type HelloReply struct {
Message string `pb:"1"`
}
An offer is only a request to publish a Point; it does not grant access or register the transport.
If no provider policy is configured, external publication is disabled, while providers remain available to internal consumers.
var Point = extensions.DefinePoint[Greeter](
"org.mobyproject.extension.example.greeter.v0",
)
```
Published services are responsible for enforcing their own access control.
`mobyextgen` infers the fully qualified service name as
`<PointID>.<InterfaceName>`, here
`org.mobyproject.extension.example.greeter.v0.Greeter`. The interface name is
part of the wire identity, so keep it stable within a Point version. After
generation, import the generated `protogen` package as `greeterpb`. It provides
`ServerPoint` to serve the ordinary Point, `ClientPoint` to construct a typed
provider over a launched connection, and `NewClient(conn)` to construct the
handwritten Go interface over a Host connection.
The extension implements the ordinary Point and separately offers it:
```go
type greeter struct{}
func (greeter) Greet(
_ context.Context,
req *greeterv0.HelloRequest,
) (*greeterv0.HelloReply, error) {
return &greeterv0.HelloReply{Message: "hello " + req.Name}, nil
}
var extension = extensions.New(extensions.Declaration{
ID: "org.example.greeter.v1",
Providers: []extensions.Provider{
greeterv0.Point.Provide(greeter{}),
servicev0.Offer(greeterv0.Point),
},
})
```
An offer can select a subset of several implemented Points:
```go
Providers: []extensions.Provider{
foov1.Point.Provide(fooImpl{}),
barv1.Point.Provide(barImpl{}),
servicev0.Offer(foov1.Point),
}
```
`Offer` is eligibility, not authorization or transport registration. It does
not select gRPC or register a service. The service metadata Point carries the
offer; Host policy makes the publication decision for the service generated
from the ordinary Point.
The policy receives the Host-attested extension identity and requested Point.
For example, an in-process Host can allow this extension's ordinary Point and
its publication metadata Point:
```go
h, err := host.New(ctx,
host.WithExtensions(extension),
host.WithProviderPolicy(host.PointPolicyFunc(
func(
identity extensions.ExtensionIdentity,
point extensions.PointID,
) host.PointPolicyResult {
allowedPoint := point == greeterv0.Point.ID() ||
point == servicev0.Point.ID()
if identity.ID == "org.example.greeter.v1" && allowedPoint {
return host.Allow()
}
return host.Drop()
},
)),
host.WithPointServers(greeterpb.ServerPoint),
)
if err != nil {
return err
}
defer h.Shutdown(context.Background())
```
A nil policy preserves all internally wired providers but drops publication.
`host.Allow()` keeps the requested ordinary provider or publishes the offered
set. `host.Drop()` silently omits an ordinary provider without unloading its
extension or leaves the offered set private. `host.Reject(cause)` fails Host
construction while preserving its error cause. The zero value of
`host.PointPolicyResult` is invalid or unspecified and rejects the request. A
typed-nil `host.PointPolicyFunc(nil)` is still a policy value and rejects the
request as well. For an in-process offer, allow the ordinary Point ID as well
as `servicev0.Point.ID()` so provider admission and publication both succeed;
an offered-only process consults policy only with the service metadata Point
ID.
For a separate binary, pass every ordinary provider's generated `ServerPoint`
through the SDK:
```go
sdk.Main(extension, sdk.WithServerPoints(greeterpb.ServerPoint))
```
An in-process provider has no private SDK server, so the Host needs
`host.WithPointServers(greeterpb.ServerPoint)` to translate requests into calls
on the Go implementation. Use
`host.WithClientProviders(greeterpb.ClientPoint)` only when the Host itself
or another in-process consumer needs to call a launched provider. That is the
opposite direction from a dependency callback: `WithClientProviders` wires
Host-to-process calls, while `WithDependencyProviders` and `Depends` wire a
launched consumer's calls back to an already registered Host provider.
External callers use the generated handwritten client over the Host connection:
```go
client := greeterpb.NewClient(hostConn)
reply, err := client.Greet(ctx, &greeterv0.HelloRequest{Name: "world"})
```
The current backend is gRPC. Generated service registration records the fully
qualified service name, and the proxy forwards the raw gRPC stream, including
metadata and status. Arbitrary raw gRPC publication is not part of the
extension API.
See the [publication examples](./docs/AUTHORING.md#publishing-an-ordinary-point) for complete configurations and generated client usage.
## Glossary
| Term | Definition |
|---|---|
| **Point** | A versioned, namespaced, transport-neutral Go interface and message contract. It may be provided by an in-process or separate-process extension, including a Host capability wrapper; a breaking change gets a new Point version, and `.v0` is experimental. |
| **Host capability** | A capability owned by a daemon or application and exposed as an ordinary Point through an in-process extension declaration. |
| **Host** | The framework component embedded in or configured by a daemon/application. It registers extensions, resolves Points, assembles generated wiring, and manages lifecycle; it is not synonymous with the daemon/application. |
| **Extension** | A deployable unit that runs in-process with the Host or in a separate process, and declares providers, dependencies, conflicts, and lifecycle callbacks. |
| **Extension identity** | The logical extension ID plus the Host-attested origin (`builtin` or `executable`). The extension declares the logical ID; the Host validates it and supplies the origin. |
| **Provider** | An implementation of a Point declared by an extension. It has no separate ID, is associated with the declaring extension's Host-attested identity, and each extension may declare at most one implementation per Point. |
| **Consumer / dependent** | A daemon/application flow or extension that resolves and calls a Point. An extension dependent declares the Point or extension dependency that controls initialization order. |
| **Dependency** | A declared need for a Point provider or extension, resolved before the dependent initializes. |
| **ClientPoint** | Generated wiring for an ordinary Point. On the Host side, it turns a launched provider connection into the typed interface used by Host consumers; on the process side, it turns the dependency-callback connection into the typed interface used by a launched consumer. |
| **ServerPoint** | Generated registration that serves an ordinary Point from an SDK server, a dependency callback, or an in-process publication adapter. |
| **Offer** | Extension metadata marking selected ordinary Points as eligible for external publication; Host policy still makes the publication decision. |
| **Publication** | Host-controlled exposure of an offered Point's generated service to external callers. |
| **Dependency callback** | The Host gRPC server through which a launched consumer calls an already registered Host provider. |
| **Broker** | The Host component that registers extensions, resolves dependencies, initializes them, and shuts them down. |
| **Adapter** | Generated code that makes an out-of-process gRPC provider look like the same in-process Go interface to consumers. |
| **Engine** | The daemon or application that embeds or configures a Host and runs the flows that consume and provide Points. |
| **Legacy plugin** | The older Moby plugin system being replaced, or containerd plugins when discussed as prior art. The new unit is an extension. |
### Point
## Start here
A named and versioned Go interface, together with its message types.
It describes a capability independently of its implementation or location.
- The mental model, principles, authoring flow, and glossary provide the short normative rules and definitions.
- [DESIGN.md](./docs/DESIGN.md) - current behavior, constraints, wire protocol, and discovery security.
- [AUTHORING.md](./docs/AUTHORING.md) - procedures, commands, code, and checklists.
### Extension
A component that can provide Points, depend on other Points or extensions, and perform initialization and shutdown work.
It may run inside the Host or in a separate process.
### Host
The framework component embedded in a daemon or application.
It registers extensions, resolves their dependencies, and manages their lifecycle.
### Provider
An extension that implements a Point.
### Consumer
Code that resolves and calls a Point.
An extension may be both a consumer and a provider.
### Dependency
A declared requirement for a Point provider or another extension.
The Host uses dependencies to verify that requirements are available and to determine initialization order.
### Host capability
Functionality owned by the daemon or application and exposed as a Point through an in-process extension.
### ClientPoint / ServerPoint
Generated code used to call a Point across a process boundary.
`ClientPoint` presents the Point's Go interface to the consumer, while `ServerPoint` forwards incoming calls to the implementation.
### Offer
A request from an extension to make one or more of its Points available to external callers.
### Publication
The Host exposing an offered Point to external callers after applying its policy and registering the required server wiring.
## License
+9 -2
View File
@@ -80,11 +80,18 @@ func fileDescriptor(pt point) (*descriptorpb.FileDescriptorProto, error) {
svc := &descriptorpb.ServiceDescriptorProto{Name: new(pt.service)}
for _, m := range pt.methods {
svc.Method = append(svc.Method, &descriptorpb.MethodDescriptorProto{
method := &descriptorpb.MethodDescriptorProto{
Name: new(m.name),
InputType: new("." + pt.id + "." + m.request),
OutputType: new("." + pt.id + "." + m.response),
})
}
if m.kind == methodClientStreaming {
method.ClientStreaming = new(true)
}
if m.kind == methodServerStreaming {
method.ServerStreaming = new(true)
}
svc.Method = append(svc.Method, method)
}
fd.Service = []*descriptorpb.ServiceDescriptorProto{svc}
return fd, nil
+12 -2
View File
@@ -6,7 +6,6 @@ package main
// model
type point struct {
pkgName string // Go package name
importPath string // package import path
protoPath string // module-relative path of the .proto
licenseHeader string // SPDX notices from the contract interface's source file
@@ -15,6 +14,7 @@ type point struct {
iface string // Go service interface name
isPoint bool // whether the contract declares an extensions.Point
isSingle bool // whether the point was declared with DefineSinglePoint
doc string // documentation on the provider interface
methods []method
messages []message
}
@@ -25,12 +25,22 @@ func (p point) grpcService() string { return p.id + "." + p.service }
func (p point) protogenImport() string { return p.importPath + "/protogen" }
type method struct {
kind methodKind
name string // method/rpc name
request string // request message name
response string // response message name
bareError bool // true for `M(ctx, *Req) error`; response is a generated empty message
bareError bool // true only for legacy unary `M(ctx, *Req) error`
doc string // method documentation
}
type methodKind int
const (
methodUnary methodKind = iota
methodServerStreaming
methodClientStreaming
)
type message struct {
name string
fields []field
+324 -31
View File
@@ -10,6 +10,7 @@ import (
"go/parser"
"go/token"
"os"
"path"
"reflect"
"sort"
"strconv"
@@ -20,6 +21,8 @@ import (
// parsing
const streamImportPath = "github.com/moby/extensions/stream"
func parsePoint(dir string) (point, error) {
return parseContract(dir, "")
}
@@ -38,21 +41,21 @@ func parseContract(dir, serviceIdentity string) (point, error) {
}
var files []*ast.File
var pkgName string
foundPackage := false
for name, pkg := range pkgs {
if strings.HasSuffix(name, "_test") {
continue
}
pkgName = name
foundPackage = true
for _, f := range pkg.Files {
files = append(files, f)
}
}
if pkgName == "" {
if !foundPackage {
return point{}, fmt.Errorf("no package found in %s", dir)
}
pt := point{pkgName: pkgName}
var pt point
if serviceIdentity != "" {
pkg, service, ok := splitServiceIdentity(serviceIdentity)
@@ -77,6 +80,7 @@ func parseContract(dir, serviceIdentity string) (point, error) {
if ifaceType == nil {
return point{}, fmt.Errorf("interface %q not found", pt.iface)
}
pt.doc = interfaceComment(files, pt.iface)
// Inherit the contract's copyright and license rather than imposing the
// generator's notices on output produced for other projects.
for _, f := range files {
@@ -98,7 +102,7 @@ func parseContract(dir, serviceIdentity string) (point, error) {
if pt.licenseHeader != "" {
pt.licenseHeader += "\n"
}
pt.methods, err = parseMethods(ifaceType)
pt.methods, err = parseMethods(files, ifaceType)
if err != nil {
return point{}, err
}
@@ -220,7 +224,34 @@ func structHasPBTag(st *ast.StructType) bool {
return false
}
func parseMethods(iface *ast.InterfaceType) ([]method, error) {
func interfaceComment(files []*ast.File, name string) string {
for _, file := range files {
for _, decl := range file.Decls {
group, ok := decl.(*ast.GenDecl)
if !ok || group.Tok != token.TYPE {
continue
}
for _, spec := range group.Specs {
typeSpec := spec.(*ast.TypeSpec)
if typeSpec.Name.Name != name {
continue
}
doc := typeSpec.Doc
// Like go/doc, a group comment documents only a single-type group.
if doc == nil && len(group.Specs) == 1 {
doc = group.Doc
}
if doc != nil {
return strings.TrimSpace(doc.Text())
}
return ""
}
}
}
return ""
}
func parseMethods(files []*ast.File, iface *ast.InterfaceType) ([]method, error) {
var methods []method
for _, m := range iface.Methods.List {
ft, ok := m.Type.(*ast.FuncType)
@@ -228,31 +259,287 @@ func parseMethods(iface *ast.InterfaceType) ([]method, error) {
continue
}
name := m.Names[0].Name
if ft.Params == nil || len(ft.Params.List) == 0 {
return nil, fmt.Errorf("method %q: expected a request parameter", name)
}
reqType := ft.Params.List[len(ft.Params.List)-1].Type
req, err := pointerIdent(reqType)
if err != nil {
return nil, fmt.Errorf("method %q request: %w", name, err)
paramFields := fieldsPerName(ft.Params)
params := make([]methodParameter, len(paramFields))
for i, param := range paramFields {
var err error
params[i], err = classifyMethodParameter(files, param.Type, i)
if err != nil {
return nil, fmt.Errorf("method %q parameter %d: %w", name, i+1, err)
}
}
res := results(ft)
switch {
case len(res) == 1 && isIdent(res[0], "error"):
methods = append(methods, method{name: name, request: req, response: name + "Response", bareError: true})
case len(res) == 2 && isIdent(res[1], "error"):
resp, err := pointerIdent(res[0])
if err != nil {
return nil, fmt.Errorf("method %q response: %w", name, err)
for _, result := range res {
if _, isStream, _ := parseStreamParameter(files, result); isStream {
return nil, methodShapeError(name)
}
methods = append(methods, method{name: name, request: req, response: resp})
}
doc := fieldComment(m)
switch {
case len(params) == 2 && params[0].kind == methodContextParameter && params[1].kind == methodMessageParameter:
var (
response string
bareError bool
)
switch {
case len(res) == 1 && isIdent(res[0], "error"):
response, bareError = name+"Response", true
case len(res) == 2 && isIdent(res[1], "error"):
var err error
response, err = pointerIdent(res[0])
if err != nil {
return nil, methodShapeError(name)
}
default:
return nil, methodShapeError(name)
}
methods = append(methods, method{
name: name, request: params[1].message, response: response,
bareError: bareError, doc: doc,
})
case len(params) == 3 && params[0].kind == methodContextParameter &&
params[1].kind == methodMessageParameter && params[2].kind == methodSenderParameter:
if len(res) != 1 || !isIdent(res[0], "error") {
return nil, methodShapeError(name)
}
methods = append(methods, method{
kind: methodServerStreaming, name: name,
request: params[1].message, response: params[2].message, doc: doc,
})
case len(params) == 2 && params[0].kind == methodContextParameter && params[1].kind == methodReceiverParameter:
if len(res) != 2 || !isIdent(res[1], "error") {
return nil, methodShapeError(name)
}
response, err := pointerIdent(res[0])
if err != nil {
return nil, methodShapeError(name)
}
methods = append(methods, method{
kind: methodClientStreaming, name: name,
request: params[1].message, response: response, doc: doc,
})
default:
return nil, fmt.Errorf("method %q: result must be `error` or `(*Resp, error)`", name)
return nil, methodShapeError(name)
}
}
return methods, nil
}
type methodParameterKind int
const (
methodContextParameter methodParameterKind = iota + 1
methodMessageParameter
methodSenderParameter
methodReceiverParameter
)
type methodParameter struct {
kind methodParameterKind
message string
}
const acceptedMethodShapes = "accepted shapes are M(context.Context, *Request) (*Reply, error), " +
"M(context.Context, *Request) error, " +
"M(context.Context, *Request, stream.Sender[Chunk]) error, and " +
"M(context.Context, stream.Receiver[Chunk]) (*Reply, error)"
func methodShapeError(name string) error {
return fmt.Errorf("method %q: unsupported signature; %s", name, acceptedMethodShapes)
}
func classifyMethodParameter(files []*ast.File, expr ast.Expr, index int) (methodParameter, error) {
param, isStream, err := parseStreamParameter(files, expr)
switch {
case err != nil:
return methodParameter{}, err
case isStream && index == 0:
return methodParameter{}, errors.New("stream parameters cannot replace context.Context")
case isStream:
return param, nil
case index == 0 && !isContextType(files, expr):
return methodParameter{}, errors.New("must be context.Context")
case index == 0:
return methodParameter{kind: methodContextParameter}, nil
}
message, err := pointerIdent(expr)
if err != nil {
return methodParameter{}, err
}
return methodParameter{kind: methodMessageParameter, message: message}, nil
}
// parseStreamParameter reports whether expr names stream.Sender or
// stream.Receiver and, if so, validates its import and message argument.
func parseStreamParameter(files []*ast.File, expr ast.Expr) (methodParameter, bool, error) {
var (
selector *ast.SelectorExpr
args []ast.Expr
)
switch expr := expr.(type) {
case *ast.SelectorExpr:
selector = expr
case *ast.IndexExpr:
selector, _ = expr.X.(*ast.SelectorExpr)
args = []ast.Expr{expr.Index}
case *ast.IndexListExpr:
selector, _ = expr.X.(*ast.SelectorExpr)
args = expr.Indices
default:
return methodParameter{}, false, nil
}
if selector == nil || (selector.Sel.Name != "Sender" && selector.Sel.Name != "Receiver") {
return methodParameter{}, false, nil
}
use := "stream." + selector.Sel.Name
file := fileForExpr(files, expr)
if file == nil {
return methodParameter{}, true, fmt.Errorf("%s must be imported from %q", use, streamImportPath)
}
if err := resolveStreamImport(file, selector.X, use); err != nil {
return methodParameter{}, true, err
}
if len(args) != 1 {
return methodParameter{}, true, fmt.Errorf("%s must have exactly one local message type argument", use)
}
message, err := streamMessageArgument(args[0])
if err != nil {
return methodParameter{}, true, fmt.Errorf("%s: %w", use, err)
}
if err := validateStreamMessage(files, message); err != nil {
return methodParameter{}, true, fmt.Errorf("%s message %q: %w", use, message, err)
}
kind := methodSenderParameter
if selector.Sel.Name == "Receiver" {
kind = methodReceiverParameter
}
return methodParameter{kind: kind, message: message}, true, nil
}
func fileForExpr(files []*ast.File, expr ast.Expr) *ast.File {
for _, file := range files {
if file.Pos() <= expr.Pos() && expr.End() <= file.End() {
return file
}
}
return nil
}
func resolveStreamImport(file *ast.File, packageExpr ast.Expr, use string) error {
ident, ok := packageExpr.(*ast.Ident)
if !ok {
return fmt.Errorf("%s must use a package imported from %q", use, streamImportPath)
}
importPath, ok := importedPath(file, ident.Name)
if !ok {
return fmt.Errorf("%s package name %q is not imported from %q", use, ident.Name, streamImportPath)
}
if importPath != streamImportPath {
return fmt.Errorf("%s must be imported from %q", use, streamImportPath)
}
return nil
}
// importedPath returns the path of the package that file imports as name.
func importedPath(file *ast.File, name string) (string, bool) {
for _, spec := range file.Imports {
importPath, err := strconv.Unquote(spec.Path.Value)
if err != nil {
continue
}
local := path.Base(importPath)
if spec.Name != nil {
local = spec.Name.Name
}
if local == name {
return importPath, true
}
}
return "", false
}
// isContextType reports whether expr names context.Context under any import
// name.
func isContextType(files []*ast.File, expr ast.Expr) bool {
selector, ok := expr.(*ast.SelectorExpr)
if !ok || selector.Sel.Name != "Context" {
return false
}
ident, ok := selector.X.(*ast.Ident)
if !ok {
return false
}
file := fileForExpr(files, expr)
if file == nil {
return false
}
importPath, ok := importedPath(file, ident.Name)
return ok && importPath == "context"
}
func streamMessageArgument(expr ast.Expr) (string, error) {
if ident, ok := expr.(*ast.Ident); ok {
return ident.Name, nil
}
switch expr.(type) {
case *ast.StarExpr:
return "", errors.New("message type argument must not be a pointer")
case *ast.SelectorExpr:
return "", errors.New("imported message types are unsupported; use a local struct message")
case *ast.IndexExpr, *ast.IndexListExpr:
return "", errors.New("message type argument must be a non-generic local named type")
default:
return "", errors.New("message type argument must be a local named type")
}
}
func validateStreamMessage(files []*ast.File, name string) error {
var found *ast.TypeSpec
for _, file := range files {
for _, decl := range file.Decls {
group, ok := decl.(*ast.GenDecl)
if !ok || group.Tok != token.TYPE {
continue
}
for _, spec := range group.Specs {
typeSpec := spec.(*ast.TypeSpec)
if typeSpec.Name.Name != name {
continue
}
if found != nil {
return errors.New("is ambiguous; declare one local struct message")
}
found = typeSpec
}
}
}
if found == nil {
return errors.New("is unresolved")
}
if found.Assign.IsValid() {
return errors.New("is an alias")
}
if found.TypeParams != nil && len(found.TypeParams.List) != 0 {
return errors.New("must not be generic")
}
if _, ok := found.Type.(*ast.StructType); !ok {
return errors.New("must be a local struct declaration")
}
return nil
}
func fieldComment(field *ast.Field) string {
group := field.Doc
if group == nil {
group = field.Comment
}
if group == nil {
return ""
}
return strings.TrimSpace(group.Text())
}
func parseMessages(files []*ast.File, msgNames map[string]bool) ([]message, error) {
var messages []message
for _, f := range files {
@@ -474,17 +761,23 @@ func isIdent(expr ast.Expr, name string) bool {
}
func results(ft *ast.FuncType) []ast.Expr {
if ft.Results == nil {
var out []ast.Expr
for _, f := range fieldsPerName(ft.Results) {
out = append(out, f.Type)
}
return out
}
// fieldsPerName repeats each field once per declared name, so a grouped
// declaration such as `a, b *T` counts as two entries.
func fieldsPerName(list *ast.FieldList) []*ast.Field {
if list == nil {
return nil
}
var out []ast.Expr
for _, f := range ft.Results.List {
if len(f.Names) == 0 {
out = append(out, f.Type)
continue
}
for range f.Names {
out = append(out, f.Type)
var out []*ast.Field
for _, f := range list.List {
for range max(1, len(f.Names)) {
out = append(out, f)
}
}
return out
+13 -1
View File
@@ -21,9 +21,21 @@ func emitProto(pt point) ([]byte, error) {
fmt.Fprintf(&b, "package %s;\n\n", pt.id)
fmt.Fprintf(&b, "option go_package = %q;\n\n", pt.protogenImport())
emitComment(&b, "", pt.doc)
fmt.Fprintf(&b, "service %s {\n", pt.service)
for _, m := range pt.methods {
fmt.Fprintf(&b, " rpc %s(%s) returns (%s);\n", m.name, m.request, m.response)
emitComment(&b, " ", m.doc)
request, response := m.request, m.response
switch m.kind {
case methodUnary:
case methodServerStreaming:
response = "stream " + response
case methodClientStreaming:
request = "stream " + request
default:
return nil, fmt.Errorf("%s: unknown method kind %d", m.name, m.kind)
}
fmt.Fprintf(&b, " rpc %s(%s) returns (%s);\n", m.name, request, response)
}
fmt.Fprintln(&b, "}")
+241 -15
View File
@@ -14,19 +14,35 @@ import (
func emitWire(pt point) ([]byte, error) {
var b strings.Builder
cpkg := pt.pkgName
const cpkg = "mobyextcontract"
svc, iface := pt.service, pt.iface
streamIndices := make(map[string]int)
for _, m := range pt.methods {
if m.kind != methodUnary {
streamIndices[m.name] = len(streamIndices)
}
}
hasStreaming := len(streamIndices) > 0
b.WriteString(pt.licenseHeader)
fmt.Fprintln(&b, "// Code generated by mobyextgen. DO NOT EDIT.")
fmt.Fprintf(&b, "package %s\n\n", path.Base(pt.protogenImport()))
fmt.Fprintln(&b, "import (")
fmt.Fprintln(&b, ` context "context"`)
if hasStreaming {
fmt.Fprintln(&b, ` errors "errors"`)
fmt.Fprintln(&b, ` io "io"`)
}
if pt.isPoint {
fmt.Fprintln(&b, ` extensions "github.com/moby/extensions"`)
fmt.Fprintln(&b, ` clientpoint "github.com/moby/extensions/clientpoint"`)
fmt.Fprintln(&b, ` serverpoint "github.com/moby/extensions/serverpoint"`)
}
fmt.Fprintf(&b, "\t%s %q\n", cpkg, pt.importPath)
if hasStreaming {
fmt.Fprintln(&b, ` extensionstream "github.com/moby/extensions/stream"`)
fmt.Fprintln(&b, ` codes "google.golang.org/grpc/codes"`)
fmt.Fprintln(&b, ` status "google.golang.org/grpc/status"`)
}
fmt.Fprintln(&b, ` grpc "google.golang.org/grpc"`)
fmt.Fprintln(&b, ")")
@@ -59,7 +75,15 @@ func emitWire(pt point) ([]byte, error) {
`, svc)
}
for _, m := range pt.methods {
fmt.Fprintf(&b, "\t%s(context.Context, *%s) (*%s, error)\n", m.name, m.request, m.response)
emitComment(&b, "\t", m.doc)
switch m.kind {
case methodUnary:
fmt.Fprintf(&b, "\t%s(context.Context, *%s) (*%s, error)\n", m.name, m.request, m.response)
case methodServerStreaming:
fmt.Fprintf(&b, "\t%s(*%s, grpc.ServerStreamingServer[%s]) error\n", m.name, m.request, m.response)
case methodClientStreaming:
fmt.Fprintf(&b, "\t%s(grpc.ClientStreamingServer[%s, %s]) error\n", m.name, m.request, m.response)
}
}
fmt.Fprintln(&b, "}")
@@ -78,12 +102,33 @@ func emitWire(pt point) ([]byte, error) {
Methods: []grpc.MethodDesc{
`, svc, pt.protoPath)
for _, m := range pt.methods {
fmt.Fprintf(&b, "\t\t{MethodName: %q, Handler: %s},\n", m.name, handlerName(m))
if m.kind == methodUnary {
fmt.Fprintf(&b, "\t\t{MethodName: %q, Handler: %s},\n", m.name, handlerName(m))
}
}
fmt.Fprintln(&b, "\t},\n}")
fmt.Fprintln(&b, "\t},")
if hasStreaming {
fmt.Fprintln(&b, "\t\tStreams: []grpc.StreamDesc{")
for _, m := range pt.methods {
if m.kind == methodUnary {
continue
}
fmt.Fprintf(&b, "\t\t\t{StreamName: %q, Handler: %s", m.name, handlerName(m))
if m.kind == methodServerStreaming {
fmt.Fprint(&b, ", ServerStreams: true")
} else {
fmt.Fprint(&b, ", ClientStreams: true")
}
fmt.Fprintln(&b, "},")
}
fmt.Fprintln(&b, "\t\t},")
}
fmt.Fprintln(&b, "}")
for _, m := range pt.methods {
fmt.Fprintf(&b, `
switch m.kind {
case methodUnary:
fmt.Fprintf(&b, `
func %[1]s(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) {
in := new(%[2]s)
if err := dec(in); err != nil {
@@ -98,6 +143,23 @@ func emitWire(pt point) ([]byte, error) {
})
}
`, handlerName(m), m.request, svc, m.name, methodConst(m))
case methodServerStreaming:
fmt.Fprintf(&b, `
func %[1]s(srv any, stream grpc.ServerStream) error {
in := new(%[2]s)
if err := stream.RecvMsg(in); err != nil {
return err
}
return srv.(%[3]sServer).%[4]s(in, &grpc.GenericServerStream[%[2]s, %[5]s]{ServerStream: stream})
}
`, handlerName(m), m.request, svc, m.name, m.response)
case methodClientStreaming:
fmt.Fprintf(&b, `
func %[1]s(srv any, stream grpc.ServerStream) error {
return srv.(%[2]sServer).%[3]s(&grpc.GenericServerStream[%[4]s, %[5]s]{ServerStream: stream})
}
`, handlerName(m), svc, m.name, m.request, m.response)
}
}
if pt.isPoint {
@@ -115,7 +177,15 @@ func emitWire(pt point) ([]byte, error) {
`, svc)
}
for _, m := range pt.methods {
fmt.Fprintf(&b, "\t%s(ctx context.Context, in *%s, opts ...grpc.CallOption) (*%s, error)\n", m.name, m.request, m.response)
emitComment(&b, "\t", m.doc)
switch m.kind {
case methodUnary:
fmt.Fprintf(&b, "\t%s(ctx context.Context, in *%s, opts ...grpc.CallOption) (*%s, error)\n", m.name, m.request, m.response)
case methodServerStreaming:
fmt.Fprintf(&b, "\t%s(ctx context.Context, in *%s, opts ...grpc.CallOption) (grpc.ServerStreamingClient[%s], error)\n", m.name, m.request, m.response)
case methodClientStreaming:
fmt.Fprintf(&b, "\t%s(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[%s, %s], error)\n", m.name, m.request, m.response)
}
}
fmt.Fprintln(&b, "}")
@@ -130,7 +200,9 @@ func emitWire(pt point) ([]byte, error) {
`, svc)
for _, m := range pt.methods {
fmt.Fprintf(&b, `
switch m.kind {
case methodUnary:
fmt.Fprintf(&b, `
func (c *serviceClient) %[1]s(ctx context.Context, in *%[2]s, opts ...grpc.CallOption) (*%[3]s, error) {
out := new(%[3]s)
if err := c.cc.Invoke(ctx, %[4]s, in, out, append([]grpc.CallOption{grpc.StaticMethod()}, opts...)...); err != nil {
@@ -139,6 +211,34 @@ func emitWire(pt point) ([]byte, error) {
return out, nil
}
`, m.name, m.request, m.response, methodConst(m))
case methodServerStreaming:
fmt.Fprintf(&b, `
func (c *serviceClient) %[1]s(ctx context.Context, in *%[2]s, opts ...grpc.CallOption) (grpc.ServerStreamingClient[%[3]s], error) {
stream, err := c.cc.NewStream(ctx, &serviceDesc.Streams[%[4]d], %[5]s, append([]grpc.CallOption{grpc.StaticMethod()}, opts...)...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[%[2]s, %[3]s]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
`, m.name, m.request, m.response, streamIndices[m.name], methodConst(m))
case methodClientStreaming:
fmt.Fprintf(&b, `
func (c *serviceClient) %[1]s(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[%[2]s, %[3]s], error) {
stream, err := c.cc.NewStream(ctx, &serviceDesc.Streams[%[4]d], %[5]s, append([]grpc.CallOption{grpc.StaticMethod()}, opts...)...)
if err != nil {
return nil, err
}
return &grpc.GenericClientStream[%[2]s, %[3]s]{ClientStream: stream}, nil
}
`, m.name, m.request, m.response, streamIndices[m.name], methodConst(m))
}
}
if pt.isPoint {
@@ -187,8 +287,10 @@ func emitWire(pt point) ([]byte, error) {
`, cpkg, iface)
for _, m := range pt.methods {
if m.bareError {
fmt.Fprintf(&b, `
switch m.kind {
case methodUnary:
if m.bareError {
fmt.Fprintf(&b, `
func (s *grpcServer) %[1]s(ctx context.Context, req *%[2]s) (*%[3]s, error) {
if err := s.impl.%[1]s(ctx, %[4]sFromProto(req)); err != nil {
return nil, err
@@ -196,8 +298,8 @@ func emitWire(pt point) ([]byte, error) {
return &%[3]s{}, nil
}
`, m.name, m.request, m.response, lowerFirst(m.request))
} else {
fmt.Fprintf(&b, `
} else {
fmt.Fprintf(&b, `
func (s *grpcServer) %[1]s(ctx context.Context, req *%[2]s) (*%[3]s, error) {
resp, err := s.impl.%[1]s(ctx, %[4]sFromProto(req))
if err != nil {
@@ -206,6 +308,48 @@ func emitWire(pt point) ([]byte, error) {
return %[5]sToProto(resp), nil
}
`, m.name, m.request, m.response, lowerFirst(m.request), lowerFirst(m.response))
}
case methodServerStreaming:
fmt.Fprintf(&b, `
func (s *grpcServer) %[1]s(req *%[2]s, out grpc.ServerStreamingServer[%[3]s]) error {
sender := &mobyext%[1]sSender{stream: out}
return s.impl.%[1]s(out.Context(), %[4]sFromProto(req), sender)
}
type mobyext%[1]sSender struct {
stream grpc.ServerStreamingServer[%[3]s]
}
func (s *mobyext%[1]sSender) Send(message *%[5]s.%[6]s) error {
if message == nil {
return status.Error(codes.Internal, %[7]q)
}
return s.stream.Send(%[8]sToProto(message))
}
`, m.name, m.request, m.response, lowerFirst(m.request), cpkg, m.response, m.name+": nil message", lowerFirst(m.response))
case methodClientStreaming:
fmt.Fprintf(&b, `
func (s *grpcServer) %[1]s(stream grpc.ClientStreamingServer[%[2]s, %[3]s]) error {
receiver := &mobyext%[1]sReceiver{stream: stream}
resp, err := s.impl.%[1]s(stream.Context(), receiver)
if err != nil {
return err
}
return stream.SendAndClose(%[4]sToProto(resp))
}
type mobyext%[1]sReceiver struct {
stream grpc.ClientStreamingServer[%[2]s, %[3]s]
}
func (r *mobyext%[1]sReceiver) Recv() (*%[5]s.%[6]s, error) {
message, err := r.stream.Recv()
if err != nil {
return nil, err
}
return %[7]sFromProto(message), nil
}
`, m.name, m.request, m.response, lowerFirst(m.response), cpkg, m.request, lowerFirst(m.request))
}
}
@@ -215,15 +359,17 @@ func emitWire(pt point) ([]byte, error) {
}
`, svc)
for _, m := range pt.methods {
if m.bareError {
fmt.Fprintf(&b, `
switch m.kind {
case methodUnary:
if m.bareError {
fmt.Fprintf(&b, `
func (c *grpcClient) %[1]s(ctx context.Context, req *%[2]s.%[3]s) error {
_, err := c.client.%[1]s(ctx, %[4]sToProto(req))
return err
}
`, m.name, cpkg, m.request, lowerFirst(m.request))
} else {
fmt.Fprintf(&b, `
} else {
fmt.Fprintf(&b, `
func (c *grpcClient) %[1]s(ctx context.Context, req *%[2]s.%[3]s) (*%[2]s.%[5]s, error) {
resp, err := c.client.%[1]s(ctx, %[4]sToProto(req))
if err != nil {
@@ -232,6 +378,73 @@ func emitWire(pt point) ([]byte, error) {
return %[6]sFromProto(resp), nil
}
`, m.name, cpkg, m.request, lowerFirst(m.request), m.response, lowerFirst(m.response))
}
case methodServerStreaming:
fmt.Fprintf(&b, `
func (c *grpcClient) %[1]s(ctx context.Context, req *%[2]s.%[3]s, sink extensionstream.Sender[%[2]s.%[4]s]) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
stream, err := c.client.%[1]s(ctx, %[5]sToProto(req))
if err != nil {
return err
}
for {
if err := ctx.Err(); err != nil {
return status.FromContextError(err).Err()
}
message, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
if err := sink.Send(%[6]sFromProto(message)); err != nil {
return err
}
}
}
`, m.name, cpkg, m.request, m.response, lowerFirst(m.request), lowerFirst(m.response))
case methodClientStreaming:
fmt.Fprintf(&b, `
func (c *grpcClient) %[1]s(ctx context.Context, source extensionstream.Receiver[%[2]s.%[3]s]) (*%[2]s.%[4]s, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
stream, err := c.client.%[1]s(ctx)
if err != nil {
return nil, err
}
for {
message, err := source.Recv()
if err != nil {
if !errors.Is(err, io.EOF) {
return nil, err
}
resp, err := stream.CloseAndRecv()
if err != nil {
return nil, err
}
return %[5]sFromProto(resp), nil
}
if message == nil {
return nil, status.Error(codes.InvalidArgument, %[6]q)
}
if err := ctx.Err(); err != nil {
return nil, status.FromContextError(err).Err()
}
if err := stream.Send(%[7]sToProto(message)); err != nil {
if !errors.Is(err, io.EOF) {
return nil, err
}
resp, err := stream.CloseAndRecv()
if err != nil {
return nil, err
}
return %[5]sFromProto(resp), nil
}
}
}
`, m.name, cpkg, m.request, m.response, lowerFirst(m.response), m.name+": nil message", lowerFirst(m.request))
}
}
@@ -242,6 +455,19 @@ func emitWire(pt point) ([]byte, error) {
return format.Source([]byte(b.String()))
}
func emitComment(b *strings.Builder, indent, doc string) {
if doc == "" {
return
}
for line := range strings.SplitSeq(doc, "\n") {
if line == "" {
fmt.Fprintf(b, "%s//\n", indent)
continue
}
fmt.Fprintf(b, "%s// %s\n", indent, line)
}
}
func emitConversions(b *strings.Builder, cpkg string, msg message) {
conv := lowerFirst(msg.name)
+1
View File
@@ -9,6 +9,7 @@ package org.mobyproject.extension.example.greeter.v0;
option go_package = "github.com/moby/extensions/example/greeter/v0/protogen";
// Greeter is the example service.
service Greeter {
rpc Greet(HelloRequest) returns (HelloReply);
}
+15 -15
View File
@@ -8,7 +8,7 @@ import (
context "context"
extensions "github.com/moby/extensions"
clientpoint "github.com/moby/extensions/clientpoint"
greeterv0 "github.com/moby/extensions/example/greeter/v0"
mobyextcontract "github.com/moby/extensions/example/greeter/v0"
serverpoint "github.com/moby/extensions/serverpoint"
grpc "google.golang.org/grpc"
)
@@ -76,29 +76,29 @@ func (c *serviceClient) Greet(ctx context.Context, in *HelloRequest, opts ...grp
// ServerPoint serves the Greeter 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: greeterv0.Point.ID(),
Point: mobyextcontract.Point.ID(),
Register: func(r grpc.ServiceRegistrar, impl any) {
r.RegisterService(&serviceDesc, &grpcServer{impl: impl.(greeterv0.Greeter)})
r.RegisterService(&serviceDesc, &grpcServer{impl: impl.(mobyextcontract.Greeter)})
},
}
// ClientProvider builds a broker provider for the Greeter point from an
// out-of-process gRPC connection.
func ClientProvider(conn grpc.ClientConnInterface) extensions.Provider {
return greeterv0.Point.Provide(NewClient(conn))
return mobyextcontract.Point.Provide(NewClient(conn))
}
// ClientPoint registers ClientProvider for the Greeter point with a host.
var ClientPoint = clientpoint.Registration{Point: greeterv0.Point.ID(), Provider: ClientProvider}
var ClientPoint = clientpoint.Registration{Point: mobyextcontract.Point.ID(), Provider: ClientProvider}
// NewClient returns a greeterv0.Greeter that calls the Greeter point over conn.
func NewClient(conn grpc.ClientConnInterface) greeterv0.Greeter {
// NewClient returns a mobyextcontract.Greeter that calls the Greeter point over conn.
func NewClient(conn grpc.ClientConnInterface) mobyextcontract.Greeter {
return &grpcClient{client: NewGreeterClient(conn)}
}
// grpcServer serves an implementation of the contract's Go interface.
type grpcServer struct {
impl greeterv0.Greeter
impl mobyextcontract.Greeter
}
func (s *grpcServer) Greet(ctx context.Context, req *HelloRequest) (*HelloReply, error) {
@@ -113,7 +113,7 @@ type grpcClient struct {
client GreeterClient
}
func (c *grpcClient) Greet(ctx context.Context, req *greeterv0.HelloRequest) (*greeterv0.HelloReply, error) {
func (c *grpcClient) Greet(ctx context.Context, req *mobyextcontract.HelloRequest) (*mobyextcontract.HelloReply, error) {
resp, err := c.client.Greet(ctx, helloRequestToProto(req))
if err != nil {
return nil, err
@@ -121,7 +121,7 @@ func (c *grpcClient) Greet(ctx context.Context, req *greeterv0.HelloRequest) (*g
return helloReplyFromProto(resp), nil
}
func helloReplyToProto(in *greeterv0.HelloReply) *HelloReply {
func helloReplyToProto(in *mobyextcontract.HelloReply) *HelloReply {
if in == nil {
return nil
}
@@ -130,16 +130,16 @@ func helloReplyToProto(in *greeterv0.HelloReply) *HelloReply {
return out
}
func helloReplyFromProto(in *HelloReply) *greeterv0.HelloReply {
func helloReplyFromProto(in *HelloReply) *mobyextcontract.HelloReply {
if in == nil {
return nil
}
out := &greeterv0.HelloReply{}
out := &mobyextcontract.HelloReply{}
out.Message = in.GetMessage()
return out
}
func helloRequestToProto(in *greeterv0.HelloRequest) *HelloRequest {
func helloRequestToProto(in *mobyextcontract.HelloRequest) *HelloRequest {
if in == nil {
return nil
}
@@ -148,11 +148,11 @@ func helloRequestToProto(in *greeterv0.HelloRequest) *HelloRequest {
return out
}
func helloRequestFromProto(in *HelloRequest) *greeterv0.HelloRequest {
func helloRequestFromProto(in *HelloRequest) *mobyextcontract.HelloRequest {
if in == nil {
return nil
}
out := &greeterv0.HelloRequest{}
out := &mobyextcontract.HelloRequest{}
out.Name = in.GetName()
return out
}
+160 -59
View File
@@ -55,8 +55,8 @@ func Allow() PointPolicyResult {
return PointPolicyResult{action: pointPolicyActionAllow}
}
// Drop returns a decision that silently omits an ordinary provider or leaves an
// offered set private.
// Drop omits an ordinary provider (including its offer), or keeps all offers
// private when applied to servicev0.Point.ID().
func Drop() PointPolicyResult {
return PointPolicyResult{action: pointPolicyActionDrop}
}
@@ -85,8 +85,8 @@ func (result PointPolicyResult) resolve() (pointPolicyAction, error) {
}
// PointPolicy decides whether an extension identity may provide a Point.
// A decision on servicev0.Point.ID() controls publication of the complete
// offered set.
// A decision on servicev0.Point.ID() controls publication of offered points
// whose providers were not dropped by policy.
// A nil policy allows ordinary providers and drops publication.
type PointPolicy interface {
Decide(identity extensions.ExtensionIdentity, point extensions.PointID) PointPolicyResult
@@ -237,6 +237,14 @@ type registeredExtension struct {
providers []extensions.Provider
}
type publicationState struct {
policy PointPolicy
servers map[extensions.PointID]serverpoint.Registration
published map[extensions.ExtensionID]map[extensions.PointID][]string
owners map[string]extensions.ExtensionID
reserved map[string]bool
}
// hostedExtension is the runtime-neutral declaration and lifecycle surface
// needed to adapt an externally hosted extension to the broker.
type hostedExtension struct {
@@ -307,8 +315,13 @@ func New(ctx context.Context, optionList ...Option) (_ *Host, retErr error) {
conns := make(map[extensions.ExtensionID]grpc.ClientConnInterface)
var loaded []loadedExtension
var registered []registeredExtension
publishedServices := make(map[extensions.ExtensionID]map[extensions.PointID][]string)
publishedOwners := make(map[string]extensions.ExtensionID)
publication := publicationState{
policy: policy,
servers: pointServers,
published: make(map[extensions.ExtensionID]map[extensions.PointID][]string),
owners: make(map[string]extensions.ExtensionID),
reserved: reservedServices,
}
var inProcessServices []servicegrpc.Service
var callback *grpc.Server
// The broker only shuts down initialized extensions, so construction failures
@@ -343,7 +356,23 @@ func New(ctx context.Context, optionList ...Option) (_ *Host, retErr error) {
if err != nil {
return nil, err
}
services, err := collectInProcessPublications(identity, ext, policy, pointServers, publishedServices, publishedOwners, reservedServices)
if err := broker.ValidateDeclaration(decl); err != nil {
return nil, err
}
if extensionFullyDropped(decl.Providers, admittedProviders) {
// Dropping the extension must not hide malformed offers or a policy rejection.
offered, err := offeredInProcessPoints(decl)
if err != nil {
return nil, err
}
if len(offered) > 0 {
if _, err := publicationPolicyAction(identity, policy); err != nil {
return nil, err
}
}
continue
}
services, err := collectInProcessPublications(identity, ext, &publication, droppedProviderPoints(decl.Providers, admittedProviders))
if err != nil {
return nil, err
}
@@ -379,9 +408,22 @@ func New(ctx context.Context, optionList ...Option) (_ *Host, retErr error) {
if err != nil {
return nil, err
}
if err := approveProcessPublications(identity, started, policy, publishedServices, publishedOwners, reservedServices); err != nil {
if err := broker.ValidateDeclaration(decl); err != nil {
return nil, err
}
if err := approveProcessPublications(identity, started, &publication, droppedProviderPoints(decl.Providers, admittedProviders)); err != nil {
return nil, err
}
// Offered-only points have no provider for policy to drop, so they keep
// the process only when their offer is published.
if extensionFullyDropped(decl.Providers, admittedProviders) && len(publication.published[identity.ID]) == 0 {
// The process was launched for Describe; stop it before returning the host.
if err := started.Close(context.WithoutCancel(ctx)); err != nil {
return nil, fmt.Errorf("close dropped extension %q: %w", identity.ID, err)
}
loaded = loaded[:len(loaded)-1]
continue
}
decl.Providers = admittedProviders
if err := b.Register(identity, extensions.New(decl)); err != nil {
return nil, err
@@ -433,7 +475,7 @@ func New(ctx context.Context, optionList ...Option) (_ *Host, retErr error) {
}
log.G(ctx).WithFields(fields).Info("loaded extension")
}
return &Host{broker: b, conns: conns, loaded: loaded, publishedServices: publishedServices, inProcessServices: inProcessServices, callback: callback}, nil
return &Host{broker: b, conns: conns, loaded: loaded, publishedServices: publication.published, inProcessServices: inProcessServices, callback: callback}, nil
}
func validateHostIdentity(identity extensions.ExtensionIdentity, decl extensions.Declaration) error {
@@ -470,45 +512,131 @@ func admitProviders(identity extensions.ExtensionIdentity, providers []extension
return admitted, nil
}
func approveProcessPublications(identity extensions.ExtensionIdentity, started *launcher.Launched, policy PointPolicy, published map[extensions.ExtensionID]map[extensions.PointID][]string, owners map[string]extensions.ExtensionID, reserved map[string]bool) error {
// extensionFullyDropped ignores metadata providers and keeps extensions that
// declared no ordinary providers: policy had nothing to drop from them.
func extensionFullyDropped(declared, admitted []extensions.Provider) bool {
hadProvider := false
for _, provider := range declared {
if !extensions.IsMetadataPoint(provider.Point) {
hadProvider = true
break
}
}
if !hadProvider {
return false
}
for _, provider := range admitted {
if !extensions.IsMetadataPoint(provider.Point) {
return false
}
}
return true
}
// droppedProviderPoints excludes offered-only process points, which have no
// internal provider for policy to drop and remain governed by servicev0.
func droppedProviderPoints(declared, admitted []extensions.Provider) map[extensions.PointID]bool {
if len(declared) == len(admitted) {
return nil
}
dropped := make(map[extensions.PointID]bool, len(declared)-len(admitted))
for _, provider := range declared {
dropped[provider.Point] = true
}
for _, provider := range admitted {
delete(dropped, provider.Point)
}
return dropped
}
func approveProcessPublications(identity extensions.ExtensionIdentity, started *launcher.Launched, publication *publicationState, dropped map[extensions.PointID]bool) error {
if len(started.OfferedPoints) == 0 {
return nil
}
result := Drop()
if policy != nil {
result = policy.Decide(identity, servicev0.Point.ID())
action, err := publicationPolicyAction(identity, publication.policy)
if err != nil {
return err
}
action, cause := result.resolve()
switch action {
case pointPolicyActionDrop:
if action == pointPolicyActionDrop {
return nil
case pointPolicyActionReject:
return policyRejectionError("publish offered points", identity, servicev0.Point.ID(), cause)
}
for _, point := range started.OfferedPoints {
if dropped[point] {
continue
}
names := started.ProviderServices[point]
if len(names) == 0 {
return fmt.Errorf("extension %q offered point %q without a gRPC service", identity.ID, point)
}
for _, service := range names {
if reserved[service] {
if publication.reserved[service] {
return fmt.Errorf("extension %q cannot publish reserved gRPC service %q", identity.ID, service)
}
if owner, exists := owners[service]; exists {
if owner, exists := publication.owners[service]; exists {
return fmt.Errorf("extensions %q and %q both publish gRPC service %q", owner, identity.ID, service)
}
owners[service] = identity.ID
publication.owners[service] = identity.ID
}
if published[identity.ID] == nil {
published[identity.ID] = make(map[extensions.PointID][]string)
if publication.published[identity.ID] == nil {
publication.published[identity.ID] = make(map[extensions.PointID][]string)
}
published[identity.ID][point] = append([]string(nil), names...)
publication.published[identity.ID][point] = append([]string(nil), names...)
}
return nil
}
func collectInProcessPublications(identity extensions.ExtensionIdentity, ext extensions.Extension, policy PointPolicy, servers map[extensions.PointID]serverpoint.Registration, published map[extensions.ExtensionID]map[extensions.PointID][]string, owners map[string]extensions.ExtensionID, reserved map[string]bool) ([]servicegrpc.Service, error) {
func collectInProcessPublications(identity extensions.ExtensionIdentity, ext extensions.Extension, publication *publicationState, dropped map[extensions.PointID]bool) ([]servicegrpc.Service, error) {
decl := ext.Declaration()
offered, err := offeredInProcessPoints(decl)
if err != nil {
return nil, err
}
if len(offered) == 0 {
return nil, nil
}
action, err := publicationPolicyAction(identity, publication.policy)
if err != nil {
return nil, err
}
if action == pointPolicyActionDrop {
return nil, nil
}
providers := make(map[extensions.PointID]any, len(decl.Providers))
for _, provider := range decl.Providers {
providers[provider.Point] = provider.Impl
}
var services []servicegrpc.Service
for _, point := range offered {
if dropped[point] {
continue
}
impl := providers[point]
registration, ok := publication.servers[point]
if !ok {
return nil, fmt.Errorf("extension %q: allowed in-process offer for point %q has no server registration", decl.ID, point)
}
service, err := servicegrpc.Adapt(registration, impl)
if err != nil {
return nil, fmt.Errorf("extension %q: publish point %q: %w", decl.ID, point, err)
}
if publication.reserved[service.Name] {
return nil, fmt.Errorf("extension %q cannot publish reserved gRPC service %q", decl.ID, service.Name)
}
if owner, exists := publication.owners[service.Name]; exists {
return nil, fmt.Errorf("extensions %q and %q both publish gRPC service %q", owner, decl.ID, service.Name)
}
publication.owners[service.Name] = decl.ID
services = append(services, service)
if publication.published[decl.ID] == nil {
publication.published[decl.ID] = make(map[extensions.PointID][]string)
}
publication.published[decl.ID][point] = []string{service.Name}
}
return services, nil
}
func offeredInProcessPoints(decl extensions.Declaration) ([]extensions.PointID, error) {
providers := make(map[extensions.PointID]any, len(decl.Providers))
for _, provider := range decl.Providers {
if _, exists := providers[provider.Point]; exists {
@@ -545,46 +673,19 @@ func collectInProcessPublications(identity extensions.ExtensionIdentity, ext ext
offered = append(offered, point)
}
}
if len(offered) == 0 {
return nil, nil
}
return offered, nil
}
func publicationPolicyAction(identity extensions.ExtensionIdentity, policy PointPolicy) (pointPolicyAction, error) {
result := Drop()
if policy != nil {
result = policy.Decide(identity, servicev0.Point.ID())
}
action, cause := result.resolve()
switch action {
case pointPolicyActionDrop:
return nil, nil
case pointPolicyActionReject:
return nil, policyRejectionError("publish offered points", identity, servicev0.Point.ID(), cause)
if action == pointPolicyActionReject {
return action, policyRejectionError("publish offered points", identity, servicev0.Point.ID(), cause)
}
var services []servicegrpc.Service
for _, point := range offered {
impl := providers[point]
registration, ok := servers[point]
if !ok {
return nil, fmt.Errorf("extension %q: allowed in-process offer for point %q has no server registration", decl.ID, point)
}
service, err := servicegrpc.Adapt(registration, impl)
if err != nil {
return nil, fmt.Errorf("extension %q: publish point %q: %w", decl.ID, point, err)
}
if reserved[service.Name] {
return nil, fmt.Errorf("extension %q cannot publish reserved gRPC service %q", decl.ID, service.Name)
}
if owner, exists := owners[service.Name]; exists {
return nil, fmt.Errorf("extensions %q and %q both publish gRPC service %q", owner, decl.ID, service.Name)
}
owners[service.Name] = decl.ID
services = append(services, service)
if published[decl.ID] == nil {
published[decl.ID] = make(map[extensions.PointID][]string)
}
published[decl.ID][point] = []string{service.Name}
}
return services, nil
return action, nil
}
func policyRejectionError(operation string, identity extensions.ExtensionIdentity, point extensions.PointID, cause error) error {
+21 -11
View File
@@ -44,6 +44,27 @@ func (b *Broker) Register(identity extensions.ExtensionIdentity, ext extensions.
if identity.ID != decl.ID {
return fmt.Errorf("extension identity id %q does not match declared id %q", identity.ID, decl.ID)
}
if err := ValidateDeclaration(decl); err != nil {
return err
}
b.mu.Lock()
defer b.mu.Unlock()
if _, ok := b.extensions[decl.ID]; ok {
return fmt.Errorf("extension %q is already registered", decl.ID)
}
if err := b.checkConflicts(decl); err != nil {
return err
}
b.extensions[decl.ID] = &extensionState{extension: decl, identity: identity}
b.order = append(b.order, decl.ID)
return nil
}
// ValidateDeclaration checks the fields of a declaration independently of
// registration, so hosts can reject malformed extensions even when policy
// drops all their providers.
func ValidateDeclaration(decl extensions.Declaration) error {
seenPoints := make(map[extensions.PointID]struct{})
for _, provider := range decl.Providers {
if provider.Point == "" {
@@ -73,17 +94,6 @@ func (b *Broker) Register(identity extensions.ExtensionIdentity, ext extensions.
return fmt.Errorf("extension %q conflicts with itself", decl.ID)
}
}
b.mu.Lock()
defer b.mu.Unlock()
if _, ok := b.extensions[decl.ID]; ok {
return fmt.Errorf("extension %q is already registered", decl.ID)
}
if err := b.checkConflicts(decl); err != nil {
return err
}
b.extensions[decl.ID] = &extensionState{extension: decl, identity: identity}
b.order = append(b.order, decl.ID)
return nil
}
+11
View File
@@ -9,8 +9,19 @@ package moby.extension.runtime.v1;
option go_package = "github.com/moby/extensions/sdk/sdkapi/protogen";
// Extension is served by every out-of-process extension.
//
// The generator command names the gRPC service fully qualified because, unlike
// a point, there is no id to take the proto package from. Both halves are wire
// identifiers: extensions built against them are separate binaries, possibly
// built from an older tree, so neither may change without a new version.
service Extension {
// Describe returns the extension's declaration.
rpc Describe(DescribeRequest) returns (DescribeResponse);
// Initialize runs the extension's Init. The daemon calls it after connecting
// every extension, in dependency order, so that by the time an extension
// initializes its dependencies are already initialized and reachable over the
// callback channel. An Init error is returned as the RPC status.
rpc Initialize(InitializeRequest) returns (InitializeResponse);
}
+41 -31
View File
@@ -6,7 +6,7 @@ package protogen
import (
context "context"
sdkapi "github.com/moby/extensions/sdk/sdkapi"
mobyextcontract "github.com/moby/extensions/sdk/sdkapi"
grpc "google.golang.org/grpc"
)
@@ -22,7 +22,12 @@ const (
// proto-level shape of the service, not the handwritten Go interface: a contract
// method returning a bare error returns an empty response message here.
type ExtensionServer interface {
// Describe returns the extension's declaration.
Describe(context.Context, *DescribeRequest) (*DescribeResponse, error)
// Initialize runs the extension's Init. The daemon calls it after connecting
// every extension, in dependency order, so that by the time an extension
// initializes its dependencies are already initialized and reachable over the
// callback channel. An Init error is returned as the RPC status.
Initialize(context.Context, *InitializeRequest) (*InitializeResponse, error)
}
@@ -69,7 +74,12 @@ func handleInitialize(srv any, ctx context.Context, dec func(any) error, interce
// ExtensionClient calls the contract's raw gRPC service. It is exported so a client
// outside the framework can reach the service with a plain gRPC client.
type ExtensionClient interface {
// Describe returns the extension's declaration.
Describe(ctx context.Context, in *DescribeRequest, opts ...grpc.CallOption) (*DescribeResponse, error)
// Initialize runs the extension's Init. The daemon calls it after connecting
// every extension, in dependency order, so that by the time an extension
// initializes its dependencies are already initialized and reachable over the
// callback channel. An Init error is returned as the RPC status.
Initialize(ctx context.Context, in *InitializeRequest, opts ...grpc.CallOption) (*InitializeResponse, error)
}
@@ -95,18 +105,18 @@ func (c *serviceClient) Initialize(ctx context.Context, in *InitializeRequest, o
}
// RegisterServer serves impl as the Extension service on r.
func RegisterServer(r grpc.ServiceRegistrar, impl sdkapi.Extension) {
func RegisterServer(r grpc.ServiceRegistrar, impl mobyextcontract.Extension) {
r.RegisterService(&serviceDesc, &grpcServer{impl: impl})
}
// NewClient returns a sdkapi.Extension that calls the Extension service over conn.
func NewClient(conn grpc.ClientConnInterface) sdkapi.Extension {
// NewClient returns a mobyextcontract.Extension that calls the Extension service over conn.
func NewClient(conn grpc.ClientConnInterface) mobyextcontract.Extension {
return &grpcClient{client: NewExtensionClient(conn)}
}
// grpcServer serves an implementation of the contract's Go interface.
type grpcServer struct {
impl sdkapi.Extension
impl mobyextcontract.Extension
}
func (s *grpcServer) Describe(ctx context.Context, req *DescribeRequest) (*DescribeResponse, error) {
@@ -129,7 +139,7 @@ type grpcClient struct {
client ExtensionClient
}
func (c *grpcClient) Describe(ctx context.Context, req *sdkapi.DescribeRequest) (*sdkapi.DescribeResponse, error) {
func (c *grpcClient) Describe(ctx context.Context, req *mobyextcontract.DescribeRequest) (*mobyextcontract.DescribeResponse, error) {
resp, err := c.client.Describe(ctx, describeRequestToProto(req))
if err != nil {
return nil, err
@@ -137,7 +147,7 @@ func (c *grpcClient) Describe(ctx context.Context, req *sdkapi.DescribeRequest)
return describeResponseFromProto(resp), nil
}
func (c *grpcClient) Initialize(ctx context.Context, req *sdkapi.InitializeRequest) (*sdkapi.InitializeResponse, error) {
func (c *grpcClient) Initialize(ctx context.Context, req *mobyextcontract.InitializeRequest) (*mobyextcontract.InitializeResponse, error) {
resp, err := c.client.Initialize(ctx, initializeRequestToProto(req))
if err != nil {
return nil, err
@@ -145,7 +155,7 @@ func (c *grpcClient) Initialize(ctx context.Context, req *sdkapi.InitializeReque
return initializeResponseFromProto(resp), nil
}
func declarationToProto(in *sdkapi.Declaration) *Declaration {
func declarationToProto(in *mobyextcontract.Declaration) *Declaration {
if in == nil {
return nil
}
@@ -165,11 +175,11 @@ func declarationToProto(in *sdkapi.Declaration) *Declaration {
return out
}
func declarationFromProto(in *Declaration) *sdkapi.Declaration {
func declarationFromProto(in *Declaration) *mobyextcontract.Declaration {
if in == nil {
return nil
}
out := &sdkapi.Declaration{}
out := &mobyextcontract.Declaration{}
out.ID = in.GetId()
for _, e := range in.GetProviders() {
out.Providers = append(out.Providers, *pointDeclarationFromProto(e))
@@ -185,7 +195,7 @@ func declarationFromProto(in *Declaration) *sdkapi.Declaration {
return out
}
func dependencyToProto(in *sdkapi.Dependency) *Dependency {
func dependencyToProto(in *mobyextcontract.Dependency) *Dependency {
if in == nil {
return nil
}
@@ -196,18 +206,18 @@ func dependencyToProto(in *sdkapi.Dependency) *Dependency {
return out
}
func dependencyFromProto(in *Dependency) *sdkapi.Dependency {
func dependencyFromProto(in *Dependency) *mobyextcontract.Dependency {
if in == nil {
return nil
}
out := &sdkapi.Dependency{}
out := &mobyextcontract.Dependency{}
out.Point = in.GetPoint()
out.Extension = in.GetExtension()
out.Optional = in.GetOptional()
return out
}
func describeRequestToProto(in *sdkapi.DescribeRequest) *DescribeRequest {
func describeRequestToProto(in *mobyextcontract.DescribeRequest) *DescribeRequest {
if in == nil {
return nil
}
@@ -215,15 +225,15 @@ func describeRequestToProto(in *sdkapi.DescribeRequest) *DescribeRequest {
return out
}
func describeRequestFromProto(in *DescribeRequest) *sdkapi.DescribeRequest {
func describeRequestFromProto(in *DescribeRequest) *mobyextcontract.DescribeRequest {
if in == nil {
return nil
}
out := &sdkapi.DescribeRequest{}
out := &mobyextcontract.DescribeRequest{}
return out
}
func describeResponseToProto(in *sdkapi.DescribeResponse) *DescribeResponse {
func describeResponseToProto(in *mobyextcontract.DescribeResponse) *DescribeResponse {
if in == nil {
return nil
}
@@ -232,16 +242,16 @@ func describeResponseToProto(in *sdkapi.DescribeResponse) *DescribeResponse {
return out
}
func describeResponseFromProto(in *DescribeResponse) *sdkapi.DescribeResponse {
func describeResponseFromProto(in *DescribeResponse) *mobyextcontract.DescribeResponse {
if in == nil {
return nil
}
out := &sdkapi.DescribeResponse{}
out := &mobyextcontract.DescribeResponse{}
out.Declaration = declarationFromProto(in.GetDeclaration())
return out
}
func initializeRequestToProto(in *sdkapi.InitializeRequest) *InitializeRequest {
func initializeRequestToProto(in *mobyextcontract.InitializeRequest) *InitializeRequest {
if in == nil {
return nil
}
@@ -249,15 +259,15 @@ func initializeRequestToProto(in *sdkapi.InitializeRequest) *InitializeRequest {
return out
}
func initializeRequestFromProto(in *InitializeRequest) *sdkapi.InitializeRequest {
func initializeRequestFromProto(in *InitializeRequest) *mobyextcontract.InitializeRequest {
if in == nil {
return nil
}
out := &sdkapi.InitializeRequest{}
out := &mobyextcontract.InitializeRequest{}
return out
}
func initializeResponseToProto(in *sdkapi.InitializeResponse) *InitializeResponse {
func initializeResponseToProto(in *mobyextcontract.InitializeResponse) *InitializeResponse {
if in == nil {
return nil
}
@@ -265,15 +275,15 @@ func initializeResponseToProto(in *sdkapi.InitializeResponse) *InitializeRespons
return out
}
func initializeResponseFromProto(in *InitializeResponse) *sdkapi.InitializeResponse {
func initializeResponseFromProto(in *InitializeResponse) *mobyextcontract.InitializeResponse {
if in == nil {
return nil
}
out := &sdkapi.InitializeResponse{}
out := &mobyextcontract.InitializeResponse{}
return out
}
func pointDeclarationToProto(in *sdkapi.PointDeclaration) *PointDeclaration {
func pointDeclarationToProto(in *mobyextcontract.PointDeclaration) *PointDeclaration {
if in == nil {
return nil
}
@@ -282,16 +292,16 @@ func pointDeclarationToProto(in *sdkapi.PointDeclaration) *PointDeclaration {
return out
}
func pointDeclarationFromProto(in *PointDeclaration) *sdkapi.PointDeclaration {
func pointDeclarationFromProto(in *PointDeclaration) *mobyextcontract.PointDeclaration {
if in == nil {
return nil
}
out := &sdkapi.PointDeclaration{}
out := &mobyextcontract.PointDeclaration{}
out.ID = in.GetId()
return out
}
func providerServicesToProto(in *sdkapi.ProviderServices) *ProviderServices {
func providerServicesToProto(in *mobyextcontract.ProviderServices) *ProviderServices {
if in == nil {
return nil
}
@@ -301,11 +311,11 @@ func providerServicesToProto(in *sdkapi.ProviderServices) *ProviderServices {
return out
}
func providerServicesFromProto(in *ProviderServices) *sdkapi.ProviderServices {
func providerServicesFromProto(in *ProviderServices) *mobyextcontract.ProviderServices {
if in == nil {
return nil
}
out := &sdkapi.ProviderServices{}
out := &mobyextcontract.ProviderServices{}
out.Point = in.GetPoint()
out.Services = in.GetServices()
return out
+1 -1
View File
@@ -1270,7 +1270,7 @@ github.com/moby/buildkit/worker/label
# github.com/moby/docker-image-spec v1.3.1
## explicit; go 1.18
github.com/moby/docker-image-spec/specs-go/v1
# github.com/moby/extensions v0.0.0-20260918151408-e4c23d866037
# github.com/moby/extensions v0.0.0-20261020100352-d2819d88bcbe
## explicit; go 1.26.0
github.com/moby/extensions
github.com/moby/extensions/client