Skip to content

Commit a3fe434

Browse files
fix(deploy): hand the process CODEFLY_GRPC_PORT in the deployment (#124)
A deployed FastAPI service with grpc-server on got the gRPC containerPort and Service port from the kustomize template, but its container env carried only CODEFLY_CONNECT_PORT. Locally the runtime hands the process CODEFLY_GRPC_PORT; in the cluster the app had to guess the port the agent had already rendered into the Service. The scaffold guessed 9090, and a service that reads the variable strictly crashlooped with KeyError. The deployment template now projects CODEFLY_GRPC_PORT from Parameters.GRPCPort whenever GRPCEnabled is set, mirroring the Connect block, so the variable and the Service port come from the same value. The Parameters doc comment and the scaffold's main.py comment say so. TestDeploymentRendersGRPCPort asserts the variable and its value on a gRPC-enabled render and its absence when gRPC is off; it fails against the previous template. Co-authored-by: antoinetoussaint-byte <312536060+antoinetoussaint-byte@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 7513c79 commit a3fe434

23 files changed

Lines changed: 1378 additions & 7 deletions

File tree

‎builder.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,9 @@ func (s *Builder) Upgrade(ctx context.Context, req *builderv0.UpgradeRequest) (*
504504
// Parameters is the template parameter set for the k8s deployment.
505505
type Parameters struct {
506506
Probes string
507-
// GRPCEnabled adds the gRPC containerPort and Service port to the rendered
508-
// manifests. GRPCPort is the fixed in-cluster port the grpc.aio listener
509-
// binds (the app defaults to it when CODEFLY_GRPC_PORT is unset).
507+
// GRPCEnabled adds the gRPC containerPort, Service port and
508+
// CODEFLY_GRPC_PORT, the variable the app binds its grpc.aio listener on.
509+
// GRPCPort is the fixed in-cluster port it binds.
510510
GRPCEnabled bool
511511
GRPCPort int
512512
// ConnectEnabled adds the Connect face's containerPort, Service port and

‎grpc_test.go‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,10 @@ func TestGRPCEndpointReloadsContract(t *testing.T) {
190190
require.Contains(t, names, "Echo")
191191
}
192192

193-
// TestDeploymentRendersGRPCPort proves the gRPC container/service ports appear
194-
// only when the deployment parameters opt in.
193+
// TestDeploymentRendersGRPCPort proves the gRPC container/service ports, and
194+
// the CODEFLY_GRPC_PORT the app binds on, appear only when the deployment
195+
// parameters opt in: a deployed service never guesses the port the Service
196+
// routes to.
195197
func TestDeploymentRendersGRPCPort(t *testing.T) {
196198
enabled := agenttesting.AssertKustomizeTemplates(t, deploymentFS, deploymentTestParameters(t, Parameters{GRPCEnabled: true, GRPCPort: 9090}))
197199
svc, err := os.ReadFile(filepath.Join(enabled, "base", "service.yaml"))
@@ -202,9 +204,13 @@ func TestDeploymentRendersGRPCPort(t *testing.T) {
202204
deployment, err := os.ReadFile(filepath.Join(enabled, "base", "deployment.yaml"))
203205
require.NoError(t, err)
204206
require.Contains(t, string(deployment), "containerPort: 9090")
207+
require.Contains(t, string(deployment), "- name: CODEFLY_GRPC_PORT\n value: \"9090\"")
205208

206209
disabled := agenttesting.AssertKustomizeTemplates(t, deploymentFS, deploymentTestParameters(t, Parameters{}))
207210
svc, err = os.ReadFile(filepath.Join(disabled, "base", "service.yaml"))
208211
require.NoError(t, err)
209212
require.NotContains(t, string(svc), "grpc-port")
213+
deployment, err = os.ReadFile(filepath.Join(disabled, "base", "deployment.yaml"))
214+
require.NoError(t, err)
215+
require.NotContains(t, string(deployment), "CODEFLY_GRPC_PORT")
210216
}

‎templates/deployment/kustomize/base/deployment.yaml.tmpl‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ spec:
7474
# writable scratch mount so libraries that use ~/.cache still work.
7575
- name: HOME
7676
value: /tmp
77+
{{- if .Deployment.Parameters.GRPCEnabled }}
78+
# grpc-server is on: the app binds its grpc.aio listener on this
79+
# port, the one the grpc containerPort and the Service declare.
80+
- name: CODEFLY_GRPC_PORT
81+
value: "{{ .Deployment.Parameters.GRPCPort }}"
82+
{{- end }}
7783
{{- if .Deployment.Parameters.ConnectEnabled }}
7884
# connect-endpoint is on: the app serves the Connect face on this
7985
# port, and only when the variable is set.

‎templates/factory/code/src/main.py.tmpl‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ for plugin in plugins:
5656
{{- if .GRPCEnabled }}
5757

5858
# gRPC server — runs a grpc.aio listener in this process alongside FastAPI.
59-
# The port is provided by codefly at runtime; it falls back to the standard
60-
# gRPC port in container/Kubernetes where the mapping is fixed.
59+
# codefly hands the process its port as CODEFLY_GRPC_PORT, locally and in the
60+
# deployment.
6161
_grpc_server = None
6262
# The Connect face (Connect, gRPC-Web and gRPC on one listener over the gRPC
6363
# server): codefly hands the process CODEFLY_CONNECT_PORT exactly when the
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
## Welcome to your `fastapi` project!
2+
3+
This agent follows the [fastapi best practices](https://github.com/zhanymkanov/fastapi-best-practices) layout.
4+
5+
Dependency management uses [uv](https://docs.astral.sh/uv/). Common commands:
6+
7+
- `uv sync` — install/refresh the venv from `pyproject.toml` + `uv.lock`
8+
- `uv add <package>` — add a runtime dependency
9+
- `uv add --dev <package>` — add a dev dependency
10+
- `uv run pytest` — run tests
11+
- `uv run uvicorn src.main:app --reload` — run the dev server
12+
13+
`codefly run service <name>` does all of the above for you via the agent.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Codefly's uvicorn logging filter. Generated by the python-fastapi agent;
2+
regenerated on every start, so edits here do not survive. Health probes are
3+
not traffic: an access-log line for /healthz or /readyz is dropped before it is
4+
formatted, and every other line is written exactly as uvicorn writes it."""
5+
import logging
6+
import re
7+
8+
PROBE_PATHS = re.compile(r"^/(healthz|readyz|livez|health|ready|live|ping)$")
9+
10+
11+
class ProbeFilter(logging.Filter):
12+
def filter(self, record: logging.LogRecord) -> bool:
13+
args = record.args
14+
if isinstance(args, tuple) and len(args) >= 3 and isinstance(args[2], str):
15+
path = args[2].split("?", 1)[0]
16+
if PROBE_PATHS.match(path):
17+
return False
18+
return True
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[project]
2+
name = "base"
3+
version = "0.0.0"
4+
description = ""
5+
requires-python = ">=3.10"
6+
dependencies = [
7+
"fastapi>=0.109.0",
8+
"uvicorn>=0.25.0",
9+
"codefly-sdk>=0.0.14",
10+
"pydantic>=2.9.2",
11+
]
12+
13+
[dependency-groups]
14+
dev = [
15+
"pytest>=8.2.0",
16+
"httpx>=0.27.0",
17+
"pytest-asyncio>=0.23.6",
18+
"codefly-cli>=0.0.19",
19+
"ruff>=0.6.0",
20+
]
21+
22+
[tool.uv]
23+
package = false
24+
# A CONSTRAINT, not a dependency: it bounds a version only if something
25+
# else pulls the package in. multidict 7.1.0 (PyPI, 2026-10-09) shipped
26+
# without manylinux wheels, so `uv sync` in the Debian runtime had nothing
27+
# to install and no compiler to build it -- every fresh service failed at
28+
# Create. Lift it once a 7.1.x with manylinux wheels exists; the durable
29+
# fix is a uv.lock shipped with this template so a create never resolves
30+
# whatever PyPI published an hour ago.
31+
constraint-dependencies = ["multidict<7.1"]
32+
33+
[tool.pytest.ini_options]
34+
# "." — not "src" — so the `from src.X` imports in the scaffolded tests
35+
# resolve. src/ ships with an __init__.py making it a proper package.
36+
pythonpath = ["."]
37+
testpaths = ["tests"]
38+
# pytest-asyncio 1.x needs an explicit mode. "auto" picks up async def
39+
# tests without needing @pytest.mark.asyncio on every one; keeps the
40+
# boilerplate low for scaffolded services.
41+
asyncio_mode = "auto"

‎testdata/recipe315327749/mod/svc-1793294758768/code/src/__init__.py‎

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Version(BaseModel):
5+
version: str
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from fastapi import APIRouter
2+
3+
from src.admin.models import Version
4+
from src.admin.version import get_version
5+
6+
router = APIRouter()
7+
8+
9+
@router.get("/version", response_model=Version)
10+
async def version():
11+
return get_version()

0 commit comments

Comments
 (0)