Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions docs/examples/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"strings"
"time"

"github.com/compose-spec/compose-go/v2/types"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
Expand Down Expand Up @@ -223,11 +224,16 @@ func up(options options, args []string) {
// buffered bytes and hang on the next answer.
responses := json.NewDecoder(os.Stdin)
fmt.Printf(`{ "type": "get-service-config" }%s`, lineSeparator)
var config struct {
Provider struct {
Type string `json:"type"`
} `json:"provider"`
}
// Decoding into the real types.ServiceConfig (the same struct Compose
// marshaled) means GetScale() is available: Compose never rejects or
// interprets a scale request on a provider-backed service -- the relay
// it deploys is always a single container regardless -- the value is
// passed through unmodified here instead, for the provider itself to
// act on (or ignore) for the real resource it manages. GetScale() is
// the canonical way to read it, already handling both the scale: and
// deploy.replicas: spellings; reading either field directly would miss
// whichever one the compose file didn't use.
var config types.ServiceConfig
if err := responses.Decode(&config); err != nil {
// error text is not JSON-safe either: encode, don't interpolate
msg, _ := json.Marshal(map[string]string{"type": "error", "message": fmt.Sprintf("get-service-config failed: %v", err)})
Expand All @@ -238,6 +244,8 @@ func up(options options, args []string) {
// encode the message instead of interpolating it into a JSON literal
setenv, _ := json.Marshal(map[string]string{"type": "setenv", "message": "CONFIG_TYPE=" + config.Provider.Type})
fmt.Println(string(setenv))
scaleEnv, _ := json.Marshal(map[string]string{"type": "setenv", "message": fmt.Sprintf("CONFIG_SCALE=%d", config.GetScale())})
fmt.Println(string(scaleEnv))

// When asked to, stand up a real endpoint on the host and publish it, so
// compose deploys a relay and consumers reach it as http://<service>:80.
Expand Down
10 changes: 10 additions & 0 deletions pkg/compose/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,16 @@ func (r *reconciler) reconcileService(service types.ServiceConfig) error {
return nil
}
if service.Provider != nil {
// A provider's relay is a singleton network stand-in, never itself a
// replica: this branch always plans exactly one OpRunProvider node,
// regardless of service.GetScale(). scale/deploy.replicas is NOT
// rejected or otherwise special-cased here -- it is deliberately
// passed through unmodified as part of the service config the
// provider receives over its control channel (GetServiceConfigType
// in plugins.go marshals the whole resolved ServiceConfig, Scale
// included), so interpreting it -- scaling the real
// resource the provider manages, ignoring it, or rejecting it itself
// -- is entirely the provider's call, never Compose's.
serviceCopy := service
deps := r.infrastructureDeps(service)
// A service migrated from replicas to a provider leaves its old
Expand Down
27 changes: 27 additions & 0 deletions pkg/compose/reconcile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,33 @@ func TestReconcileService_ProviderRelayLeftAlone(t *testing.T) {
`)+"\n")
}

// A provider's relay is a singleton network stand-in, never itself a
// replica: `compose scale`/`deploy.replicas` on a provider-backed service
// must still plan exactly one OpRunProvider node, whatever count was
// requested -- the value is neither rejected nor acted on by Compose, it is
// passed through unmodified to the provider over its control channel
// (GetServiceConfigType marshals the whole resolved ServiceConfig), which
// decides what to do with it for the resource it actually manages.
func TestReconcileService_ProviderScaleNeverMultipliesTheRelay(t *testing.T) {
scale := 3
db := types.ServiceConfig{
Name: "db",
Provider: &types.ServiceProviderConfig{Type: "test"},
Scale: &scale,
}
project := &types.Project{
Name: "myproject",
Services: types.Services{"db": db},
}
observed := emptyObserved()

plan, err := reconcile(t.Context(), project, observed, defaultReconcileOptions(), noPrompt)
assert.NilError(t, err)
assert.Equal(t, plan.String(), strings.TrimSpace(`
[] -> #1 provider:db, RunProvider, provider service
`)+"\n")
}

// TestReconcileNetworks_DivergedAlsoRecreatesChangedContainer verifies the
// entangled case: when a container attached to a diverged network also has its
// own config changed, it is both reconnected (by the network recreate) and
Expand Down
24 changes: 24 additions & 0 deletions pkg/e2e/providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,30 @@ func TestProviderMigration(t *testing.T) {
OutputContains("test-1 | hello from provider"))
}

// TestProviderScaleReachesTheProvider locks that `compose scale` on a
// provider-backed service neither fails nor multiplies the relay: the
// requested count is passed through unmodified to the provider over its
// control channel (get-service-config), which is free to act on it for the
// real resource it manages, while Compose's own relay stays a single
// container regardless. PROVIDER_DEMO_ENDPOINT publishes a real endpoint so
// a relay container actually exists to assert singularity against; test is
// included in the scale targets so it gets created in the same call and its
// env records the exact value the provider received.
func TestProviderScaleReachesTheProvider(t *testing.T) {
relayImage := "compose-relay-e2e"
s := providerScenario(t, "scale on a provider-backed service must reach the provider, not be rejected or applied to the relay")
s.CLI().RunCmd(t, "docker", "build", "-t", relayImage, "../../relay")
s.Env("PROVIDER_DEMO_ENDPOINT=1", "COMPOSE_RELAY_IMAGE="+relayImage)
s.Step("scale converges without error and the dependent sees the exact value the provider received",
ComposeCmd("scale", "db=5", "test=1"),
ContainerEnv("test", "DB_CONFIG_SCALE", "5"),
ServiceScale("db", 1)).
Step("a second scale updates the value without ever multiplying the relay",
ComposeCmd("scale", "db=7", "test=1"),
ContainerEnv("test", "DB_CONFIG_SCALE", "7"),
Comment thread
ndeloof marked this conversation as resolved.
ServiceScale("db", 1))
}

func TestProviderPublishEndpoint(t *testing.T) {
// The example provider stands up a real endpoint on the host and
// publishes it; compose deploys a relay under the service's name, so the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
services:
test:
image: alpine
command: env
depends_on:
- db
db:
provider:
type: example-provider
options:
name: db
type: test1
size: 1
Loading