v2 bloat pruning phase 2

this is second phase of removing unneeded bloat in the remote client. this is important to be able to reduce the client size as well as possible native compilation for windows/mac.

Signed-off-by: Brent Baude <bbaude@redhat.com>
This commit is contained in:
Brent Baude
2020-04-15 10:52:12 -05:00
parent 6e9622aa98
commit 30d2964ff8
12 changed files with 127 additions and 102 deletions

View File

@ -13,11 +13,11 @@ import (
"github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/util"
"github.com/containers/libpod/utils" "github.com/containers/libpod/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag"
) )
var ( var (
@ -157,7 +157,7 @@ func runlabelCmd(c *cliconfig.RunlabelValues) error {
return errors.Errorf("%s does not have a label of %s", runlabelImage, label) return errors.Errorf("%s does not have a label of %s", runlabelImage, label)
} }
globalOpts := util.GetGlobalOpts(c) globalOpts := GetGlobalOpts(c)
cmd, env, err := shared.GenerateRunlabelCommand(runLabel, imageName, c.Name, opts, extraArgs, globalOpts) cmd, env, err := shared.GenerateRunlabelCommand(runLabel, imageName, c.Name, opts, extraArgs, globalOpts)
if err != nil { if err != nil {
return err return err
@ -193,3 +193,32 @@ func runlabelCmd(c *cliconfig.RunlabelValues) error {
return utils.ExecCmdWithStdStreams(stdIn, stdOut, stdErr, env, cmd[0], cmd[1:]...) return utils.ExecCmdWithStdStreams(stdIn, stdOut, stdErr, env, cmd[0], cmd[1:]...)
} }
// GetGlobalOpts checks all global flags and generates the command string
func GetGlobalOpts(c *cliconfig.RunlabelValues) string {
globalFlags := map[string]bool{
"cgroup-manager": true, "cni-config-dir": true, "conmon": true, "default-mounts-file": true,
"hooks-dir": true, "namespace": true, "root": true, "runroot": true,
"runtime": true, "storage-driver": true, "storage-opt": true, "syslog": true,
"trace": true, "network-cmd-path": true, "config": true, "cpu-profile": true,
"log-level": true, "tmpdir": true}
const stringSliceType string = "stringSlice"
var optsCommand []string
c.PodmanCommand.Command.Flags().VisitAll(func(f *pflag.Flag) {
if !f.Changed {
return
}
if _, exist := globalFlags[f.Name]; exist {
if f.Value.Type() == stringSliceType {
flagValue := strings.TrimSuffix(strings.TrimPrefix(f.Value.String(), "["), "]")
for _, value := range strings.Split(flagValue, ",") {
optsCommand = append(optsCommand, fmt.Sprintf("--%s %s", f.Name, value))
}
} else {
optsCommand = append(optsCommand, fmt.Sprintf("--%s %s", f.Name, f.Value.String()))
}
}
})
return strings.Join(optsCommand, " ")
}

View File

@ -6,7 +6,6 @@ import (
buildahcli "github.com/containers/buildah/pkg/cli" buildahcli "github.com/containers/buildah/pkg/cli"
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@ -214,22 +213,22 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet {
) )
createFlags.StringVar( createFlags.StringVar(
&cf.HealthInterval, &cf.HealthInterval,
"health-interval", cliconfig.DefaultHealthCheckInterval, "health-interval", DefaultHealthCheckInterval,
"set an interval for the healthchecks (a value of disable results in no automatic timer setup)", "set an interval for the healthchecks (a value of disable results in no automatic timer setup)",
) )
createFlags.UintVar( createFlags.UintVar(
&cf.HealthRetries, &cf.HealthRetries,
"health-retries", cliconfig.DefaultHealthCheckRetries, "health-retries", DefaultHealthCheckRetries,
"the number of retries allowed before a healthcheck is considered to be unhealthy", "the number of retries allowed before a healthcheck is considered to be unhealthy",
) )
createFlags.StringVar( createFlags.StringVar(
&cf.HealthStartPeriod, &cf.HealthStartPeriod,
"health-start-period", cliconfig.DefaultHealthCheckStartPeriod, "health-start-period", DefaultHealthCheckStartPeriod,
"the initialization time needed for a container to bootstrap", "the initialization time needed for a container to bootstrap",
) )
createFlags.StringVar( createFlags.StringVar(
&cf.HealthTimeout, &cf.HealthTimeout,
"health-timeout", cliconfig.DefaultHealthCheckTimeout, "health-timeout", DefaultHealthCheckTimeout,
"the maximum time allowed to complete the healthcheck before an interval is considered failed", "the maximum time allowed to complete the healthcheck before an interval is considered failed",
) )
createFlags.StringVarP( createFlags.StringVarP(
@ -244,7 +243,7 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet {
) )
createFlags.StringVar( createFlags.StringVar(
&cf.ImageVolume, &cf.ImageVolume,
"image-volume", cliconfig.DefaultImageVolume, "image-volume", DefaultImageVolume,
`Tells podman how to handle the builtin image volumes ("bind"|"tmpfs"|"ignore")`, `Tells podman how to handle the builtin image volumes ("bind"|"tmpfs"|"ignore")`,
) )
createFlags.BoolVar( createFlags.BoolVar(

View File

@ -12,6 +12,19 @@ import (
"github.com/opencontainers/selinux/go-selinux" "github.com/opencontainers/selinux/go-selinux"
) )
var (
// DefaultHealthCheckInterval default value
DefaultHealthCheckInterval = "30s"
// DefaultHealthCheckRetries default value
DefaultHealthCheckRetries uint = 3
// DefaultHealthCheckStartPeriod default value
DefaultHealthCheckStartPeriod = "0s"
// DefaultHealthCheckTimeout default value
DefaultHealthCheckTimeout = "30s"
// DefaultImageVolume default value
DefaultImageVolume = "bind"
)
// TODO these options are directly embedded into many of the CLI cobra values, as such // TODO these options are directly embedded into many of the CLI cobra values, as such
// this approach will not work in a remote client. so we will need to likely do something like a // this approach will not work in a remote client. so we will need to likely do something like a
// supported and unsupported approach here and backload these options into the specgen // supported and unsupported approach here and backload these options into the specgen

View File

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
lpfilters "github.com/containers/libpod/libpod/filters"
"github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/api/handlers/utils"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -15,6 +16,7 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) {
var ( var (
delContainers []string delContainers []string
space int64 space int64
filterFuncs []libpod.ContainerFilter
) )
runtime := r.Context().Value("runtime").(*libpod.Runtime) runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder) decoder := r.Context().Value("decoder").(*schema.Decoder)
@ -26,12 +28,16 @@ func PruneContainers(w http.ResponseWriter, r *http.Request) {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String())) utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
return return
} }
for k, v := range query.Filters {
filterFuncs, err := utils.GenerateFilterFuncsFromMap(runtime, query.Filters) for _, val := range v {
generatedFunc, err := lpfilters.GenerateContainerFilterFuncs(k, val, runtime)
if err != nil { if err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return
} }
filterFuncs = append(filterFuncs, generatedFunc)
}
}
prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs) prunedContainers, pruneErrors, err := runtime.PruneContainers(filterFuncs)
if err != nil { if err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)

View File

@ -4,12 +4,12 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
"github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/api/handlers/utils"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/domain/filters" "github.com/containers/libpod/pkg/domain/filters"
"github.com/containers/libpod/pkg/domain/infra/abi/parse"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -46,7 +46,7 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) {
volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label)) volumeOptions = append(volumeOptions, libpod.WithVolumeLabels(input.Label))
} }
if len(input.Options) > 0 { if len(input.Options) > 0 {
parsedOptions, err := shared.ParseVolumeOptions(input.Options) parsedOptions, err := parse.ParseVolumeOptions(input.Options)
if err != nil { if err != nil {
utils.InternalServerError(w, err) utils.InternalServerError(w, err)
return return

View File

@ -6,9 +6,10 @@ import (
"time" "time"
"github.com/containers/libpod/cmd/podman/shared" "github.com/containers/libpod/cmd/podman/shared"
createconfig "github.com/containers/libpod/pkg/spec"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
createconfig "github.com/containers/libpod/pkg/spec"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -68,24 +69,6 @@ func WaitContainer(w http.ResponseWriter, r *http.Request) (int32, error) {
return con.WaitForConditionWithInterval(interval, condition) return con.WaitForConditionWithInterval(interval, condition)
} }
// GenerateFilterFuncsFromMap is used to generate un-executed functions that can be used to filter
// containers. It is specifically designed for the RESTFUL API input.
func GenerateFilterFuncsFromMap(r *libpod.Runtime, filters map[string][]string) ([]libpod.ContainerFilter, error) {
var (
filterFuncs []libpod.ContainerFilter
)
for k, v := range filters {
for _, val := range v {
f, err := shared.GenerateContainerFilterFuncs(k, val, r)
if err != nil {
return filterFuncs, err
}
filterFuncs = append(filterFuncs, f)
}
}
return filterFuncs, nil
}
func CreateContainer(ctx context.Context, w http.ResponseWriter, runtime *libpod.Runtime, cc *createconfig.CreateConfig) { func CreateContainer(ctx context.Context, w http.ResponseWriter, runtime *libpod.Runtime, cc *createconfig.CreateConfig) {
var pod *libpod.Pod var pod *libpod.Pod
ctr, err := shared.CreateContainerFromCreateConfig(runtime, cc, ctx, pod) ctr, err := shared.CreateContainerFromCreateConfig(runtime, cc, ctx, pod)

View File

@ -1,11 +1,10 @@
package utils package utils
import ( import (
"fmt"
"net/http" "net/http"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
lpfilters "github.com/containers/libpod/libpod/filters"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
"github.com/gorilla/schema" "github.com/gorilla/schema"
) )
@ -14,7 +13,7 @@ func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport
var ( var (
lps []*entities.ListPodsReport lps []*entities.ListPodsReport
pods []*libpod.Pod pods []*libpod.Pod
podErr error filters []libpod.PodFilter
) )
runtime := r.Context().Value("runtime").(*libpod.Runtime) runtime := r.Context().Value("runtime").(*libpod.Runtime)
decoder := r.Context().Value("decoder").(*schema.Decoder) decoder := r.Context().Value("decoder").(*schema.Decoder)
@ -28,28 +27,24 @@ func GetPods(w http.ResponseWriter, r *http.Request) ([]*entities.ListPodsReport
if err := decoder.Decode(&query, r.URL.Query()); err != nil { if err := decoder.Decode(&query, r.URL.Query()); err != nil {
return nil, err return nil, err
} }
var filters = []string{}
if _, found := r.URL.Query()["digests"]; found && query.Digests { if _, found := r.URL.Query()["digests"]; found && query.Digests {
UnSupportedParameter("digests") UnSupportedParameter("digests")
} }
if len(query.Filters) > 0 {
for k, v := range query.Filters { for k, v := range query.Filters {
for _, val := range v { for _, filter := range v {
filters = append(filters, fmt.Sprintf("%s=%s", k, val)) f, err := lpfilters.GeneratePodFilterFunc(k, filter)
}
}
filterFuncs, err := shared.GenerateFilterFunction(runtime, filters)
if err != nil { if err != nil {
return nil, err return nil, err
} }
pods, podErr = shared.FilterAllPodsWithFilterFunc(runtime, filterFuncs...) filters = append(filters, f)
} else {
pods, podErr = runtime.GetAllPods()
} }
if podErr != nil {
return nil, podErr
} }
pods, err := runtime.Pods(filters...)
if err != nil {
return nil, err
}
for _, pod := range pods { for _, pod := range pods {
status, err := pod.GetPodStatus() status, err := pod.GetPodStatus()
if err != nil { if err != nil {

View File

@ -4,8 +4,8 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/ps/define"
"github.com/cri-o/ocicni/pkg/ocicni" "github.com/cri-o/ocicni/pkg/ocicni"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -48,7 +48,7 @@ type ListContainer struct {
// Port mappings // Port mappings
Ports []ocicni.PortMapping Ports []ocicni.PortMapping
// Size of the container rootfs. Requires the size boolean to be true // Size of the container rootfs. Requires the size boolean to be true
Size *shared.ContainerSize Size *define.ContainerSize
// Time when container started // Time when container started
StartedAt int64 StartedAt int64
// State of container // State of container

View File

@ -11,6 +11,8 @@ import (
"strings" "strings"
"sync" "sync"
lpfilters "github.com/containers/libpod/libpod/filters"
"github.com/containers/buildah" "github.com/containers/buildah"
"github.com/containers/common/pkg/config" "github.com/containers/common/pkg/config"
"github.com/containers/image/v5/manifest" "github.com/containers/image/v5/manifest"
@ -19,7 +21,6 @@ import (
"github.com/containers/libpod/libpod/events" "github.com/containers/libpod/libpod/events"
"github.com/containers/libpod/libpod/image" "github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/libpod/logs" "github.com/containers/libpod/libpod/logs"
"github.com/containers/libpod/pkg/api/handlers/utils"
"github.com/containers/libpod/pkg/checkpoint" "github.com/containers/libpod/pkg/checkpoint"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/domain/infra/abi/terminal" "github.com/containers/libpod/pkg/domain/infra/abi/terminal"
@ -175,10 +176,16 @@ func (ic *ContainerEngine) ContainerStop(ctx context.Context, namesOrIds []strin
} }
func (ic *ContainerEngine) ContainerPrune(ctx context.Context, options entities.ContainerPruneOptions) (*entities.ContainerPruneReport, error) { func (ic *ContainerEngine) ContainerPrune(ctx context.Context, options entities.ContainerPruneOptions) (*entities.ContainerPruneReport, error) {
filterFuncs, err := utils.GenerateFilterFuncsFromMap(ic.Libpod, options.Filters) var filterFuncs []libpod.ContainerFilter
for k, v := range options.Filters {
for _, val := range v {
generatedFunc, err := lpfilters.GenerateContainerFilterFuncs(k, val, ic.Libpod)
if err != nil { if err != nil {
return nil, err return nil, err
} }
filterFuncs = append(filterFuncs, generatedFunc)
}
}
prunedContainers, pruneErrors, err := ic.Libpod.PruneContainers(filterFuncs) prunedContainers, pruneErrors, err := ic.Libpod.PruneContainers(filterFuncs)
if err != nil { if err != nil {
return nil, err return nil, err

8
pkg/ps/define/types.go Normal file
View File

@ -0,0 +1,8 @@
package define
// ContainerSize holds the size of the container's root filesystem and top
// read-write layer.
type ContainerSize struct {
RootFsSize int64 `json:"rootFsSize"`
RwSize int64 `json:"rwSize"`
}

View File

@ -1,16 +1,19 @@
package ps package ps
import ( import (
"os"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/containers/libpod/cmd/podman/shared"
"github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/define" "github.com/containers/libpod/libpod/define"
lpfilters "github.com/containers/libpod/libpod/filters" lpfilters "github.com/containers/libpod/libpod/filters"
"github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/domain/entities"
psdefine "github.com/containers/libpod/pkg/ps/define"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -80,7 +83,7 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities
exitCode int32 exitCode int32
exited bool exited bool
pid int pid int
size *shared.ContainerSize size *psdefine.ContainerSize
startedTime time.Time startedTime time.Time
exitedTime time.Time exitedTime time.Time
cgroup, ipc, mnt, net, pidns, user, uts string cgroup, ipc, mnt, net, pidns, user, uts string
@ -116,16 +119,16 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities
return errors.Wrapf(err, "unable to obtain container pid") return errors.Wrapf(err, "unable to obtain container pid")
} }
ctrPID := strconv.Itoa(pid) ctrPID := strconv.Itoa(pid)
cgroup, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "cgroup")) cgroup, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "cgroup"))
ipc, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "ipc")) ipc, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "ipc"))
mnt, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "mnt")) mnt, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "mnt"))
net, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "net")) net, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "net"))
pidns, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "pid")) pidns, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "pid"))
user, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "user")) user, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "user"))
uts, _ = shared.GetNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "uts")) uts, _ = getNamespaceInfo(filepath.Join("/proc", ctrPID, "ns", "uts"))
} }
if opts.Size { if opts.Size {
size = new(shared.ContainerSize) size = new(psdefine.ContainerSize)
rootFsSize, err := c.RootFsSize() rootFsSize, err := c.RootFsSize()
if err != nil { if err != nil {
@ -187,3 +190,18 @@ func ListContainerBatch(rt *libpod.Runtime, ctr *libpod.Container, opts entities
} }
return ps, nil return ps, nil
} }
func getNamespaceInfo(path string) (string, error) {
val, err := os.Readlink(path)
if err != nil {
return "", errors.Wrapf(err, "error getting info from %q", path)
}
return getStrFromSquareBrackets(val), nil
}
// getStrFromSquareBrackets gets the string inside [] from a string.
func getStrFromSquareBrackets(cmd string) string {
reg := regexp.MustCompile(`.*\[|\].*`)
arr := strings.Split(reg.ReplaceAllLiteralString(cmd, ""), ",")
return strings.Join(arr, ",")
}

View File

@ -14,7 +14,6 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/containers/image/v5/types" "github.com/containers/image/v5/types"
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/pkg/errorhandling" "github.com/containers/libpod/pkg/errorhandling"
"github.com/containers/libpod/pkg/namespaces" "github.com/containers/libpod/pkg/namespaces"
"github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/rootless"
@ -24,7 +23,6 @@ import (
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/pflag"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
@ -515,37 +513,6 @@ func ParseInputTime(inputTime string) (time.Time, error) {
return time.Now().Add(-duration), nil return time.Now().Add(-duration), nil
} }
// GetGlobalOpts checks all global flags and generates the command string
// FIXME: Port input to config.Config
// TODO: Is there a "better" way to reverse values to flags? This seems brittle.
func GetGlobalOpts(c *cliconfig.RunlabelValues) string {
globalFlags := map[string]bool{
"cgroup-manager": true, "cni-config-dir": true, "conmon": true, "default-mounts-file": true,
"hooks-dir": true, "namespace": true, "root": true, "runroot": true,
"runtime": true, "storage-driver": true, "storage-opt": true, "syslog": true,
"trace": true, "network-cmd-path": true, "config": true, "cpu-profile": true,
"log-level": true, "tmpdir": true}
const stringSliceType string = "stringSlice"
var optsCommand []string
c.PodmanCommand.Command.Flags().VisitAll(func(f *pflag.Flag) {
if !f.Changed {
return
}
if _, exist := globalFlags[f.Name]; exist {
if f.Value.Type() == stringSliceType {
flagValue := strings.TrimSuffix(strings.TrimPrefix(f.Value.String(), "["), "]")
for _, value := range strings.Split(flagValue, ",") {
optsCommand = append(optsCommand, fmt.Sprintf("--%s %s", f.Name, value))
}
} else {
optsCommand = append(optsCommand, fmt.Sprintf("--%s %s", f.Name, f.Value.String()))
}
}
})
return strings.Join(optsCommand, " ")
}
// OpenExclusiveFile opens a file for writing and ensure it doesn't already exist // OpenExclusiveFile opens a file for writing and ensure it doesn't already exist
func OpenExclusiveFile(path string) (*os.File, error) { func OpenExclusiveFile(path string) (*os.File, error) {
baseDir := filepath.Dir(path) baseDir := filepath.Dir(path)