Skip to content
Merged
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
3 changes: 3 additions & 0 deletions cmd/run/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ type launcher struct {
// delivered is the last manifest delivered per module, a withdrawal
// included; protected by delivery rather than the short-lived state lock.
delivered map[string]*solutionhost.ModuleManifest
// outsideTargets are the bindings to a module outside the run that
// admission has reported, so each is said once; protected by delivery.
outsideTargets map[string]bool
// pendingWithdrawals are modules whose withdrawal the host has not applied
// yet: retried on the next composition refresh, even with no gone roots.
pendingWithdrawals map[string]bool
Expand Down
48 changes: 41 additions & 7 deletions cmd/run/launcher_admission.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,13 @@ func (cluster *launcher) admitSelected(ctx context.Context) error {
return err
}
defer cluster.delivery.Release(1)
modules := map[string]bool{}
// One snapshot of the selection decides both what is admitted and which
// binding targets are part of the run.
modules, selected := map[string]bool{}, map[string]bool{}
for _, unique := range cluster.flow.LocalRuntimeServices() {
if ref, err := resources.ParseServiceWithOptionalModule(unique); err == nil {
selected[ref.Module] = true
}
if identity, ok := cluster.identity(unique); ok && cluster.delivers(identity) {
modules[identity.Module] = true
}
Expand Down Expand Up @@ -114,12 +119,8 @@ func (cluster *launcher) admitSelected(ctx context.Context) error {
if err != nil {
return err
}
if manifest.Authority != nil {
for _, binding := range manifest.Authority.Bindings {
if !modules[binding.Audience] {
return fmt.Errorf("module %s binding %s targets %s, which has no selected local identity workload; authority admission requires its target declaration", module, binding.ID, binding.Audience)
}
}
if err := cluster.checkBindingTargets(ctx, module, manifest, selected); err != nil {
return err
}
manifests = append(manifests, manifest)
}
Expand All @@ -130,3 +131,36 @@ func (cluster *launcher) admitSelected(ctx context.Context) error {
}
return nil
}

// checkBindingTargets decides, at admission, a module's bindings by where they
// point. A binding to the host's own module could never be served, since the
// run declares that module to no authority, so admission refuses it before
// any manifest is sent. Every other binding is delivered as declared: a run
// composes a unit and only the modules it consumes, and the authority decides
// a binding at call time, serving it when its audience is composed (by this
// run, or by the fixture's stand-in for a process the run does not supervise)
// and refusing a call through it by name when nothing composes it. A binding
// to a module outside the run is said once, since admission repeats per start.
// Caller holds delivery.
func (cluster *launcher) checkBindingTargets(ctx context.Context, module string, manifest *solutionhost.ModuleManifest, selected map[string]bool) error {
if manifest.Authority == nil {
return nil
}
for _, binding := range manifest.Authority.Bindings {
if binding.Audience == cluster.hostModule {
return fmt.Errorf("module %s binding %s targets %s, the host's own module, which this run declares to no authority; authority admission requires its target declaration", module, binding.ID, binding.Audience)
}
key := module + "/" + binding.ID
if selected[binding.Audience] || cluster.outsideTargets[key] {
continue
}
if cluster.outsideTargets == nil {
cluster.outsideTargets = map[string]bool{}
}
cluster.outsideTargets[key] = true
wool.Get(ctx).In("launcher.admit").Warn(
fmt.Sprintf("module %s binding %s targets %s, which is not part of this run: it is delivered as declared, and the authority refuses a call through it (audience %q is not composed) unless something else composes %s, such as a fixture's stand-in", module, binding.ID, binding.Audience, binding.Audience, binding.Audience),
wool.Field("module", module), wool.Field("binding", binding.ID), wool.Field("target", binding.Audience))
}
return nil
}
74 changes: 63 additions & 11 deletions cmd/run/launcher_admission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
Expand All @@ -25,6 +26,7 @@ import (
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
"github.com/codefly-dev/core/resources"
"github.com/codefly-dev/core/solutionhost"
"github.com/codefly-dev/core/wool"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
)
Expand Down Expand Up @@ -202,19 +204,69 @@ func TestNativeAuthorityOrdersHostWithoutAuthoredDependency(t *testing.T) {
require.Empty(t, order)
}

func TestAdmissionRefusesUndeliveredBindingBeforeSending(t *testing.T) {
for _, audience := range []string{"missing", "model"} {
// writeTargetBinding gives alpha one binding, read-target, addressed to the
// audience the composition names.
func writeTargetBinding(t *testing.T, cluster *launcher, audience string) {
t.Helper()
contract := "schema: codefly/module-contract/v1\nprincipal: alpha\nnamespaces: []\nqueues: []\ndestinations: []\nscope_ceilings: []\nbindings:\n - id: read-target\n operations: [invoke]\n audience: {from: target/read-audience}\n scope_ceiling:\n invoke:\n - resource_kind: target.item\n actions: [read]\n"
configDir := filepath.Join(cluster.composed().Dir(), "configurations", "local")
require.NoError(t, os.MkdirAll(configDir, 0700))
require.NoError(t, os.WriteFile(filepath.Join(configDir, "target.env"), []byte("READ_AUDIENCE="+audience+"\n"), 0600))
path := filepath.Join(cluster.composed().Dir(), "modules", "alpha", "module.contract.codefly.yaml")
require.NoError(t, os.WriteFile(path, []byte(contract), 0600))
}

// A binding to the host's own module could never be served: the run declares
// that module to no authority, so admission refuses the whole selection
// before sending any manifest.
func TestAdmissionRefusesBindingToHostBeforeSending(t *testing.T) {
cluster, peer := admissionLauncher(t)
writeTargetBinding(t, cluster, "model")
err := cluster.admitSelected(t.Context())
require.ErrorContains(t, err, "binding read-target targets model, the host's own module")
require.Empty(t, peer.snapshots(), "validate the entire selection before applying any manifest")
}

// A run composes a unit and only the modules it consumes: a binding to a
// module outside the run, or to one selected only through services with no
// identity workload, does not stop startup. It is delivered as declared, for
// the authority to decide at call time; every selected module is admitted
// with its own workloads only; a binding outside the run is said once.
func TestAdmissionDeliversBindingsOutsideTheRunAsDeclared(t *testing.T) {
for _, audience := range []string{"missing", "store", "beta"} {
t.Run(audience, func(t *testing.T) {
cluster, peer := admissionLauncher(t)
contract := "schema: codefly/module-contract/v1\nprincipal: alpha\nnamespaces: []\nqueues: []\ndestinations: []\nscope_ceilings: []\nbindings:\n - id: read-target\n operations: [invoke]\n audience: {from: target/read-audience}\n scope_ceiling:\n invoke:\n - resource_kind: target.item\n actions: [read]\n"
configDir := filepath.Join(cluster.composed().Dir(), "configurations", "local")
require.NoError(t, os.MkdirAll(configDir, 0700))
require.NoError(t, os.WriteFile(filepath.Join(configDir, "target.env"), []byte("READ_AUDIENCE="+audience+"\n"), 0600))
path := filepath.Join(cluster.composed().Dir(), "modules", "alpha", "module.contract.codefly.yaml")
require.NoError(t, os.WriteFile(path, []byte(contract), 0600))
err := cluster.admitSelected(t.Context())
require.ErrorContains(t, err, "binding read-target targets "+audience)
require.Empty(t, peer.snapshots(), "validate the entire selection before applying any manifest")
flow, ok := cluster.flow.(*admissionFlow)
require.True(t, ok)
flow.selected = append(flow.selected, "store/db") // a datastore: selected, no identity workload
writeTargetBinding(t, cluster, audience)
capture := &refreshLogCapture{}
provider := wool.New(context.Background(), &wool.Resource{}).WithLogger(capture)
defer provider.Done()
ctx := provider.Inject(t.Context())
for range 2 { // admission repeats on every start
require.NoError(t, cluster.admitSelected(ctx))
}
admitted := map[string]bool{}
for _, m := range peer.snapshots() {
admitted[m.Alias] = true
require.NotNil(t, m.Authority)
require.Len(t, m.Workloads, 1)
require.Equal(t, "api", m.Workloads[0].Service, "an identity is declared for a selected workload only")
if m.Alias != "alpha" {
continue
}
require.Len(t, m.Authority.Bindings, 1)
require.Equal(t, "read-target", m.Authority.Bindings[0].ID)
require.Equal(t, audience, m.Authority.Bindings[0].Audience, "the binding is delivered as declared")
}
require.Equal(t, map[string]bool{"alpha": true, "beta": true}, admitted, "only selected modules with an identity workload are admitted")
said := strings.Count(strings.Join(capture.messages, "\n"), "binding read-target targets "+audience+", which is not part of this run")
if audience == "missing" {
require.Equal(t, 1, said, "a binding outside the run is said, once")
} else {
require.Zero(t, said, "a binding to a selected module is part of the run")
}
})
}
}
Expand Down
Loading