From 5cdbc4ec72badf36de0b221e8231efbd69223c57 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 20 Aug 2026 11:42:11 -0500 Subject: [PATCH 1/2] Support default-storage-class config for user volume PVCs Allow hatchery to pin the user volume PersistentVolumeClaims to a configured StorageClass via a new 'default-storage-class' config option in hatchery.json. When the option is empty (the default), claims are created without a storageClassName so the cluster-default StorageClass is used, preserving existing behavior. --- hatchery/config.go | 6 +++++ hatchery/pods.go | 60 +++++++++++++++++++++---------------------- hatchery/pods_test.go | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 30 deletions(-) diff --git a/hatchery/config.go b/hatchery/config.go index 005d53ef..415891c2 100644 --- a/hatchery/config.go +++ b/hatchery/config.go @@ -174,6 +174,12 @@ type HatcheryConfig struct { NextflowGlobalConfig NextflowGlobalConfig `json:"nextflow-global"` Pricing Pricing `json:"pricing"` SharedWorkspace SharedWorkspaceConfig `json:"shared-workspace"` + + // DefaultStorageClass, when set, is used as the storageClassName of the + // user volume PersistentVolumeClaims created for workspaces. When empty, + // the claim is created without a storageClassName and the cluster-default + // StorageClass is used. + DefaultStorageClass string `json:"default-storage-class"` } // Config to allow for Prisma Agents diff --git a/hatchery/pods.go b/hatchery/pods.go index 9be9ed3e..aabb893d 100644 --- a/hatchery/pods.go +++ b/hatchery/pods.go @@ -1027,6 +1027,34 @@ func applySquashFSMounter(pod *k8sv1.Pod, opts SquashFSMountConfig) error { return nil } +// newUserVolumePVC builds the PersistentVolumeClaim for a user's home directory +// (the "user volume" mounted into workspace pods). If a default storage class is +// configured via the `default-storage-class` config option, the claim is pinned to +// that storage class. Otherwise storageClassName is left empty and the claim is +// provisioned by the cluster-default StorageClass. +func newUserVolumePVC(claimName string, pod *k8sv1.Pod) *k8sv1.PersistentVolumeClaim { + spec := k8sv1.PersistentVolumeClaimSpec{ + AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}, + Resources: k8sv1.VolumeResourceRequirements{ + Requests: k8sv1.ResourceList{ + k8sv1.ResourceStorage: resource.MustParse(Config.Config.UserVolumeSize), + }, + }, + } + if Config.Config.DefaultStorageClass != "" { + storageClass := Config.Config.DefaultStorageClass + spec.StorageClassName = &storageClass + } + return &k8sv1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: claimName, + Annotations: pod.Annotations, + Labels: pod.Labels, + }, + Spec: spec, + } +} + var createLocalK8sPod = func(ctx context.Context, hash string, userName string, accessToken string, envVars []k8sv1.EnvVar, payModelId ...string) error { // Set default if not provided payModelIdValue := "" @@ -1105,21 +1133,7 @@ var createLocalK8sPod = func(ctx context.Context, hash string, userName string, _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Get(ctx, claimName, metav1.GetOptions{}) if err != nil { Config.Logger.Printf("Creating PersistentVolumeClaim %s.\n", claimName) - pvc := &k8sv1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: claimName, - Annotations: pod.Annotations, - Labels: pod.Labels, - }, - Spec: k8sv1.PersistentVolumeClaimSpec{ - AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}, - Resources: k8sv1.VolumeResourceRequirements{ - Requests: k8sv1.ResourceList{ - k8sv1.ResourceStorage: resource.MustParse(Config.Config.UserVolumeSize), - }, - }, - }, - } + pvc := newUserVolumePVC(claimName, pod) _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Create(ctx, pvc, metav1.CreateOptions{}) if err != nil { Config.Logger.Printf("Failed to create PVC %s. Error: %s\n", claimName, err) @@ -1262,21 +1276,7 @@ var createExternalK8sPod = func(ctx context.Context, hash string, userName strin _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Get(ctx, claimName, metav1.GetOptions{}) if err != nil { Config.Logger.Printf("Creating PersistentVolumeClaim %s.\n", claimName) - pvc := &k8sv1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: claimName, - Annotations: pod.Annotations, - Labels: pod.Labels, - }, - Spec: k8sv1.PersistentVolumeClaimSpec{ - AccessModes: []k8sv1.PersistentVolumeAccessMode{k8sv1.ReadWriteOnce}, - Resources: k8sv1.VolumeResourceRequirements{ - Requests: k8sv1.ResourceList{ - k8sv1.ResourceStorage: resource.MustParse(Config.Config.UserVolumeSize), - }, - }, - }, - } + pvc := newUserVolumePVC(claimName, pod) _, err := podClient.PersistentVolumeClaims(Config.Config.UserNamespace).Create(ctx, pvc, metav1.CreateOptions{}) if err != nil { diff --git a/hatchery/pods_test.go b/hatchery/pods_test.go index 8e048eaf..564a97ec 100644 --- a/hatchery/pods_test.go +++ b/hatchery/pods_test.go @@ -3,6 +3,8 @@ package hatchery import ( "encoding/json" "testing" + + k8sv1 "k8s.io/api/core/v1" ) func TestBuildPodFromJSON(t *testing.T) { @@ -70,3 +72,50 @@ func TestBuildPodFromDockstore(t *testing.T) { config.Logger.Printf("pod_test marshalled pod: %v", string(jsBytes)) } + +func TestNewUserVolumePVCWithoutDefaultStorageClass(t *testing.T) { + defer SetupAndTeardownTest()() + + config, err := LoadConfig("../testData/testConfig.json", nil) + if nil != err { + t.Errorf("failed to load config, got: %v", err) + return + } + // newUserVolumePVC reads the global Config + Config = config + config.Config.DefaultStorageClass = "" + + pod := &k8sv1.Pod{} + pvc := newUserVolumePVC("user-claim", pod) + + if pvc.Spec.StorageClassName != nil { + t.Errorf("expected no storageClassName to be set when default-storage-class is empty, got: %v", *pvc.Spec.StorageClassName) + } + if pvc.Spec.Resources.Requests.Storage().String() != "10Gi" { + t.Errorf("expected user volume size 10Gi, got: %v", pvc.Spec.Resources.Requests.Storage().String()) + } +} + +func TestNewUserVolumePVCWithDefaultStorageClass(t *testing.T) { + defer SetupAndTeardownTest()() + + config, err := LoadConfig("../testData/testConfig.json", nil) + if nil != err { + t.Errorf("failed to load config, got: %v", err) + return + } + // newUserVolumePVC reads the global Config + Config = config + config.Config.DefaultStorageClass = "nfs-home" + + pod := &k8sv1.Pod{} + pvc := newUserVolumePVC("user-claim", pod) + + if pvc.Spec.StorageClassName == nil { + t.Errorf("expected storageClassName to be set when default-storage-class is configured") + return + } + if *pvc.Spec.StorageClassName != "nfs-home" { + t.Errorf("expected storageClassName 'nfs-home', got: %v", *pvc.Spec.StorageClassName) + } +} From 540db548bd9f5027b97fe91949833df54f5b66d2 Mon Sep 17 00:00:00 2001 From: Justin Barnowski Date: Thu, 20 Aug 2026 16:53:47 -0500 Subject: [PATCH 2/2] Exclude aws-sdk-go v1 deprecation warnings (SA1019) from lint aws-sdk-go v1 was deprecated by AWS (end of support 2025-07-31) and staticcheck now flags every v1 import as SA1019. The codebase still depends on v1 across ~20 files; migrating to aws-sdk-go-v2 is a separate effort. Until then, exclude these diagnostics so CI reflects newly introduced issues only. --- .golangci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..1e33546d --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,12 @@ +version: "2" +linters: + exclusions: + rules: + # aws-sdk-go (v1) was deprecated by AWS on 2025-07-31 and recent releases + # carry deprecation markers that staticcheck reports as SA1019 on every + # import. The whole codebase still uses v1; migrating to aws-sdk-go-v2 + # is a dedicated effort. Revisit this exclusion once that migration + # lands and the v1 dependency is removed. + - linters: + - staticcheck + text: "SA1019: github.com/aws/aws-sdk-go"