Skip to content
Draft
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
16 changes: 8 additions & 8 deletions docs/network-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Connection strings change across environments:

| Environment | Postgres address |
|---|---|
| Local (native) | `localhost:5432` |
| Local (native) | `127.0.0.1:5432` |
| Local (Docker) | `host.docker.internal:5432` |
| k8s (same namespace) | `postgres.default.svc:5432` |
| Production | `db.prod.internal:5432` |
Expand All @@ -21,7 +21,7 @@ Every endpoint gets a **NetworkMapping** containing multiple **NetworkInstances*
mapping := &basev0.NetworkMapping{
Endpoint: grpcEndpoint, // the logical endpoint
Instances: []*basev0.NetworkInstance{
Native(endpoint, port), // localhost:PORT
Native(endpoint, port), // 127.0.0.1:PORT
Container(endpoint, port), // host.docker.internal:PORT
Public(endpoint, port), // configurable hostname:PORT
},
Expand All @@ -33,14 +33,14 @@ At runtime, the consumer filters for the access type matching its context:
```go
access := resources.NewNativeNetworkAccess() // or Container, Public
instance := resources.FilterNetworkInstance(ctx, mapping.Instances, access)
// instance.Address = "localhost:34523"
// instance.Address = "127.0.0.1:34523"
```

### Access Types

| Type | Hostname | When to use |
|---|---|---|
| `native` | `localhost` | Process runs directly on host |
| `native` | `127.0.0.1` | Process runs directly on host |
| `container` | `host.docker.internal` | Process runs in Docker, connecting to host |
| `public` | configurable | Production, external access |

Expand Down Expand Up @@ -106,7 +106,7 @@ configs := []*basev0.Configuration{
{
Name: "connection",
ConfigurationValues: []*basev0.ConfigurationValue{
{Key: "url", Value: "postgresql://localhost:5432/mydb"},
{Key: "url", Value: "postgresql://127.0.0.1:5432/mydb"},
{Key: "password", Value: "secret", Secret: true},
},
},
Expand All @@ -120,7 +120,7 @@ configs := []*basev0.Configuration{
The CLI resolves the dependency graph, collects configs from all upstream services, and injects them as environment variables:

```
CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://localhost:5432/mydb
CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://127.0.0.1:5432/mydb
CODEFLY__SERVICE_SECRET_CONFIGURATION__STORE__POSTGRES__CONNECTION__PASSWORD=secret
```

Expand Down Expand Up @@ -534,10 +534,10 @@ When the CLI starts:

1. Starts `postgres` agent → Docker container on deterministic port (e.g., 23450)
2. Generates NetworkMapping:
- Native: `localhost:23450`
- Native: `127.0.0.1:23450`
- Container: `host.docker.internal:23450`
3. Starts `api-server` agent, passes postgres NetworkMapping
4. Sets env var: `CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://localhost:23450/postgres`
4. Sets env var: `CODEFLY__SERVICE_CONFIGURATION__STORE__POSTGRES__CONNECTION__URL=postgresql://127.0.0.1:23450/postgres`
5. API server reads env var, connects to postgres

If the API server runs in Docker instead of natively, the CLI detects `CODEFLY__RUNTIME_CONTEXT=container` and the env var becomes:
Expand Down
2 changes: 1 addition & 1 deletion network/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// the CLI's remotenetwork package.
//
// Three NetworkAccess types describe how a peer reaches an instance:
// Native (localhost), Container (host.docker.internal), and Public
// Native (127.0.0.1), Container (host.docker.internal), and Public
// (load balancer / ingress). Callers filter mappings by access type
// to pick the right address for their context.
//
Expand Down
33 changes: 33 additions & 0 deletions network/native_loopback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package network_test

import (
"net"
"testing"

basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
"github.com/codefly-dev/core/network"
"github.com/codefly-dev/core/standards"
"github.com/stretchr/testify/require"
)

// Native mappings must use the same address family as the IPv4 loopback
// publication used by local containers. A hostname can resolve to IPv6, where
// Docker Desktop can reset the connection instead of allowing IPv4 fallback.
func TestNativeMappingMatchesIPv4LoopbackPublication(t *testing.T) {
listener, err := net.Listen("tcp4", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, listener.Close()) })
port := uint16(listener.Addr().(*net.TCPAddr).Port)
for _, api := range []string{standards.TCP, standards.GRPC, standards.HTTP, standards.CONNECT} {
t.Run(api, func(t *testing.T) {
instance := network.Native(&basev0.Endpoint{Api: api}, port)
ip := net.ParseIP(instance.Hostname)
require.NotNil(t, ip, "native publication must not depend on hostname address-family selection")
require.NotNil(t, ip.To4())
require.True(t, ip.IsLoopback())
connection, err := net.Dial("tcp", instance.Host)
require.NoError(t, err)
require.NoError(t, connection.Close())
})
}
}
8 changes: 6 additions & 2 deletions network/runtime_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import (

const Localhost = "localhost"

// NativeLoopbackHost matches IPv4-only local container publication. Using a
// hostname can select IPv6 and reach a different listener or a resetting proxy.
const NativeLoopbackHost = "127.0.0.1"

// RuntimeManager tracks per-port allocation across the entire
// service graph. The CLI fan-allocates ports for parallel services,
// so concurrent GenerateNetworkMappings / AllocateTemporaryPort calls share
Expand Down Expand Up @@ -51,7 +55,7 @@ func Container(endpoint *basev0.Endpoint, port uint16) *basev0.NetworkInstance {
}

func Native(endpoint *basev0.Endpoint, port uint16) *basev0.NetworkInstance {
host := Localhost
host := NativeLoopbackHost
var instance *basev0.NetworkInstance
if standards.IsHTTPBasedAPI(endpoint.Api) {
instance = resources.NewHTTPNetworkInstance(host, port, resources.EndpointSecured(endpoint))
Expand All @@ -62,7 +66,7 @@ func Native(endpoint *basev0.Endpoint, port uint16) *basev0.NetworkInstance {
return instance
}

// NativeFor computes the deterministic native (localhost) NetworkInstance an
// NativeFor computes the deterministic native loopback NetworkInstance an
// endpoint binds to under a normal `codefly run` — WITHOUT a running flow.
//
// It mirrors RuntimeManager.GenerateNetworkMappings' Native() path exactly
Expand Down
4 changes: 2 additions & 2 deletions network/runtime_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestConnectNetworkInstancesUseHTTPAddresses(t *testing.T) {
} {
require.Contains(t, instance.Address, "://", name)
}
require.Equal(t, "http://localhost:8081", network.Native(endpoint, 8081).Address)
require.Equal(t, "http://127.0.0.1:8081", network.Native(endpoint, 8081).Address)
require.Equal(
t,
"https://accounts.example.test:443",
Expand All @@ -50,7 +50,7 @@ func TestDeclaredTLSEndpointsUseHTTPSMappings(t *testing.T) {
}},
}

require.Equal(t, "https://localhost:8443", network.Native(endpoint, 8443).Address)
require.Equal(t, "https://127.0.0.1:8443", network.Native(endpoint, 8443).Address)
require.Equal(t, "https://host.docker.internal:8443", network.Container(endpoint, 8443).Address)
require.Equal(t, "https://localhost:8443", network.PublicDefault(endpoint, 8443).Address)
}
Expand Down