Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -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"
6 changes: 6 additions & 0 deletions hatchery/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 30 additions & 30 deletions hatchery/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := ""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
49 changes: 49 additions & 0 deletions hatchery/pods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package hatchery
import (
"encoding/json"
"testing"

k8sv1 "k8s.io/api/core/v1"
)

func TestBuildPodFromJSON(t *testing.T) {
Expand Down Expand Up @@ -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)
}
}
Loading