Files
dokku/plugins/storage/commands_entries_test.go
Jose Diaz-Gonzalez 74f9acf7ba refactor: align storage:set with dokku conventions
`storage:set` now takes `<name> <property> [<value>]` like every other `:set` command, where omitting the value unsets the property. Previously it took flags and could not distinguish an empty value from an omitted one, so nothing it set could ever be cleared. The flag form keeps working and emits a deprecation warning.

Annotations and labels move to `storage:annotations:set`, `storage:annotations:report`, `storage:labels:set`, and `storage:labels:report`, matching the `scheduler-k3s` equivalents. These operate on a single key, so clearing one leaves the rest in place rather than replacing the whole map as the `--annotation` and `--label` flags do.
2026-08-10 15:00:45 -04:00

221 lines
7.6 KiB
Go

package storage
import (
"path/filepath"
"testing"
. "github.com/onsi/gomega"
)
func TestEnsureDockerLocalPathCreatesDefaultPath(t *testing.T) {
RegisterTestingT(t)
withTempLibRoot(t)
hostPath := filepath.Join(GetStorageDirectory(), "demo")
entry := &Entry{Name: "demo", Scheduler: SchedulerDockerLocal, HostPath: hostPath}
Expect(ensureDockerLocalPath(entry)).To(Succeed())
Expect(hostPath).To(BeADirectory())
// Idempotent: a second run leaves the existing directory in place.
Expect(ensureDockerLocalPath(entry)).To(Succeed())
Expect(hostPath).To(BeADirectory())
}
// TestEnsureDockerLocalPathSkipsNamedVolumes guards the docker named-volume
// case: the host path is a token the docker engine resolves, not a path on
// disk, so stat'ing and creating it would produce a stray directory in
// whatever working directory the command happened to run from.
func TestEnsureDockerLocalPathSkipsNamedVolumes(t *testing.T) {
RegisterTestingT(t)
withTempLibRoot(t)
cwd := t.TempDir()
t.Chdir(cwd)
entry := &Entry{Name: "demo", Scheduler: SchedulerDockerLocal, HostPath: "myvolume"}
Expect(ensureDockerLocalPath(entry)).To(Succeed())
Expect(filepath.Join(cwd, "myvolume")).NotTo(BeADirectory())
}
func TestEnsureDockerLocalPathRefusesModeOnCustomPath(t *testing.T) {
RegisterTestingT(t)
withTempLibRoot(t)
entry := &Entry{
Name: "demo",
Scheduler: SchedulerDockerLocal,
HostPath: filepath.Join(t.TempDir(), "custom"),
Mode: "0777",
}
err := ensureDockerLocalPath(entry)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("--mode is only supported when the storage entry uses the default host path"))
Expect(entry.HostPath).NotTo(BeADirectory())
}
func TestEnsureDockerLocalPathRefusesChownOnCustomPath(t *testing.T) {
RegisterTestingT(t)
withTempLibRoot(t)
entry := &Entry{
Name: "demo",
Scheduler: SchedulerDockerLocal,
HostPath: filepath.Join(t.TempDir(), "custom"),
Chown: "herokuish",
}
err := ensureDockerLocalPath(entry)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("--chown is only supported when the storage entry uses the default host path"))
}
// TestEnsureDockerLocalPathAllowsChownFalseOnCustomPath documents that the
// escape hatch the refusal message points at actually works.
func TestEnsureDockerLocalPathAllowsChownFalseOnCustomPath(t *testing.T) {
RegisterTestingT(t)
withTempLibRoot(t)
hostPath := filepath.Join(t.TempDir(), "custom")
entry := &Entry{Name: "demo", Scheduler: SchedulerDockerLocal, HostPath: hostPath, Chown: "false"}
Expect(ensureDockerLocalPath(entry)).To(Succeed())
Expect(hostPath).To(BeADirectory())
}
func TestApplyPropertyChangeScalars(t *testing.T) {
RegisterTestingT(t)
entry := &Entry{Name: "demo", Scheduler: SchedulerDockerLocal}
Expect(applyPropertyChange(entry, PropertyChange{Property: "chown", Value: "herokuish"})).To(Succeed())
Expect(entry.Chown).To(Equal("herokuish"))
Expect(applyPropertyChange(entry, PropertyChange{Property: "namespace", Value: "dokku"})).To(Succeed())
Expect(entry.Namespace).To(Equal("dokku"))
Expect(applyPropertyChange(entry, PropertyChange{Property: "reclaim-policy", Value: ReclaimPolicyDelete})).To(Succeed())
Expect(entry.ReclaimPolicy).To(Equal(ReclaimPolicyDelete))
Expect(applyPropertyChange(entry, PropertyChange{Property: "size", Value: "2Gi"})).To(Succeed())
Expect(entry.Size).To(Equal("2Gi"))
// mode is canonicalized on the way in
Expect(applyPropertyChange(entry, PropertyChange{Property: "mode", Value: "770"})).To(Succeed())
Expect(entry.Mode).To(Equal("0770"))
}
// TestApplyPropertyChangeUnsets is the hole this shape exists to close: an
// empty value clears the field rather than being indistinguishable from an
// omitted flag.
func TestApplyPropertyChangeUnsets(t *testing.T) {
RegisterTestingT(t)
entry := &Entry{
Name: "demo",
Scheduler: SchedulerDockerLocal,
Chown: "herokuish",
Mode: "0770",
Namespace: "dokku",
ReclaimPolicy: ReclaimPolicyDelete,
Size: "2Gi",
}
for _, property := range []string{"chown", "mode", "namespace", "reclaim-policy", "size"} {
Expect(applyPropertyChange(entry, PropertyChange{Property: property})).To(Succeed(), "unsetting %q", property)
}
Expect(entry.Chown).To(BeEmpty())
Expect(entry.Mode).To(BeEmpty())
Expect(entry.Namespace).To(BeEmpty())
Expect(entry.ReclaimPolicy).To(BeEmpty())
Expect(entry.Size).To(BeEmpty())
}
func TestApplyPropertyChangeRejectsInvalidInput(t *testing.T) {
RegisterTestingT(t)
entry := &Entry{Name: "demo", Scheduler: SchedulerDockerLocal}
err := applyPropertyChange(entry, PropertyChange{Property: "bogus", Value: "x"})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Invalid property specified, valid properties include: access-mode, chown, mode, namespace, reclaim-policy, size, storage-class-name"))
err = applyPropertyChange(entry, PropertyChange{Property: "", Value: "x"})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("No property specified"))
err = applyPropertyChange(entry, PropertyChange{Property: "mode", Value: "0888"})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Unsupported directory mode"))
}
// TestApplyPropertyChangeRefusesInPlaceSwaps covers both a differing value
// and an empty one, since clearing a bound PVC's access-mode or storage
// class is equally a change Kubernetes cannot apply.
func TestApplyPropertyChangeRefusesInPlaceSwaps(t *testing.T) {
RegisterTestingT(t)
entry := &Entry{
Name: "demo",
Scheduler: SchedulerK3s,
Size: "2Gi",
AccessMode: "ReadWriteOnce",
StorageClass: "longhorn",
}
err := applyPropertyChange(entry, PropertyChange{Property: "access-mode", Value: "ReadWriteMany"})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("cannot change access-mode in place"))
err = applyPropertyChange(entry, PropertyChange{Property: "access-mode"})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("cannot change access-mode in place"))
err = applyPropertyChange(entry, PropertyChange{Property: "storage-class-name", Value: "other"})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("cannot change storage-class-name in place"))
err = applyPropertyChange(entry, PropertyChange{Property: "storage-class-name"})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("cannot change storage-class-name in place"))
// Re-stating the current value is a no-op, not a change.
Expect(applyPropertyChange(entry, PropertyChange{Property: "access-mode", Value: "ReadWriteOnce"})).To(Succeed())
Expect(applyPropertyChange(entry, PropertyChange{Property: "storage-class-name", Value: "longhorn"})).To(Succeed())
}
func TestCommandSetPersistsAndValidates(t *testing.T) {
RegisterTestingT(t)
withTempLibRoot(t)
stageDockerLocalEntry(t, "demo")
Expect(CommandSet(CommandSetInput{
Name: "demo",
Changes: []PropertyChange{{Property: "namespace", Value: "dokku"}},
})).To(Succeed())
loaded, err := LoadEntry("demo")
Expect(err).NotTo(HaveOccurred())
Expect(loaded.Namespace).To(Equal("dokku"))
// An invalid change leaves the stored entry untouched.
err = CommandSet(CommandSetInput{
Name: "demo",
Changes: []PropertyChange{{Property: "bogus", Value: "x"}},
})
Expect(err).To(HaveOccurred())
loaded, err = LoadEntry("demo")
Expect(err).NotTo(HaveOccurred())
Expect(loaded.Namespace).To(Equal("dokku"))
err = CommandSet(CommandSetInput{
Name: "missing",
Changes: []PropertyChange{{Property: "namespace", Value: "dokku"}},
})
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("does not exist"))
}