diff --git a/core/commoncmd/config.go b/core/commoncmd/config.go index 4554006eb..5ed071d81 100644 --- a/core/commoncmd/config.go +++ b/core/commoncmd/config.go @@ -3,7 +3,6 @@ package commoncmd import ( "bufio" "bytes" - "fmt" "regexp" "strings" @@ -92,6 +91,9 @@ func ColorizeINI(b []byte) []byte { var continuedValue string var continuation bool + // Compile regexes once + referenceRE := regexp.MustCompile(`\{[^{}]*\}`) + for scanner.Scan() { line := scanner.Text() @@ -109,35 +111,108 @@ func ColorizeINI(b []byte) []byte { // Section header if sectionRE.MatchString(line) { - color.Set(color.FgYellow).Fprintln(out, line) + color.Set(color.FgHiYellow, color.Bold).Fprintln(out, line) continue } // Comment if commentRE.MatchString(line) { - color.Set(color.FgHiBlack).Fprintln(out, line) + color.Set(color.FgHiBlack, color.Italic).Fprintln(out, line) continue } // Key-value if strings.Contains(line, "=") && !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") { - parts := strings.SplitN(line, "=", 2) - key := strings.TrimSpace(parts[0]) - value := parts[1] // preserve spacing - color.Set(color.FgMagenta).Fprint(out, key) - out.WriteString(fmt.Sprintf(" =%s\n", value)) - - // Check if line continues - if strings.HasSuffix(strings.TrimRight(value, " \t"), `\`) { - continuedValue = "" - continuedValue += value - continuation = true + // Use regex to preserve spacing around equals sign + kvRE := regexp.MustCompile(`^(\s*[^=\s]+(?:\s+[^=\s]+)*)\s*=\s*(.*)$`) + matches := kvRE.FindStringSubmatch(line) + if len(matches) == 3 { + key := matches[1] + equalAndValue := matches[2] + + // Colorize key + key, scope, scopeFound := strings.Cut(key, "@") + color.Set(color.FgCyan).Fprint(out, key) + if scopeFound { + color.Set(color.FgHiMagenta).Fprint(out, "@"+scope) + } + + // Find the equals sign position to preserve exact spacing + equalPos := strings.Index(line, "=") + if equalPos >= 0 { + // Extract the equals sign with surrounding spaces + start := equalPos + end := equalPos + 1 + // Include leading spaces + for start > 0 && line[start-1] == ' ' { + start-- + } + // Include trailing spaces + for end < len(line) && line[end] == ' ' { + end++ + } + equalSign := line[start:end] + color.Set(color.FgHiBlack).Fprint(out, equalSign) + + // The rest is the value + value := line[end:] + + // Highlight references in the value + referenceMatches := referenceRE.FindAllStringIndex(value, -1) + if len(referenceMatches) > 0 { + lastPos := 0 + for _, match := range referenceMatches { + // Write non-reference part + out.WriteString(value[lastPos:match[0]]) + + // Write reference part in green + bold + referenceText := value[match[0]:match[1]] + color.Set(color.FgGreen, color.Bold).Fprint(out, referenceText) + lastPos = match[1] + } + // Write remaining part after last reference + out.WriteString(value[lastPos:]) + } else { + // No references + out.WriteString(value) + } + } else { + // Fallback: output the rest as-is + out.WriteString(equalAndValue) + } + out.WriteString("\n") + + // Check if line continues + if strings.HasSuffix(strings.TrimRight(line, " \t"), `\`) { + continuedValue = "" + continuedValue += line + continuation = true + } + continue } - continue } - // Unmatched line (output as-is) - out.WriteString(line + "\n") + // Unmatched line - check for references + referenceMatches := referenceRE.FindAllStringIndex(line, -1) + if len(referenceMatches) > 0 { + lastPos := 0 + for _, match := range referenceMatches { + // Write non-reference part + out.WriteString(line[lastPos:match[0]]) + + // Write reference part in green + bold + referenceText := line[match[0]:match[1]] + color.Set(color.FgGreen, color.Bold).Fprint(out, referenceText) + lastPos = match[1] + } + // Write remaining part after last reference + out.WriteString(line[lastPos:]) + out.WriteString("\n") + } else { + // Unmatched line (output as-is) + out.WriteString(line) + out.WriteString("\n") + } } return out.Bytes() diff --git a/core/object/core_config.go b/core/object/core_config.go index 549265134..3dd90eaec 100644 --- a/core/object/core_config.go +++ b/core/object/core_config.go @@ -230,13 +230,13 @@ func (t *core) dereferenceVolumeHead(ref string) (string, error) { var i any = t.config.Referrer actor, ok := i.(Actor) if !ok { - return ref, fmt.Errorf("can't dereference volume mnt on a non-actor object: %s", ref) + return ref, fmt.Errorf("can't dereference volume head on a non-actor object: %s", ref) } type header interface { Head() string } if len(l) != 2 { - return ref, fmt.Errorf("misformatted volume mnt ref: %s", ref) + return ref, fmt.Errorf("misformatted volume head ref: %s", ref) } rid := l[0] r := actor.ResourceByID(rid) diff --git a/core/object/node_keywords.go b/core/object/node_keywords.go index 29fcad2f9..328235c74 100644 --- a/core/object/node_keywords.go +++ b/core/object/node_keywords.go @@ -1160,6 +1160,7 @@ var ( Text: keywords.NewText(fs, "text/kw/node/pool.mkfs_opt"), } kwNodePoolMkblkOpt = keywords.Keyword{ + Example: "-b 16k", Option: "mkblk_opt", Section: "pool", Text: keywords.NewText(fs, "text/kw/node/pool.mkblk_opt"), diff --git a/core/object/text/kw/node/pool.mkblk_opt b/core/object/text/kw/node/pool.mkblk_opt index e8118eac0..86e4ccb27 100644 --- a/core/object/text/kw/node/pool.mkblk_opt +++ b/core/object/text/kw/node/pool.mkblk_opt @@ -1,2 +1,2 @@ The zvol, lv, and other block device creation command options to use to -prepare the pool devices. +prepare the pool volumes devices. diff --git a/core/omcmd/lib_remote_config.go b/core/omcmd/lib_remote_config.go index 42df696ce..2f70b6a37 100644 --- a/core/omcmd/lib_remote_config.go +++ b/core/omcmd/lib_remote_config.go @@ -8,6 +8,7 @@ import ( "github.com/opensvc/om3/v3/core/client" "github.com/opensvc/om3/v3/core/naming" + "github.com/opensvc/om3/v3/core/rawconfig" ) func createTempRemoteConfig(p naming.Path, c *client.T) (string, error) { @@ -19,7 +20,7 @@ func createTempRemoteConfig(p naming.Path, c *client.T) (string, error) { if buff, err = fetchConfig(p, c); err != nil { return "", err } - if f, err = os.CreateTemp("", ".opensvc.remote.config.*"); err != nil { + if f, err = os.CreateTemp(rawconfig.Paths.Tmp, "remote.*.conf.tmp"); err != nil { return "", err } filename := f.Name() diff --git a/core/oxcmd/lib_remote_config.go b/core/oxcmd/lib_remote_config.go index d21eaad1e..9108f246d 100644 --- a/core/oxcmd/lib_remote_config.go +++ b/core/oxcmd/lib_remote_config.go @@ -8,6 +8,7 @@ import ( "github.com/opensvc/om3/v3/core/client" "github.com/opensvc/om3/v3/core/naming" + "github.com/opensvc/om3/v3/core/rawconfig" ) func createTempRemoteNodeConfig(nodename string, c *client.T) (string, error) { @@ -27,7 +28,7 @@ func createTempRemoteObjectConfig(p naming.Path, c *client.T) (string, error) { } func createTempRemoteConfig(buff []byte) (string, error) { - f, err := os.CreateTemp("", ".opensvc.remote.config.*") + f, err := os.CreateTemp(rawconfig.Paths.Tmp, "remote.*.conf.tmp") if err != nil { return "", err } diff --git a/core/pool/main.go b/core/pool/main.go index af0dd32d1..60780ea39 100644 --- a/core/pool/main.go +++ b/core/pool/main.go @@ -15,6 +15,7 @@ import ( "github.com/opensvc/om3/v3/core/nodesinfo" "github.com/opensvc/om3/v3/core/volaccess" "github.com/opensvc/om3/v3/core/xconfig" + "github.com/opensvc/om3/v3/util/args" "github.com/opensvc/om3/v3/util/key" "github.com/opensvc/om3/v3/util/san" "github.com/opensvc/om3/v3/util/sizeconv" @@ -309,36 +310,58 @@ func (t *T) MntOptions() string { return t.GetString("mnt_opt") } -func (t *T) AddFS(name string, shared bool, fsIndex int, diskIndex int, onDisk string) []string { +type fsPooler interface { + MntOptions() string + MkblkOptions() string + MkfsOptions() string + FSType() string +} + +type FS struct { + Pool fsPooler + Name string + Shared bool + FsIndex int + DiskIndex int + OnDisk string + DefaultMkfsOptions []string + DefaultMntOptions []string +} + +func (t *FS) Keywords() []string { data := make([]string, 0) - fsType := t.FSType() + fsType := t.Pool.FSType() switch fsType { case "zfs": data = append(data, []string{ - fmt.Sprintf("disk#%d.type=zpool", diskIndex), - fmt.Sprintf("disk#%d.name=%s", diskIndex, name), - fmt.Sprintf("disk#%d.vdev={%s.exposed_devs[0]}", diskIndex, onDisk), - fmt.Sprintf("disk#%d.shared=%t", diskIndex, shared), - fmt.Sprintf("fs#%d.type=zfs", fsIndex), - fmt.Sprintf("fs#%d.dev=%s/root", fsIndex, name), - fmt.Sprintf("fs#%d.mnt=%s", fsIndex, MountPointFromName(name)), - fmt.Sprintf("fs#%d.shared=%t", fsIndex, shared), + fmt.Sprintf("disk#%d.type=zpool", t.DiskIndex), + fmt.Sprintf("disk#%d.name=%s", t.DiskIndex, t.Name), + fmt.Sprintf("disk#%d.vdev={%s.exposed_devs[0]}", t.DiskIndex, t.OnDisk), + fmt.Sprintf("disk#%d.shared=%t", t.DiskIndex, t.Shared), + fmt.Sprintf("fs#%d.type=zfs", t.FsIndex), + fmt.Sprintf("fs#%d.dev=%s/root", t.FsIndex, t.Name), + fmt.Sprintf("fs#%d.mnt=%s", t.FsIndex, MountPointFromName(t.Name)), + fmt.Sprintf("fs#%d.shared=%t", t.FsIndex, t.Shared), }...) case "": panic("fsType should not be empty at this point") default: data = append(data, []string{ - fmt.Sprintf("fs#%d.type=%s", fsIndex, fsType), - fmt.Sprintf("fs#%d.dev={%s.exposed_devs[0]}", fsIndex, onDisk), - fmt.Sprintf("fs#%d.mnt=%s", fsIndex, MountPointFromName(name)), - fmt.Sprintf("fs#%d.shared=%t", fsIndex, shared), + fmt.Sprintf("fs#%d.type=%s", t.FsIndex, fsType), + fmt.Sprintf("fs#%d.dev={%s.exposed_devs[0]}", t.FsIndex, t.OnDisk), + fmt.Sprintf("fs#%d.mnt=%s", t.FsIndex, MountPointFromName(t.Name)), + fmt.Sprintf("fs#%d.shared=%t", t.FsIndex, t.Shared), }...) } - if opts := t.MkfsOptions(); opts != "" { - data = append(data, fmt.Sprintf("fs#%d.mkfs_opt=%s", fsIndex, opts)) + if opts := t.Pool.MkfsOptions(); opts != "" { + data = append(data, fmt.Sprintf("fs#%d.mkfs_opt=%s", t.FsIndex, opts)) + } else if len(t.DefaultMkfsOptions) > 0 { + data = append(data, fmt.Sprintf("fs#%d.mkfs_opt=%s", t.FsIndex, args.New(t.DefaultMkfsOptions...).String())) } - if opts := t.MntOptions(); opts != "" { - data = append(data, fmt.Sprintf("fs#%d.mnt_opt=%s", fsIndex, opts)) + if opts := t.Pool.MntOptions(); opts != "" { + data = append(data, fmt.Sprintf("fs#%d.mnt_opt=%s", t.FsIndex, opts)) + } else if len(t.DefaultMntOptions) > 0 { + data = append(data, fmt.Sprintf("fs#%d.mnt_opt=%s", t.FsIndex, args.New(t.DefaultMntOptions...).String())) } return data } diff --git a/core/tui/main.go b/core/tui/main.go index 2039ba65b..0751ca8dc 100644 --- a/core/tui/main.go +++ b/core/tui/main.go @@ -24,6 +24,7 @@ import ( "github.com/opensvc/om3/v3/core/client" "github.com/opensvc/om3/v3/core/clientcontext" "github.com/opensvc/om3/v3/core/clusterdump" + "github.com/opensvc/om3/v3/core/commoncmd" "github.com/opensvc/om3/v3/core/event" "github.com/opensvc/om3/v3/core/monitor" "github.com/opensvc/om3/v3/core/naming" @@ -2248,8 +2249,11 @@ func (t *App) updateClusterConfigView() { return } - text := tview.TranslateANSI(string(resp.Body)) - t.textView.SetDynamicColors(false) + // Apply syntax highlighting to the config + colorizedConfig := commoncmd.ColorizeINI(resp.Body) + text := tview.Escape(string(colorizedConfig)) + text = tview.TranslateANSI(text) + t.textView.SetDynamicColors(true) t.textView.Clear() t.textView.SetTitle("cluster configuration") fmt.Fprint(t.textView, text) @@ -2268,8 +2272,11 @@ func (t *App) updateNodeConfigView() { return } - text := tview.TranslateANSI(string(resp.Body)) - t.textView.SetDynamicColors(false) + // Apply syntax highlighting to the config + colorizedConfig := commoncmd.ColorizeINI(resp.Body) + text := tview.Escape(string(colorizedConfig)) + text = tview.TranslateANSI(text) + t.textView.SetDynamicColors(true) t.textView.SetTitle(fmt.Sprintf("%s configuration", t.viewNode)) t.textView.Clear() fmt.Fprint(t.textView, text) @@ -2287,8 +2294,11 @@ func (t *App) updateObjectConfigView() { return } - text := tview.TranslateANSI(string(resp.Body)) - t.textView.SetDynamicColors(false) + // Apply syntax highlighting to the config + colorizedConfig := commoncmd.ColorizeINI(resp.Body) + text := tview.Escape(string(colorizedConfig)) + text = tview.TranslateANSI(text) + t.textView.SetDynamicColors(true) t.textView.SetTitle(fmt.Sprintf("%s configuration", t.viewPath.String())) t.textView.Clear() fmt.Fprint(t.textView, text) diff --git a/core/vpath/vpath.go b/core/vpath/vpath.go index 2d93f0de0..8918c59b3 100644 --- a/core/vpath/vpath.go +++ b/core/vpath/vpath.go @@ -5,21 +5,28 @@ package vpath import ( "context" - "errors" "fmt" "strings" "github.com/opensvc/om3/v3/core/naming" "github.com/opensvc/om3/v3/core/object" "github.com/opensvc/om3/v3/core/status" + "github.com/opensvc/om3/v3/core/xerrors" "github.com/opensvc/om3/v3/util/file" "github.com/opensvc/om3/v3/util/loop" ) -var ( - ErrAccess = errors.New("vol is not accessible") +type ( + ErrAccess struct { + Path naming.Path + Avail status.T + } ) +func (t ErrAccess) Error() string { + return fmt.Sprintf("vol is not accessible: %s is avail %s", t.Path, t.Avail) +} + // HostPathAndVol expand a volume-relative path to a host full path. It returns // the host full path and the associated object volume if defined. // @@ -48,7 +55,7 @@ func HostPathAndVol(ctx context.Context, s string, namespace string) (hostPath s return } if !vol.Path().Exists() { - err = fmt.Errorf("%s does not exist", vol.Path()) + err = fmt.Errorf("%w: %s", xerrors.ObjectNotFound, vol.Path()) return } @@ -60,7 +67,10 @@ func HostPathAndVol(ctx context.Context, s string, namespace string) (hostPath s switch volStatus.Avail { case status.Up, status.NotApplicable, status.StandbyUp: default: - err = fmt.Errorf("%w: %s(%s)", ErrAccess, volPath, volStatus.Avail) + err = ErrAccess{ + Path: volPath, + Avail: volStatus.Avail, + } return } hostPath = vol.Head() + "/" + volRelativeSourcePath @@ -127,7 +137,11 @@ func HostDevpath(ctx context.Context, s string, namespace string) (string, error switch st.Avail { case status.Up, status.NotApplicable, status.StandbyUp: default: - return s, fmt.Errorf("%w: %s(%s)", ErrAccess, volPath, st.Avail) + err = ErrAccess{ + Path: volPath, + Avail: st.Avail, + } + return s, err } dev := vol.ExposedDevice(ctx) if dev == nil { diff --git a/drivers/pooldrbd/main.go b/drivers/pooldrbd/main.go index a7ce0d0f2..33ec78d79 100644 --- a/drivers/pooldrbd/main.go +++ b/drivers/pooldrbd/main.go @@ -286,7 +286,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - kws = append(kws, t.AddFS(name, shared, 1, 0, rid)...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: rid, + } + kws = append(kws, fs.Keywords()...) kws = append(kws, "devices_from="+rid) return kws, nil } diff --git a/drivers/poolfreenas/main.go b/drivers/poolfreenas/main.go index cd7d99ee9..b9a1db699 100644 --- a/drivers/poolfreenas/main.go +++ b/drivers/poolfreenas/main.go @@ -121,7 +121,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - data = append(data, t.AddFS(name, shared, 1, 0, "disk#0")...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: "disk#0", + } + data = append(data, fs.Keywords()...) return data, nil } diff --git a/drivers/poolhoc/main.go b/drivers/poolhoc/main.go index 8c2b53a47..70502896d 100644 --- a/drivers/poolhoc/main.go +++ b/drivers/poolhoc/main.go @@ -123,7 +123,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - data = append(data, t.AddFS(name, shared, 1, 0, "disk#0")...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: "disk#0", + } + data = append(data, fs.Keywords()...) return data, nil } diff --git a/drivers/poolloop/main.go b/drivers/poolloop/main.go index ac9fd1aac..7f770e490 100644 --- a/drivers/poolloop/main.go +++ b/drivers/poolloop/main.go @@ -81,7 +81,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - data = append(data, t.AddFS(name, shared, 1, 0, "disk#0")...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: "disk#0", + } + data = append(data, fs.Keywords()...) return data, nil } diff --git a/drivers/poolpure/main.go b/drivers/poolpure/main.go index 8543e091a..421faf5f9 100644 --- a/drivers/poolpure/main.go +++ b/drivers/poolpure/main.go @@ -114,7 +114,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - data = append(data, t.AddFS(name, shared, 1, 0, "disk#0")...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: "disk#0", + } + data = append(data, fs.Keywords()...) return data, nil } diff --git a/drivers/poolrados/main.go b/drivers/poolrados/main.go index d6f87efdb..5f44d9d21 100644 --- a/drivers/poolrados/main.go +++ b/drivers/poolrados/main.go @@ -142,7 +142,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - data = append(data, t.AddFS(name, shared, 1, 0, "disk#0")...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: "disk#0", + } + data = append(data, fs.Keywords()...) return data, nil } diff --git a/drivers/poolsymmetrix/main.go b/drivers/poolsymmetrix/main.go index 50b00ed40..90958a92c 100644 --- a/drivers/poolsymmetrix/main.go +++ b/drivers/poolsymmetrix/main.go @@ -191,7 +191,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - data = append(data, t.AddFS(name, shared, 1, 0, "disk#0")...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: "disk#0", + } + data = append(data, fs.Keywords()...) return data, nil } diff --git a/drivers/poolvg/main.go b/drivers/poolvg/main.go index bfd48a63e..7364116d7 100644 --- a/drivers/poolvg/main.go +++ b/drivers/poolvg/main.go @@ -89,7 +89,15 @@ func (t *T) Translate(name string, size int64, shared bool) ([]string, error) { if err != nil { return nil, err } - data = append(data, t.AddFS(name, shared, 1, 0, "disk#0")...) + fs := pool.FS{ + Pool: t, + Name: name, + Shared: shared, + FsIndex: 1, + DiskIndex: 0, + OnDisk: "disk#0", + } + data = append(data, fs.Keywords()...) return data, nil } diff --git a/drivers/resapp/base.go b/drivers/resapp/base.go index 933252170..9ccafb6bb 100644 --- a/drivers/resapp/base.go +++ b/drivers/resapp/base.go @@ -30,8 +30,7 @@ type BaseT struct { ObjectID uuid.UUID `json:"objectID"` } -func (t *T) getEnv(ctx context.Context) (env []string, err error) { - var tempEnv []string +func (t *T) getEnv(ctx context.Context, onIgnoreCallback func(err error)) (env []string, err error) { env = []string{ "OPENSVC_RID=" + t.RID(), "OPENSVC_NAME=" + t.Path.String(), @@ -42,14 +41,16 @@ func (t *T) getEnv(ctx context.Context) (env []string, err error) { if len(t.Env) > 0 { env = append(env, t.Env...) } - if tempEnv, err = envprovider.From(t.ConfigsEnv, t.Path.Namespace, "cfg"); err != nil { + if tempEnv, err := envprovider.From(t.ConfigsEnv, t.Path.Namespace, "cfg", envprovider.IgnoreExpected(onIgnoreCallback)); err != nil { return nil, err + } else { + env = append(env, tempEnv...) } - env = append(env, tempEnv...) - if tempEnv, err = envprovider.From(t.SecretsEnv, t.Path.Namespace, "sec"); err != nil { + if tempEnv, err := envprovider.From(t.SecretsEnv, t.Path.Namespace, "sec", envprovider.IgnoreExpected(onIgnoreCallback)); err != nil { return nil, err + } else { + env = append(env, tempEnv...) } - env = append(env, tempEnv...) env = append(env, actioncontext.Env(ctx)...) return env, nil } diff --git a/drivers/resapp/unix.go b/drivers/resapp/unix.go index 774eafa53..67e30c4c5 100644 --- a/drivers/resapp/unix.go +++ b/drivers/resapp/unix.go @@ -21,6 +21,7 @@ import ( "github.com/opensvc/om3/v3/core/resource" "github.com/opensvc/om3/v3/core/status" "github.com/opensvc/om3/v3/core/statusbus" + "github.com/opensvc/om3/v3/core/vpath" "github.com/opensvc/om3/v3/util/command" "github.com/opensvc/om3/v3/util/converters" "github.com/opensvc/om3/v3/util/executable" @@ -69,11 +70,12 @@ func (t *T) SortKey() string { // CommonStop stops the Resource func (t *T) CommonStop(ctx context.Context, r statuser) (err error) { var opts []funcopt.O - if opts, err = t.GetFuncOpts(ctx, t.StopCmd, "stop"); err != nil { + var errAccess vpath.ErrAccess + opts, err = t.GetFuncOpts(ctx, t.StopCmd, "stop") + if errors.As(err, &errAccess) { + t.Log().Infof("skip 'stop' command (%s is %s)", errAccess.Path, errAccess.Avail) + } else if err != nil { t.Log().Errorf("prepare 'stop' command: %s", err) - if t.StatusLogKw { - t.StatusLog().Error("prepare cmd %s", err) - } return err } if len(opts) == 0 { @@ -172,12 +174,16 @@ func (t *T) CommonStatus(ctx context.Context) status.T { cannotExec = true } } - if opts, err = t.GetFuncOpts(ctx, t.CheckCmd, "status"); err != nil { - t.Log().Errorf("prepare 'status' command: %s", err) - if t.StatusLogKw { - t.StatusLog().Error("prepare cmd %s", err) - cannotExec = true - } + + opts, err = t.GetFuncOpts(ctx, t.CheckCmd, "status") + var errAccess vpath.ErrAccess + if errors.As(err, &errAccess) { + t.StatusLog().Info("not evaluated (%s is %s)", errAccess.Path, errAccess.Avail) + // Other issues still can return Undef. + // If no other issues, N/A will be returned as opts is nil. + } else if err != nil { + t.StatusLog().Error("prepare cmd %s", err) + cannotExec = true } if cannotExec { return status.Undef @@ -232,7 +238,7 @@ func (t *T) Provisioned(ctx context.Context) (provisioned.T, error) { return provisioned.NotApplicable, nil } -func (t *T) BaseCmdArgs(s string, action string) ([]string, error) { +func (t *T) BaseCmdArgs(ctx context.Context, s string, action string) ([]string, error) { var err error var baseCommand string if baseCommand, err = t.getCmdStringFromBoolRule(s, action); err != nil { @@ -242,16 +248,28 @@ func (t *T) BaseCmdArgs(s string, action string) ([]string, error) { t.Log().Tracef("no base command for action '%v'", action) return nil, nil } + baseCommand, err = t.replaceVolumeHead(ctx, baseCommand) + if err != nil { + return nil, err + } return command.CmdArgsFromString(baseCommand) } +func (t *T) replaceVolumeHead(ctx context.Context, s string) (string, error) { + words := strings.Fields(s) + if !strings.HasPrefix(words[0], "/") && strings.Contains(words[0], "/") { + return vpath.HostPath(ctx, s, t.Path.Namespace) + } + return s, nil +} + // CmdArgs returns the command argv of an action func (t *T) CmdArgs(ctx context.Context, s string, action string) ([]string, error) { if len(s) == 0 { t.Log().Tracef("nothing to do for action '%v'", action) return nil, nil } - baseCommandSlice, err := t.BaseCmdArgs(s, action) + baseCommandSlice, err := t.BaseCmdArgs(ctx, s, action) if err != nil || baseCommandSlice == nil { return nil, err } @@ -280,7 +298,14 @@ func (t *T) GetFuncOpts(ctx context.Context, s string, action string) ([]funcopt if err != nil || cmdArgs == nil { return nil, err } - env, err := t.getEnv(ctx) + var onIgnoreCallback func(err error) + if action != "status" { + onIgnoreCallback = func(err error) { + t.Log().Infof("prepare '%s' command: %s", action, err) + } + } + + env, err := t.getEnv(ctx, onIgnoreCallback) if err != nil { return nil, err } @@ -318,12 +343,13 @@ func (t *T) Info(ctx context.Context) (resource.InfoKeys, error) { ) var opts []funcopt.O var err error - if opts, err = t.GetFuncOpts(ctx, t.InfoCmd, "info"); err != nil { - t.Log().Errorf("prepare 'info' command: %s", err) - if t.StatusLogKw { - t.StatusLog().Error("prepare cmd %s", err) - } - return nil, err + opts, err = t.GetFuncOpts(ctx, t.InfoCmd, "info") + var errAccess vpath.ErrAccess + if errors.As(err, &errAccess) { + t.Log().Tracef("skip 'info' command (%s is %s)", errAccess.Path, errAccess.Avail) + return result, nil + } else if err != nil { + return result, err } if len(opts) == 0 { return result, nil diff --git a/drivers/resappsimple/main.go b/drivers/resappsimple/main.go index 8587094b3..aec5cf097 100644 --- a/drivers/resappsimple/main.go +++ b/drivers/resappsimple/main.go @@ -108,7 +108,7 @@ func (t *T) Stop(ctx context.Context) error { } func (t *T) stop(ctx context.Context) error { - cmdArgs, err := t.BaseCmdArgs(t.StartCmd, "stop") + cmdArgs, err := t.BaseCmdArgs(ctx, t.StartCmd, "stop") if err != nil { return err } @@ -148,7 +148,7 @@ func (t *T) Status(ctx context.Context) status.T { if t.CheckCmd != "" { return t.CommonStatus(ctx) } - return t.status() + return t.status(ctx) } // Label implements Label from resource.Driver interface, @@ -157,8 +157,8 @@ func (t *T) Label(_ context.Context) string { return drvID.String() } -func (t *T) status() status.T { - cmdArgs, err := t.BaseCmdArgs(t.StartCmd, "start") +func (t *T) status(ctx context.Context) status.T { + cmdArgs, err := t.BaseCmdArgs(ctx, t.StartCmd, "start") if err != nil { t.StatusLog().Error("%s", err) return status.Undef diff --git a/drivers/rescontainerlxc/main.go b/drivers/rescontainerlxc/main.go index 1f6c2d1b7..afef3a755 100644 --- a/drivers/rescontainerlxc/main.go +++ b/drivers/rescontainerlxc/main.go @@ -327,12 +327,15 @@ func (t *T) createEnv() ([]string, error) { } env = append(env, t.createEnvProxy()...) env = append(env, t.CreateEnvironment...) - more, err := envprovider.From(t.CreateSecretsEnvironment, t.Path.Namespace, "sec") + ignoreCallback := func(err error) { + t.Log().Infof("ignore env %s", err) + } + more, err := envprovider.From(t.CreateSecretsEnvironment, t.Path.Namespace, "sec", envprovider.IgnoreExpected(ignoreCallback)) if err != nil { return env, err } env = append(env, more...) - more, err = envprovider.From(t.CreateConfigsEnvironment, t.Path.Namespace, "cfg") + more, err = envprovider.From(t.CreateConfigsEnvironment, t.Path.Namespace, "cfg", envprovider.IgnoreExpected(ignoreCallback)) if err != nil { return env, err } @@ -340,14 +343,6 @@ func (t *T) createEnv() ([]string, error) { return env, nil } -func (t *T) createEnvSecrets() ([]string, error) { - return envprovider.From(t.CreateSecretsEnvironment, t.Path.Namespace, "sec") -} - -func (t *T) createEnvConfigs() ([]string, error) { - return envprovider.From(t.CreateConfigsEnvironment, t.Path.Namespace, "cfg") -} - func (t *T) createEnvProxy() []string { env := []string{} keys := []string{ diff --git a/drivers/rescontainerocibase/main.go b/drivers/rescontainerocibase/main.go index 286b60abf..bc0a7bce2 100644 --- a/drivers/rescontainerocibase/main.go +++ b/drivers/rescontainerocibase/main.go @@ -362,12 +362,15 @@ func (t *BT) GenEnv(ctx context.Context) (envL []string, envM map[string]string, if len(t.Env) > 0 { envL = append(envL, t.Env...) } - if tempEnv, err := envprovider.From(t.ConfigsEnv, t.Path.Namespace, "cfg"); err != nil { + ignoreCallback := func(err error) { + t.Log().Infof("ignore env %s", err) + } + if tempEnv, err := envprovider.From(t.ConfigsEnv, t.Path.Namespace, "cfg", envprovider.IgnoreExpected(ignoreCallback)); err != nil { return nil, nil, err } else { envL = append(envL, tempEnv...) } - if tempEnv, err := envprovider.From(t.SecretsEnv, t.Path.Namespace, "sec"); err != nil { + if tempEnv, err := envprovider.From(t.SecretsEnv, t.Path.Namespace, "sec", envprovider.IgnoreExpected(ignoreCallback)); err != nil { return nil, nil, err } else { for _, s := range tempEnv { @@ -653,6 +656,19 @@ func (t *BT) Stop(ctx context.Context) error { return nil } +func (t *BT) StatusInfo(ctx context.Context) map[string]any { + m := make(map[string]any) + m["name"] = t.ContainerName() + + inspect, err := t.ContainerInspect(ctx) + if err == nil && inspect != nil { + m["id"] = inspect.ID() + m["image_id"] = inspect.ImageID() + m["pid"] = inspect.PID() + } + return m +} + func (t *BT) Status(ctx context.Context) status.T { if !t.Detach { t.Log().Tracef("status n/a on not detach") diff --git a/drivers/resdiskdrbd/main.go b/drivers/resdiskdrbd/main.go index a78a9c59c..984b23f09 100644 --- a/drivers/resdiskdrbd/main.go +++ b/drivers/resdiskdrbd/main.go @@ -70,6 +70,7 @@ type ( Secondary(context.Context) error Up(context.Context) error WipeMD(context.Context) error + WaitIsDefined(ctx context.Context, timeout time.Duration, target bool) error WaitCState(ctx context.Context, nodeID string, timeout time.Duration, candidates ...string) (string, error) WaitConnectingOrConnected(ctx context.Context, nodeID string) (string, error) StartConnections(context.Context, ...string) error @@ -999,7 +1000,8 @@ func (t *T) UnprovisionAsFollower(ctx context.Context) error { } func (t *T) unprovisionCommon(ctx context.Context) error { - isDefined, err := t.drbd(ctx).IsDefined(ctx) + dev := t.drbd(ctx) + isDefined, err := dev.IsDefined(ctx) if err != nil { return err } @@ -1007,6 +1009,9 @@ func (t *T) unprovisionCommon(ctx context.Context) error { if err := t.DownForce(ctx); err != nil { return err } + if err := dev.WaitIsDefined(ctx, 5*time.Second, false); err != nil { + return err + } if err := t.WipeMD(ctx); err != nil { return err } diff --git a/drivers/resdiskxp8/text/kw/split_start b/drivers/resdiskxp8/text/kw/split_start index 2ce95992d..c26eed832 100644 --- a/drivers/resdiskxp8/text/kw/split_start +++ b/drivers/resdiskxp8/text/kw/split_start @@ -1 +1,13 @@ -split_start must be set to allow start on a SSUS S-VOL (split pair). +Controls whether the start action is allowed on a split S-VOL (in states like SSUS, SSUE, or SSWS). + +- `false` (default): + + The start action fails on a split S-VOL to prevent accidental divergence between pair legs. This is the recommended setting for normal operations. + +- `true` + + The start action succeeds on a split S-VOL, allowing the administrator to manually decide which diverging dataset should survive during resynchronization. + + **Use case**: Only enable this during disaster recovery or when intentional divergence is required. + +**Note**: Leaving split_start=false ensures data consistency by default. Switch to true only when necessary and with caution. diff --git a/drivers/resdiskzvol/main.go b/drivers/resdiskzvol/main.go index 277a57474..984073cce 100644 --- a/drivers/resdiskzvol/main.go +++ b/drivers/resdiskzvol/main.go @@ -134,6 +134,9 @@ func (t *T) zvolCreate() error { if t.BlockSize != nil { opts = append(opts, zfs.VolCreateWithBlockSize(uint64(*t.BlockSize))) } + if len(t.CreateOptions) > 0 { + opts = append(opts, zfs.VolCreateWithArgs(t.CreateOptions)) + } return t.zvol().Create(opts...) } diff --git a/drivers/resipnetns/manifest.go b/drivers/resipnetns/manifest.go index 721f0851f..ba3c17f22 100644 --- a/drivers/resipnetns/manifest.go +++ b/drivers/resipnetns/manifest.go @@ -73,7 +73,8 @@ var ( Attr: "Mode", Candidates: []string{"bridge", "dedicated", "macvlan", "ipvlan-l2", "ipvlan-l3", "ipvlan-l3s", "ovs"}, Default: "bridge", - Example: "access", + Example: "bridge", + Minimal: true, Option: "mode", Scopable: true, Text: keywords.NewText(fs, "text/kw/mode"), @@ -104,6 +105,7 @@ var ( Aliases: []string{"ipname"}, Attr: "Name", Example: "1.2.3.4", + Minimal: true, Option: "name", Scopable: true, Text: keywords.NewText(fs, "text/kw/name"), @@ -111,7 +113,7 @@ var ( { Aliases: []string{"ipdev"}, Attr: "Dev", - Example: "eth0", + Example: "br-prd", Option: "dev", Required: true, Scopable: true, @@ -120,6 +122,7 @@ var ( { Attr: "Netmask", Example: "24", + Minimal: true, Option: "netmask", Scopable: true, Text: keywords.NewText(fs, "text/kw/netmask"), diff --git a/drivers/resipnetns/text/kw/mode b/drivers/resipnetns/text/kw/mode index 561c4751b..29393fb7e 100644 --- a/drivers/resipnetns/text/kw/mode +++ b/drivers/resipnetns/text/kw/mode @@ -1,4 +1,42 @@ The ip link mode. -If `dev` is set to a bridge interface the mode defaults to `bridge`, else -defaults to `macvlan`. The `ipvlan` mode requires a 4.2+ Linux kernel. +- `bridge` + + Proven, simple and versatile default mode. + +- `dedicated` + + Used when a container or namespace gets exclusive ownership of a physical NIC. Ideal for performance-sensitive workloads that need direct hardware access without virtualization overhead. + +- `macvlan` + + Used when each container needs its own MAC address and appears as a distinct device on the physical network (L2 presence). Common in environments where containers must be directly reachable on the LAN, like VMs would be. This mode is not appropiate on VMWare with Vswitch default policies. + +- `ipvlan-l2` + + Similar to macvlan but all sub-interfaces share the parent's MAC address. Used when the upstream switch limits the number of MAC addresses per port (e.g., cloud environments or strict switch policies). + +- `ipvlan-l3` + + Used when you want the host to act as a router between namespaces and the external network. No L2 broadcast/multicast is propagated; each namespace is isolated at Layer 2. Suited for high-density, routed environments. + +- `ipvlan-l3s` + + A variant of ipvlan-l3 that keeps traffic in the kernel's netfilter/iptables path (s = symmetric routing). Used when you still need NAT, firewalling, or policy routing rules to apply to ipvlan traffic, unlike pure l3 mode. + +- `ovs` (Open vSwitch) + + Used in SDN (Software-Defined Networking) and multi-host environments. Chosen when you need advanced L2/L3 features like VLANs, tunneling (VXLAN, GRE), flow-based forwarding, or centralized network control. + + +**Communication scope:** + +| Mode | Host → Container | Container → Host | Container → Container | +|------------|------------------|------------------|-----------------------| +| bridge | ok | ok | ok | +| dedicated | | | | +| macvlan | | | ok | +| ipvlan-l2 | | | ok | +| ipvlan-l3 | | | ok | +| ipvlan-l3s | ok | ok | ok | +| ovs | ok | ok | ok | diff --git a/drivers/restaskhost/main.go b/drivers/restaskhost/main.go index f6facf03f..d3de39a6c 100644 --- a/drivers/restaskhost/main.go +++ b/drivers/restaskhost/main.go @@ -161,7 +161,7 @@ func (t *T) Kill(ctx context.Context) error { func (t *T) stop(ctx context.Context) error { app := t.App() - cmdArgs, err := app.BaseCmdArgs(app.StartCmd, "stop") + cmdArgs, err := app.BaseCmdArgs(ctx, app.StartCmd, "stop") if err != nil { return err } diff --git a/pkg/vim/ftdetect/opensvc.vim b/pkg/vim/ftdetect/opensvc.vim new file mode 100644 index 000000000..aaa40c380 --- /dev/null +++ b/pkg/vim/ftdetect/opensvc.vim @@ -0,0 +1 @@ +au BufRead,BufNewFile {/etc,/var/tmp}/opensvc/**.conf{,.tmp} set filetype=opensvc diff --git a/pkg/vim/syntax/opensvc.vim b/pkg/vim/syntax/opensvc.vim new file mode 100644 index 000000000..17bd21058 --- /dev/null +++ b/pkg/vim/syntax/opensvc.vim @@ -0,0 +1,31 @@ +" opensvc config file syntax +if exists("b:current_syntax") + finish +endif + +" Section headers [DEFAULT] [disk#0] etc. +syn match opensvcSection /^\[.\{-}\]/ + +" Key scope @ within a key +syn match opensvcScope /@\w\+/ contained + +" Keys (word before the =), optionally with @scope +syn match opensvcKey /^\s*\w\+\(@\w\+\)\?\ze\s*=/ contains=opensvcScope + +" Equal sign +syn match opensvcEqual /=/ + +" Values in curly braces {name} {fqdn} etc. +syn match opensvcReference /{[^}]*}/ + +" Comments +syn match opensvcComment /^\s*[#;].*/ + +hi def opensvcSection ctermfg=Yellow cterm=bold guifg=#FFD700 gui=bold +hi def opensvcKey ctermfg=Cyan guifg=#00BFFF +hi def opensvcScope ctermfg=Magenta cterm=bold guifg=#FF00FF gui=bold +hi def opensvcEqual ctermfg=DarkGray guifg=#666666 +hi def opensvcReference ctermfg=Green cterm=bold guifg=#00FF7F gui=bold +hi def opensvcComment ctermfg=DarkGray cterm=italic guifg=#555555 gui=italic + +let b:current_syntax = "opensvc" diff --git a/util/args/args.go b/util/args/args.go index e1e3b9091..353aff6d2 100644 --- a/util/args/args.go +++ b/util/args/args.go @@ -12,6 +12,7 @@ package args import ( "regexp" + "strings" "github.com/anmitsu/go-shlex" ) @@ -198,3 +199,38 @@ func (t *T) dropOption(opt matchOpt) { func (t *T) Append(s ...string) { t.args = append(t.args, s...) } + +// shellQuote quotes a single argument for shell usage. +// It follows basic shell quoting rules: +// - If the argument contains no special characters, return as-is +// - Otherwise, wrap in single quotes and escape any single quotes within +func shellQuote(arg string) string { + // Characters that require quoting in shell + // Note: '=' is allowed in simple assignments like key=value + needsQuoting := false + for _, c := range arg { + if c <= ' ' || c == '\'' || c == '"' || c == '`' || c == '$' || c == '\\' || c == ';' || c == '&' || c == '|' || c == '(' || c == ')' || c == '<' || c == '>' || c == '[' || c == ']' || c == '{' || c == '}' || c == '*' || c == '?' || c == '#' || c == '~' || c == '%' { + needsQuoting = true + break + } + } + + if !needsQuoting { + return arg + } + + // Use single quotes and escape any single quotes within by closing, + // adding escaped single quote, and reopening + return "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" +} + +// String returns a shell-escaped string representation of the arguments. +// It quotes arguments containing spaces or special characters so the output +// can be safely pasted into a shell. +func (t T) String() string { + var quotedArgs []string + for _, arg := range t.args { + quotedArgs = append(quotedArgs, shellQuote(arg)) + } + return strings.Join(quotedArgs, " ") +} diff --git a/util/args/args_test.go b/util/args/args_test.go index c942678c9..e54eb1a6f 100644 --- a/util/args/args_test.go +++ b/util/args/args_test.go @@ -56,4 +56,25 @@ func Test(t *testing.T) { args.DropOption("--init") assert.Equal(t, l7, args.Get(), "") }) + t.Run("String", func(t *testing.T) { + // Test with various arguments including spaces and special characters + args, err := Parse("--init -o a=b -o b=c --comment 'bad trip' --comment 'good trap' -d /tmp/foo -f") + assert.NoError(t, err, "") + + // The String() method should properly quote arguments with spaces + result := args.String() + expected := `--init -o a=b -o b=c --comment 'bad trip' --comment 'good trap' -d /tmp/foo -f` + assert.Equal(t, expected, result, "String() should properly quote arguments") + + // Test with arguments that need quoting + args2 := New("arg1", "arg with spaces", "arg'with'quotes", `arg"with"double`, "$special") + result2 := args2.String() + // All arguments with special characters should be quoted + assert.Contains(t, result2, "arg1") + assert.Contains(t, result2, "'arg with spaces'") + assert.Contains(t, result2, "'arg'\\''with'\\''quotes'") + // For double quotes, shlex.Quote should handle them appropriately + assert.Contains(t, result2, "double") + assert.Contains(t, result2, "'$special'") + }) } diff --git a/util/drbd/main.go b/util/drbd/main.go index cd1044fa0..f4d16e911 100644 --- a/util/drbd/main.go +++ b/util/drbd/main.go @@ -778,6 +778,37 @@ func (t *T) TryStartConnection(ctx context.Context, nodeID string) error { return t.StartConnections(ctx, nodeID) } +func (t *T) WaitIsDefined(ctx context.Context, timeout time.Duration, target bool) error { + t.log.Tracef("wait for %s isDefined=%v", t.res, target) + var state, lastState bool + ok, err := waitfor.TrueNoErrorCtx(ctx, timeout, waitConnectionStateDelay, func() (bool, error) { + var err error + state, err = t.IsDefined(ctx) + + if err != nil { + return false, err + } + + if target == state { + return true, nil + } else { + if state != lastState { + t.log.Infof("wait for %s defined %v, found current defined %v", t.res, target, state) + lastState = state + } + return false, nil + } + }) + if err != nil { + return fmt.Errorf("wait for %s defined %v: %w", t.res, target, err) + } else if !ok { + return fmt.Errorf("wait for %s defined %v: timeout, last state was: %v", t.res, target, state) + } + t.log.Infof("wait for %s defined %v: succeed", t.res, target) + return nil + +} + func (t *T) WaitCState(ctx context.Context, nodeID string, timeout time.Duration, candidates ...string) (string, error) { t.log.Tracef("wait for %s node-id %s cstate in (%s)", t.res, nodeID, strings.Join(candidates, ",")) var state, lastState string diff --git a/util/envprovider/main.go b/util/envprovider/main.go index e1b4c547f..39c5339ff 100644 --- a/util/envprovider/main.go +++ b/util/envprovider/main.go @@ -3,6 +3,7 @@ package envprovider import ( + "errors" "fmt" "strings" @@ -14,22 +15,135 @@ type ( decoder interface { DecodeKey(keyname string) ([]byte, error) MatchingKeys(match string) ([]string, error) + HasKey(string) bool + } + + ErrObjectNotExist struct { + Path naming.Path + } + ErrKeyNotExist struct { + Path naming.Path + Key string + } + ErrObjectNotDecoder struct { + Path naming.Path } ) -// From return []string env from configs_environment or secrets_environment +func (t ErrObjectNotExist) Error() string { + return fmt.Sprintf("object %s does not exists", t.Path) +} + +func (t ErrObjectNotDecoder) Error() string { + return fmt.Sprintf("object %s is not a decoder", t.Path) +} + +func (t ErrKeyNotExist) Error() string { + return fmt.Sprintf("object %s has no key matching '%s'", t.Path, t.Key) +} + +type IgnoreOption struct { + ignore func(error) bool + onIgnore func(error) +} + +func IgnoreNotExist(onIgnore func(error)) IgnoreOption { + o := IgnoreOption{ignore: func(err error) bool { + var e ErrObjectNotExist + return errors.As(err, &e) + }} + o.onIgnore = onIgnore + return o +} + +func IgnoreNotDecoder(onIgnore func(error)) IgnoreOption { + o := IgnoreOption{ignore: func(err error) bool { + var e ErrObjectNotDecoder + return errors.As(err, &e) + }} + o.onIgnore = onIgnore + return o +} + +func IgnoreKeyNotExist(onIgnore func(error)) IgnoreOption { + o := IgnoreOption{ignore: func(err error) bool { + var e ErrKeyNotExist + return errors.As(err, &e) + }} + o.onIgnore = onIgnore + return o +} + +func IgnoreExpected(onIgnore func(error)) IgnoreOption { + o := IgnoreOption{ignore: func(err error) bool { + var ( + e1 ErrObjectNotExist + e2 ErrObjectNotDecoder + e3 ErrKeyNotExist + ) + return errors.As(err, &e1) || errors.As(err, &e2) || errors.As(err, &e3) + }} + o.onIgnore = onIgnore + return o +} + +func eachError(err error, fn func(error) error) error { + if err == nil { + return nil + } + type multi interface{ Unwrap() []error } + if m, ok := err.(multi); ok { + var errs error + for _, e := range m.Unwrap() { + err1 := eachError(e, fn) + if err1 != nil { + errs = errors.Join(errs, err1) + } + } + return errs + } else { + return fn(err) + } +} + +func filterErrors(err error, ignore ...IgnoreOption) error { + if len(ignore) == 0 { + return err + } + return eachError(err, func(e error) error { + for _, opt := range ignore { + if opt.ignore(e) { + if opt.onIgnore != nil { + opt.onIgnore(e) + } + return nil + } + } + return e + }) +} + +func From(items []string, namespace, kind string, ignore ...IgnoreOption) ([]string, error) { + result, err := from(items, namespace, kind) + if err != nil { + err = filterErrors(err, ignore...) + } + return result, err +} + +// from return []string env from configs_environment or secrets_environment // Examples: // // From([]string{"FOO=cfg1/key1"}, "namespace1", "cfg") // From([]string{"FOO=sec1/key1"}, "namespace1", "sec") -func From(items []string, ns, kd string) (result []string, err error) { +func from(items []string, ns, kd string) (result []string, err error) { for _, item := range items { if item == "[]" { continue } - envs, err := envVars(item, ns, kd) - if err != nil { - return nil, err + envs, err1 := envVars(item, ns, kd) + if err1 != nil { + err = errors.Join(err, fmt.Errorf("from %s: %w", item, err1)) } result = append(result, envs...) } @@ -55,33 +169,34 @@ func envVars(envItem, ns, kd string) (result []string, err error) { return } -func getKeysDecoder(name, ns, kd string) (decoder, error) { - if p, err := naming.NewPathFromStrings(ns, kd, name); err != nil { - return nil, err - } else if !p.Exists() { - return nil, fmt.Errorf("object %s does not exists", p) - } else if o, err := object.New(p); err != nil { +func getKeysDecoder(path naming.Path) (decoder, error) { + if !path.Exists() { + return nil, ErrObjectNotExist{Path: path} + } else if o, err := object.New(path); err != nil { return nil, err } else if do, ok := o.(decoder); !ok { - return nil, fmt.Errorf("object %s is not a decoder", p) + return nil, ErrObjectNotDecoder{Path: path} } else { return do, nil } } func getKeys(name, ns, kd, match string) (s []string, err error) { + path, err := naming.NewPathFromStrings(ns, kd, name) + if err != nil { + return nil, err + } var o decoder var keys []string var value string - if o, err = getKeysDecoder(name, ns, kd); err != nil { + if o, err = getKeysDecoder(path); err != nil { return nil, err } if keys, err = o.MatchingKeys(match); err != nil { return nil, err } if len(keys) == 0 { - return nil, fmt.Errorf("object %s has no key matching '%s'", o, match) - + return nil, ErrKeyNotExist{Path: path, Key: match} } for _, key := range keys { if value, err = decodeKey(o, key); err != nil { @@ -93,8 +208,14 @@ func getKeys(name, ns, kd, match string) (s []string, err error) { } func getKey(name, ns, kd, key string) (string, error) { - if o, err := getKeysDecoder(name, ns, kd); err != nil { + path, err := naming.NewPathFromStrings(ns, kd, name) + if err != nil { + return "", err + } + if o, err := getKeysDecoder(path); err != nil { return "", err + } else if !o.HasKey(key) { + return "", ErrKeyNotExist{Path: path, Key: key} } else { return decodeKey(o, key) } diff --git a/util/zfs/vol_create.go b/util/zfs/vol_create.go index 95df4b8c5..eef1e47a2 100644 --- a/util/zfs/vol_create.go +++ b/util/zfs/vol_create.go @@ -25,9 +25,6 @@ type ( func VolCreateWithArgs(l []string) funcopt.O { return funcopt.F(func(i interface{}) error { t := i.(*volCreateOpts) - if t.Args == nil { - t.Args = make([]string, 0) - } t.Args = append(t.Args, l...) return nil }) @@ -61,24 +58,22 @@ func createSizeString(size uint64) string { func volCreateOptsToArgs(t volCreateOpts) []string { a := args.New() a.Append("create") + + // zvol create -V + // ^^^^^^^^^ + a.Append(t.Args...) + if t.BlockSize > 0 { a.DropOption("-b") a.DropOptionAndMatchingValue("-o", "^volblocksize=.*") a.Append("-b", sizeconv.ExactBSizeCompact(float64(t.BlockSize))) } - a.Append("-V") - - // zvol create -V - // ^^^^^^^^^ - if t.Args != nil { - a.Append(t.Args...) - } - // zvol create -V + // zvol create -V // ^^^^^^ - a.Append(createSizeString(t.Size)) + a.Append("-V", createSizeString(t.Size)) - // zvol create -V + // zvol create -V // ^^^^^^ a.Append(t.Name) return a.Get()