diff --git a/hatchery/config.go b/hatchery/config.go index 6041cef..005d53e 100644 --- a/hatchery/config.go +++ b/hatchery/config.go @@ -56,31 +56,32 @@ type LicenseInfo struct { // Container Struct to hold the configuration for Pod Container type Container struct { - Name string `json:"name"` - GPU bool `json:"gpu"` - CPULimit string `json:"cpu-limit"` - MemoryLimit string `json:"memory-limit"` - Image string `json:"image"` - PullPolicy string `json:"pull_policy"` - Env map[string]string `json:"env"` - TargetPort int32 `json:"target-port"` - Args []string `json:"args"` - Command []string `json:"command"` - PathRewrite string `json:"path-rewrite"` - UseTLS string `json:"use-tls"` - ReadyProbe string `json:"ready-probe"` - LifecyclePreStop []string `json:"lifecycle-pre-stop"` - LifecyclePostStart []string `json:"lifecycle-post-start"` - UserUID int64 `json:"user-uid"` - GroupUID int64 `json:"group-uid"` - FSGID int64 `json:"fs-gid"` - UserVolumeLocation string `json:"user-volume-location"` - Gen3VolumeLocation string `json:"gen3-volume-location"` - UseSharedMemory string `json:"use-shared-memory"` - Friends []k8sv1.Container `json:"friends"` - NextflowConfig NextflowConfig `json:"nextflow"` - License LicenseInfo `json:"license"` - Authz AuthzConfig `json:"authz"` + Name string `json:"name"` + GPU bool `json:"gpu"` + CPULimit string `json:"cpu-limit"` + MemoryLimit string `json:"memory-limit"` + Image string `json:"image"` + PullPolicy string `json:"pull_policy"` + Env map[string]string `json:"env"` + TargetPort int32 `json:"target-port"` + Args []string `json:"args"` + Command []string `json:"command"` + PathRewrite string `json:"path-rewrite"` + UseTLS string `json:"use-tls"` + ReadyProbe string `json:"ready-probe"` + LifecyclePreStop []string `json:"lifecycle-pre-stop"` + LifecyclePostStart []string `json:"lifecycle-post-start"` + UserUID int64 `json:"user-uid"` + GroupUID int64 `json:"group-uid"` + FSGID int64 `json:"fs-gid"` + UserVolumeLocation string `json:"user-volume-location"` + Gen3VolumeLocation string `json:"gen3-volume-location"` + UseSharedMemory string `json:"use-shared-memory"` + Friends []k8sv1.Container `json:"friends"` + NextflowConfig NextflowConfig `json:"nextflow"` + License LicenseInfo `json:"license"` + Authz AuthzConfig `json:"authz"` + SquashFSMount SquashFSMountConfig `json:"squashfs_mount"` } // SidecarContainer holds fuse sidecar configuration @@ -139,29 +140,39 @@ type SharedWorkspaceConfig struct { OIDCProviderARN string `json:"oidc-provider-arn"` // Full ARN of the EKS OIDC provider, e.g. arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E } +// SquashFSMountConfig defines parameters for dynamic loop-mounting of squashfs inside pods +type SquashFSMountConfig struct { + Enabled bool `json:"enabled"` + SourceSqsh string `json:"source_sqsh"` + ExpectedSha256 string `json:"expected_sha256"` + MounterImage string `json:"mounter_image"` + CacheSizeLimit string `json:"cache_size_limit"` // e.g., "20Gi" + PVCClaimName string `json:"pvc_claim_name"` // e.g., "software-library-pvc" +} + // HatcheryConfig is the root of all the configuration type HatcheryConfig struct { UserNamespace string `json:"user-namespace"` DefaultPayModel PayModel `json:"default-pay-model"` HashedUsernames bool `json:"hashed-usernames"` // DisableLocalWS bool `json:"disable-local-ws"` - SkipNodeSelector bool `json:"skip-node-selector"` - UseInteralServicesURL bool `json:"use-internal-services-url"` - PayModels []PayModel `json:"pay-models"` - PayModelsDynamodbTable string `json:"pay-models-dynamodb-table"` - PayModelsDynamodbArn string `json:"pay-models-dynamodb-arn"` - LicenseUserMapsTable string `json:"license-user-maps-dynamodb-table"` - LicenseUserMapsGSI string `json:"license-user-maps-global-secondary-index"` - License LicenseInfo `json:"license"` - SubDir string `json:"sub-dir"` - Containers []Container `json:"containers"` - UserVolumeSize string `json:"user-volume-size"` - Sidecar SidecarContainer `json:"sidecar"` - MoreConfigs []AppConfigInfo `json:"more-configs"` - PrismaConfig PrismaConfig `json:"prisma"` - S3Config S3Config `json:"s3-config"` - NextflowGlobalConfig NextflowGlobalConfig `json:"nextflow-global"` - Pricing Pricing `json:"pricing"` + SkipNodeSelector bool `json:"skip-node-selector"` + UseInteralServicesURL bool `json:"use-internal-services-url"` + PayModels []PayModel `json:"pay-models"` + PayModelsDynamodbTable string `json:"pay-models-dynamodb-table"` + PayModelsDynamodbArn string `json:"pay-models-dynamodb-arn"` + LicenseUserMapsTable string `json:"license-user-maps-dynamodb-table"` + LicenseUserMapsGSI string `json:"license-user-maps-global-secondary-index"` + License LicenseInfo `json:"license"` + SubDir string `json:"sub-dir"` + Containers []Container `json:"containers"` + UserVolumeSize string `json:"user-volume-size"` + Sidecar SidecarContainer `json:"sidecar"` + MoreConfigs []AppConfigInfo `json:"more-configs"` + PrismaConfig PrismaConfig `json:"prisma"` + S3Config S3Config `json:"s3-config"` + NextflowGlobalConfig NextflowGlobalConfig `json:"nextflow-global"` + Pricing Pricing `json:"pricing"` SharedWorkspace SharedWorkspaceConfig `json:"shared-workspace"` } diff --git a/hatchery/pods.go b/hatchery/pods.go index 508e623..9be9ed3 100644 --- a/hatchery/pods.go +++ b/hatchery/pods.go @@ -2,6 +2,7 @@ package hatchery import ( "context" + _ "embed" // Required for go:embed "encoding/base64" "fmt" "log" @@ -31,6 +32,14 @@ import ( "sigs.k8s.io/aws-iam-authenticator/pkg/token" ) +// Compile-time embedding of the script files +// +//go:embed scripts/squashfs_sidecar_script.sh +var squashFSSidecarScript string + +//go:embed scripts/squashfs_wrapper_script.sh +var squashFSWrapperScript string + var ( trueVal = true falseVal = false @@ -391,148 +400,148 @@ func userToResourceName(userName string, resourceType string) string { return fmt.Sprintf("%s-%s", resourceType, safeUserName) } -func s3NamesForUser(userName string, namespace string) (pvName, pvcName, volumeHandle string) { - base := userToResourceName(userName, "s3") // e.g. john-doe-s3 - return base + "-" + namespace + "-pv", base + "-pvc", base + "-" + namespace // unique handle -} - -func s3PrefixForUser(userName string) string { - // If you prefer purely the username: return fmt.Sprintf("%s/", userName) - // Using resource-safe version tends to be nicer: - return fmt.Sprintf("%s/", userToResourceName(userName, "")) -} - -func ensureS3PVandPVC( - ctx context.Context, - podClient corev1.CoreV1Interface, - namespace string, - userName string, - bucket string, - region string, -) (pvcName string, err error) { - - pvName, pvcName, volumeHandle := s3NamesForUser(userName, namespace) - // Commented out for now as we don't need a prefix, this bucket will be mounted at / for all users, but read only. - // This will be used for the "software-repository" feature - // prefix := s3PrefixForUser(userName) - // prefix := "/" - - // ----- Ensure PV exists (cluster-scoped) ----- - // Try GET; if not found, create. - if _, errGet := podClient.PersistentVolumes().Get(ctx, pvName, metav1.GetOptions{}); errGet != nil { - pv := &k8sv1.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: pvName, - }, - Spec: k8sv1.PersistentVolumeSpec{ - Capacity: k8sv1.ResourceList{ - k8sv1.ResourceStorage: resource.MustParse("1200Gi"), // ignored by S3 CSI, but required - }, - AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}, - StorageClassName: "", // static provisioning - ClaimRef: &k8sv1.ObjectReference{ - Namespace: namespace, - Name: pvcName, - }, - MountOptions: []string{ - "allow-delete", - "allow-other", - "gid=100", - "file-mode=555", - "dir-mode=555", - fmt.Sprintf("region %s", region), - // fmt.Sprintf("prefix %s", prefix), - // Caches directory listings and file stats for 1 hour (fixes the 77x walk/stat slowdown) - "metadata-ttl=3600", - }, - PersistentVolumeSource: k8sv1.PersistentVolumeSource{ - CSI: &k8sv1.CSIPersistentVolumeSource{ - Driver: "s3.csi.aws.com", - VolumeHandle: volumeHandle, // must be unique - VolumeAttributes: map[string]string{ - "bucketName": bucket, - // Enables safe file caching using an ephemeral emptyDir on the K8s node - // and hard-caps the cache at 10 Gigabytes to prevent disk pressure - "cacheEmptyDirSizeLimit": "10Gi", - }, - }, - }, - }, - } - if _, errCreatePV := podClient.PersistentVolumes().Create(ctx, pv, metav1.CreateOptions{}); errCreatePV != nil { - return "", fmt.Errorf("failed to create S3 PV %s: %w", pvName, errCreatePV) - } - } - - // ----- Ensure PVC exists (namespaced) ----- - if _, errGet := podClient.PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{}); errGet != nil { - empty := "" // PVC.StorageClassName is *string - pvc := &k8sv1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: pvcName, - Namespace: namespace, - }, - Spec: k8sv1.PersistentVolumeClaimSpec{ - AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}, - StorageClassName: &empty, - Resources: k8sv1.VolumeResourceRequirements{ - Requests: k8sv1.ResourceList{ - k8sv1.ResourceStorage: resource.MustParse("1200Gi"), - }, - }, - VolumeName: pvName, - }, - } - if _, errCreatePVC := podClient.PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{}); errCreatePVC != nil { - return "", fmt.Errorf("failed to create S3 PVC %s: %w", pvcName, errCreatePVC) - } - } - - return pvcName, nil -} - -// Adds the S3 volume + mount to the pod in-place. -// Call this after buildPod(), before creating it. -func addS3VolumeToPod(pod *k8sv1.Pod, pvcName string) { - // 1) Add Volume - hasVolume := false - for _, v := range pod.Spec.Volumes { - if v.Name == "s3-volume" { - hasVolume = true - break - } - } - if !hasVolume { - pod.Spec.Volumes = append(pod.Spec.Volumes, k8sv1.Volume{ - Name: "s3-volume", - VolumeSource: k8sv1.VolumeSource{ - PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{ - ClaimName: pvcName, - // ReadOnly: false, // optional - }, - }, - }) - } - - // 2) Add VolumeMount (/apps) to every container (or just the main one) - for ci := range pod.Spec.Containers { - c := &pod.Spec.Containers[ci] - alreadyMounted := false - for _, m := range c.VolumeMounts { - if m.Name == "s3-volume" { - alreadyMounted = true - break - } - } - if !alreadyMounted { - c.VolumeMounts = append(c.VolumeMounts, k8sv1.VolumeMount{ - Name: "s3-volume", - // TODO: Read this path from config - MountPath: "/apps", - }) - } - } -} +// func s3NamesForUser(userName string, namespace string) (pvName, pvcName, volumeHandle string) { +// base := userToResourceName(userName, "s3") // e.g. john-doe-s3 +// return base + "-" + namespace + "-pv", base + "-pvc", base + "-" + namespace // unique handle +// } + +// func s3PrefixForUser(userName string) string { +// // If you prefer purely the username: return fmt.Sprintf("%s/", userName) +// // Using resource-safe version tends to be nicer: +// return fmt.Sprintf("%s/", userToResourceName(userName, "")) +// } + +// func ensureS3PVandPVC( +// ctx context.Context, +// podClient corev1.CoreV1Interface, +// namespace string, +// userName string, +// bucket string, +// region string, +// ) (pvcName string, err error) { + +// pvName, pvcName, volumeHandle := s3NamesForUser(userName, namespace) +// // Commented out for now as we don't need a prefix, this bucket will be mounted at / for all users, but read only. +// // This will be used for the "software-repository" feature +// // prefix := s3PrefixForUser(userName) +// // prefix := "/" + +// // ----- Ensure PV exists (cluster-scoped) ----- +// // Try GET; if not found, create. +// if _, errGet := podClient.PersistentVolumes().Get(ctx, pvName, metav1.GetOptions{}); errGet != nil { +// pv := &k8sv1.PersistentVolume{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: pvName, +// }, +// Spec: k8sv1.PersistentVolumeSpec{ +// Capacity: k8sv1.ResourceList{ +// k8sv1.ResourceStorage: resource.MustParse("1200Gi"), // ignored by S3 CSI, but required +// }, +// AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}, +// StorageClassName: "", // static provisioning +// ClaimRef: &k8sv1.ObjectReference{ +// Namespace: namespace, +// Name: pvcName, +// }, +// MountOptions: []string{ +// "allow-delete", +// "allow-other", +// "gid=100", +// "file-mode=555", +// "dir-mode=555", +// fmt.Sprintf("region %s", region), +// // fmt.Sprintf("prefix %s", prefix), +// // Caches directory listings and file stats for 1 hour (fixes the 77x walk/stat slowdown) +// "metadata-ttl=3600", +// }, +// PersistentVolumeSource: k8sv1.PersistentVolumeSource{ +// CSI: &k8sv1.CSIPersistentVolumeSource{ +// Driver: "s3.csi.aws.com", +// VolumeHandle: volumeHandle, // must be unique +// VolumeAttributes: map[string]string{ +// "bucketName": bucket, +// // Enables safe file caching using an ephemeral emptyDir on the K8s node +// // and hard-caps the cache at 10 Gigabytes to prevent disk pressure +// "cacheEmptyDirSizeLimit": "10Gi", +// }, +// }, +// }, +// }, +// } +// if _, errCreatePV := podClient.PersistentVolumes().Create(ctx, pv, metav1.CreateOptions{}); errCreatePV != nil { +// return "", fmt.Errorf("failed to create S3 PV %s: %w", pvName, errCreatePV) +// } +// } + +// // ----- Ensure PVC exists (namespaced) ----- +// if _, errGet := podClient.PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{}); errGet != nil { +// empty := "" // PVC.StorageClassName is *string +// pvc := &k8sv1.PersistentVolumeClaim{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: pvcName, +// Namespace: namespace, +// }, +// Spec: k8sv1.PersistentVolumeClaimSpec{ +// AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteMany}, +// StorageClassName: &empty, +// Resources: k8sv1.VolumeResourceRequirements{ +// Requests: k8sv1.ResourceList{ +// k8sv1.ResourceStorage: resource.MustParse("1200Gi"), +// }, +// }, +// VolumeName: pvName, +// }, +// } +// if _, errCreatePVC := podClient.PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{}); errCreatePVC != nil { +// return "", fmt.Errorf("failed to create S3 PVC %s: %w", pvcName, errCreatePVC) +// } +// } + +// return pvcName, nil +// } + +// // Adds the S3 volume + mount to the pod in-place. +// // Call this after buildPod(), before creating it. +// func addS3VolumeToPod(pod *k8sv1.Pod, pvcName string) { +// // 1) Add Volume +// hasVolume := false +// for _, v := range pod.Spec.Volumes { +// if v.Name == "s3-volume" { +// hasVolume = true +// break +// } +// } +// if !hasVolume { +// pod.Spec.Volumes = append(pod.Spec.Volumes, k8sv1.Volume{ +// Name: "s3-volume", +// VolumeSource: k8sv1.VolumeSource{ +// PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{ +// ClaimName: pvcName, +// // ReadOnly: false, // optional +// }, +// }, +// }) +// } + +// // 2) Add VolumeMount (/apps) to every container (or just the main one) +// for ci := range pod.Spec.Containers { +// c := &pod.Spec.Containers[ci] +// alreadyMounted := false +// for _, m := range c.VolumeMounts { +// if m.Name == "s3-volume" { +// alreadyMounted = true +// break +// } +// } +// if !alreadyMounted { +// c.VolumeMounts = append(c.VolumeMounts, k8sv1.VolumeMount{ +// Name: "s3-volume", +// // TODO: Read this path from config +// MountPath: "/apps", +// }) +// } +// } +// } // buildPod returns a pod ready to pass to the k8s API given // a hatchery Container instance, and the name of the user @@ -902,6 +911,122 @@ func setupSharedWorkspacesForPod(ctx context.Context, podClient corev1.CoreV1Int } } +func applySquashFSMounter(pod *k8sv1.Pod, opts SquashFSMountConfig) error { + if !opts.Enabled { + return nil + } + + mounterImage := opts.MounterImage + if mounterImage == "" { + mounterImage = "quay.io/cdis/ecs-ws-sidecar:master" + } + sourceSqsh := opts.SourceSqsh + if sourceSqsh == "" { + sourceSqsh = "/image/apps-current.sqsh" + } + cacheSizeStr := opts.CacheSizeLimit + if cacheSizeStr == "" { + cacheSizeStr = "20Gi" + } + cacheSize, err := resource.ParseQuantity(cacheSizeStr) + if err != nil { + return fmt.Errorf("invalid squashfs cache size limit %q: %v", cacheSizeStr, err) + } + pvcName := opts.PVCClaimName + if pvcName == "" { + pvcName = "software-library-pvc" + } + + extraVolumes := []k8sv1.Volume{ + { + Name: "apps-mount", + VolumeSource: k8sv1.VolumeSource{ + EmptyDir: &k8sv1.EmptyDirVolumeSource{ + Medium: k8sv1.StorageMediumMemory, + }, + }, + }, + { + Name: "sqsh-cache", + VolumeSource: k8sv1.VolumeSource{ + EmptyDir: &k8sv1.EmptyDirVolumeSource{ + SizeLimit: &cacheSize, + }, + }, + }, + { + Name: "software-library", + VolumeSource: k8sv1.VolumeSource{ + PersistentVolumeClaim: &k8sv1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvcName, + ReadOnly: true, + }, + }, + }, + } + pod.Spec.Volumes = append(pod.Spec.Volumes, extraVolumes...) + + propBidirectional := k8sv1.MountPropagationBidirectional + propHostToContainer := k8sv1.MountPropagationHostToContainer + + // Construct the apps-mounter sidecar container + privileged := true + rootID := int64(0) + + sidecar := k8sv1.Container{ + Name: "apps-mounter", + Image: mounterImage, + ImagePullPolicy: k8sv1.PullIfNotPresent, + SecurityContext: &k8sv1.SecurityContext{ + Privileged: &privileged, + RunAsUser: &rootID, + RunAsGroup: &rootID, + }, + Env: []k8sv1.EnvVar{ + {Name: "SOURCE_SQSH", Value: sourceSqsh}, + {Name: "EXPECTED_SHA256", Value: opts.ExpectedSha256}, + }, + Command: []string{"/bin/sh", "-ceu"}, + Args: []string{squashFSSidecarScript}, // Used embedded variable + VolumeMounts: []k8sv1.VolumeMount{ + {Name: "software-library", MountPath: "/image", ReadOnly: true}, + {Name: "sqsh-cache", MountPath: "/sqsh-cache"}, + {Name: "apps-mount", MountPath: "/apps", MountPropagation: &propBidirectional}, + }, + } + pod.Spec.Containers = append([]k8sv1.Container{sidecar}, pod.Spec.Containers...) + + for i := range pod.Spec.Containers { + if pod.Spec.Containers[i].Name == "hatchery-container" { + + container := &pod.Spec.Containers[i] + + container.VolumeMounts = append(container.VolumeMounts, + k8sv1.VolumeMount{Name: "apps-mount", MountPath: "/apps", MountPropagation: &propHostToContainer}, + ) + + origCommand := container.Command + origArgs := container.Args + + if len(origCommand) == 0 { + origCommand = []string{"/bin/sh", "-c"} + } + + container.Command = []string{"/bin/sh", "-ceu"} + + // Pass "hatchery-wrapper" as a dummy argument. + // This becomes $0 inside the shell script, ensuring that following command (/jupyterlab-start.sh) becomes $1 and is included in "$@". + newArgs := []string{squashFSWrapperScript, "hatchery-wrapper"} // Used embedded variable + newArgs = append(newArgs, origCommand...) + newArgs = append(newArgs, origArgs...) + container.Args = newArgs + break + } + } + + return nil +} + var createLocalK8sPod = func(ctx context.Context, hash string, userName string, accessToken string, envVars []k8sv1.EnvVar, payModelId ...string) error { // Set default if not provided payModelIdValue := "" @@ -942,23 +1067,35 @@ var createLocalK8sPod = func(ctx context.Context, hash string, userName string, Config.Logger.Panicf("Error in createLocalK8sPod: %v", err) return err } - // ensure S3 PV/PVC (dynamic per user) and wire into pod - if Config.Config.S3Config.BucketName != "" && Config.Config.S3Config.Region != "" { - Config.Logger.Print("Mounting S3 bucket as well..") - s3PVCName, err := ensureS3PVandPVC( - ctx, - podClient, - Config.Config.UserNamespace, - userName, - Config.Config.S3Config.BucketName, - Config.Config.S3Config.Region, - ) + + if hatchApp.SquashFSMount.Enabled { + Config.Logger.Printf("Applying SquashFS mounter setup for container: %s", hatchApp.Name) + + err := applySquashFSMounter(pod, hatchApp.SquashFSMount) if err != nil { - Config.Logger.Printf("Failed ensuring S3 PV/PVC for user %s: %v", userName, err) + Config.Logger.Printf("failed to apply squashfs sidecar for container %s: %v", hatchApp.Name, err) return err } - addS3VolumeToPod(pod, s3PVCName) } + // else { + // // ensure S3 PV/PVC (dynamic per user) and wire into pod + // if Config.Config.S3Config.BucketName != "" && Config.Config.S3Config.Region != "" { + // Config.Logger.Print("Mounting S3 bucket as well..") + // s3PVCName, err := ensureS3PVandPVC( + // ctx, + // podClient, + // Config.Config.UserNamespace, + // userName, + // Config.Config.S3Config.BucketName, + // Config.Config.S3Config.Region, + // ) + // if err != nil { + // Config.Logger.Printf("Failed ensuring S3 PV/PVC for user %s: %v", userName, err) + // return err + // } + // addS3VolumeToPod(pod, s3PVCName) + // } + // } // a null image indicates a dockstore app - always mount user volume mountUserVolume := hatchApp.UserVolumeLocation != "" diff --git a/hatchery/scripts/squashfs_sidecar_script.sh b/hatchery/scripts/squashfs_sidecar_script.sh new file mode 100644 index 0000000..57b7b28 --- /dev/null +++ b/hatchery/scripts/squashfs_sidecar_script.sh @@ -0,0 +1,86 @@ +#!/bin/sh +SRC="${SOURCE_SQSH:-/image/apps-current.sqsh}" +CACHE_DIR="/sqsh-cache" +TARGET="/apps" + +mkdir -p "$CACHE_DIR" "$TARGET" + +get_fstype() { + awk -v target="$1" ' + $5 == target { line = $0 } + END { + if (line == "") exit 1 + split(line, parts, " - ") + split(parts[2], fs, " ") + print fs[1] + } + ' /proc/self/mountinfo +} + +echo "Checking source SquashFS: $SRC" + +if [ ! -f "$SRC" ]; then + echo "ERROR: SquashFS source not found: $SRC" >&2 + exit 1 +fi + +if [ -n "${EXPECTED_SHA256:-}" ]; then + CACHE="$CACHE_DIR/${EXPECTED_SHA256}.sqsh" +else + CACHE="$CACHE_DIR/apps-current.sqsh" +fi + +if [ ! -f "$CACHE" ]; then + echo "Copying SquashFS from $SRC to local cache $CACHE" + + rm -f "$CACHE.tmp" + cp "$SRC" "$CACHE.tmp" + + if [ -n "${EXPECTED_SHA256:-}" ]; then + echo "${EXPECTED_SHA256} $CACHE.tmp" | sha256sum -c - + else + echo "WARNING: EXPECTED_SHA256 is empty; skipping digest verification" >&2 + fi + + mv "$CACHE.tmp" "$CACHE" +else + echo "Using existing local SquashFS cache: $CACHE" +fi + +if [ -n "${EXPECTED_SHA256:-}" ]; then + echo "${EXPECTED_SHA256} $CACHE" | sha256sum -c - +fi + +CURRENT_FSTYPE="$(get_fstype "$TARGET" || true)" +echo "Current filesystem at $TARGET: ${CURRENT_FSTYPE:-none}" + +if [ "$CURRENT_FSTYPE" != "squashfs" ]; then + echo "Mounting SquashFS $CACHE at $TARGET" + mount -t squashfs -o loop,ro,nodev,nosuid "$CACHE" "$TARGET" +else + echo "$TARGET is already mounted as squashfs" +fi + +NEW_FSTYPE="$(get_fstype "$TARGET" || true)" +echo "Filesystem at $TARGET after mount: ${NEW_FSTYPE:-none}" + +if [ "$NEW_FSTYPE" != "squashfs" ]; then + echo "ERROR: expected $TARGET to be squashfs, got ${NEW_FSTYPE:-none}" >&2 + awk '$5 == "/apps" {print}' /proc/self/mountinfo || true + exit 1 +fi + +echo "Apps SquashFS mounted successfully" +ls -la "$TARGET" | head || true + +cleanup() { + echo "Unmounting $TARGET" + umount "$TARGET" || true +} + +trap cleanup TERM INT EXIT + +while true; do + sleep 3600 & + wait $! +done diff --git a/hatchery/scripts/squashfs_wrapper_script.sh b/hatchery/scripts/squashfs_wrapper_script.sh new file mode 100644 index 0000000..c8741ca --- /dev/null +++ b/hatchery/scripts/squashfs_wrapper_script.sh @@ -0,0 +1,23 @@ +#!/bin/sh +get_fstype() { + awk -v target="$1" ' + $5 == target { line = $0 } + END { + if (line == "") exit 1 + split(line, parts, " - ") + split(parts[2], fs, " ") + print fs[1] + } + ' /proc/self/mountinfo +} + +echo "Waiting for /apps squashfs mount from apps-mounter sidecar..." +until [ "$(get_fstype /apps || true)" = "squashfs" ]; do + echo "Current /apps fstype: $(get_fstype /apps || echo none)" + sleep 1 +done + +echo "/apps is mounted as squashfs" +ls -la /apps | head || true + +exec "$@"