Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
280662f
csi-driver: replication RPCs act on the chain's last LOCAL member, no…
michixs Oct 2, 2026
5106e18
csi-driver: lint (gofmt, goconst) for the local-replica resolver
michixs Oct 2, 2026
18c0824
csi-driver: demoting or detaching a reaped chain member succeeds
michixs Oct 2, 2026
9ee0e79
csi-driver: Resync and the status read of a reaped local member go to…
michixs Oct 2, 2026
bede3dd
csi-driver: PromoteVolume addresses the chain's active end, not the l…
michixs Oct 2, 2026
2b6e428
csi-driver: Resync and the status read address the chain's active end
michixs Oct 2, 2026
8988179
volstack/csi-driver: a plan that names no filesystem mounts what the …
michixs Oct 3, 2026
599a741
operator: a test fail-over's bubble PV and PVC carry the source's vol…
michixs Oct 3, 2026
7d5f38b
chart: the Control Center on the integrate_csi_addons line
michixs Oct 3, 2026
e356b56
csi-driver: a replication-chain walk is bounded by a cycle guard, not…
michixs Oct 3, 2026
f8760ac
operator: TestFailover CRD as controller-gen renders it
michixs Oct 3, 2026
ad4bad5
Merge fix/csi-local-cluster-resolution: chain-walk cycle guard, regen…
michixs Oct 3, 2026
8233d4b
operator: StorageSiteDeployment, a managed site's storage deployed fr…
michixs Oct 3, 2026
2ab649a
csi-driver: wrap lines over 120 characters; operator: regenerate dist…
michixs Oct 3, 2026
6050f17
Merge fix/csi-local-cluster-resolution (lint, installer)
michixs Oct 3, 2026
cce7357
operator: lint — a list-kind suffix constant; wrap the hub-only log line
michixs Oct 3, 2026
16f9713
operator: regenerate dist/install.yaml with the StorageSiteDeployment…
michixs Oct 3, 2026
81cdbf5
operator: the CSV owns StorageSiteDeployment
michixs Oct 3, 2026
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
64 changes: 58 additions & 6 deletions atlas-lib/volstack/layers/filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,19 @@ type FilesystemConfig struct {
// formatted as, and it is also the only filesystem the layer will mount: a
// device carrying another is refused, because neither reformatting it nor
// serving what is on it is safe.
//
// Empty, the plan expresses no opinion: a device carrying a filesystem is
// mounted as what it carries, and a blank one is formatted as DefaultFsType.
// That is what a PersistentVolume without fsType means -- a static PV that
// adopts an existing volume (a test fail-over's clone, 2026-10-03), or one
// Ramen restored without the field -- and turning it into "ext4" before the
// device was looked at made the layer refuse every such XFS volume.
FsType string

// DefaultFsType is what a blank device is formatted as when FsType names
// nothing. Empty means ext4.
DefaultFsType string

// StagingPath is where the filesystem is mounted.
StagingPath string

Expand Down Expand Up @@ -107,6 +118,43 @@ type FilesystemConfig struct {
// the volume is, and refuses every other device.
type Filesystem struct {
cfg FilesystemConfig
// detected is the filesystem found on the device when the plan named none.
detected string
}

// effective is the filesystem this layer acts with: the one the plan named,
// else the one the device carries, else the default for a blank device.
func (f *Filesystem) effective(reading blockdev.Reading) string {
if f.cfg.FsType != "" {
return f.cfg.FsType
}
if reading.Content == blockdev.ContentFilesystem && reading.Type != "" {
f.detected = reading.Type
return reading.Type
}
if f.detected != "" {
return f.detected
}
return f.defaultFsType()
}

// known is the filesystem this layer stands for once it has acted: named,
// detected, or the default it formats with.
func (f *Filesystem) known() string {
if f.cfg.FsType != "" {
return f.cfg.FsType
}
if f.detected != "" {
return f.detected
}
return f.defaultFsType()
}

func (f *Filesystem) defaultFsType() string {
if f.cfg.DefaultFsType != "" {
return f.cfg.DefaultFsType
}
return "ext4"
}

// NewFilesystem returns the filesystem layer for one volume.
Expand Down Expand Up @@ -206,11 +254,12 @@ func (f *Filesystem) Ensure(ctx context.Context, below volstack.Artifact) (volst
// decide the state and again to act on it. The reading itself is not needed
// past that, because the filesystem to act on is the one the plan named and
// observe has already refused every device carrying another.
state, _, own, err := f.observe(ctx, below)
state, reading, own, err := f.observe(ctx, below)
if err != nil {
return volstack.Artifact{}, err
}
if state == volstack.StateReady {
f.effective(reading)
return own, nil
}

Expand All @@ -219,7 +268,7 @@ func (f *Filesystem) Ensure(ctx context.Context, below volstack.Artifact) (volst
// disagreement, which is the point: the only two ways to reconcile one are to
// reformat, which destroys the volume, and to serve the other filesystem,
// which hides the misconfiguration until something else acts on it.
fsType := f.cfg.FsType
fsType := f.effective(reading)
if state == volstack.StateAbsent {
if err := f.cfg.Ops.Format(ctx, dev.Path, fsType, f.formatOptions(below)); err != nil {
return volstack.Artifact{}, fmt.Errorf("filesystem: format %s as %s: %w", dev.Path, fsType, err)
Expand Down Expand Up @@ -321,7 +370,7 @@ func (f *Filesystem) Heal(ctx context.Context, below, _ volstack.Artifact) error
return err
}

if err := f.cfg.Ops.Mount(ctx, dev.Path, f.cfg.StagingPath, f.cfg.FsType, f.mountFlags()); err != nil {
if err := f.cfg.Ops.Mount(ctx, dev.Path, f.cfg.StagingPath, f.effective(reading), f.mountFlags()); err != nil {
return fmt.Errorf("filesystem: remount %s at %s: %w", dev.Path, f.cfg.StagingPath, err)
}
return nil
Expand Down Expand Up @@ -379,7 +428,7 @@ type FilesystemParams struct {
// recorded is the one the volume asked for, and a teardown needs no more than
// that: what is actually on the device is read from the device.
func (f *Filesystem) Params() any {
return FilesystemParams{FsType: f.cfg.FsType}
return FilesystemParams{FsType: f.known()}
}

// agrees reports whether the filesystem on the device is the one the plan asked
Expand Down Expand Up @@ -436,13 +485,16 @@ func (f *Filesystem) blank(
switch {
case prior == "":
return volstack.StateAbsent, reading, volstack.Artifact{}, nil
case prior != f.cfg.FsType:
case f.cfg.FsType != "" && prior != f.cfg.FsType:
return volstack.StateAbsent, reading, volstack.Artifact{}, fmt.Errorf(
"filesystem: refusing to stage %s, which is recorded as carrying %s where the plan "+
"asks for %s: reformatting would destroy the volume, and mounting it as %s would "+
"serve a filesystem the plan does not declare",
deviceOf(below), prior, f.cfg.FsType, prior)
default:
if f.cfg.FsType == "" {
f.detected = prior
}
// Recorded as formatted while nothing was found on it: the reading is a
// failed probe rather than an empty device, so the filesystem is treated as
// present and unmounted. Mounting it is the honest next step, and a mount
Expand Down Expand Up @@ -486,7 +538,7 @@ func (f *Filesystem) mountFlags() []string {
// asked for. That is also the only one the layer acts on, since a device
// carrying another is refused rather than reconciled.
func (f *Filesystem) strategy() FilesystemLayerStrategy {
return FilesystemStrategyFor(f.cfg.FsType)
return FilesystemStrategyFor(f.known())
}

// deviceOf names the device below for an error message, without asserting there
Expand Down
38 changes: 38 additions & 0 deletions atlas-lib/volstack/layers/filesystem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,3 +592,41 @@ func TestReleaseForcesWhenAPlainUnmountRefuses(t *testing.T) {
t.Fatal("a plain unmount refused and the release did not fall back to its force path")
}
}

// A plan that names no filesystem -- a static PV without fsType, such as a
// test fail-over's clone or a PV Ramen restored without the field -- mounts
// what the device carries instead of refusing it for not being ext4
// (2026-10-03), and formats a blank device as the default.
func TestAPlanNamingNoFilesystemMountsWhatTheDeviceCarries(t *testing.T) {
fs := newFakeFS()
l := newFSAsking(t, fs, "", blockdev.Reading{Content: blockdev.ContentFilesystem, Type: "xfs"}, nil)
if _, err := l.Ensure(context.Background(), belowArtifact()); err != nil {
t.Fatal(err)
}
if len(fs.formatted) != 0 {
t.Fatalf("formatted a device that carries a filesystem: %+v", fs.formatted)
}
if len(fs.mounted) != 1 || fs.mounted[0].fsType != "xfs" {
t.Fatalf("mounted %+v, want once as xfs", fs.mounted)
}
if p, _ := l.Params().(FilesystemParams); p.FsType != "xfs" {
t.Fatalf("params %+v, want the detected xfs recorded", p)
}
}

func TestAPlanNamingNoFilesystemFormatsABlankDeviceAsTheDefault(t *testing.T) {
fs := newFakeFS()
l := NewFilesystem(FilesystemConfig{
FsType: "", DefaultFsType: "ext4", StagingPath: stagingPath, Ops: fs,
Content: fakeReader{reading: blockdev.Reading{Content: blockdev.ContentBlank}},
})
if _, err := l.Ensure(context.Background(), belowArtifact()); err != nil {
t.Fatal(err)
}
if len(fs.formatted) != 1 || fs.formatted[0].fsType != "ext4" {
t.Fatalf("formatted %+v, want once as ext4", fs.formatted)
}
if len(fs.mounted) != 1 || fs.mounted[0].fsType != "ext4" {
t.Fatalf("mounted %+v, want once as ext4", fs.mounted)
}
}
1 change: 1 addition & 0 deletions atlas-lib/volstack/plans/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func (n *Node) fabric(connection lvol.Connection) volstack.Layer {
func (n *Node) filesystem(volume Volume) volstack.Layer {
return layers.NewFilesystem(layers.FilesystemConfig{
FsType: volume.FsType,
DefaultFsType: volume.DefaultFsType,
StagingPath: volume.StagingPath,
MountFlags: volume.MountFlags,
FormatOptions: volume.FormatOptions,
Expand Down
7 changes: 6 additions & 1 deletion atlas-lib/volstack/plans/plans.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,14 @@ type Volume struct {

// FsType is the filesystem this volume is. It decides what a blank device is
// formatted as, and it is also the only filesystem that will be mounted: a
// device carrying another is refused.
// device carrying another is refused. Empty: whatever the device carries,
// and DefaultFsType for a blank one.
FsType string

// DefaultFsType is what a blank device is formatted as when FsType names
// nothing.
DefaultFsType string

// MountFlags are the flags the volume asked for, ahead of the ones the
// filesystem layer derives from the filesystem itself.
MountFlags []string
Expand Down
22 changes: 22 additions & 0 deletions csi-driver/internal/clusters/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type Config struct {
ClusterID string `json:"cluster_id"`
ClusterEndpoint string `json:"cluster_endpoint"`
ClusterSecret string `json:"cluster_secret"`
// Local marks a cluster of the site this driver runs on (written by the
// site's operator); an entry without it belongs to another site, kept so
// that a failed-over volume's handle still resolves.
Local bool `json:"local,omitempty"`
}

// Info is the secret file as a whole.
Expand Down Expand Up @@ -86,6 +90,24 @@ func Load() (Info, error) {
return clusters, nil
}

// Local returns the ids of the clusters the secret marks local, and whether
// the secret marks any: a secret written by an operator that predates the
// flag marks none, and callers then fall back to treating every cluster as
// local.
func Local() (map[string]bool, bool, error) {
clusters, err := Load()
if err != nil {
return nil, false, err
}
local := map[string]bool{}
for _, cluster := range clusters.Clusters {
if cluster.Local {
local[cluster.ClusterID] = true
}
}
return local, len(local) > 0, nil
}

// List returns the ID of every cluster in the secret.
func List() ([]string, error) {
clusters, err := Load()
Expand Down
Loading
Loading