Skip to content
Merged
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
57 changes: 55 additions & 2 deletions api/dataset/v1alpha1/dataset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import (
type DatasetStatusPhase string
type DatasetType string

// AccessMode is the access granted to a namespace which references a Dataset.
// It deliberately describes a per-reference mount policy, not the access mode
// of the backing PersistentVolume.
type AccessMode string

const (
DatasetTypeGit DatasetType = "GIT"
DatasetTypeS3 DatasetType = "S3"
Expand Down Expand Up @@ -51,6 +56,11 @@ const (
_ = DatasetStatusPhaseFailed
)

const (
AccessModeReadOnly AccessMode = "ReadOnly"
AccessModeReadWrite AccessMode = "ReadWrite"
)

type DatasetSource struct {
// +kubebuilder:validation:Enum=GIT;S3;HTTP;PVC;NFS;CONDA;REFERENCE;HUGGING_FACE;MODEL_SCOPE;DATABASE;HADOOP;MANUAL
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
Expand Down Expand Up @@ -116,6 +126,8 @@ type MountOptions struct {
}

// DatasetSpec defines the desired state of Dataset
// +kubebuilder:validation:XValidation:rule="oldSelf == null || (has(self.shareAccess) == has(oldSelf.shareAccess) && (!has(self.shareAccess) || self.shareAccess == oldSelf.shareAccess))",message="shareAccess is immutable and cannot be added or removed"
// +kubebuilder:validation:XValidation:rule="!has(self.shareAccess) || size(self.shareAccess.rules) > 0",message="shareAccess.rules must contain at least one rule when shareAccess is configured"
type DatasetSpec struct {
// Share indicates whether the model is shareable with others.
// When set to true, the model can be shared according to the specified selector.
Expand All @@ -126,6 +138,12 @@ type DatasetSpec struct {
// If Share is true and ShareToNamespaceSelector is empty, that means all namespaces can access this.
// +kubebuilder:validation:Optional
ShareToNamespaceSelector *metav1.LabelSelector `json:"shareToNamespaceSelector,omitempty"`
// ShareAccess assigns the effective access mode to namespaces which consume
// this Dataset through a REFERENCE Dataset. A nil value is intentionally
// distinct from an empty value: nil preserves the legacy read-only sharing
// behaviour.
// +kubebuilder:validation:Optional
ShareAccess *ShareAccess `json:"shareAccess,omitempty"`
// +kubebuilder:validation:Required
// source is the source of the dataset.
Source DatasetSource `json:"source"`
Expand All @@ -150,6 +168,22 @@ type DatasetSpec struct {
DataWarmUpResources v1.ResourceRequirements `json:"resources,omitempty"`
}

// ShareAccess describes the namespace based access policy for a shared Dataset.
type ShareAccess struct {
// +kubebuilder:validation:MinItems=1
Rules []ShareAccessRule `json:"rules"`
}

type ShareAccessRule struct {
// NamespaceSelector selects target namespaces. Empty selectors are not valid:
// an explicit rule must name at least one label requirement.
// +kubebuilder:validation:Required
// +kubebuilder:validation:XValidation:rule="size(self.matchLabels) > 0 || size(self.matchExpressions) > 0",message="namespaceSelector must not be empty"
NamespaceSelector metav1.LabelSelector `json:"namespaceSelector"`
// +kubebuilder:validation:Enum=ReadOnly;ReadWrite
AccessMode AccessMode `json:"accessMode"`
}

type VolumeClaimRef struct {
// +kubebuilder:validation:Required
// name is the name of the pvc.
Expand Down Expand Up @@ -197,8 +231,27 @@ type DatasetStatus struct {
PVCName string `json:"pvcName,omitempty"`
// +kubebuilder:validation:Optional
// readOnly indicates whether the dataset is mounted as read-only.
ReadOnly bool `json:"readOnly,omitempty"`
Comment thread
usernameisnull marked this conversation as resolved.
LastSyncTime metav1.Time `json:"lastSyncTime,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
// mountSources is the direct-to-root source chain from the last successful
// MountPolicy resolution for a REFERENCE Dataset. It deliberately excludes this
// Dataset's own PVC; that PVC is checked from status.pvcName at verification time.
// A MountPolicy=False condition retains this binding as an anti-rebind identity pin;
// consumers must never treat it as current authorization.
MountSources []MountSource `json:"mountSources,omitempty"`
LastSyncTime metav1.Time `json:"lastSyncTime,omitempty"`
}

// MountSource pins every Dataset and storage object used by a reference mount.
// Consumers must verify these identities before treating ReadOnly=false as a
// writable mount.
type MountSource struct {
Namespace string `json:"namespace"`
Name string `json:"name"`
UID string `json:"uid"`
PVCName string `json:"pvcName"`
PVCUID string `json:"pvcUID"`
PVName string `json:"pvName"`
PVUID string `json:"pvUID"`
}

// Dataset is the Schema for the datasets API
Expand Down
80 changes: 80 additions & 0 deletions api/dataset/v1alpha1/dataset_types_envtest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package v1alpha1_test

import (
"context"
"os"
"path/filepath"
goruntime "runtime"
"testing"

"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"

datasetv1alpha1 "github.com/BaizeAI/dataset/api/dataset/v1alpha1"
)

func TestShareAccessCRDValidation(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("envtest process cleanup is unsupported on Windows; run this test on Linux CI")
}
if os.Getenv("KUBEBUILDER_ASSETS") == "" {
t.Skip("KUBEBUILDER_ASSETS is required for envtest")
}
_, file, _, ok := goruntime.Caller(0)
require.True(t, ok)
crdPath := filepath.Join(filepath.Dir(file), "..", "..", "..", "config", "crd", "bases")
testEnv := &envtest.Environment{CRDDirectoryPaths: []string{crdPath}, ErrorIfCRDPathMissing: true}
cfg, err := testEnv.Start()
require.NoError(t, err)
defer func() { require.NoError(t, testEnv.Stop()) }()

scheme := runtime.NewScheme()
require.NoError(t, datasetv1alpha1.AddToScheme(scheme))
apiClient, err := client.New(cfg, client.Options{Scheme: scheme})
require.NoError(t, err)
ctx := context.Background()

validRule := datasetv1alpha1.ShareAccessRule{
NamespaceSelector: metav1.LabelSelector{MatchLabels: map[string]string{"workspace": "one"}},
AccessMode: datasetv1alpha1.AccessModeReadOnly,
}
newDataset := func(name string) *datasetv1alpha1.Dataset {
return &datasetv1alpha1.Dataset{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "default"},
Spec: datasetv1alpha1.DatasetSpec{Source: datasetv1alpha1.DatasetSource{Type: datasetv1alpha1.DatasetTypeManual, URI: "manual://"}},
}
}

t.Run("rejects explicit empty rules even before sharing is enabled", func(t *testing.T) {
ds := newDataset("empty-rules")
ds.Spec.ShareAccess = &datasetv1alpha1.ShareAccess{}
require.Error(t, apiClient.Create(ctx, ds))
})
t.Run("rejects empty namespace selector", func(t *testing.T) {
ds := newDataset("empty-selector")
ds.Spec.ShareAccess = &datasetv1alpha1.ShareAccess{Rules: []datasetv1alpha1.ShareAccessRule{{AccessMode: datasetv1alpha1.AccessModeReadOnly}}}
require.Error(t, apiClient.Create(ctx, ds))
})
t.Run("allows a nonempty preconfigured policy and later enabling sharing", func(t *testing.T) {
ds := newDataset("preconfigured")
ds.Spec.ShareAccess = &datasetv1alpha1.ShareAccess{Rules: []datasetv1alpha1.ShareAccessRule{validRule}}
require.NoError(t, apiClient.Create(ctx, ds))
ds.Spec.Share = true
require.NoError(t, apiClient.Update(ctx, ds))
})
t.Run("rejects adding or changing a policy after creation", func(t *testing.T) {
missing := newDataset("missing-policy")
require.NoError(t, apiClient.Create(ctx, missing))
missing.Spec.ShareAccess = &datasetv1alpha1.ShareAccess{Rules: []datasetv1alpha1.ShareAccessRule{validRule}}
require.Error(t, apiClient.Update(ctx, missing))

immutable := newDataset("immutable-policy")
immutable.Spec.ShareAccess = &datasetv1alpha1.ShareAccess{Rules: []datasetv1alpha1.ShareAccessRule{validRule}}
require.NoError(t, apiClient.Create(ctx, immutable))
immutable.Spec.ShareAccess.Rules[0].AccessMode = datasetv1alpha1.AccessModeReadWrite
require.Error(t, apiClient.Update(ctx, immutable))
})
}
64 changes: 64 additions & 0 deletions api/dataset/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading