Skip to content

Commit b39ce10

Browse files
Merge pull request #8 from codefly-dev/issue-7-chore-fix-test-failures-before-release
fix: restore build and Kubernetes conformance for core v0.3.0 (#7)
2 parents b7e4c82 + 140ebc7 commit b39ce10

11 files changed

Lines changed: 154 additions & 18 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ concurrency:
1313
jobs:
1414
ci:
1515
uses: codefly-dev/core/.github/workflows/go-service-ci.yml@main
16+
with:
17+
# The Python runtime tests drive `uv` on the host (dependency sync,
18+
# pytest); it isn't on the runner by default.
19+
setup-run: |
20+
curl -LsSf https://astral.sh/uv/install.sh | sh
21+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

‎deployment_test.go‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
11
package main
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57

68
agenttesting "github.com/codefly-dev/core/agents/testing"
9+
"github.com/stretchr/testify/require"
710
)
811

912
func TestDeploymentTemplates(t *testing.T) {
10-
agenttesting.AssertKustomizeTemplates(t, deploymentFS, Parameters{})
13+
destination := agenttesting.AssertKustomizeTemplates(t, deploymentFS, Parameters{})
14+
15+
rendered, err := os.ReadFile(filepath.Join(destination, "base", "deployment.yaml"))
16+
require.NoError(t, err)
17+
manifest := string(rendered)
18+
19+
// Kubernetes rejects runAsNonRoot when the image user is non-numeric, so the
20+
// workload must pin a numeric UID matching the Dockerfile's appuser (1000).
21+
require.Contains(t, manifest, "runAsUser: 1000")
22+
// readOnlyRootFilesystem leaves the container without writable storage, so a
23+
// scratch mount at /tmp is required for the app to run.
24+
require.Contains(t, manifest, "mountPath: /tmp")
25+
// The scratch volume is bounded so a runaway writer evicts only this pod
26+
// rather than filling node ephemeral storage.
27+
require.Contains(t, manifest, "sizeLimit: 1Gi")
28+
// $HOME must point at the writable mount so ~/.cache writes don't hit the
29+
// read-only root filesystem.
30+
require.Contains(t, manifest, "value: /tmp")
1131
}

‎main_test.go‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func testCreateToRun(t *testing.T, runtimeContext *basev0.RuntimeContext) {
9494

9595
require.Equal(t, 1, len(runtime.Endpoints))
9696

97-
networkMappings, err := networkManager.GenerateNetworkMappings(ctx, env, workspace, runtime.Identity, runtime.Endpoints)
97+
networkMappings, err := networkManager.GenerateNetworkMappings(ctx, env, workspace, runtime.Identity, runtime.Endpoints, runtimeContext)
9898
require.NoError(t, err)
9999
require.Equal(t, 1, len(networkMappings))
100100

@@ -124,6 +124,10 @@ func testRun(t *testing.T, runtime *Runtime, ctx context.Context, identity *base
124124
ProposedNetworkMappings: networkMappings})
125125
require.NoError(t, err)
126126
require.NotNil(t, init)
127+
// Init reports failures through the response status, not the Go error, so a
128+
// bare NoError check would let a failed Init through and surface as a
129+
// confusing empty-network-mappings error further down.
130+
require.Equal(t, runtimev0.InitStatus_READY, init.GetStatus().GetState(), init.GetStatus().GetMessage())
127131

128132
instance, err := resources.FindNetworkInstanceInNetworkMappings(ctx, init.NetworkMappings, runtime.FastAPI.RestEndpoint, resources.NewNativeNetworkAccess())
129133
require.NoError(t, err)

‎runtime.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"path"
78
"strings"
89

@@ -126,6 +127,12 @@ func (s *Runtime) CreateRunnerEnvironment(ctx context.Context) error {
126127
return s.Wool.Wrapf(err, "cannot create docker runner")
127128
}
128129
dockerEnv.WithPause()
130+
// Run as the invoking host user. uv sync writes uv.lock into the
131+
// bind-mounted source and populates the venv; as root those files
132+
// become root-owned on the host, and a later Init that hashes uv.lock
133+
// for its dependency cache then fails with "permission denied" on any
134+
// host where the user isn't root (e.g. Linux CI).
135+
dockerEnv.WithUser(fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()))
129136

130137
instance, err := resources.FindNetworkInstanceInNetworkMappings(ctx, s.NetworkMappings, s.FastAPI.RestEndpoint, resources.NewNativeNetworkAccess())
131138
if err != nil {
@@ -147,6 +154,14 @@ func (s *Runtime) CreateRunnerEnvironment(ctx context.Context) error {
147154
if err != nil {
148155
return s.Wool.Wrapf(err, "cannot create cache location")
149156
}
157+
// uv's download cache defaults to $HOME/.cache/uv; the host user has no
158+
// home inside the image, so give uv a writable, host-owned cache mount.
159+
uvCache, err := s.LocalDirCreate(ctx, ".cache/container/uv")
160+
if err != nil {
161+
return s.Wool.Wrapf(err, "cannot create uv cache location")
162+
}
163+
dockerEnv.WithMount(uvCache, "/uv-cache")
164+
dockerEnv.WithEnvironmentVariables(ctx, resources.Env("UV_CACHE_DIR", "/uv-cache"))
150165
s.runnerEnvironment = dockerEnv
151166

152167
case s.Base.Runtime.IsNixRuntime():

‎templates/builder/Dockerfile.tmpl‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ FROM {{.Builder}} as runtime
1717

1818
WORKDIR /app
1919

20-
RUN adduser -D appuser
20+
# UID pinned to match runAsUser in the kustomize Deployment: Kubernetes'
21+
# runAsNonRoot check rejects a container whose image user is non-numeric.
22+
RUN adduser -D -u 1000 appuser
2123
USER appuser
2224

2325
ENV VIRTUAL_ENV=/app/.venv \

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

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,75 @@ spec:
1414
app: {{ .Service.Name.DNSCase }}
1515
sha: {{ .Sha }}
1616
spec:
17+
automountServiceAccountToken: false
18+
terminationGracePeriodSeconds: 30
19+
securityContext:
20+
runAsNonRoot: true
21+
runAsUser: 1000
22+
runAsGroup: 1000
23+
seccompProfile:
24+
type: RuntimeDefault
1725
containers:
1826
- name: {{ .Service.Name.DNSCase }}
19-
image: image:tag
27+
image: {{ .Image.FullName }}
28+
ports:
29+
- containerPort: 8080
30+
securityContext:
31+
allowPrivilegeEscalation: false
32+
runAsNonRoot: true
33+
readOnlyRootFilesystem: true
34+
seccompProfile:
35+
type: RuntimeDefault
36+
capabilities:
37+
drop:
38+
- ALL
39+
resources:
40+
requests:
41+
cpu: 10m
42+
memory: 64Mi
43+
limits:
44+
cpu: 500m
45+
memory: 256Mi
46+
startupProbe:
47+
httpGet:
48+
path: /version
49+
port: 8080
50+
failureThreshold: 30
51+
periodSeconds: 2
52+
readinessProbe:
53+
httpGet:
54+
path: /version
55+
port: 8080
56+
livenessProbe:
57+
httpGet:
58+
path: /version
59+
port: 8080
60+
volumeMounts:
61+
- name: tmp
62+
mountPath: /tmp
2063
envFrom:
2164
- configMapRef:
2265
name: config-{{ .Service.Name.DNSCase }}
66+
{{- if not .Restricted }}
2367
- secretRef:
2468
name: secret-{{ .Service.Name.DNSCase }}
69+
{{- end }}
70+
env:
71+
# readOnlyRootFilesystem makes $HOME unwritable; point caches at the
72+
# writable scratch mount so libraries that use ~/.cache still work.
73+
- name: HOME
74+
value: /tmp
75+
{{- if .Restricted }}
76+
{{- range $environmentVariable, $reference := .SecretReferences }}
77+
- name: {{ $environmentVariable }}
78+
valueFrom:
79+
secretKeyRef:
80+
name: {{ $reference.Name }}
81+
key: {{ $reference.Key }}
82+
optional: {{ $reference.Optional }}
83+
{{- end }}
84+
{{- end }}
85+
volumes:
86+
- name: tmp
87+
emptyDir:
88+
sizeLimit: 1Gi

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ kind: Namespace
33
metadata:
44
name: "{{ .Namespace }}"
55
labels:
6+
app.kubernetes.io/managed-by: codefly
67
istio-injection: "enabled"
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
resources:
22
- ../../base
33
- configmap.yaml
4+
{{- if not .Restricted }}
45
- secret.yaml
5-
6-
images:
7-
- name: image:tag
8-
newName: {{.Image.Name}}
9-
newTag: {{.Image.Tag}}
6+
{{- end }}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{{- if not .Restricted }}
12
apiVersion: v1
23
kind: Secret
34
metadata:
@@ -6,4 +7,5 @@ metadata:
67
data:
78
{{- range $key, $value := .Deployment.SecretMap }}
89
{{ $key }}: "{{ $value }}"
9-
{{- end }}
10+
{{- end }}
11+
{{- end }}

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
1+
import os
2+
13
from fastapi import FastAPI
24
from fastapi.middleware.cors import CORSMiddleware
35
import codefly_sdk.codefly as codefly
46

5-
codefly.init()
7+
8+
def _service_manifest_reachable() -> bool:
9+
# codefly.init() walks parent directories for service.codefly.yaml but does
10+
# not stop at the filesystem root, so it loops forever when the manifest is
11+
# absent (e.g. tests running against an isolated source snapshot). Only init
12+
# when the manifest is actually reachable; runtime config is otherwise
13+
# supplied through CODEFLY__* environment variables.
14+
directory = os.getcwd()
15+
while True:
16+
if os.path.isfile(os.path.join(directory, "service.codefly.yaml")):
17+
return True
18+
parent = os.path.dirname(directory)
19+
if parent == directory:
20+
return False
21+
directory = parent
22+
23+
24+
if _service_manifest_reachable():
25+
codefly.init()
626

727
app = FastAPI()
828

0 commit comments

Comments
 (0)