Skip to content

Commit 3eb2fca

Browse files
committed
Give actionable build errors
1 parent 2de482f commit 3eb2fca

File tree

5 files changed

+236
-162
lines changed

5 files changed

+236
-162
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ spec:
125125
capabilities:
126126
drop: [ALL]
127127
add: [SETUID, SETGID]
128+
appArmorProfile:
129+
type: Unconfined
130+
seccompProfile:
131+
type: Unconfined
128132
volumeMounts:
129133
- name: docker-config
130134
mountPath: /home/kimia/.docker

src/internal/preflight/check_environment.go

Lines changed: 174 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,44 @@ import (
66
"os/exec"
77
"path/filepath"
88
"strings"
9+
910
"github.com/rapidfort/kimia/internal/build"
1011
"github.com/rapidfort/kimia/pkg/logger"
1112
)
1213

14+
// Environment represents the runtime environment
15+
type Environment int
16+
17+
const (
18+
EnvStandalone Environment = iota
19+
EnvDocker
20+
EnvKubernetes
21+
)
22+
23+
// DetectEnvironment determines if running in Kubernetes, Docker, or standalone
24+
func DetectEnvironment() Environment {
25+
// Check Kubernetes first (most specific)
26+
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" {
27+
return EnvKubernetes
28+
}
29+
30+
// Check Docker via .dockerenv file
31+
if _, err := os.Stat("/.dockerenv"); err == nil {
32+
return EnvDocker
33+
}
34+
35+
// Check Docker via cgroups
36+
if data, err := os.ReadFile("/proc/1/cgroup"); err == nil {
37+
content := string(data)
38+
if strings.Contains(content, "docker") ||
39+
strings.Contains(content, "containerd") {
40+
return EnvDocker
41+
}
42+
}
43+
44+
return EnvStandalone
45+
}
46+
1347
// CheckEnvironment performs comprehensive environment check
1448
func CheckEnvironment() int {
1549
// Get storage driver from environment or default to vfs
@@ -34,19 +68,45 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
3468
builder := build.DetectBuilder()
3569
logger.Info("")
3670
logger.Info("Kimia Environment Check (%s)", builder)
37-
logger.Info("═══════════════════════════════════════════════════════════")
71+
logger.Info("═══════════════════════════════════════════════════════")
3872
logger.Info("")
3973

4074
allGood := true
4175

4276
// Runtime Context
4377
logger.Info("RUNTIME CONTEXT")
4478
uid := os.Getuid()
45-
isK8s := IsInKubernetes()
79+
80+
// CRITICAL: Kimia is rootless-only and does NOT support root mode
81+
if uid == 0 {
82+
logger.Error(" User ID: %d ✗", uid)
83+
logger.Error("")
84+
logger.Error("╔══════════════════════════════════════════════════════╗")
85+
logger.Error("║ KIMIA DOES NOT SUPPORT ROOT ║")
86+
logger.Error("╠══════════════════════════════════════════════════════╣")
87+
logger.Error("║ Kimia is designed to run as a non-root user with ║")
88+
logger.Error("║ capabilities, not as root. ║")
89+
logger.Error("║ ║")
90+
logger.Error("║ Please run as non-root user (UID > 0) ║")
91+
logger.Error("║ ║")
92+
logger.Error("║ For Kubernetes, set: ║")
93+
logger.Error("║ securityContext: ║")
94+
logger.Error("║ runAsUser: 1000 ║")
95+
logger.Error("║ runAsNonRoot: true ║")
96+
logger.Error("║ allowPrivilegeEscalation: true ║")
97+
logger.Error("║ capabilities: ║")
98+
logger.Error("║ drop: [ALL] ║")
99+
logger.Error("║ add: [SETUID, SETGID] ║")
100+
logger.Error("╚══════════════════════════════════════════════════════╝")
101+
logger.Error("")
102+
return 1
103+
}
104+
105+
env := DetectEnvironment()
46106

47107
checkmark := getCheckmark(true)
48108
logger.Info(" User ID: %d %s", uid, checkmark)
49-
logger.Info(" Environment: %s", getEnvironment(isK8s))
109+
logger.Info(" Environment: %s", getEnvironment(env))
50110
logger.Info(" Storage Driver: %s", storageDriver)
51111

52112
if username := os.Getenv("USER"); username != "" {
@@ -78,12 +138,12 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
78138
case "overlay":
79139
hasMknod := caps.HasCapability("CAP_MKNOD")
80140
hasDACOverride := caps.HasCapability("CAP_DAC_OVERRIDE")
81-
141+
82142
logger.Info(" CAP_MKNOD: %s %s (required for overlay)",
83143
getPresence(hasMknod), getCheckmark(hasMknod))
84144
logger.Info(" CAP_DAC_OVERRIDE: %s %s (required for overlay)",
85145
getPresence(hasDACOverride), getCheckmark(hasDACOverride))
86-
146+
87147
if !hasMknod {
88148
logger.Warning(" Warning: MKNOD capability missing (required for overlay storage)")
89149
allGood = false
@@ -138,7 +198,7 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
138198

139199
logger.Info("")
140200

141-
// User Namespaces (always check - Kimia is rootless-only)
201+
// User Namespaces (Kimia is rootless-only, always check)
142202
logger.Info("USER NAMESPACES")
143203
userns, err := CheckUserNamespaces()
144204
if err != nil {
@@ -174,8 +234,8 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
174234
hasRequiredCaps = caps.HasRequiredCapabilities()
175235
// For overlay, also check MKNOD and DAC_OVERRIDE
176236
if storageDriver == "overlay" {
177-
hasRequiredCaps = hasRequiredCaps &&
178-
caps.HasCapability("CAP_MKNOD") &&
237+
hasRequiredCaps = hasRequiredCaps &&
238+
caps.HasCapability("CAP_MKNOD") &&
179239
caps.HasCapability("CAP_DAC_OVERRIDE")
180240
}
181241
}
@@ -199,7 +259,7 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
199259
}
200260
logger.Info("")
201261

202-
// BUILD MODE
262+
// BUILD MODE (Kimia is rootless-only)
203263
fmt.Println("BUILD MODE")
204264

205265
var buildModeAvailable bool
@@ -274,13 +334,13 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
274334

275335
// Verdict
276336
logger.Info("VERDICT")
277-
logger.Info("═══════════════════════════════════════════════════════════")
337+
logger.Info("═══════════════════════════════════════════════════════")
278338
logger.Info("")
279339

280340
if allGood {
281341
logger.Info("✓ Environment is properly configured for building images")
282342
logger.Info("✓ %s", buildModeMethod)
283-
343+
284344
if storage != nil && storage.OverlayAvailable {
285345
logger.Info("✓ Overlay storage available for better performance")
286346
} else {
@@ -332,51 +392,85 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
332392
}
333393

334394
if needsHelp {
335-
logger.Info("Kubernetes Configuration Required:")
336-
logger.Info("")
337-
logger.Info("Some Kubernetes clusters require unconfined seccomp and AppArmor profiles")
338-
logger.Info("for user namespace operations to work properly.")
339-
logger.Info("")
340-
logger.Info("Complete Pod/Job specification:")
341-
logger.Info("")
342-
logger.Info("---")
343-
logger.Info("apiVersion: batch/v1")
344-
logger.Info("kind: Job")
345-
logger.Info("metadata:")
346-
logger.Info(" name: kimia-build")
347-
logger.Info("spec:")
348-
logger.Info(" template:")
349-
logger.Info(" spec:")
350-
logger.Info(" restartPolicy: Never")
351-
logger.Info(" securityContext:")
352-
logger.Info(" runAsUser: 1000")
353-
logger.Info(" runAsGroup: 1000")
354-
logger.Info(" fsGroup: 1000")
355-
logger.Info(" seccompProfile:")
356-
logger.Info(" type: Unconfined # May be required for user namespaces")
357-
logger.Info(" containers:")
358-
logger.Info(" - name: kimia")
359-
logger.Info(" image: ghcr.io/rapidfort/kimia:latest")
360-
logger.Info(" securityContext:")
361-
logger.Info(" runAsUser: 1000")
362-
logger.Info(" runAsGroup: 1000")
363-
logger.Info(" allowPrivilegeEscalation: true # CRITICAL: Required!")
364-
logger.Info(" appArmorProfile:")
365-
logger.Info(" type: Unconfined # May be required for user namespaces")
366-
logger.Info(" seccompProfile:")
367-
logger.Info(" type: Unconfined # May be required for user namespaces")
368-
logger.Info(" capabilities:")
369-
logger.Info(" drop: [ALL]")
370-
371-
if storageDriver == "overlay" {
372-
logger.Info(" add: [SETUID, SETGID, MKNOD, DAC_OVERRIDE]")
373-
} else {
374-
logger.Info(" add: [SETUID, SETGID]")
395+
// Provide environment-specific guidance
396+
switch env {
397+
case EnvKubernetes:
398+
logger.Info("Kubernetes Configuration Required:")
399+
logger.Info("")
400+
logger.Info("Some Kubernetes clusters require unconfined seccomp and AppArmor profiles")
401+
logger.Info("for user namespace operations to work properly.")
402+
logger.Info("")
403+
logger.Info("Complete Pod/Job specification:")
404+
logger.Info("")
405+
logger.Info("---")
406+
logger.Info("apiVersion: batch/v1")
407+
logger.Info("kind: Job")
408+
logger.Info("metadata:")
409+
logger.Info(" name: kimia-build")
410+
logger.Info("spec:")
411+
logger.Info(" template:")
412+
logger.Info(" spec:")
413+
logger.Info(" restartPolicy: Never")
414+
logger.Info(" securityContext:")
415+
logger.Info(" runAsUser: 1000")
416+
logger.Info(" runAsGroup: 1000")
417+
logger.Info(" fsGroup: 1000")
418+
logger.Info(" seccompProfile:")
419+
logger.Info(" type: Unconfined # May be required for user namespaces")
420+
logger.Info(" containers:")
421+
logger.Info(" - name: kimia")
422+
logger.Info(" image: ghcr.io/rapidfort/kimia:latest")
423+
logger.Info(" securityContext:")
424+
logger.Info(" runAsUser: 1000")
425+
logger.Info(" runAsGroup: 1000")
426+
logger.Info(" allowPrivilegeEscalation: true # CRITICAL: Required!")
427+
logger.Info(" appArmorProfile:")
428+
logger.Info(" type: Unconfined # May be required for user namespaces")
429+
logger.Info(" seccompProfile:")
430+
logger.Info(" type: Unconfined # May be required for user namespaces")
431+
logger.Info(" capabilities:")
432+
logger.Info(" drop: [ALL]")
433+
434+
if storageDriver == "overlay" {
435+
logger.Info(" add: [SETUID, SETGID, MKNOD, DAC_OVERRIDE]")
436+
} else {
437+
logger.Info(" add: [SETUID, SETGID]")
438+
}
439+
440+
case EnvDocker:
441+
logger.Info("Docker Configuration Required:")
442+
logger.Info("")
443+
logger.Info("Run Kimia with the following Docker options:")
444+
logger.Info("")
445+
if storageDriver == "overlay" {
446+
logger.Info(" docker run --cap-add SETUID --cap-add SETGID --cap-add MKNOD \\")
447+
logger.Info(" --user 1000:1000 \\")
448+
logger.Info(" ghcr.io/rapidfort/kimia:latest")
449+
} else {
450+
logger.Info(" docker run --cap-add SETUID --cap-add SETGID \\")
451+
logger.Info(" --user 1000:1000 \\")
452+
logger.Info(" ghcr.io/rapidfort/kimia:latest")
453+
}
454+
logger.Info("")
455+
logger.Info("If capabilities don't work, try with SETUID binaries:")
456+
logger.Info(" docker run --security-opt seccomp=unconfined \\")
457+
logger.Info(" --user 1000:1000 \\")
458+
logger.Info(" ghcr.io/rapidfort/kimia:latest")
459+
460+
case EnvStandalone:
461+
logger.Info("Standalone/VM Configuration Required:")
462+
logger.Info("")
463+
logger.Info("Ensure the following are available:")
464+
logger.Info(" 1. User namespaces enabled in kernel")
465+
logger.Info(" 2. Subuid/subgid mappings configured in /etc/subuid and /etc/subgid")
466+
logger.Info(" 3. Either:")
467+
logger.Info(" - Run with capabilities (CAP_SETUID, CAP_SETGID)")
468+
logger.Info(" - Have newuidmap/newgidmap SETUID binaries available")
375469
}
376-
470+
377471
logger.Info("")
378472
logger.Info("NOTE: seccompProfile and appArmorProfile set to Unconfined may be required")
379-
logger.Info(" depending on your cluster's security policies.")
473+
logger.Info(" depending on your environment's security policies.")
380474
logger.Info("")
381475

382476
// Provide specific guidance based on what's wrong
@@ -423,13 +517,24 @@ func CheckEnvironmentWithDriver(storageDriver string) int {
423517
logger.Error(" - Consider adding seccompProfile: Unconfined if still failing")
424518
logger.Error("")
425519
}
426-
520+
427521
logger.Info("Troubleshooting Steps:")
428-
logger.Info(" 1. Apply the YAML configuration above")
429-
logger.Info(" 2. Check pod status: kubectl get pod <pod-name>")
430-
logger.Info(" 3. Describe pod: kubectl describe pod <pod-name>")
431-
logger.Info(" 4. View logs: kubectl logs <pod-name>")
432-
logger.Info(" 5. Run preflight check: kubectl exec <pod-name> -- kimia check-environment")
522+
if env == EnvKubernetes {
523+
logger.Info(" 1. Apply the YAML configuration above")
524+
logger.Info(" 2. Check pod status: kubectl get pod <pod-name>")
525+
logger.Info(" 3. Describe pod: kubectl describe pod <pod-name>")
526+
logger.Info(" 4. View logs: kubectl logs <pod-name>")
527+
logger.Info(" 5. Run preflight check: kubectl exec <pod-name> -- kimia check-environment")
528+
} else if env == EnvDocker {
529+
logger.Info(" 1. Run with the recommended Docker options above")
530+
logger.Info(" 2. Check container status: docker ps")
531+
logger.Info(" 3. View logs: docker logs <container-name>")
532+
logger.Info(" 4. Run preflight check: docker exec <container-name> kimia check-environment")
533+
} else {
534+
logger.Info(" 1. Verify user namespace support: unshare --user --pid --map-root-user echo OK")
535+
logger.Info(" 2. Check subuid/subgid: cat /etc/subuid /etc/subgid")
536+
logger.Info(" 3. Run preflight check: kimia check-environment")
537+
}
433538
logger.Info("")
434539
}
435540

@@ -466,11 +571,17 @@ func getSuccess(success bool) string {
466571
return "Failed"
467572
}
468573

469-
func getEnvironment(isK8s bool) string {
470-
if isK8s {
574+
func getEnvironment(env Environment) string {
575+
switch env {
576+
case EnvKubernetes:
471577
return "Kubernetes"
578+
case EnvDocker:
579+
return "Docker"
580+
case EnvStandalone:
581+
return "Standalone"
582+
default:
583+
return "Unknown"
472584
}
473-
return "Standalone"
474585
}
475586

476587
func checkDependency(name, path string) {
@@ -493,4 +604,4 @@ func checkDependencyVersion(name, command string, versionArg string) {
493604
}
494605
logger.Info(" %s version:%-*s %s %s", name, 12-len(name), "", version, getCheckmark(true))
495606
}
496-
}
607+
}

0 commit comments

Comments
 (0)