From e4e981d71df8946c4e3b644980da18c6c24b0870 Mon Sep 17 00:00:00 2001 From: crisfiorini-oss <301773486+crisfiorini-oss@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:52:32 -0400 Subject: [PATCH] fix: resolve injected endpoints in local containers --- AGENTS.md | 7 +++++-- README.md | 19 ++++++++++--------- endpoint_deployed_test.go | 40 +++++++++++++++++++++------------------ for.go | 28 +++++++++------------------ 4 files changed, 46 insertions(+), 48 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 192073b..ea38da1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,12 +114,15 @@ see the skill below. `SelectEndpointForReference` owns selection on module-adjusted declarations. `Init` pins that consumer once; endpoint resolution refuses identity drift. - **A deployed process has no workspace, so its carriers are authoritative.** - The builder image ships the binary alone. Outside `local` with no workspace + The builder image ships the binary alone, including in local Kubernetes pods. + In any environment with no workspace on disk, `ResolveNetworkInstance` reads the carrier keyed by the query's canonical identity and refuses only absence (`ErrEndpointCarrierAbsent`) or a malformed value: the composition judged eligibility at render and the mesh enforces it. With a workspace, selection comes first in every environment. - No flag decides the path; the environment and the workspace's presence do. + No flag decides the path; the workspace's + presence does. Native fallback requires a workspace and a `local` or unset + environment. - **`CODEFLY__` is spelled in this repo, nowhere else.** A consumer needing a value gets a typed accessor here; the prefix constant lives in `runtime_value.go` for that reason. diff --git a/README.md b/README.md index 2fe6821..cadf566 100644 --- a/README.md +++ b/README.md @@ -25,21 +25,21 @@ allocates an external instance. With a workspace on disk, every query resolves its name and API through core's typed selector on the producer's declarations before reading the injected carrier, including queries with an explicit API. Ambiguous, forbidden, and -API-mismatched references are refused even if a matching carrier exists. A -local process without a workspace is refused with `ErrNoDeclaredEndpoints`: an -address carrier alone cannot establish eligibility there. +API-mismatched references are refused even if a matching carrier exists. A deployed process has no workspace: the builder image ships the binary alone. -Outside `local`, with no workspace on disk, the carriers the composition +In every environment, with no workspace on disk, the carriers the composition injected are the composition's judgement — the CLI's join decided at render which endpoints the consumer may reach, and the mesh enforces it on the cell. Such a process resolves from the carrier keyed by the query's canonical identity (module, service, name, API; a name that is a supported API serves that API, as core reads a declaration) and refuses only an absent carrier, with `ErrEndpointCarrierAbsent`, or a malformed one. It never computes a native -address. A non-local process that does ship its workspace keeps selection -first. Nothing selects the path but the environment and the presence of a -workspace. A reference to an endpoint not named after its API must qualify the +address. This includes Kubernetes pods in a `local` environment: the environment +name does not mean the workspace is shipped in their image. A process that does +ship its workspace keeps selection first. The workspace's presence determines +the path; native fallback requires a workspace and a `local` or unset environment. +A reference to an endpoint not named after its API must qualify the API with `.API(...)` there, since no declaration is on disk to supply it. Call `codefly.Init(ctx)` at boot to pin the calling module from the runtime @@ -62,8 +62,9 @@ queried across modules. an address for an unavailable, external, forbidden, or invalid endpoint. After selection, core's key/value lookup distinguishes an absent carrier from a present value under the selected declaration's key. In unset or `local` -environments only absence permits native fallback; outside `local`, absence is -refused with `ErrEndpointCarrierAbsent`. A carrier keyed differently from the +environments with a workspace only absence permits native fallback; without +a workspace or outside `local`, absence is refused with +`ErrEndpointCarrierAbsent`. A carrier keyed differently from the selected declaration — another API, or another spelling — is not found under that key and reads as absent, so in `local` the native fallback runs in its place until the declaration-aware carrier validator (core#716) lands with the diff --git a/endpoint_deployed_test.go b/endpoint_deployed_test.go index b73080b..0c81779 100644 --- a/endpoint_deployed_test.go +++ b/endpoint_deployed_test.go @@ -16,8 +16,9 @@ import ( // the composition's judgement — the CLI's join decided at render which // endpoints this consumer may reach, and the mesh enforces it on the cell — so // the SDK resolves from the carrier keyed by the query's canonical identity and -// refuses only an absent carrier or a malformed one. The environment and the -// presence of a workspace decide the path; no flag does. +// refuses only an absent carrier or a malformed one. This also describes pods +// deployed into a local environment: the workspace's presence, not its +// environment name, determines whether declarations can be checked on disk. var carriedEndpoint = resources.EndpointInformation{ Module: "platform", Service: "location-records", Name: "rest", API: "rest", @@ -65,7 +66,7 @@ var referenceForms = []struct { } func TestDeployedProcessResolvesItsCarrierWithoutAWorkspace(t *testing.T) { - for _, environment := range []string{"production", "staging"} { + for _, environment := range []string{"production", "staging", "local", ""} { for _, source := range []string{"runtime", "embedded"} { for _, form := range referenceForms { t.Run("environment="+environment+"/source="+source+"/"+form.name, func(t *testing.T) { @@ -87,7 +88,7 @@ func TestDeployedProcessResolvesItsCarrierWithoutAWorkspace(t *testing.T) { } func TestDeployedProcessRefusesAnAbsentCarrierWithTheSentinel(t *testing.T) { - for _, environment := range []string{"production", "staging"} { + for _, environment := range []string{"production", "staging", "local", ""} { for _, form := range referenceForms { t.Run("environment="+environment+"/"+form.name, func(t *testing.T) { prepareProcessWithoutWorkspace(t, environment) @@ -205,21 +206,24 @@ func TestNonlocalProcessWithAWorkspaceRefusesAbsenceWithTheSentinel(t *testing.T require.Nil(t, instance) } -// In local, or before an environment is selected, the declarations are the -// authority and a process without them is refused as #57 built it. A carrier -// alone establishes nothing there. -func TestLocalProcessWithoutAWorkspaceStillRequiresDeclarations(t *testing.T) { +// Local Kubernetes pods carry cluster service addresses but ship no workspace. +// A malformed injected address is refused; a missing workspace must not hide it +// behind a declaration error or permit a native address to be substituted. +func TestLocalProcessWithoutAWorkspaceRefusesAMalformedCarrier(t *testing.T) { for _, environment := range []string{"", "local"} { - for _, form := range referenceForms { - t.Run("environment="+environment+"/"+form.name, func(t *testing.T) { - prepareProcessWithoutWorkspace(t, environment) - injectEndpointForTest(t, "embedded", carrierFor("rest", "rest", "https://upstream.example:9443")) - query := form.query(queryCarriedService()) - instance, err := query.ResolveNetworkInstance() - require.ErrorIs(t, err, resources.ErrNoDeclaredEndpoints) - require.Nil(t, instance) - require.Nil(t, query.NetworkInstance()) - }) + for _, source := range []string{"runtime", "embedded"} { + for _, form := range referenceForms { + t.Run("environment="+environment+"/source="+source+"/"+form.name, func(t *testing.T) { + prepareProcessWithoutWorkspace(t, environment) + injectEndpointForTest(t, source, carrierFor("rest", "rest", "localhost:not-a-port")) + query := form.query(queryCarriedService()) + instance, err := query.ResolveNetworkInstance() + require.ErrorContains(t, err, "malformed address") + require.NotErrorIs(t, err, codefly.ErrEndpointCarrierAbsent) + require.Nil(t, instance) + require.Nil(t, query.NetworkInstance()) + }) + } } } } diff --git a/for.go b/for.go index ea4f6e8..ae4bd46 100644 --- a/for.go +++ b/for.go @@ -87,10 +87,10 @@ func (q *Query) NetworkInstance() *resources.NetworkInstance { return nil } -// ErrEndpointCarrierAbsent reports that a process outside the local -// environment has no carrier for the endpoint the query names. The composition -// injects a carrier for every endpoint it grants a consumer, so absence is the -// composition's answer and nothing is substituted for it: a deployed process +// ErrEndpointCarrierAbsent reports that a process has no carrier for the +// endpoint the query names and cannot use a local workspace fallback. +// The composition injects a carrier for every endpoint it grants a consumer, +// so absence is its answer and nothing is substituted for it: a deployed process // has no workspace to compute a native address from, and a process that ships // its workspace never falls back outside local. var ErrEndpointCarrierAbsent = errors.New("codefly: no carrier for the endpoint the query names") @@ -108,11 +108,12 @@ var ErrEndpointCarrierAbsent = errors.New("codefly: no carrier for the endpoint // A deployed process has no workspace: the builder image ships the binary // alone. There the carriers the composition injected are the composition's // judgement — the CLI's join decided at render which endpoints this consumer -// may reach, and the mesh enforces it on the cell — so a non-local process +// may reach, and the mesh enforces it on the cell. A process in any environment // with no workspace resolves from the carrier keyed by the query's canonical // identity and refuses only an absent carrier (ErrEndpointCarrierAbsent) or a // malformed one. It never computes a native address. No flag selects the -// path: the environment and the presence of a workspace decide. +// path: the presence of a workspace decides. Native fallback requires a +// workspace and a local or unset environment. func (q *Query) ResolveNetworkInstance() (*resources.NetworkInstance, error) { if err := checkConsumerIdentity(q.consumerModule); err != nil { return nil, err @@ -126,9 +127,6 @@ func (q *Query) ResolveNetworkInstance() (*resources.NetworkInstance, error) { return nil, err } if workspace == nil { - if !deployed() { - return nil, resources.ErrNoDeclaredEndpoints - } return q.resolveFromAuthoritativeCarrier() } // The declaration decides eligibility before any carrier can supply an @@ -147,7 +145,7 @@ func (q *Query) ResolveNetworkInstance() (*resources.NetworkInstance, error) { if present { return instance, err } - if deployed() { + if Environment() != "" && !IsLocal() { return nil, absentCarrier(info) } local, localErr := q.localNetworkInstance(workspace, selected) @@ -157,15 +155,7 @@ func (q *Query) ResolveNetworkInstance() (*resources.NetworkInstance, error) { return local, nil } -// deployed reports whether this process runs in an explicit, non-local -// environment: one the runtime composed and started, rather than one run from -// a workspace. Environment reads the runtime's carrier live, as every -// process-identity accessor does. -func deployed() bool { - return Environment() != "" && !IsLocal() -} - -// resolveFromAuthoritativeCarrier answers a deployed process from its carriers +// resolveFromAuthoritativeCarrier answers a workspace-free process from its carriers // alone. The carrier is keyed by the query's canonical identity — the // declaration is not on disk to supply any part of it — and nothing is judged // here: the composition already decided, at render, that this consumer may