Skip to content

Commit d282010

Browse files
antoinetoussaint-byteAntoineToussaintclaude
authored
fix(deploy): a restricted render probes natively; refuse what a native probe cannot express (#91)
A cell's execution admission refuses any container with an exec probe, and model is down on it. Restricted renders (what a cell runs) now render the kubelet's own handlers: a transport predicate is a tcpSocket probe, an HTTP predicate an httpGet on the same path (HTTPS when the endpoint is secured), with the authored timing kept. A predicate those handlers cannot express — a response-body check, or a status outside the kubelet's 200–399 — is refused at render, never weakened. The local apply keeps probe.py's exec probe. Co-authored-by: Antoine Toussaint <toussaint.antoine@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f4f5f3f commit d282010

3 files changed

Lines changed: 138 additions & 9 deletions

File tree

‎builder.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,9 @@ func (s *Builder) Deploy(ctx context.Context, req *builderv0.DeploymentRequest)
452452
if plan.Startup == nil || plan.Liveness == nil {
453453
return nil, fmt.Errorf("the Kubernetes manifest profile requires explicit endpoint health.startup and health.liveness; readiness is not a restart policy")
454454
}
455-
probes, err := deploymentProbes(s.FastAPI.RestEndpoint)
455+
// A restricted render is what a cell runs, and a cell's execution
456+
// admission refuses an exec probe: probe natively there.
457+
probes, err := deploymentProbesFor(s.FastAPI.RestEndpoint, req.GetDeployment().GetKubernetes().GetProfile())
456458
if err != nil {
457459
return nil, err
458460
}

‎probes.go‎

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"strings"
88
"time"
99

10+
"github.com/codefly-dev/core/agents/services"
1011
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
12+
builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0"
1113
"github.com/codefly-dev/core/resources"
1214
"gopkg.in/yaml.v3"
1315
)
@@ -30,7 +32,16 @@ type localProbe struct {
3032
// deploymentProbes preserves the producing endpoint's three separate intents.
3133
// Kubernetes has only one probe per intent; predicates for additional listeners
3234
// need an explicit combined implementation rather than being silently ignored.
33-
func deploymentProbes(endpoint *basev0.Endpoint) (string, error) {
35+
//
36+
// native selects Kubernetes' own probes (tcpSocket, httpGet) instead of the
37+
// container-local exec probe. A restricted render is what a cell runs, and a
38+
// cell's execution admission refuses any container with an exec probe: it runs
39+
// a command of the delivering party's choosing inside a pod that already holds
40+
// the workload identity. A native probe expresses less than probe.py (no body
41+
// predicate, no redirect refusal, no certificate pinning, and the kubelet takes
42+
// any 200–399 status as success), so a predicate it cannot express is refused
43+
// at render rather than weakened.
44+
func deploymentProbes(endpoint *basev0.Endpoint, native bool) (string, error) {
3445
if endpoint == nil {
3546
return "", fmt.Errorf("HTTP endpoint is required for deployment probes")
3647
}
@@ -48,7 +59,7 @@ func deploymentProbes(endpoint *basev0.Endpoint) (string, error) {
4859
if intent.probe == nil {
4960
continue
5061
}
51-
value, err := kubernetesProbe(endpoint, intent.probe, intent.name)
62+
value, err := kubernetesProbe(endpoint, intent.probe, intent.name, native)
5263
if err != nil {
5364
return "", fmt.Errorf("%s: %w", intent.name, err)
5465
}
@@ -61,7 +72,7 @@ func deploymentProbes(endpoint *basev0.Endpoint) (string, error) {
6172
return " " + strings.ReplaceAll(strings.TrimSuffix(string(raw), "\n"), "\n", "\n "), nil
6273
}
6374

64-
func kubernetesProbe(endpoint *basev0.Endpoint, probe *basev0.Probe, intent string) (map[string]any, error) {
75+
func kubernetesProbe(endpoint *basev0.Endpoint, probe *basev0.Probe, intent string, native bool) (map[string]any, error) {
6576
config := localProbe{Port: 8080, Secured: resources.EndpointSecured(endpoint), Timeout: 1}
6677
switch predicate := probe.GetPredicate().(type) {
6778
case *basev0.Probe_Transport:
@@ -111,10 +122,68 @@ func kubernetesProbe(endpoint *basev0.Endpoint, probe *basev0.Probe, intent stri
111122
}
112123
result["successThreshold"] = n
113124
}
125+
if native {
126+
handler, err := nativeProbe(config)
127+
if err != nil {
128+
return nil, err
129+
}
130+
for key, value := range handler {
131+
result[key] = value
132+
}
133+
return result, nil
134+
}
114135
raw, err := json.Marshal(config)
115136
if err != nil {
116137
return nil, err
117138
}
118139
result["exec"] = map[string]any{"command": []string{"python", "-I", "-c", probeScript, string(raw)}}
119140
return result, nil
120141
}
142+
143+
// deploymentProbesFor renders the endpoint's probes for the requested output
144+
// profile: native for a restricted render (what a cell runs), the
145+
// container-local exec probe otherwise.
146+
func deploymentProbesFor(endpoint *basev0.Endpoint, profile builderv0.KubernetesOutputProfile) (string, error) {
147+
return deploymentProbes(endpoint, restrictedOutput(profile))
148+
}
149+
150+
// restrictedOutput reports whether the deployment selects the restricted,
151+
// portable output contract a cell runs. An unparseable profile is treated as
152+
// restricted, the stricter of the two.
153+
func restrictedOutput(profile builderv0.KubernetesOutputProfile) bool {
154+
parsed, err := services.ParseOutputProfile(profile)
155+
if err != nil {
156+
return true
157+
}
158+
return parsed.Restricted()
159+
}
160+
161+
// kubeletSuccess is the status range a kubelet httpGet probe takes as success.
162+
var kubeletSuccess = [2]uint32{200, 399}
163+
164+
// nativeProbe expresses a predicate with the kubelet's own handler, or refuses
165+
// it. A transport predicate is a TCP connect. An HTTP predicate is an httpGet on
166+
// the same path, over HTTPS when the endpoint is secured; it is refused when it
167+
// checks the response body, or accepts a status the kubelet would not, because
168+
// the kubelet cannot check either.
169+
func nativeProbe(config localProbe) (map[string]any, error) {
170+
switch config.Kind {
171+
case "transport":
172+
return map[string]any{"tcpSocket": map[string]any{"port": config.Port}}, nil
173+
case "http":
174+
if config.Body != "" {
175+
return nil, fmt.Errorf("a cell refuses an exec probe, and a native HTTP probe cannot check that the response body contains %q; drop the body predicate or probe a path whose status alone answers", config.Body)
176+
}
177+
for _, status := range config.Statuses {
178+
if status[0] < kubeletSuccess[0] || status[1] > kubeletSuccess[1] {
179+
return nil, fmt.Errorf("a cell refuses an exec probe, and a native HTTP probe takes only %d–%d as success, so it cannot accept %d–%d", kubeletSuccess[0], kubeletSuccess[1], status[0], status[1])
180+
}
181+
}
182+
get := map[string]any{"path": config.Path, "port": config.Port}
183+
if config.Secured {
184+
get["scheme"] = "HTTPS"
185+
}
186+
return map[string]any{"httpGet": get}, nil
187+
}
188+
return nil, fmt.Errorf("predicate kind %q has no native probe", config.Kind)
189+
}

‎probes_test.go‎

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func healthEndpoint(t *testing.T, secured bool, health *resources.Health) *basev
3030

3131
func renderedProbe(t *testing.T, endpoint *basev0.Endpoint) map[string]any {
3232
t.Helper()
33-
probes, err := deploymentProbes(endpoint)
33+
probes, err := deploymentProbes(endpoint, false)
3434
require.NoError(t, err)
3535
var result map[string]any
3636
require.NoError(t, yaml.Unmarshal([]byte(probes), &result))
@@ -76,10 +76,10 @@ func TestDeploymentRefusesUnrepresentableTimingAndPredicates(t *testing.T) {
7676
{Kind: "http", Path: "/readyz", Period: "1500ms"},
7777
{Kind: "agent"},
7878
} {
79-
_, err := deploymentProbes(healthEndpoint(t, false, &resources.Health{Readiness: probe}))
79+
_, err := deploymentProbes(healthEndpoint(t, false, &resources.Health{Readiness: probe}), false)
8080
require.Error(t, err)
8181
}
82-
_, err := deploymentProbes(healthEndpoint(t, false, &resources.Health{Liveness: &resources.Probe{Kind: "transport", SuccessThreshold: 2}}))
82+
_, err := deploymentProbes(healthEndpoint(t, false, &resources.Health{Liveness: &resources.Probe{Kind: "transport", SuccessThreshold: 2}}), false)
8383
require.Error(t, err)
8484
}
8585

@@ -161,7 +161,7 @@ func deploymentTestParameters(t *testing.T, parameters Parameters) Parameters {
161161
Liveness: &resources.Probe{Kind: "transport"},
162162
}
163163
var err error
164-
parameters.Probes, err = deploymentProbes(healthEndpoint(t, false, health))
164+
parameters.Probes, err = deploymentProbes(healthEndpoint(t, false, health), false)
165165
require.NoError(t, err)
166166
return parameters
167167
}
@@ -186,8 +186,66 @@ func TestBuilderLoadsAuthoredTLSAndHealthBeforeProjection(t *testing.T) {
186186
endpoint := loaded.FastAPI.RestEndpoint
187187
require.True(t, resources.EndpointSecured(endpoint))
188188
require.Equal(t, "/readyz", endpoint.GetHealth().GetReadiness().GetHttp().GetPath())
189-
probes, err := deploymentProbes(endpoint)
189+
probes, err := deploymentProbes(endpoint, false)
190190
require.NoError(t, err)
191191
require.Contains(t, probes, "/readyz")
192192
require.NotContains(t, probes, "/version")
193193
}
194+
195+
// A restricted render is what a cell runs, and a cell's execution admission
196+
// refuses any container with an exec probe. There the intents become the
197+
// kubelet's own handlers, with the authored timing kept; a predicate those
198+
// handlers cannot express is refused at render, never weakened.
199+
func TestRestrictedRenderProbesNativelyAndRefusesWhatItCannotExpress(t *testing.T) {
200+
native := func(t *testing.T, secured bool, health *resources.Health) map[string]any {
201+
t.Helper()
202+
probes, err := deploymentProbes(healthEndpoint(t, secured, health), true)
203+
require.NoError(t, err)
204+
var result map[string]any
205+
require.NoError(t, yaml.Unmarshal([]byte(probes), &result))
206+
return result
207+
}
208+
rendered := native(t, false, &resources.Health{
209+
Startup: &resources.Probe{Kind: "http", Path: "/healthz", Statuses: []string{"200"}, Period: "5s", Timeout: "3s", FailureThreshold: 12},
210+
Readiness: &resources.Probe{Kind: "http", Path: "/readyz", Statuses: []string{"200"}, Period: "5s", Timeout: "3s", FailureThreshold: 3},
211+
Liveness: &resources.Probe{Kind: "transport", Period: "10s", Timeout: "3s", FailureThreshold: 3},
212+
})
213+
for intent, want := range map[string]map[string]any{
214+
"startupProbe": {"httpGet": map[string]any{"path": "/healthz", "port": 8080}},
215+
"readinessProbe": {"httpGet": map[string]any{"path": "/readyz", "port": 8080}},
216+
"livenessProbe": {"tcpSocket": map[string]any{"port": 8080}},
217+
} {
218+
probe := rendered[intent].(map[string]any)
219+
require.NotContainsf(t, probe, "exec", "%s execs", intent)
220+
for handler, spec := range want {
221+
require.Equalf(t, spec, probe[handler], "%s %s", intent, handler)
222+
}
223+
require.EqualValues(t, 3, probe["timeoutSeconds"], "%s keeps its authored timing", intent)
224+
}
225+
226+
secured := native(t, true, &resources.Health{Readiness: &resources.Probe{Kind: "http", Path: "/readyz", Statuses: []string{"200"}}})
227+
require.Equal(t, "HTTPS", secured["readinessProbe"].(map[string]any)["httpGet"].(map[string]any)["scheme"])
228+
229+
for name, probe := range map[string]*resources.Probe{
230+
"a body predicate": {Kind: "http", Path: "/readyz", Statuses: []string{"200"}, BodyContains: "ready"},
231+
"a status the kubelet refuses": {Kind: "http", Path: "/readyz", Statuses: []string{"404"}},
232+
} {
233+
t.Run(name, func(t *testing.T) {
234+
_, err := deploymentProbes(healthEndpoint(t, false, &resources.Health{Readiness: probe}), true)
235+
require.ErrorContains(t, err, "a cell refuses an exec probe")
236+
})
237+
}
238+
}
239+
240+
// The profile picks the probe: the restricted render a cell runs probes
241+
// natively; the ephemeral local apply keeps the container-local exec probe.
242+
func TestTheOutputProfileSelectsTheProbe(t *testing.T) {
243+
endpoint := healthEndpoint(t, false, &resources.Health{Readiness: &resources.Probe{Kind: "http", Path: "/readyz", Statuses: []string{"200"}}})
244+
restricted, err := deploymentProbesFor(endpoint, builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_RESTRICTED_PORTABLE_V1)
245+
require.NoError(t, err)
246+
require.NotContains(t, restricted, "exec:")
247+
require.Contains(t, restricted, "httpGet:")
248+
local, err := deploymentProbesFor(endpoint, builderv0.KubernetesOutputProfile_KUBERNETES_OUTPUT_PROFILE_EPHEMERAL_LOCAL_APPLY_V1)
249+
require.NoError(t, err)
250+
require.Contains(t, local, "exec:")
251+
}

0 commit comments

Comments
 (0)