diff options
Diffstat (limited to 'pkg')
63 files changed, 1431 insertions, 1849 deletions
diff --git a/pkg/api/handlers/compat/containers_create.go b/pkg/api/handlers/compat/containers_create.go index 12af40876..3d4bd4fb5 100644 --- a/pkg/api/handlers/compat/containers_create.go +++ b/pkg/api/handlers/compat/containers_create.go @@ -46,12 +46,12 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "NewFromLocal()")) return } - defaultContainerConfig, err := runtime.GetConfig() + containerConfig, err := runtime.GetConfig() if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "GetConfig()")) return } - cc, err := makeCreateConfig(defaultContainerConfig, input, newImage) + cc, err := makeCreateConfig(containerConfig, input, newImage) if err != nil { utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "makeCreatConfig()")) return @@ -60,7 +60,7 @@ func CreateContainer(w http.ResponseWriter, r *http.Request) { utils.CreateContainer(r.Context(), w, runtime, &cc) } -func makeCreateConfig(defaultContainerConfig *config.Config, input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { +func makeCreateConfig(containerConfig *config.Config, input handlers.CreateContainerConfig, newImage *image2.Image) (createconfig.CreateConfig, error) { var ( err error init bool @@ -81,7 +81,7 @@ func makeCreateConfig(defaultContainerConfig *config.Config, input handlers.Crea workDir = input.WorkingDir } - stopTimeout := defaultContainerConfig.Engine.StopTimeout + stopTimeout := containerConfig.Engine.StopTimeout if input.StopTimeout != nil { stopTimeout = uint(*input.StopTimeout) } diff --git a/pkg/api/handlers/compat/events.go b/pkg/api/handlers/compat/events.go index 8ef32716d..7ebfb0d1e 100644 --- a/pkg/api/handlers/compat/events.go +++ b/pkg/api/handlers/compat/events.go @@ -6,8 +6,8 @@ import ( "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/events" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" + "github.com/containers/libpod/pkg/domain/entities" "github.com/gorilla/schema" jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" @@ -70,7 +70,7 @@ func GetEvents(w http.ResponseWriter, r *http.Request) { coder.SetEscapeHTML(true) for event := range eventChannel { - e := handlers.EventToApiEvent(event) + e := entities.ConvertToEntitiesEvent(*event) if err := coder.Encode(e); err != nil { logrus.Errorf("unable to write json: %q", err) } diff --git a/pkg/api/handlers/compat/info.go b/pkg/api/handlers/compat/info.go index 179b4a3e0..e9756a03f 100644 --- a/pkg/api/handlers/compat/info.go +++ b/pkg/api/handlers/compat/info.go @@ -10,12 +10,12 @@ import ( "time" "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/sysinfo" "github.com/containers/libpod/libpod" "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/rootless" - "github.com/containers/libpod/pkg/sysinfo" docker "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/google/uuid" diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 284b33637..46401e4f2 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -22,6 +22,7 @@ import ( "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/api/handlers/utils" "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/libpod/pkg/domain/infra/abi" "github.com/containers/libpod/pkg/util" utils2 "github.com/containers/libpod/utils" "github.com/gorilla/schema" @@ -698,3 +699,30 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { utils.WriteResponse(w, http.StatusOK, reports) } + +// ImagesRemove is the endpoint for image removal. +func ImagesRemove(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + decoder := r.Context().Value("decoder").(*schema.Decoder) + query := struct { + All bool `schema:"all"` + Force bool `schema:"force"` + Images []string `schema:"images"` + }{ + All: false, + Force: false, + } + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest, + errors.Wrapf(err, "failed to parse parameters for %s", r.URL.String())) + return + } + + opts := entities.ImageRemoveOptions{All: query.All, Force: query.Force} + + imageEngine := abi.ImageEngine{Libpod: runtime} + rmReport, rmError := imageEngine.Remove(r.Context(), query.Images, opts) + report := handlers.LibpodImagesRemoveReport{ImageRemoveReport: *rmReport, Error: rmError.Error()} + utils.WriteResponse(w, http.StatusOK, report) +} diff --git a/pkg/api/handlers/swagger/swagger.go b/pkg/api/handlers/swagger/swagger.go index ba97a4755..87891d4a8 100644 --- a/pkg/api/handlers/swagger/swagger.go +++ b/pkg/api/handlers/swagger/swagger.go @@ -49,6 +49,13 @@ type swagLibpodImagesPullResponse struct { Body handlers.LibpodImagesPullReport } +// Remove response +// swagger:response DocsLibpodImagesRemoveResponse +type swagLibpodImagesRemoveResponse struct { + // in:body + Body handlers.LibpodImagesRemoveReport +} + // Delete response // swagger:response DocsImageDeleteResponse type swagImageDeleteResponse struct { diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 4c5e38437..58a12ea6a 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -4,16 +4,13 @@ import ( "context" "encoding/json" "fmt" - "strconv" "time" "github.com/containers/image/v5/manifest" - "github.com/containers/libpod/libpod/events" libpodImage "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/domain/entities" docker "github.com/docker/docker/api/types" dockerContainer "github.com/docker/docker/api/types/container" - dockerEvents "github.com/docker/docker/api/types/events" dockerNetwork "github.com/docker/docker/api/types/network" "github.com/docker/go-connections/nat" "github.com/pkg/errors" @@ -39,6 +36,14 @@ type LibpodImagesPullReport struct { ID string `json:"id"` } +// LibpodImagesRemoveReport is the return type for image removal via the rest +// api. +type LibpodImagesRemoveReport struct { + entities.ImageRemoveReport + // Image removal requires is to return data and an error. + Error string +} + type ContainersPruneReport struct { docker.ContainersPruneReport } @@ -143,10 +148,6 @@ type PodCreateConfig struct { Share string `json:"share"` } -type Event struct { - dockerEvents.Message -} - type HistoryResponse struct { ID string `json:"Id"` Created int64 `json:"Created"` @@ -173,49 +174,6 @@ type ExecCreateResponse struct { docker.IDResponse } -func (e *Event) ToLibpodEvent() *events.Event { - exitCode, err := strconv.Atoi(e.Actor.Attributes["containerExitCode"]) - if err != nil { - return nil - } - status, err := events.StringToStatus(e.Action) - if err != nil { - return nil - } - t, err := events.StringToType(e.Type) - if err != nil { - return nil - } - lp := events.Event{ - ContainerExitCode: exitCode, - ID: e.Actor.ID, - Image: e.Actor.Attributes["image"], - Name: e.Actor.Attributes["name"], - Status: status, - Time: time.Unix(e.Time, e.TimeNano), - Type: t, - } - return &lp -} - -func EventToApiEvent(e *events.Event) *Event { - return &Event{dockerEvents.Message{ - Type: e.Type.String(), - Action: e.Status.String(), - Actor: dockerEvents.Actor{ - ID: e.ID, - Attributes: map[string]string{ - "image": e.Image, - "name": e.Name, - "containerExitCode": strconv.Itoa(e.ContainerExitCode), - }, - }, - Scope: "local", - Time: e.Time.Unix(), - TimeNano: e.Time.UnixNano(), - }} -} - func ImageToImageSummary(l *libpodImage.Image) (*entities.ImageSummary, error) { containers, err := l.Containers() if err != nil { diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 6cc6f0cfa..f59dca6f5 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -822,6 +822,38 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // 500: // $ref: '#/responses/InternalError' r.Handle(VersionedPath("/libpod/images/import"), s.APIHandler(libpod.ImagesImport)).Methods(http.MethodPost) + // swagger:operation GET /libpod/images/remove libpod libpodImagesRemove + // --- + // tags: + // - images + // summary: Remove one or more images from the storage. + // description: Remove one or more images from the storage. + // parameters: + // - in: query + // name: images + // description: Images IDs or names to remove. + // type: array + // items: + // type: string + // - in: query + // name: all + // description: Remove all images. + // type: boolean + // default: true + // - in: query + // name: force + // description: Force image removal (including containers using the images). + // type: boolean + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/DocsLibpodImagesRemoveResponse" + // 400: + // $ref: "#/responses/BadParamError" + // 500: + // $ref: '#/responses/InternalError' + r.Handle(VersionedPath("/libpod/images/remove"), s.APIHandler(libpod.ImagesRemove)).Methods(http.MethodGet) // swagger:operation POST /libpod/images/pull libpod libpodImagesPull // --- // tags: diff --git a/pkg/api/types/types.go b/pkg/api/types/types.go new file mode 100644 index 000000000..1b91364e3 --- /dev/null +++ b/pkg/api/types/types.go @@ -0,0 +1,9 @@ +package types + +const ( + // DefaultAPIVersion is the version of the API the server defaults to. + DefaultAPIVersion = "1.40" // See https://docs.docker.com/engine/api/v1.40/ + + // DefaultAPIVersion is the minimal required version of the API. + MinimalAPIVersion = "1.24" +) diff --git a/pkg/apparmor/apparmor.go b/pkg/apparmor/apparmor.go deleted file mode 100644 index 8e17361cb..000000000 --- a/pkg/apparmor/apparmor.go +++ /dev/null @@ -1,19 +0,0 @@ -package apparmor - -import ( - "errors" - - "github.com/containers/common/pkg/config" - libpodVersion "github.com/containers/libpod/version" -) - -var ( - // DefaultLipodProfilePrefix is used for version-independent presence checks. - DefaultLipodProfilePrefix = config.DefaultApparmorProfile - // DefaultLibpodProfile is the name of default libpod AppArmor profile. - DefaultLibpodProfile = DefaultLipodProfilePrefix + "-" + libpodVersion.Version - // ErrApparmorUnsupported indicates that AppArmor support is not supported. - ErrApparmorUnsupported = errors.New("AppArmor is not supported") - // ErrApparmorRootless indicates that AppArmor support is not supported in rootless mode. - ErrApparmorRootless = errors.New("AppArmor is not supported in rootless mode") -) diff --git a/pkg/apparmor/apparmor_linux.go b/pkg/apparmor/apparmor_linux.go deleted file mode 100644 index 33710ff56..000000000 --- a/pkg/apparmor/apparmor_linux.go +++ /dev/null @@ -1,289 +0,0 @@ -// +build linux,apparmor - -package apparmor - -import ( - "bufio" - "bytes" - "fmt" - "io" - "os" - "os/exec" - "path" - "strconv" - "strings" - "text/template" - - "github.com/containers/libpod/pkg/rootless" - runcaa "github.com/opencontainers/runc/libcontainer/apparmor" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -// profileDirectory is the file store for apparmor profiles and macros. -var profileDirectory = "/etc/apparmor.d" - -// IsEnabled returns true if AppArmor is enabled on the host. -func IsEnabled() bool { - if rootless.IsRootless() { - return false - } - return runcaa.IsEnabled() -} - -// profileData holds information about the given profile for generation. -type profileData struct { - // Name is profile name. - Name string - // Imports defines the apparmor functions to import, before defining the profile. - Imports []string - // InnerImports defines the apparmor functions to import in the profile. - InnerImports []string - // Version is the {major, minor, patch} version of apparmor_parser as a single number. - Version int -} - -// generateDefault creates an apparmor profile from ProfileData. -func (p *profileData) generateDefault(out io.Writer) error { - compiled, err := template.New("apparmor_profile").Parse(libpodProfileTemplate) - if err != nil { - return err - } - - if macroExists("tunables/global") { - p.Imports = append(p.Imports, "#include <tunables/global>") - } else { - p.Imports = append(p.Imports, "@{PROC}=/proc/") - } - - if macroExists("abstractions/base") { - p.InnerImports = append(p.InnerImports, "#include <abstractions/base>") - } - - ver, err := getAAParserVersion() - if err != nil { - return err - } - p.Version = ver - - return compiled.Execute(out, p) -} - -// macrosExists checks if the passed macro exists. -func macroExists(m string) bool { - _, err := os.Stat(path.Join(profileDirectory, m)) - return err == nil -} - -// InstallDefault generates a default profile and loads it into the kernel -// using 'apparmor_parser'. -func InstallDefault(name string) error { - if rootless.IsRootless() { - return ErrApparmorRootless - } - - p := profileData{ - Name: name, - } - - cmd := exec.Command("apparmor_parser", "-Kr") - pipe, err := cmd.StdinPipe() - if err != nil { - return err - } - if err := cmd.Start(); err != nil { - if pipeErr := pipe.Close(); pipeErr != nil { - logrus.Errorf("unable to close apparmor pipe: %q", pipeErr) - } - return err - } - if err := p.generateDefault(pipe); err != nil { - if pipeErr := pipe.Close(); pipeErr != nil { - logrus.Errorf("unable to close apparmor pipe: %q", pipeErr) - } - if cmdErr := cmd.Wait(); cmdErr != nil { - logrus.Errorf("unable to wait for apparmor command: %q", cmdErr) - } - return err - } - - if pipeErr := pipe.Close(); pipeErr != nil { - logrus.Errorf("unable to close apparmor pipe: %q", pipeErr) - } - return cmd.Wait() -} - -// DefaultContent returns the default profile content as byte slice. The -// profile is named as the provided `name`. The function errors if the profile -// generation fails. -func DefaultContent(name string) ([]byte, error) { - p := profileData{Name: name} - var bytes bytes.Buffer - if err := p.generateDefault(&bytes); err != nil { - return nil, err - } - return bytes.Bytes(), nil -} - -// IsLoaded checks if a profile with the given name has been loaded into the -// kernel. -func IsLoaded(name string) (bool, error) { - if name != "" && rootless.IsRootless() { - return false, errors.Wrapf(ErrApparmorRootless, "cannot load AppArmor profile %q", name) - } - - file, err := os.Open("/sys/kernel/security/apparmor/profiles") - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - defer file.Close() - - r := bufio.NewReader(file) - for { - p, err := r.ReadString('\n') - if err == io.EOF { - break - } - if err != nil { - return false, err - } - if strings.HasPrefix(p, name+" ") { - return true, nil - } - } - - return false, nil -} - -// execAAParser runs `apparmor_parser` with the passed arguments. -func execAAParser(dir string, args ...string) (string, error) { - c := exec.Command("apparmor_parser", args...) - c.Dir = dir - - output, err := c.CombinedOutput() - if err != nil { - return "", fmt.Errorf("running `%s %s` failed with output: %s\nerror: %v", c.Path, strings.Join(c.Args, " "), output, err) - } - - return string(output), nil -} - -// getAAParserVersion returns the major and minor version of apparmor_parser. -func getAAParserVersion() (int, error) { - output, err := execAAParser("", "--version") - if err != nil { - return -1, err - } - return parseAAParserVersion(output) -} - -// parseAAParserVersion parses the given `apparmor_parser --version` output and -// returns the major and minor version number as an integer. -func parseAAParserVersion(output string) (int, error) { - // output is in the form of the following: - // AppArmor parser version 2.9.1 - // Copyright (C) 1999-2008 Novell Inc. - // Copyright 2009-2012 Canonical Ltd. - lines := strings.SplitN(output, "\n", 2) - words := strings.Split(lines[0], " ") - version := words[len(words)-1] - - // split by major minor version - v := strings.Split(version, ".") - if len(v) == 0 || len(v) > 3 { - return -1, fmt.Errorf("parsing version failed for output: `%s`", output) - } - - // Default the versions to 0. - var majorVersion, minorVersion, patchLevel int - - majorVersion, err := strconv.Atoi(v[0]) - if err != nil { - return -1, err - } - - if len(v) > 1 { - minorVersion, err = strconv.Atoi(v[1]) - if err != nil { - return -1, err - } - } - if len(v) > 2 { - patchLevel, err = strconv.Atoi(v[2]) - if err != nil { - return -1, err - } - } - - // major*10^5 + minor*10^3 + patch*10^0 - numericVersion := majorVersion*1e5 + minorVersion*1e3 + patchLevel - return numericVersion, nil - -} - -// CheckProfileAndLoadDefault checks if the specified profile is loaded and -// loads the DefaultLibpodProfile if the specified on is prefixed by -// DefaultLipodProfilePrefix. This allows to always load and apply the latest -// default AppArmor profile. Note that AppArmor requires root. If it's a -// default profile, return DefaultLipodProfilePrefix, otherwise the specified -// one. -func CheckProfileAndLoadDefault(name string) (string, error) { - if name == "unconfined" { - return name, nil - } - - // AppArmor is not supported in rootless mode as it requires root - // privileges. Return an error in case a specific profile is specified. - if rootless.IsRootless() { - if name != "" { - return "", errors.Wrapf(ErrApparmorRootless, "cannot load AppArmor profile %q", name) - } else { - logrus.Debug("skipping loading default AppArmor profile (rootless mode)") - return "", nil - } - } - - // Check if AppArmor is disabled and error out if a profile is to be set. - if !runcaa.IsEnabled() { - if name == "" { - return "", nil - } else { - return "", fmt.Errorf("profile %q specified but AppArmor is disabled on the host", name) - } - } - - // If the specified name is not empty or is not a default libpod one, - // ignore it and return the name. - if name != "" && !strings.HasPrefix(name, DefaultLipodProfilePrefix) { - isLoaded, err := IsLoaded(name) - if err != nil { - return "", err - } - if !isLoaded { - return "", fmt.Errorf("AppArmor profile %q specified but not loaded", name) - } - return name, nil - } - - name = DefaultLibpodProfile - // To avoid expensive redundant loads on each invocation, check - // if it's loaded before installing it. - isLoaded, err := IsLoaded(name) - if err != nil { - return "", err - } - if !isLoaded { - err = InstallDefault(name) - if err != nil { - return "", err - } - logrus.Infof("successfully loaded AppAmor profile %q", name) - } else { - logrus.Infof("AppAmor profile %q is already loaded", name) - } - - return name, nil -} diff --git a/pkg/apparmor/apparmor_linux_template.go b/pkg/apparmor/apparmor_linux_template.go deleted file mode 100644 index 8d9a92ef7..000000000 --- a/pkg/apparmor/apparmor_linux_template.go +++ /dev/null @@ -1,49 +0,0 @@ -// +build linux,apparmor - -package apparmor - -const libpodProfileTemplate = ` -{{range $value := .Imports}} -{{$value}} -{{end}} - -profile {{.Name}} flags=(attach_disconnected,mediate_deleted) { -{{range $value := .InnerImports}} - {{$value}} -{{end}} - - network, - capability, - file, - umount, - -{{if ge .Version 208096}} - # Allow signals from privileged profiles and from within the same profile - signal (receive) peer=unconfined, - signal (send,receive) peer={{.Name}}, -{{end}} - - deny @{PROC}/* w, # deny write for all files directly in /proc (not in a subdir) - # deny write to files not in /proc/<number>/** or /proc/sys/** - deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w, - deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel) - deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/ - deny @{PROC}/sysrq-trigger rwklx, - deny @{PROC}/kcore rwklx, - - deny mount, - - deny /sys/[^f]*/** wklx, - deny /sys/f[^s]*/** wklx, - deny /sys/fs/[^c]*/** wklx, - deny /sys/fs/c[^g]*/** wklx, - deny /sys/fs/cg[^r]*/** wklx, - deny /sys/firmware/** rwklx, - deny /sys/kernel/security/** rwklx, - -{{if ge .Version 208095}} - # suppress ptrace denials when using using 'ps' inside a container - ptrace (trace,read) peer={{.Name}}, -{{end}} -} -` diff --git a/pkg/apparmor/apparmor_linux_test.go b/pkg/apparmor/apparmor_linux_test.go deleted file mode 100644 index 3ff6e18bc..000000000 --- a/pkg/apparmor/apparmor_linux_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// +build linux,apparmor - -package apparmor - -import ( - "os" - "testing" -) - -type versionExpected struct { - output string - version int -} - -func TestParseAAParserVersion(t *testing.T) { - if !IsEnabled() { - t.Skip("AppArmor disabled: skipping tests") - } - versions := []versionExpected{ - { - output: `AppArmor parser version 2.10 -Copyright (C) 1999-2008 Novell Inc. -Copyright 2009-2012 Canonical Ltd. - -`, - version: 210000, - }, - { - output: `AppArmor parser version 2.8 -Copyright (C) 1999-2008 Novell Inc. -Copyright 2009-2012 Canonical Ltd. - -`, - version: 208000, - }, - { - output: `AppArmor parser version 2.20 -Copyright (C) 1999-2008 Novell Inc. -Copyright 2009-2012 Canonical Ltd. - -`, - version: 220000, - }, - { - output: `AppArmor parser version 2.05 -Copyright (C) 1999-2008 Novell Inc. -Copyright 2009-2012 Canonical Ltd. - -`, - version: 205000, - }, - { - output: `AppArmor parser version 2.9.95 -Copyright (C) 1999-2008 Novell Inc. -Copyright 2009-2012 Canonical Ltd. - -`, - version: 209095, - }, - { - output: `AppArmor parser version 3.14.159 -Copyright (C) 1999-2008 Novell Inc. -Copyright 2009-2012 Canonical Ltd. - -`, - version: 314159, - }, - } - - for _, v := range versions { - version, err := parseAAParserVersion(v.output) - if err != nil { - t.Fatalf("expected error to be nil for %#v, got: %v", v, err) - } - if version != v.version { - t.Fatalf("expected version to be %d, was %d, for: %#v\n", v.version, version, v) - } - } -} - -const ( - aapath = "/sys/kernel/security/apparmor/" - profile = "libpod-default-testing" -) - -func TestInstallDefault(t *testing.T) { - if _, err := os.Stat(aapath); err != nil { - t.Skip("AppArmor isn't available in this environment") - } - - // removes `profile` - removeProfile := func() error { - path := aapath + ".remove" - - f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, os.ModeAppend) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(profile) - return err - } - - // makes sure `profile` is loaded according to `state` - checkLoaded := func(state bool) { - loaded, err := IsLoaded(profile) - if err != nil { - t.Fatalf("Error searching AppArmor profile '%s': %v", profile, err) - } - if state != loaded { - if state { - t.Fatalf("AppArmor profile '%s' isn't loaded but should", profile) - } else { - t.Fatalf("AppArmor profile '%s' is loaded but shouldn't", profile) - } - } - } - - // test installing the profile - if err := InstallDefault(profile); err != nil { - t.Fatalf("Couldn't install AppArmor profile '%s': %v", profile, err) - } - checkLoaded(true) - - // remove the profile and check again - if err := removeProfile(); err != nil { - t.Fatalf("Couldn't remove AppArmor profile '%s': %v", profile, err) - } - checkLoaded(false) -} - -func TestDefaultContent(t *testing.T) { - if _, err := os.Stat(aapath); err != nil { - t.Skip("AppArmor isn't available in this environment") - } - if _, err := DefaultContent(profile); err != nil { - t.Fatalf("Couldn't retrieve default AppArmor profile content '%s': %v", profile, err) - } -} diff --git a/pkg/apparmor/apparmor_unsupported.go b/pkg/apparmor/apparmor_unsupported.go deleted file mode 100644 index 13469f1b6..000000000 --- a/pkg/apparmor/apparmor_unsupported.go +++ /dev/null @@ -1,31 +0,0 @@ -// +build !linux !apparmor - -package apparmor - -// IsEnabled dummy. -func IsEnabled() bool { - return false -} - -// InstallDefault dummy. -func InstallDefault(name string) error { - return ErrApparmorUnsupported -} - -// IsLoaded dummy. -func IsLoaded(name string) (bool, error) { - return false, ErrApparmorUnsupported -} - -// CheckProfileAndLoadDefault dummy. -func CheckProfileAndLoadDefault(name string) (string, error) { - if name == "" { - return "", nil - } - return "", ErrApparmorUnsupported -} - -// DefaultContent dummy. -func DefaultContent(name string) ([]byte, error) { - return nil, nil -} diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index 29b6f04ec..da3755fc8 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -15,7 +15,7 @@ import ( "strings" "time" - "github.com/containers/libpod/pkg/api/handlers" + "github.com/containers/libpod/pkg/api/types" jsoniter "github.com/json-iterator/go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -27,7 +27,7 @@ var ( basePath = &url.URL{ Scheme: "http", Host: "d", - Path: "/v" + handlers.MinimalApiVersion + "/libpod", + Path: "/v" + types.MinimalAPIVersion + "/libpod", } ) diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index e0f523ebd..06f01c7a0 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -109,23 +109,34 @@ func Load(ctx context.Context, r io.Reader, name *string) (*entities.ImageLoadRe return &report, response.Process(&report) } -// Remove deletes an image from local storage. The optional force parameter will forcibly remove -// the image by removing all all containers, including those that are Running, first. -func Remove(ctx context.Context, nameOrID string, force *bool) ([]map[string]string, error) { - var deletes []map[string]string +// Remove deletes an image from local storage. The optional force parameter +// will forcibly remove the image by removing all all containers, including +// those that are Running, first. +func Remove(ctx context.Context, images []string, opts entities.ImageRemoveOptions) (*entities.ImageRemoveReport, error) { + var report handlers.LibpodImagesRemoveReport conn, err := bindings.GetClient(ctx) if err != nil { return nil, err } params := url.Values{} - if force != nil { - params.Set("force", strconv.FormatBool(*force)) + params.Set("all", strconv.FormatBool(opts.All)) + params.Set("force", strconv.FormatBool(opts.Force)) + for _, i := range images { + params.Add("images", i) } - response, err := conn.DoRequest(nil, http.MethodDelete, "/images/%s", params, nameOrID) + + response, err := conn.DoRequest(nil, http.MethodGet, "/images/remove", params) if err != nil { return nil, err } - return deletes, response.Process(&deletes) + if err := response.Process(&report); err != nil { + return nil, err + } + var rmError error + if report.Error != "" { + rmError = errors.New(report.Error) + } + return &report.ImageRemoveReport, rmError } // Export saves an image from local storage as a tarball or image archive. The optional format diff --git a/pkg/bindings/system/system.go b/pkg/bindings/system/system.go index fce8bbb8e..e2f264139 100644 --- a/pkg/bindings/system/system.go +++ b/pkg/bindings/system/system.go @@ -7,8 +7,8 @@ import ( "net/http" "net/url" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings" + "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -16,7 +16,7 @@ import ( // Events allows you to monitor libdpod related events like container creation and // removal. The events are then passed to the eventChan provided. The optional cancelChan // can be used to cancel the read of events and close down the HTTP connection. -func Events(ctx context.Context, eventChan chan (handlers.Event), cancelChan chan bool, since, until *string, filters map[string][]string) error { +func Events(ctx context.Context, eventChan chan (entities.Event), cancelChan chan bool, since, until *string, filters map[string][]string) error { conn, err := bindings.GetClient(ctx) if err != nil { return err @@ -48,7 +48,7 @@ func Events(ctx context.Context, eventChan chan (handlers.Event), cancelChan cha } dec := json.NewDecoder(response.Body) for { - e := handlers.Event{} + e := entities.Event{} if err := dec.Decode(&e); err != nil { if err == io.EOF { break diff --git a/pkg/domain/entities/containers.go b/pkg/domain/entities/containers.go index 52327a905..e58258b75 100644 --- a/pkg/domain/entities/containers.go +++ b/pkg/domain/entities/containers.go @@ -8,6 +8,7 @@ import ( "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/specgen" + "github.com/cri-o/ocicni/pkg/ocicni" ) type WaitOptions struct { @@ -341,3 +342,27 @@ type ContainerPruneReport struct { ID map[string]int64 Err map[string]error } + +// ContainerPortOptions describes the options to obtain +// port information on containers +type ContainerPortOptions struct { + All bool + Latest bool +} + +// ContainerPortReport describes the output needed for +// the CLI to output ports +type ContainerPortReport struct { + Id string + Ports []ocicni.PortMapping +} + +// ContainerCpOptions describes input options for cp +type ContainerCpOptions struct { + Pause bool + Extract bool +} + +// ContainerCpReport describes the output from a cp operation +type ContainerCpReport struct { +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index b730f8743..60833d879 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -16,6 +16,7 @@ type ContainerEngine interface { ContainerCleanup(ctx context.Context, namesOrIds []string, options ContainerCleanupOptions) ([]*ContainerCleanupReport, error) ContainerPrune(ctx context.Context, options ContainerPruneOptions) (*ContainerPruneReport, error) ContainerCommit(ctx context.Context, nameOrId string, options CommitOptions) (*CommitReport, error) + ContainerCp(ctx context.Context, source, dest string, options ContainerCpOptions) (*ContainerCpReport, error) ContainerCreate(ctx context.Context, s *specgen.SpecGenerator) (*ContainerCreateReport, error) ContainerDiff(ctx context.Context, nameOrId string, options DiffOptions) (*DiffReport, error) ContainerExec(ctx context.Context, nameOrId string, options ExecOptions) (int, error) @@ -28,6 +29,7 @@ type ContainerEngine interface { ContainerLogs(ctx context.Context, containers []string, options ContainerLogsOptions) error ContainerMount(ctx context.Context, nameOrIds []string, options ContainerMountOptions) ([]*ContainerMountReport, error) ContainerPause(ctx context.Context, namesOrIds []string, options PauseUnPauseOptions) ([]*PauseUnpauseReport, error) + ContainerPort(ctx context.Context, nameOrId string, options ContainerPortOptions) ([]*ContainerPortReport, error) ContainerRestart(ctx context.Context, namesOrIds []string, options RestartOptions) ([]*RestartReport, error) ContainerRestore(ctx context.Context, namesOrIds []string, options RestoreOptions) ([]*RestoreReport, error) ContainerRm(ctx context.Context, namesOrIds []string, options RmOptions) ([]*RmReport, error) @@ -54,7 +56,6 @@ type ContainerEngine interface { PodStop(ctx context.Context, namesOrIds []string, options PodStopOptions) ([]*PodStopReport, error) PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error) PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) - RestService(ctx context.Context, opts ServiceOptions) error SetupRootless(ctx context.Context, cmd *cobra.Command) error VarlinkService(ctx context.Context, opts ServiceOptions) error VolumeCreate(ctx context.Context, opts VolumeCreateOptions) (*IdOrNameResponse, error) diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go index b6283b6ad..84680ab1b 100644 --- a/pkg/domain/entities/engine_image.go +++ b/pkg/domain/entities/engine_image.go @@ -9,7 +9,6 @@ import ( type ImageEngine interface { Build(ctx context.Context, containerFiles []string, opts BuildOptions) (*BuildReport, error) Config(ctx context.Context) (*config.Config, error) - Delete(ctx context.Context, nameOrId []string, opts ImageDeleteOptions) (*ImageDeleteReport, error) Diff(ctx context.Context, nameOrId string, options DiffOptions) (*DiffReport, error) Exists(ctx context.Context, nameOrId string) (*BoolReport, error) History(ctx context.Context, nameOrId string, opts ImageHistoryOptions) (*ImageHistoryReport, error) @@ -20,6 +19,7 @@ type ImageEngine interface { Prune(ctx context.Context, opts ImagePruneOptions) (*ImagePruneReport, error) Pull(ctx context.Context, rawImage string, opts ImagePullOptions) (*ImagePullReport, error) Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error + Remove(ctx context.Context, images []string, opts ImageRemoveOptions) (*ImageRemoveReport, error) Save(ctx context.Context, nameOrId string, tags []string, options ImageSaveOptions) error Search(ctx context.Context, term string, opts ImageSearchOptions) ([]ImageSearchReport, error) Tag(ctx context.Context, nameOrId string, tags []string, options ImageTagOptions) error diff --git a/pkg/domain/entities/events.go b/pkg/domain/entities/events.go new file mode 100644 index 000000000..8861be158 --- /dev/null +++ b/pkg/domain/entities/events.go @@ -0,0 +1,61 @@ +package entities + +import ( + "strconv" + "time" + + libpodEvents "github.com/containers/libpod/libpod/events" + dockerEvents "github.com/docker/docker/api/types/events" +) + +// Event combines various event-related data such as time, event type, status +// and more. +type Event struct { + // TODO: it would be nice to have full control over the types at some + // point and fork such Docker types. + dockerEvents.Message +} + +// ConvertToLibpodEvent converts an entities event to a libpod one. +func ConvertToLibpodEvent(e Event) *libpodEvents.Event { + exitCode, err := strconv.Atoi(e.Actor.Attributes["containerExitCode"]) + if err != nil { + return nil + } + status, err := libpodEvents.StringToStatus(e.Action) + if err != nil { + return nil + } + t, err := libpodEvents.StringToType(e.Type) + if err != nil { + return nil + } + return &libpodEvents.Event{ + ContainerExitCode: exitCode, + ID: e.Actor.ID, + Image: e.Actor.Attributes["image"], + Name: e.Actor.Attributes["name"], + Status: status, + Time: time.Unix(e.Time, e.TimeNano), + Type: t, + } +} + +// ConvertToEntitiesEvent converts a libpod event to an entities one. +func ConvertToEntitiesEvent(e libpodEvents.Event) *Event { + return &Event{dockerEvents.Message{ + Type: e.Type.String(), + Action: e.Status.String(), + Actor: dockerEvents.Actor{ + ID: e.ID, + Attributes: map[string]string{ + "image": e.Image, + "name": e.Name, + "containerExitCode": strconv.Itoa(e.ContainerExitCode), + }, + }, + Scope: "local", + Time: e.Time.Unix(), + TimeNano: e.Time.UnixNano(), + }} +} diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 56c4c0ac5..773cd90b4 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -82,19 +82,24 @@ func (i *ImageSummary) IsDangling() bool { return i.Dangling } -type ImageDeleteOptions struct { - All bool +// ImageRemoveOptions can be used to alter image removal. +type ImageRemoveOptions struct { + // All will remove all images. + All bool + // Foce will force image removal including containers using the images. Force bool } -// ImageDeleteResponse is the response for removing one or more image(s) from storage -// and containers what was untagged vs actually removed -type ImageDeleteReport struct { - Untagged []string `json:",omitempty"` - Deleted []string `json:",omitempty"` - Errors []error - ImageNotFound error - ImageInUse error +// ImageRemoveResponse is the response for removing one or more image(s) from storage +// and containers what was untagged vs actually removed. +type ImageRemoveReport struct { + // Deleted images. + Deleted []string `json:",omitempty"` + // Untagged images. Can be longer than Deleted. + Untagged []string `json:",omitempty"` + // ExitCode describes the exit codes as described in the `podman rmi` + // man page. + ExitCode int } type ImageHistoryOptions struct{} diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 04673ef18..aa1445a6a 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -134,7 +134,7 @@ func (p PodCreateOptions) ToPodSpecGen(s *specgen.PodSpecGenerator) { s.StaticMAC = p.Net.StaticMAC s.PortMappings = p.Net.PublishPorts s.CNINetworks = p.Net.CNINetworks - if p.Net.DNSHost { + if p.Net.UseImageResolvConf { s.NoManageResolvConf = true } s.DNSServer = p.Net.DNSServers diff --git a/pkg/domain/entities/types.go b/pkg/domain/entities/types.go index 096af2df2..d742cc53d 100644 --- a/pkg/domain/entities/types.go +++ b/pkg/domain/entities/types.go @@ -32,17 +32,17 @@ type VolumeDeleteReport struct{ Report } // NetOptions reflect the shared network options between // pods and containers type NetOptions struct { - AddHosts []string - CNINetworks []string - DNSHost bool - DNSOptions []string - DNSSearch []string - DNSServers []net.IP - Network specgen.Namespace - NoHosts bool - PublishPorts []ocicni.PortMapping - StaticIP *net.IP - StaticMAC *net.HardwareAddr + AddHosts []string + CNINetworks []string + UseImageResolvConf bool + DNSOptions []string + DNSSearch []string + DNSServers []net.IP + Network specgen.Namespace + NoHosts bool + PublishPorts []ocicni.PortMapping + StaticIP *net.IP + StaticMAC *net.HardwareAddr } // All CLI inspect commands and inspect sub-commands use the same options diff --git a/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 50003dbe2..73a0d8ec3 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -1,5 +1,3 @@ -// +build ABISupport - package abi import ( @@ -931,3 +929,31 @@ func (ic *ContainerEngine) ContainerUnmount(ctx context.Context, nameOrIds []str func (ic *ContainerEngine) Config(_ context.Context) (*config.Config, error) { return ic.Libpod.GetConfig() } + +func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrId string, options entities.ContainerPortOptions) ([]*entities.ContainerPortReport, error) { + var reports []*entities.ContainerPortReport + ctrs, err := getContainersByContext(options.All, false, []string{nameOrId}, ic.Libpod) + if err != nil { + return nil, err + } + for _, con := range ctrs { + state, err := con.State() + if err != nil { + return nil, err + } + if state != define.ContainerStateRunning { + continue + } + portmappings, err := con.PortMappings() + if err != nil { + return nil, err + } + if len(portmappings) > 0 { + reports = append(reports, &entities.ContainerPortReport{ + Id: con.ID(), + Ports: portmappings, + }) + } + } + return reports, nil +} diff --git a/pkg/domain/infra/abi/cp.go b/pkg/domain/infra/abi/cp.go new file mode 100644 index 000000000..9fc1e3bee --- /dev/null +++ b/pkg/domain/infra/abi/cp.go @@ -0,0 +1,433 @@ +package abi + +import ( + "archive/tar" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/containers/buildah/pkg/chrootuser" + "github.com/containers/buildah/util" + "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/domain/entities" + "github.com/containers/storage" + "github.com/containers/storage/pkg/chrootarchive" + "github.com/containers/storage/pkg/idtools" + securejoin "github.com/cyphar/filepath-securejoin" + "github.com/docker/docker/pkg/archive" + "github.com/opencontainers/go-digest" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) (*entities.ContainerCpReport, error) { + var extract bool + + srcCtr, srcPath := parsePath(ic.Libpod, source) + destCtr, destPath := parsePath(ic.Libpod, dest) + + if (srcCtr == nil && destCtr == nil) || (srcCtr != nil && destCtr != nil) { + return nil, errors.Errorf("invalid arguments %s, %s you must use just one container", source, dest) + } + + if len(srcPath) == 0 || len(destPath) == 0 { + return nil, errors.Errorf("invalid arguments %s, %s you must specify paths", source, dest) + } + ctr := srcCtr + isFromHostToCtr := ctr == nil + if isFromHostToCtr { + ctr = destCtr + } + + mountPoint, err := ctr.Mount() + if err != nil { + return nil, err + } + defer func() { + if err := ctr.Unmount(false); err != nil { + logrus.Errorf("unable to umount container '%s': %q", ctr.ID(), err) + } + }() + + if options.Pause { + if err := ctr.Pause(); err != nil { + // An invalid state error is fine. + // The container isn't running or is already paused. + // TODO: We can potentially start the container while + // the copy is running, which still allows a race where + // malicious code could mess with the symlink. + if errors.Cause(err) != define.ErrCtrStateInvalid { + return nil, err + } + } else { + // Only add the defer if we actually paused + defer func() { + if err := ctr.Unpause(); err != nil { + logrus.Errorf("Error unpausing container after copying: %v", err) + } + }() + } + } + + user, err := getUser(mountPoint, ctr.User()) + if err != nil { + return nil, err + } + idMappingOpts, err := ctr.IDMappings() + if err != nil { + return nil, errors.Wrapf(err, "error getting IDMappingOptions") + } + destOwner := idtools.IDPair{UID: int(user.UID), GID: int(user.GID)} + hostUID, hostGID, err := util.GetHostIDs(convertIDMap(idMappingOpts.UIDMap), convertIDMap(idMappingOpts.GIDMap), user.UID, user.GID) + if err != nil { + return nil, err + } + + hostOwner := idtools.IDPair{UID: int(hostUID), GID: int(hostGID)} + + if isFromHostToCtr { + if isVol, volDestName, volName := isVolumeDestName(destPath, ctr); isVol { //nolint(gocritic) + path, err := pathWithVolumeMount(ctr, ic.Libpod, volDestName, volName, destPath) + if err != nil { + return nil, errors.Wrapf(err, "error getting destination path from volume %s", volDestName) + } + destPath = path + } else if isBindMount, mount := isBindMountDestName(destPath, ctr); isBindMount { //nolint(gocritic) + path, err := pathWithBindMountSource(mount, destPath) + if err != nil { + return nil, errors.Wrapf(err, "error getting destination path from bind mount %s", mount.Destination) + } + destPath = path + } else if filepath.IsAbs(destPath) { //nolint(gocritic) + cleanedPath, err := securejoin.SecureJoin(mountPoint, destPath) + if err != nil { + return nil, err + } + destPath = cleanedPath + } else { //nolint(gocritic) + ctrWorkDir, err := securejoin.SecureJoin(mountPoint, ctr.WorkingDir()) + if err != nil { + return nil, err + } + if err = idtools.MkdirAllAndChownNew(ctrWorkDir, 0755, hostOwner); err != nil { + return nil, errors.Wrapf(err, "error creating directory %q", destPath) + } + cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), destPath)) + if err != nil { + return nil, err + } + destPath = cleanedPath + } + } else { + destOwner = idtools.IDPair{UID: os.Getuid(), GID: os.Getgid()} + if isVol, volDestName, volName := isVolumeDestName(srcPath, ctr); isVol { //nolint(gocritic) + path, err := pathWithVolumeMount(ctr, ic.Libpod, volDestName, volName, srcPath) + if err != nil { + return nil, errors.Wrapf(err, "error getting source path from volume %s", volDestName) + } + srcPath = path + } else if isBindMount, mount := isBindMountDestName(srcPath, ctr); isBindMount { //nolint(gocritic) + path, err := pathWithBindMountSource(mount, srcPath) + if err != nil { + return nil, errors.Wrapf(err, "error getting source path from bind mount %s", mount.Destination) + } + srcPath = path + } else if filepath.IsAbs(srcPath) { //nolint(gocritic) + cleanedPath, err := securejoin.SecureJoin(mountPoint, srcPath) + if err != nil { + return nil, err + } + srcPath = cleanedPath + } else { //nolint(gocritic) + cleanedPath, err := securejoin.SecureJoin(mountPoint, filepath.Join(ctr.WorkingDir(), srcPath)) + if err != nil { + return nil, err + } + srcPath = cleanedPath + } + } + + if !filepath.IsAbs(destPath) { + dir, err := os.Getwd() + if err != nil { + return nil, errors.Wrapf(err, "err getting current working directory") + } + destPath = filepath.Join(dir, destPath) + } + + if source == "-" { + srcPath = os.Stdin.Name() + extract = true + } + err = containerCopy(srcPath, destPath, source, dest, idMappingOpts, &destOwner, extract, isFromHostToCtr) + return &entities.ContainerCpReport{}, err +} + +func getUser(mountPoint string, userspec string) (specs.User, error) { + uid, gid, _, err := chrootuser.GetUser(mountPoint, userspec) + u := specs.User{ + UID: uid, + GID: gid, + Username: userspec, + } + if !strings.Contains(userspec, ":") { + groups, err2 := chrootuser.GetAdditionalGroupsForUser(mountPoint, uint64(u.UID)) + if err2 != nil { + if errors.Cause(err2) != chrootuser.ErrNoSuchUser && err == nil { + err = err2 + } + } else { + u.AdditionalGids = groups + } + + } + return u, err +} + +func parsePath(runtime *libpod.Runtime, path string) (*libpod.Container, string) { + pathArr := strings.SplitN(path, ":", 2) + if len(pathArr) == 2 { + ctr, err := runtime.LookupContainer(pathArr[0]) + if err == nil { + return ctr, pathArr[1] + } + } + return nil, path +} + +func evalSymlinks(path string) (string, error) { + if path == os.Stdin.Name() { + return path, nil + } + return filepath.EvalSymlinks(path) +} + +func getPathInfo(path string) (string, os.FileInfo, error) { + path, err := evalSymlinks(path) + if err != nil { + return "", nil, errors.Wrapf(err, "error evaluating symlinks %q", path) + } + srcfi, err := os.Stat(path) + if err != nil { + return "", nil, errors.Wrapf(err, "error reading path %q", path) + } + return path, srcfi, nil +} + +func containerCopy(srcPath, destPath, src, dest string, idMappingOpts storage.IDMappingOptions, chownOpts *idtools.IDPair, extract, isFromHostToCtr bool) error { + srcPath, err := evalSymlinks(srcPath) + if err != nil { + return errors.Wrapf(err, "error evaluating symlinks %q", srcPath) + } + + srcPath, srcfi, err := getPathInfo(srcPath) + if err != nil { + return err + } + + filename := filepath.Base(destPath) + if filename == "-" && !isFromHostToCtr { + err := streamFileToStdout(srcPath, srcfi) + if err != nil { + return errors.Wrapf(err, "error streaming source file %s to Stdout", srcPath) + } + return nil + } + + destdir := destPath + if !srcfi.IsDir() { + destdir = filepath.Dir(destPath) + } + _, err = os.Stat(destdir) + if err != nil && !os.IsNotExist(err) { + return errors.Wrapf(err, "error checking directory %q", destdir) + } + destDirIsExist := err == nil + if err = os.MkdirAll(destdir, 0755); err != nil { + return errors.Wrapf(err, "error creating directory %q", destdir) + } + + // return functions for copying items + copyFileWithTar := chrootarchive.CopyFileWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) + copyWithTar := chrootarchive.CopyWithTarAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) + untarPath := chrootarchive.UntarPathAndChown(chownOpts, digest.Canonical.Digester().Hash(), idMappingOpts.UIDMap, idMappingOpts.GIDMap) + + if srcfi.IsDir() { + logrus.Debugf("copying %q to %q", srcPath+string(os.PathSeparator)+"*", dest+string(os.PathSeparator)+"*") + if destDirIsExist && !strings.HasSuffix(src, fmt.Sprintf("%s.", string(os.PathSeparator))) { + destPath = filepath.Join(destPath, filepath.Base(srcPath)) + } + if err = copyWithTar(srcPath, destPath); err != nil { + return errors.Wrapf(err, "error copying %q to %q", srcPath, dest) + } + return nil + } + + if extract { + // We're extracting an archive into the destination directory. + logrus.Debugf("extracting contents of %q into %q", srcPath, destPath) + if err = untarPath(srcPath, destPath); err != nil { + return errors.Wrapf(err, "error extracting %q into %q", srcPath, destPath) + } + return nil + } + + destfi, err := os.Stat(destPath) + if err != nil { + if !os.IsNotExist(err) || strings.HasSuffix(dest, string(os.PathSeparator)) { + return errors.Wrapf(err, "failed to get stat of dest path %s", destPath) + } + } + if destfi != nil && destfi.IsDir() { + destPath = filepath.Join(destPath, filepath.Base(srcPath)) + } + + // Copy the file, preserving attributes. + logrus.Debugf("copying %q to %q", srcPath, destPath) + if err = copyFileWithTar(srcPath, destPath); err != nil { + return errors.Wrapf(err, "error copying %q to %q", srcPath, destPath) + } + return nil +} + +func convertIDMap(idMaps []idtools.IDMap) (convertedIDMap []specs.LinuxIDMapping) { + for _, idmap := range idMaps { + tempIDMap := specs.LinuxIDMapping{ + ContainerID: uint32(idmap.ContainerID), + HostID: uint32(idmap.HostID), + Size: uint32(idmap.Size), + } + convertedIDMap = append(convertedIDMap, tempIDMap) + } + return convertedIDMap +} + +func streamFileToStdout(srcPath string, srcfi os.FileInfo) error { + if srcfi.IsDir() { + tw := tar.NewWriter(os.Stdout) + err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error { + if err != nil || !info.Mode().IsRegular() || path == srcPath { + return err + } + hdr, err := tar.FileInfoHeader(info, "") + if err != nil { + return err + } + + if err = tw.WriteHeader(hdr); err != nil { + return err + } + fh, err := os.Open(path) + if err != nil { + return err + } + defer fh.Close() + + _, err = io.Copy(tw, fh) + return err + }) + if err != nil { + return errors.Wrapf(err, "error streaming directory %s to Stdout", srcPath) + } + return nil + } + + file, err := os.Open(srcPath) + if err != nil { + return errors.Wrapf(err, "error opening file %s", srcPath) + } + defer file.Close() + if !archive.IsArchivePath(srcPath) { + tw := tar.NewWriter(os.Stdout) + hdr, err := tar.FileInfoHeader(srcfi, "") + if err != nil { + return err + } + err = tw.WriteHeader(hdr) + if err != nil { + return err + } + _, err = io.Copy(tw, file) + if err != nil { + return errors.Wrapf(err, "error streaming archive %s to Stdout", srcPath) + } + return nil + } + + _, err = io.Copy(os.Stdout, file) + if err != nil { + return errors.Wrapf(err, "error streaming file to Stdout") + } + return nil +} + +func isVolumeDestName(path string, ctr *libpod.Container) (bool, string, string) { + separator := string(os.PathSeparator) + if filepath.IsAbs(path) { + path = strings.TrimPrefix(path, separator) + } + if path == "" { + return false, "", "" + } + for _, vol := range ctr.Config().NamedVolumes { + volNamePath := strings.TrimPrefix(vol.Dest, separator) + if matchVolumePath(path, volNamePath) { + return true, vol.Dest, vol.Name + } + } + return false, "", "" +} + +// if SRCPATH or DESTPATH is from volume mount's destination -v or --mount type=volume, generates the path with volume mount point +func pathWithVolumeMount(ctr *libpod.Container, runtime *libpod.Runtime, volDestName, volName, path string) (string, error) { + destVolume, err := runtime.GetVolume(volName) + if err != nil { + return "", errors.Wrapf(err, "error getting volume destination %s", volName) + } + if !filepath.IsAbs(path) { + path = filepath.Join(string(os.PathSeparator), path) + } + path, err = securejoin.SecureJoin(destVolume.MountPoint(), strings.TrimPrefix(path, volDestName)) + return path, err +} + +func isBindMountDestName(path string, ctr *libpod.Container) (bool, specs.Mount) { + separator := string(os.PathSeparator) + if filepath.IsAbs(path) { + path = strings.TrimPrefix(path, string(os.PathSeparator)) + } + if path == "" { + return false, specs.Mount{} + } + for _, m := range ctr.Config().Spec.Mounts { + if m.Type != "bind" { + continue + } + mDest := strings.TrimPrefix(m.Destination, separator) + if matchVolumePath(path, mDest) { + return true, m + } + } + return false, specs.Mount{} +} + +func matchVolumePath(path, target string) bool { + pathStr := filepath.Clean(path) + target = filepath.Clean(target) + for len(pathStr) > len(target) && strings.Contains(pathStr, string(os.PathSeparator)) { + pathStr = pathStr[:strings.LastIndex(pathStr, string(os.PathSeparator))] + } + return pathStr == target +} + +func pathWithBindMountSource(m specs.Mount, path string) (string, error) { + if !filepath.IsAbs(path) { + path = filepath.Join(string(os.PathSeparator), path) + } + return securejoin.SecureJoin(m.Source, strings.TrimPrefix(path, m.Destination)) +} diff --git a/pkg/domain/infra/abi/events.go b/pkg/domain/infra/abi/events.go index 9540a5b96..20773cdce 100644 --- a/pkg/domain/infra/abi/events.go +++ b/pkg/domain/infra/abi/events.go @@ -1,5 +1,3 @@ -//+build ABISupport - package abi import ( diff --git a/pkg/domain/infra/abi/healthcheck.go b/pkg/domain/infra/abi/healthcheck.go index 699483243..351bf4f7e 100644 --- a/pkg/domain/infra/abi/healthcheck.go +++ b/pkg/domain/infra/abi/healthcheck.go @@ -1,5 +1,3 @@ -// +build ABISupport - package abi import ( diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 4353e0798..32f7d75e5 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -1,5 +1,3 @@ -// +build ABISupport - package abi import ( @@ -23,6 +21,7 @@ import ( domainUtils "github.com/containers/libpod/pkg/domain/utils" "github.com/containers/libpod/pkg/util" "github.com/containers/storage" + "github.com/hashicorp/go-multierror" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -36,76 +35,6 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.Boo return &entities.BoolReport{Value: err == nil}, nil } -func (ir *ImageEngine) Delete(ctx context.Context, nameOrId []string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { - report := entities.ImageDeleteReport{} - - if opts.All { - var previousTargets []*libpodImage.Image - repeatRun: - targets, err := ir.Libpod.ImageRuntime().GetRWImages() - if err != nil { - return &report, errors.Wrapf(err, "unable to query local images") - } - if len(targets) == 0 { - return &report, nil - } - if len(targets) > 0 && len(targets) == len(previousTargets) { - return &report, errors.New("unable to delete all images; re-run the rmi command again.") - } - previousTargets = targets - - for _, img := range targets { - isParent, err := img.IsParent(ctx) - if err != nil { - return &report, err - } - if isParent { - continue - } - err = ir.deleteImage(ctx, img, opts, report) - report.Errors = append(report.Errors, err) - } - if len(previousTargets) != 1 { - goto repeatRun - } - return &report, nil - } - - for _, id := range nameOrId { - image, err := ir.Libpod.ImageRuntime().NewFromLocal(id) - if err != nil { - return nil, err - } - - err = ir.deleteImage(ctx, image, opts, report) - if err != nil { - return &report, err - } - } - return &report, nil -} - -func (ir *ImageEngine) deleteImage(ctx context.Context, img *libpodImage.Image, opts entities.ImageDeleteOptions, report entities.ImageDeleteReport) error { - results, err := ir.Libpod.RemoveImage(ctx, img, opts.Force) - switch errors.Cause(err) { - case nil: - break - case storage.ErrImageUsedByContainer: - report.ImageInUse = errors.New( - fmt.Sprintf("A container associated with containers/storage, i.e. via Buildah, CRI-O, etc., may be associated with this image: %-12.12s\n", img.ID())) - return nil - case libpodImage.ErrNoSuchImage: - report.ImageNotFound = err - return nil - default: - return err - } - - report.Deleted = append(report.Deleted, results.Deleted) - report.Untagged = append(report.Untagged, results.Untagged...) - return nil -} - func (ir *ImageEngine) Prune(ctx context.Context, opts entities.ImagePruneOptions) (*entities.ImagePruneReport, error) { results, err := ir.Libpod.ImageRuntime().PruneImages(ctx, opts.All, opts.Filter) if err != nil { @@ -488,3 +417,135 @@ func (ir *ImageEngine) Tree(ctx context.Context, nameOrId string, opts entities. } return &entities.ImageTreeReport{Tree: results}, nil } + +// Remove removes one or more images from local storage. +func (ir *ImageEngine) Remove(ctx context.Context, images []string, opts entities.ImageRemoveOptions) (report *entities.ImageRemoveReport, finalError error) { + var ( + // noSuchImageErrors indicates that at least one image was not found. + noSuchImageErrors bool + // inUseErrors indicates that at least one image is being used by a + // container. + inUseErrors bool + // otherErrors indicates that at least one error other than the two + // above occured. + otherErrors bool + // deleteError is a multierror to conveniently collect errors during + // removal. We really want to delete as many images as possible and not + // error out immediately. + deleteError *multierror.Error + ) + + report = &entities.ImageRemoveReport{} + + // Set the removalCode and the error after all work is done. + defer func() { + switch { + // 2 + case inUseErrors: + // One of the specified images has child images or is + // being used by a container. + report.ExitCode = 2 + // 1 + case noSuchImageErrors && !(otherErrors || inUseErrors): + // One of the specified images did not exist, and no other + // failures. + report.ExitCode = 1 + // 0 + default: + // Nothing to do. + } + if deleteError != nil { + // go-multierror has a trailing new line which we need to remove to normalize the string. + finalError = deleteError.ErrorOrNil() + finalError = errors.New(strings.TrimSpace(finalError.Error())) + } + }() + + // deleteImage is an anonymous function to conveniently delete an image + // withouth having to pass all local data around. + deleteImage := func(img *image.Image) error { + results, err := ir.Libpod.RemoveImage(ctx, img, opts.Force) + switch errors.Cause(err) { + case nil: + break + case storage.ErrImageUsedByContainer: + inUseErrors = true // Important for exit codes in Podman. + return errors.New( + fmt.Sprintf("A container associated with containers/storage, i.e. via Buildah, CRI-O, etc., may be associated with this image: %-12.12s\n", img.ID())) + default: + otherErrors = true // Important for exit codes in Podman. + return err + } + + report.Deleted = append(report.Deleted, results.Deleted) + report.Untagged = append(report.Untagged, results.Untagged...) + return nil + } + + // Delete all images from the local storage. + if opts.All { + previousImages := 0 + // Remove all images one-by-one. + for { + storageImages, err := ir.Libpod.ImageRuntime().GetRWImages() + if err != nil { + deleteError = multierror.Append(deleteError, + errors.Wrapf(err, "unable to query local images")) + otherErrors = true // Important for exit codes in Podman. + return + } + // No images (left) to remove, so we're done. + if len(storageImages) == 0 { + return + } + // Prevent infinity loops by making a delete-progress check. + if previousImages == len(storageImages) { + otherErrors = true // Important for exit codes in Podman. + deleteError = multierror.Append(deleteError, + errors.New("unable to delete all images, check errors and re-run image removal if needed")) + break + } + previousImages = len(storageImages) + // Delete all "leaves" (i.e., images without child images). + for _, img := range storageImages { + isParent, err := img.IsParent(ctx) + if err != nil { + otherErrors = true // Important for exit codes in Podman. + deleteError = multierror.Append(deleteError, err) + } + // Skip parent images. + if isParent { + continue + } + if err := deleteImage(img); err != nil { + deleteError = multierror.Append(deleteError, err) + } + } + } + + return + } + + // Delete only the specified images. + for _, id := range images { + img, err := ir.Libpod.ImageRuntime().NewFromLocal(id) + switch errors.Cause(err) { + case nil: + break + case image.ErrNoSuchImage: + noSuchImageErrors = true // Important for exit codes in Podman. + fallthrough + default: + deleteError = multierror.Append(deleteError, err) + continue + } + + err = deleteImage(img) + if err != nil { + otherErrors = true // Important for exit codes in Podman. + deleteError = multierror.Append(deleteError, err) + } + } + + return +} diff --git a/pkg/domain/infra/abi/images_list.go b/pkg/domain/infra/abi/images_list.go index 68b961cb6..9add915ea 100644 --- a/pkg/domain/infra/abi/images_list.go +++ b/pkg/domain/infra/abi/images_list.go @@ -1,5 +1,3 @@ -// +build ABISupport - package abi import ( diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index 6b6e13e24..c4ae9efbf 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -1,5 +1,3 @@ -// +build ABISupport - package abi import ( diff --git a/pkg/domain/infra/abi/runtime.go b/pkg/domain/infra/abi/runtime.go index b53fb6d3a..7394cadfc 100644 --- a/pkg/domain/infra/abi/runtime.go +++ b/pkg/domain/infra/abi/runtime.go @@ -1,5 +1,3 @@ -// +build ABISupport - package abi import ( diff --git a/pkg/domain/infra/abi/system.go b/pkg/domain/infra/abi/system.go index 078f5404d..e5c109ee6 100644 --- a/pkg/domain/infra/abi/system.go +++ b/pkg/domain/infra/abi/system.go @@ -1,20 +1,15 @@ -// +build ABISupport - package abi import ( "context" "fmt" "io/ioutil" - "net" "os" "strconv" - "strings" "syscall" "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod/define" - api "github.com/containers/libpod/pkg/api/server" "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/domain/entities" "github.com/containers/libpod/pkg/rootless" @@ -33,42 +28,6 @@ func (ic *ContainerEngine) Info(ctx context.Context) (*define.Info, error) { return ic.Libpod.Info() } -func (ic *ContainerEngine) RestService(_ context.Context, opts entities.ServiceOptions) error { - var ( - listener *net.Listener - err error - ) - - if opts.URI != "" { - fields := strings.Split(opts.URI, ":") - if len(fields) == 1 { - return errors.Errorf("%s is an invalid socket destination", opts.URI) - } - address := strings.Join(fields[1:], ":") - l, err := net.Listen(fields[0], address) - if err != nil { - return errors.Wrapf(err, "unable to create socket %s", opts.URI) - } - listener = &l - } - - server, err := api.NewServerWithSettings(ic.Libpod, opts.Timeout, listener) - if err != nil { - return err - } - defer func() { - if err := server.Shutdown(); err != nil { - logrus.Warnf("Error when stopping API service: %s", err) - } - }() - - err = server.Serve() - if listener != nil { - _ = (*listener).Close() - } - return err -} - func (ic *ContainerEngine) VarlinkService(_ context.Context, opts entities.ServiceOptions) error { var varlinkInterfaces = []*iopodman.VarlinkInterface{ iopodmanAPI.New(opts.Command, ic.Libpod), diff --git a/pkg/domain/infra/abi/terminal/sigproxy_linux.go b/pkg/domain/infra/abi/terminal/sigproxy_linux.go index d7f5853d8..b422e549e 100644 --- a/pkg/domain/infra/abi/terminal/sigproxy_linux.go +++ b/pkg/domain/infra/abi/terminal/sigproxy_linux.go @@ -1,5 +1,3 @@ -// +build ABISupport - package terminal import ( diff --git a/pkg/domain/infra/abi/terminal/terminal.go b/pkg/domain/infra/abi/terminal/terminal.go index f187bdd6b..0fc3af511 100644 --- a/pkg/domain/infra/abi/terminal/terminal.go +++ b/pkg/domain/infra/abi/terminal/terminal.go @@ -1,5 +1,3 @@ -// +build ABISupport - package terminal import ( diff --git a/pkg/domain/infra/abi/terminal/terminal_linux.go b/pkg/domain/infra/abi/terminal/terminal_linux.go index 664205df1..15701342f 100644 --- a/pkg/domain/infra/abi/terminal/terminal_linux.go +++ b/pkg/domain/infra/abi/terminal/terminal_linux.go @@ -1,5 +1,3 @@ -// +build ABISupport - package terminal import ( diff --git a/pkg/domain/infra/tunnel/containers.go b/pkg/domain/infra/tunnel/containers.go index 679bb371b..8867ce27f 100644 --- a/pkg/domain/infra/tunnel/containers.go +++ b/pkg/domain/infra/tunnel/containers.go @@ -371,3 +371,11 @@ func (ic *ContainerEngine) ContainerUnmount(ctx context.Context, nameOrIds []str func (ic *ContainerEngine) Config(_ context.Context) (*config.Config, error) { return config.Default() } + +func (ic *ContainerEngine) ContainerPort(ctx context.Context, nameOrId string, options entities.ContainerPortOptions) ([]*entities.ContainerPortReport, error) { + return nil, errors.New("not implemented") +} + +func (ic *ContainerEngine) ContainerCp(ctx context.Context, source, dest string, options entities.ContainerCpOptions) (*entities.ContainerCpReport, error) { + return nil, errors.New("not implemented") +} diff --git a/pkg/domain/infra/tunnel/events.go b/pkg/domain/infra/tunnel/events.go index 46d88341a..93da3aeb4 100644 --- a/pkg/domain/infra/tunnel/events.go +++ b/pkg/domain/infra/tunnel/events.go @@ -4,7 +4,6 @@ import ( "context" "strings" - "github.com/containers/libpod/pkg/api/handlers" "github.com/containers/libpod/pkg/bindings/system" "github.com/containers/libpod/pkg/domain/entities" "github.com/pkg/errors" @@ -21,10 +20,10 @@ func (ic *ContainerEngine) Events(ctx context.Context, opts entities.EventsOptio filters[split[0]] = append(filters[split[0]], strings.Join(split[1:], "=")) } } - binChan := make(chan handlers.Event) + binChan := make(chan entities.Event) go func() { for e := range binChan { - opts.EventChan <- e.ToLibpodEvent() + opts.EventChan <- entities.ConvertToLibpodEvent(e) } }() return system.Events(ic.ClientCxt, binChan, nil, &opts.Since, &opts.Until, filters) diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 2decd605d..822842936 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -19,25 +19,8 @@ func (ir *ImageEngine) Exists(_ context.Context, nameOrId string) (*entities.Boo return &entities.BoolReport{Value: found}, err } -func (ir *ImageEngine) Delete(ctx context.Context, nameOrId []string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) { - report := entities.ImageDeleteReport{} - - for _, id := range nameOrId { - results, err := images.Remove(ir.ClientCxt, id, &opts.Force) - if err != nil { - return nil, err - } - for _, e := range results { - if a, ok := e["Deleted"]; ok { - report.Deleted = append(report.Deleted, a) - } - - if a, ok := e["Untagged"]; ok { - report.Untagged = append(report.Untagged, a) - } - } - } - return &report, nil +func (ir *ImageEngine) Remove(ctx context.Context, imagesArg []string, opts entities.ImageRemoveOptions) (*entities.ImageRemoveReport, error) { + return images.Remove(ir.ClientCxt, imagesArg, opts) } func (ir *ImageEngine) List(ctx context.Context, opts entities.ImageListOptions) ([]*entities.ImageSummary, error) { diff --git a/pkg/domain/infra/tunnel/system.go b/pkg/domain/infra/tunnel/system.go index f373525c5..97bf885e7 100644 --- a/pkg/domain/infra/tunnel/system.go +++ b/pkg/domain/infra/tunnel/system.go @@ -14,10 +14,6 @@ func (ic *ContainerEngine) Info(ctx context.Context) (*define.Info, error) { return system.Info(ic.ClientCxt) } -func (ic *ContainerEngine) RestService(_ context.Context, _ entities.ServiceOptions) error { - panic(errors.New("rest service is not supported when tunneling")) -} - func (ic *ContainerEngine) VarlinkService(_ context.Context, _ entities.ServiceOptions) error { panic(errors.New("varlink service is not supported when tunneling")) } diff --git a/pkg/spec/spec.go b/pkg/spec/spec.go index 5de07fc28..7ee2df890 100644 --- a/pkg/spec/spec.go +++ b/pkg/spec/spec.go @@ -5,11 +5,11 @@ import ( "github.com/containers/common/pkg/capabilities" cconfig "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/sysinfo" "github.com/containers/libpod/libpod" "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/env" "github.com/containers/libpod/pkg/rootless" - "github.com/containers/libpod/pkg/sysinfo" "github.com/containers/libpod/pkg/util" "github.com/docker/go-units" "github.com/opencontainers/runc/libcontainer/user" diff --git a/pkg/spec/spec_test.go b/pkg/spec/spec_test.go index 0f63b2bbc..71434fe73 100644 --- a/pkg/spec/spec_test.go +++ b/pkg/spec/spec_test.go @@ -4,9 +4,9 @@ import ( "runtime" "testing" + "github.com/containers/common/pkg/sysinfo" "github.com/containers/libpod/pkg/cgroups" "github.com/containers/libpod/pkg/rootless" - "github.com/containers/libpod/pkg/sysinfo" "github.com/containers/storage" "github.com/containers/storage/pkg/idtools" "github.com/docker/go-units" diff --git a/pkg/specgen/container_validate.go b/pkg/specgen/container_validate.go index 9152e7ee7..df9c77cbc 100644 --- a/pkg/specgen/container_validate.go +++ b/pkg/specgen/container_validate.go @@ -86,18 +86,15 @@ func (s *SpecGenerator) Validate() error { // // ContainerNetworkConfig // - if !s.NetNS.IsPrivate() && s.ConfigureNetNS { - return errors.New("can only configure network namespace when creating a network a network namespace") - } // useimageresolveconf conflicts with dnsserver, dnssearch, dnsoption if s.UseImageResolvConf { - if len(s.DNSServer) > 0 { + if len(s.DNSServers) > 0 { return exclusiveOptions("UseImageResolvConf", "DNSServer") } if len(s.DNSSearch) > 0 { return exclusiveOptions("UseImageResolvConf", "DNSSearch") } - if len(s.DNSOption) > 0 { + if len(s.DNSOptions) > 0 { return exclusiveOptions("UseImageResolvConf", "DNSOption") } } diff --git a/pkg/specgen/generate/container.go b/pkg/specgen/generate/container.go index 31465d8bf..de3239fda 100644 --- a/pkg/specgen/generate/container.go +++ b/pkg/specgen/generate/container.go @@ -13,7 +13,9 @@ import ( ) func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerator) error { + var appendEntryPoint bool + // TODO add support for raw rootfs newImage, err := r.ImageRuntime().NewFromLocal(s.Image) if err != nil { return err @@ -64,6 +66,16 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat } // annotations + + // Add annotations from the image + annotations, err := newImage.Annotations(ctx) + if err != nil { + return err + } + for k, v := range annotations { + annotations[k] = v + } + // in the event this container is in a pod, and the pod has an infra container // we will want to configure it as a type "container" instead defaulting to // the behavior of a "sandbox" container @@ -72,20 +84,17 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat // VM, which is the default behavior // - "container" denotes the container should join the VM of the SandboxID // (the infra container) - s.Annotations = make(map[string]string) + if len(s.Pod) > 0 { - s.Annotations[ann.SandboxID] = s.Pod - s.Annotations[ann.ContainerType] = ann.ContainerTypeContainer - } - // - // Next, add annotations from the image - annotations, err := newImage.Annotations(ctx) - if err != nil { - return err + annotations[ann.SandboxID] = s.Pod + annotations[ann.ContainerType] = ann.ContainerTypeContainer } - for k, v := range annotations { + + // now pass in the values from client + for k, v := range s.Annotations { annotations[k] = v } + s.Annotations = annotations // entrypoint entrypoint, err := newImage.Entrypoint(ctx) @@ -93,6 +102,7 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat return err } if len(s.Entrypoint) < 1 && len(entrypoint) > 0 { + appendEntryPoint = true s.Entrypoint = entrypoint } command, err := newImage.Cmd(ctx) @@ -100,7 +110,10 @@ func CompleteSpec(ctx context.Context, r *libpod.Runtime, s *specgen.SpecGenerat return err } if len(s.Command) < 1 && len(command) > 0 { - s.Command = command + if appendEntryPoint { + s.Command = entrypoint + } + s.Command = append(s.Command, command...) } if len(s.Command) < 1 && len(s.Entrypoint) < 1 { return errors.Errorf("No command provided or as CMD or ENTRYPOINT in this image") diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 264e0ff8e..1be77d315 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -23,7 +23,61 @@ func MakeContainer(rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Contai return nil, err } - options, err := createContainerOptions(rt, s) + // If joining a pod, retrieve the pod for use. + var pod *libpod.Pod + if s.Pod != "" { + foundPod, err := rt.LookupPod(s.Pod) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving pod %s", s.Pod) + } + pod = foundPod + } + + // Set defaults for unset namespaces + if s.PidNS.IsDefault() { + defaultNS, err := GetDefaultNamespaceMode("pid", rtc, pod) + if err != nil { + return nil, err + } + s.PidNS = defaultNS + } + if s.IpcNS.IsDefault() { + defaultNS, err := GetDefaultNamespaceMode("ipc", rtc, pod) + if err != nil { + return nil, err + } + s.IpcNS = defaultNS + } + if s.UtsNS.IsDefault() { + defaultNS, err := GetDefaultNamespaceMode("uts", rtc, pod) + if err != nil { + return nil, err + } + s.UtsNS = defaultNS + } + if s.UserNS.IsDefault() { + defaultNS, err := GetDefaultNamespaceMode("user", rtc, pod) + if err != nil { + return nil, err + } + s.UserNS = defaultNS + } + if s.NetNS.IsDefault() { + defaultNS, err := GetDefaultNamespaceMode("net", rtc, pod) + if err != nil { + return nil, err + } + s.NetNS = defaultNS + } + if s.CgroupNS.IsDefault() { + defaultNS, err := GetDefaultNamespaceMode("cgroup", rtc, pod) + if err != nil { + return nil, err + } + s.CgroupNS = defaultNS + } + + options, err := createContainerOptions(rt, s, pod) if err != nil { return nil, err } @@ -47,7 +101,7 @@ func MakeContainer(rt *libpod.Runtime, s *specgen.SpecGenerator) (*libpod.Contai return rt.NewContainer(context.Background(), runtimeSpec, options...) } -func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator) ([]libpod.CtrCreateOption, error) { +func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *libpod.Pod) ([]libpod.CtrCreateOption, error) { var options []libpod.CtrCreateOption var err error @@ -123,7 +177,7 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator) ([]lib options = append(options, libpod.WithPrivileged(s.Privileged)) // Get namespace related options - namespaceOptions, err := GenerateNamespaceContainerOpts(s, rt) + namespaceOptions, err := GenerateNamespaceOptions(s, rt, pod) if err != nil { return nil, err } diff --git a/pkg/specgen/generate/namespaces.go b/pkg/specgen/generate/namespaces.go index 53ae335c3..4ec1e859c 100644 --- a/pkg/specgen/generate/namespaces.go +++ b/pkg/specgen/generate/namespaces.go @@ -2,291 +2,439 @@ package generate import ( "os" + "strings" + "github.com/containers/common/pkg/config" "github.com/containers/libpod/libpod" + "github.com/containers/libpod/libpod/define" + "github.com/containers/libpod/pkg/cgroups" + "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/specgen" - "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -func GenerateNamespaceContainerOpts(s *specgen.SpecGenerator, rt *libpod.Runtime) ([]libpod.CtrCreateOption, error) { - var portBindings []ocicni.PortMapping - options := make([]libpod.CtrCreateOption, 0) +// Get the default namespace mode for any given namespace type. +func GetDefaultNamespaceMode(nsType string, cfg *config.Config, pod *libpod.Pod) (specgen.Namespace, error) { + // The default for most is private + toReturn := specgen.Namespace{} + toReturn.NSMode = specgen.Private - // Cgroups - switch { - case s.CgroupNS.IsPrivate(): - ns := s.CgroupNS.Value - if _, err := os.Stat(ns); err != nil { - return nil, err + // Ensure case insensitivity + nsType = strings.ToLower(nsType) + + // If the pod is not nil - check shared namespaces + if pod != nil { + podMode := false + switch { + case nsType == "pid" && pod.SharesPID(): + podMode = true + case nsType == "ipc" && pod.SharesIPC(): + podMode = true + case nsType == "uts" && pod.SharesUTS(): + podMode = true + case nsType == "user" && pod.SharesUser(): + podMode = true + case nsType == "net" && pod.SharesNet(): + podMode = true + case nsType == "cgroup" && pod.SharesCgroup(): + podMode = true } - case s.CgroupNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.CgroupNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.CgroupNS.Value) + if podMode { + toReturn.NSMode = specgen.FromPod + return toReturn, nil } - options = append(options, libpod.WithCgroupNSFrom(connectedCtr)) - // TODO - //default: - // return nil, errors.New("cgroup name only supports private and container") } - if s.CgroupParent != "" { - options = append(options, libpod.WithCgroupParent(s.CgroupParent)) + // If we have containers.conf and are not using cgroupns, use that. + if cfg != nil && nsType != "cgroup" { + switch nsType { + case "pid": + return specgen.ParseNamespace(cfg.Containers.PidNS) + case "ipc": + return specgen.ParseNamespace(cfg.Containers.IPCNS) + case "uts": + return specgen.ParseNamespace(cfg.Containers.UTSNS) + case "user": + // TODO: This may not work for --userns=auto + return specgen.ParseNamespace(cfg.Containers.UserNS) + case "net": + ns, _, err := specgen.ParseNetworkNamespace(cfg.Containers.NetNS) + return ns, err + } } - if s.CgroupsMode != "" { - options = append(options, libpod.WithCgroupsMode(s.CgroupsMode)) + switch nsType { + case "pid", "ipc", "uts": + // PID, IPC, UTS both default to private, do nothing + case "user": + // User namespace always defaults to host + toReturn.NSMode = specgen.Host + case "net": + // Net defaults to Slirp on rootless, Bridge otherwise. + if rootless.IsRootless() { + toReturn.NSMode = specgen.Slirp + } else { + toReturn.NSMode = specgen.Bridge + } + case "cgroup": + // Cgroup is host for v1, private for v2. + // We can't trust c/common for this, as it only assumes private. + cgroupsv2, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return toReturn, err + } + if !cgroupsv2 { + toReturn.NSMode = specgen.Host + } + default: + return toReturn, errors.Wrapf(define.ErrInvalidArg, "invalid namespace type %s passed", nsType) } - // ipc - switch { - case s.IpcNS.IsHost(): - options = append(options, libpod.WithShmDir("/dev/shm")) - case s.IpcNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.IpcNS.Value) + return toReturn, nil +} + +// GenerateNamespaceOptions generates container creation options for all +// namespaces in a SpecGenerator. +// Pod is the pod the container will join. May be nil is the container is not +// joining a pod. +// TODO: Consider grouping options that are not directly attached to a namespace +// elsewhere. +func GenerateNamespaceOptions(s *specgen.SpecGenerator, rt *libpod.Runtime, pod *libpod.Pod) ([]libpod.CtrCreateOption, error) { + toReturn := []libpod.CtrCreateOption{} + + // If pod is not nil, get infra container. + var infraCtr *libpod.Container + if pod != nil { + infraID, err := pod.InfraContainerID() if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.IpcNS.Value) + // This is likely to be of the fatal kind (pod was + // removed) so hard fail + return nil, errors.Wrapf(err, "error looking up pod %s infra container", pod.ID()) + } + if infraID != "" { + ctr, err := rt.GetContainer(infraID) + if err != nil { + return nil, errors.Wrapf(err, "error retrieving pod %s infra container %s", pod.ID(), infraID) + } + infraCtr = ctr } - options = append(options, libpod.WithIPCNSFrom(connectedCtr)) - options = append(options, libpod.WithShmDir(connectedCtr.ShmDir())) } - // pid - if s.PidNS.IsContainer() { - connectedCtr, err := rt.LookupContainer(s.PidNS.Value) + errNoInfra := errors.Wrapf(define.ErrInvalidArg, "cannot use pod namespace as container is not joining a pod or pod has no infra container") + + // PID + switch s.PidNS.NSMode { + case specgen.FromPod: + if pod == nil || infraCtr == nil { + return nil, errNoInfra + } + toReturn = append(toReturn, libpod.WithPIDNSFrom(infraCtr)) + case specgen.FromContainer: + pidCtr, err := rt.LookupContainer(s.PidNS.Value) if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.PidNS.Value) + return nil, errors.Wrapf(err, "error looking up container to share pid namespace with") } - options = append(options, libpod.WithPIDNSFrom(connectedCtr)) + toReturn = append(toReturn, libpod.WithPIDNSFrom(pidCtr)) } - // uts - switch { - case s.UtsNS.IsPod(): - connectedPod, err := rt.LookupPod(s.UtsNS.Value) - if err != nil { - return nil, errors.Wrapf(err, "pod %q not found", s.UtsNS.Value) + // IPC + switch s.IpcNS.NSMode { + case specgen.Host: + // Force use of host /dev/shm for host namespace + toReturn = append(toReturn, libpod.WithShmDir("/dev/shm")) + case specgen.FromPod: + if pod == nil || infraCtr == nil { + return nil, errNoInfra } - options = append(options, libpod.WithUTSNSFromPod(connectedPod)) - case s.UtsNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.UtsNS.Value) + toReturn = append(toReturn, libpod.WithIPCNSFrom(infraCtr)) + case specgen.FromContainer: + ipcCtr, err := rt.LookupContainer(s.IpcNS.Value) if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.UtsNS.Value) + return nil, errors.Wrapf(err, "error looking up container to share ipc namespace with") } - - options = append(options, libpod.WithUTSNSFrom(connectedCtr)) + toReturn = append(toReturn, libpod.WithIPCNSFrom(ipcCtr)) + toReturn = append(toReturn, libpod.WithShmDir(ipcCtr.ShmDir())) } - if s.UseImageHosts { - options = append(options, libpod.WithUseImageHosts()) - } else if len(s.HostAdd) > 0 { - options = append(options, libpod.WithHosts(s.HostAdd)) + // UTS + switch s.UtsNS.NSMode { + case specgen.FromPod: + if pod == nil || infraCtr == nil { + return nil, errNoInfra + } + toReturn = append(toReturn, libpod.WithUTSNSFrom(infraCtr)) + case specgen.FromContainer: + utsCtr, err := rt.LookupContainer(s.UtsNS.Value) + if err != nil { + return nil, errors.Wrapf(err, "error looking up container to share uts namespace with") + } + toReturn = append(toReturn, libpod.WithUTSNSFrom(utsCtr)) } // User - - switch { - case s.UserNS.IsPath(): - ns := s.UserNS.Value - if ns == "" { - return nil, errors.Errorf("invalid empty user-defined user namespace") + switch s.UserNS.NSMode { + case specgen.FromPod: + if pod == nil || infraCtr == nil { + return nil, errNoInfra } - _, err := os.Stat(ns) + toReturn = append(toReturn, libpod.WithUserNSFrom(infraCtr)) + case specgen.FromContainer: + userCtr, err := rt.LookupContainer(s.UserNS.Value) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "error looking up container to share user namespace with") } - if s.IDMappings != nil { - options = append(options, libpod.WithIDMappings(*s.IDMappings)) + toReturn = append(toReturn, libpod.WithUserNSFrom(userCtr)) + } + + if s.IDMappings != nil { + toReturn = append(toReturn, libpod.WithIDMappings(*s.IDMappings)) + } + if s.User != "" { + toReturn = append(toReturn, libpod.WithUser(s.User)) + } + if len(s.Groups) > 0 { + toReturn = append(toReturn, libpod.WithGroups(s.Groups)) + } + + // Cgroup + switch s.CgroupNS.NSMode { + case specgen.FromPod: + if pod == nil || infraCtr == nil { + return nil, errNoInfra } - case s.UserNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.UserNS.Value) + toReturn = append(toReturn, libpod.WithCgroupNSFrom(infraCtr)) + case specgen.FromContainer: + cgroupCtr, err := rt.LookupContainer(s.CgroupNS.Value) if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.UserNS.Value) - } - options = append(options, libpod.WithUserNSFrom(connectedCtr)) - default: - if s.IDMappings != nil { - options = append(options, libpod.WithIDMappings(*s.IDMappings)) + return nil, errors.Wrapf(err, "error looking up container to share cgroup namespace with") } + toReturn = append(toReturn, libpod.WithCgroupNSFrom(cgroupCtr)) } - options = append(options, libpod.WithUser(s.User)) - options = append(options, libpod.WithGroups(s.Groups)) + if s.CgroupParent != "" { + toReturn = append(toReturn, libpod.WithCgroupParent(s.CgroupParent)) + } - if len(s.PortMappings) > 0 { - portBindings = s.PortMappings + if s.CgroupsMode != "" { + toReturn = append(toReturn, libpod.WithCgroupsMode(s.CgroupsMode)) } - switch { - case s.NetNS.IsPath(): - ns := s.NetNS.Value - if ns == "" { - return nil, errors.Errorf("invalid empty user-defined network namespace") - } - _, err := os.Stat(ns) - if err != nil { - return nil, err + // Net + // TODO image ports + // TODO validate CNINetworks, StaticIP, StaticIPv6 are only set if we + // are in bridge mode. + postConfigureNetNS := !s.UserNS.IsHost() + switch s.NetNS.NSMode { + case specgen.FromPod: + if pod == nil || infraCtr == nil { + return nil, errNoInfra } - case s.NetNS.IsContainer(): - connectedCtr, err := rt.LookupContainer(s.NetNS.Value) + toReturn = append(toReturn, libpod.WithNetNSFrom(infraCtr)) + case specgen.FromContainer: + netCtr, err := rt.LookupContainer(s.NetNS.Value) if err != nil { - return nil, errors.Wrapf(err, "container %q not found", s.NetNS.Value) + return nil, errors.Wrapf(err, "error looking up container to share net namespace with") } - options = append(options, libpod.WithNetNSFrom(connectedCtr)) - case !s.NetNS.IsHost() && s.NetNS.NSMode != specgen.NoNetwork: - postConfigureNetNS := !s.UserNS.IsHost() - options = append(options, libpod.WithNetNS(portBindings, postConfigureNetNS, string(s.NetNS.NSMode), s.CNINetworks)) + toReturn = append(toReturn, libpod.WithNetNSFrom(netCtr)) + case specgen.Slirp: + toReturn = append(toReturn, libpod.WithNetNS(s.PortMappings, postConfigureNetNS, "slirp4netns", nil)) + case specgen.Bridge: + toReturn = append(toReturn, libpod.WithNetNS(s.PortMappings, postConfigureNetNS, "bridge", s.CNINetworks)) } - if len(s.DNSSearch) > 0 { - options = append(options, libpod.WithDNSSearch(s.DNSSearch)) + if s.UseImageHosts { + toReturn = append(toReturn, libpod.WithUseImageHosts()) + } else if len(s.HostAdd) > 0 { + toReturn = append(toReturn, libpod.WithHosts(s.HostAdd)) } - if len(s.DNSServer) > 0 { - // TODO I'm not sure how we are going to handle this given the input - if len(s.DNSServer) == 1 { //&& strings.ToLower(s.DNSServer[0].) == "none" { - options = append(options, libpod.WithUseImageResolvConf()) - } else { - var dnsServers []string - for _, d := range s.DNSServer { - dnsServers = append(dnsServers, d.String()) - } - options = append(options, libpod.WithDNS(dnsServers)) + if len(s.DNSSearch) > 0 { + toReturn = append(toReturn, libpod.WithDNSSearch(s.DNSSearch)) + } + if s.UseImageResolvConf { + toReturn = append(toReturn, libpod.WithUseImageResolvConf()) + } else if len(s.DNSServers) > 0 { + var dnsServers []string + for _, d := range s.DNSServers { + dnsServers = append(dnsServers, d.String()) } + toReturn = append(toReturn, libpod.WithDNS(dnsServers)) } - if len(s.DNSOption) > 0 { - options = append(options, libpod.WithDNSOption(s.DNSOption)) + if len(s.DNSOptions) > 0 { + toReturn = append(toReturn, libpod.WithDNSOption(s.DNSOptions)) } if s.StaticIP != nil { - options = append(options, libpod.WithStaticIP(*s.StaticIP)) + toReturn = append(toReturn, libpod.WithStaticIP(*s.StaticIP)) } - if s.StaticMAC != nil { - options = append(options, libpod.WithStaticMAC(*s.StaticMAC)) + toReturn = append(toReturn, libpod.WithStaticMAC(*s.StaticMAC)) } - return options, nil + + return toReturn, nil } -func pidConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { - if s.PidNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.PIDNamespace), s.PidNS.Value) - } - if s.PidNS.IsHost() { - return g.RemoveLinuxNamespace(string(spec.PIDNamespace)) +func specConfigureNamespaces(s *specgen.SpecGenerator, g *generate.Generator, rt *libpod.Runtime) error { + // PID + switch s.PidNS.NSMode { + case specgen.Path: + if _, err := os.Stat(s.PidNS.Value); err != nil { + return errors.Wrapf(err, "cannot find specified PID namespace path %q", s.PidNS.Value) + } + if err := g.AddOrReplaceLinuxNamespace(string(spec.PIDNamespace), s.PidNS.Value); err != nil { + return err + } + case specgen.Host: + if err := g.RemoveLinuxNamespace(string(spec.PIDNamespace)); err != nil { + return err + } + case specgen.Private: + if err := g.AddOrReplaceLinuxNamespace(string(spec.PIDNamespace), ""); err != nil { + return err + } } - if s.PidNS.IsContainer() { - logrus.Debugf("using container %s pidmode", s.PidNS.Value) + + // IPC + switch s.IpcNS.NSMode { + case specgen.Path: + if _, err := os.Stat(s.IpcNS.Value); err != nil { + return errors.Wrapf(err, "cannot find specified IPC namespace path %q", s.IpcNS.Value) + } + if err := g.AddOrReplaceLinuxNamespace(string(spec.IPCNamespace), s.IpcNS.Value); err != nil { + return err + } + case specgen.Host: + if err := g.RemoveLinuxNamespace(string(spec.IPCNamespace)); err != nil { + return err + } + case specgen.Private: + if err := g.AddOrReplaceLinuxNamespace(string(spec.IPCNamespace), ""); err != nil { + return err + } } - if s.PidNS.IsPod() { - logrus.Debug("using pod pidmode") + + // UTS + switch s.UtsNS.NSMode { + case specgen.Path: + if _, err := os.Stat(s.UtsNS.Value); err != nil { + return errors.Wrapf(err, "cannot find specified UTS namespace path %q", s.UtsNS.Value) + } + if err := g.AddOrReplaceLinuxNamespace(string(spec.UTSNamespace), s.UtsNS.Value); err != nil { + return err + } + case specgen.Host: + if err := g.RemoveLinuxNamespace(string(spec.UTSNamespace)); err != nil { + return err + } + case specgen.Private: + if err := g.AddOrReplaceLinuxNamespace(string(spec.UTSNamespace), ""); err != nil { + return err + } } - return nil -} -func utsConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator, runtime *libpod.Runtime) error { hostname := s.Hostname - var err error if hostname == "" { switch { - case s.UtsNS.IsContainer(): - utsCtr, err := runtime.LookupContainer(s.UtsNS.Value) + case s.UtsNS.NSMode == specgen.FromContainer: + utsCtr, err := rt.LookupContainer(s.UtsNS.Value) if err != nil { - return errors.Wrapf(err, "unable to retrieve hostname from dependency container %s", s.UtsNS.Value) + return errors.Wrapf(err, "error looking up container to share uts namespace with") } hostname = utsCtr.Hostname() - case s.NetNS.IsHost() || s.UtsNS.IsHost(): - hostname, err = os.Hostname() + case s.NetNS.NSMode == specgen.Host || s.UtsNS.NSMode == specgen.Host: + tmpHostname, err := os.Hostname() if err != nil { return errors.Wrap(err, "unable to retrieve hostname of the host") } + hostname = tmpHostname default: logrus.Debug("No hostname set; container's hostname will default to runtime default") } } + g.RemoveHostname() - if s.Hostname != "" || !s.UtsNS.IsHost() { - // Set the hostname in the OCI configuration only - // if specified by the user or if we are creating - // a new UTS namespace. + if s.Hostname != "" || s.UtsNS.NSMode != specgen.Host { + // Set the hostname in the OCI configuration only if specified by + // the user or if we are creating a new UTS namespace. + // TODO: Should we be doing this for pod or container shared + // namespaces? g.SetHostname(hostname) } g.AddProcessEnv("HOSTNAME", hostname) - if s.UtsNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.UTSNamespace), s.UtsNS.Value) - } - if s.UtsNS.IsHost() { - return g.RemoveLinuxNamespace(string(spec.UTSNamespace)) - } - if s.UtsNS.IsContainer() { - logrus.Debugf("using container %s utsmode", s.UtsNS.Value) - } - return nil -} - -func ipcConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { - if s.IpcNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.IPCNamespace), s.IpcNS.Value) - } - if s.IpcNS.IsHost() { - return g.RemoveLinuxNamespace(s.IpcNS.Value) - } - if s.IpcNS.IsContainer() { - logrus.Debugf("Using container %s ipcmode", s.IpcNS.Value) + // User + switch s.UserNS.NSMode { + case specgen.Path: + if _, err := os.Stat(s.UserNS.Value); err != nil { + return errors.Wrapf(err, "cannot find specified user namespace path %s", s.UserNS.Value) + } + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), s.UserNS.Value); err != nil { + return err + } + // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping + g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) + g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) + case specgen.Host: + if err := g.RemoveLinuxNamespace(string(spec.UserNamespace)); err != nil { + return err + } + case specgen.Private: + if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { + return err + } + if s.IDMappings == nil || (len(s.IDMappings.UIDMap) == 0 && len(s.IDMappings.GIDMap) == 0) { + return errors.Errorf("must provide at least one UID or GID mapping to configure a user namespace") + } + for _, uidmap := range s.IDMappings.UIDMap { + g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) + } + for _, gidmap := range s.IDMappings.GIDMap { + g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) + } } - return nil -} -func cgroupConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { - if s.CgroupNS.IsPath() { - return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), s.CgroupNS.Value) - } - if s.CgroupNS.IsHost() { - return g.RemoveLinuxNamespace(s.CgroupNS.Value) - } - if s.CgroupNS.IsPrivate() { - return g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), "") - } - if s.CgroupNS.IsContainer() { - logrus.Debugf("Using container %s cgroup mode", s.CgroupNS.Value) + // Cgroup + switch s.CgroupNS.NSMode { + case specgen.Path: + if _, err := os.Stat(s.CgroupNS.Value); err != nil { + return errors.Wrapf(err, "cannot find specified cgroup namespace path %s", s.CgroupNS.Value) + } + if err := g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), s.CgroupNS.Value); err != nil { + return err + } + case specgen.Host: + if err := g.RemoveLinuxNamespace(string(spec.CgroupNamespace)); err != nil { + return err + } + case specgen.Private: + if err := g.AddOrReplaceLinuxNamespace(string(spec.CgroupNamespace), ""); err != nil { + return err + } } - return nil -} -func networkConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { - switch { - case s.NetNS.IsHost(): - logrus.Debug("Using host netmode") + // Net + switch s.NetNS.NSMode { + case specgen.Path: + if _, err := os.Stat(s.NetNS.Value); err != nil { + return errors.Wrapf(err, "cannot find specified network namespace path %s", s.NetNS.Value) + } + if err := g.AddOrReplaceLinuxNamespace(string(spec.NetworkNamespace), s.NetNS.Value); err != nil { + return err + } + case specgen.Host: if err := g.RemoveLinuxNamespace(string(spec.NetworkNamespace)); err != nil { return err } - - case s.NetNS.NSMode == specgen.NoNetwork: - logrus.Debug("Using none netmode") - case s.NetNS.NSMode == specgen.Bridge: - logrus.Debug("Using bridge netmode") - case s.NetNS.IsContainer(): - logrus.Debugf("using container %s netmode", s.NetNS.Value) - case s.NetNS.IsPath(): - logrus.Debug("Using ns netmode") - if err := g.AddOrReplaceLinuxNamespace(string(spec.NetworkNamespace), s.NetNS.Value); err != nil { + case specgen.Private, specgen.NoNetwork: + if err := g.AddOrReplaceLinuxNamespace(string(spec.NetworkNamespace), ""); err != nil { return err } - case s.NetNS.IsPod(): - logrus.Debug("Using pod netmode, unless pod is not sharing") - case s.NetNS.NSMode == specgen.Slirp: - logrus.Debug("Using slirp4netns netmode") - default: - return errors.Errorf("unknown network mode") } if g.Config.Annotations == nil { g.Config.Annotations = make(map[string]string) } - if s.PublishImagePorts { g.Config.Annotations[libpod.InspectAnnotationPublishAll] = libpod.InspectResponseTrue } else { @@ -296,32 +444,6 @@ func networkConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) return nil } -func userConfigureGenerator(s *specgen.SpecGenerator, g *generate.Generator) error { - if s.UserNS.IsPath() { - if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), s.UserNS.Value); err != nil { - return err - } - // runc complains if no mapping is specified, even if we join another ns. So provide a dummy mapping - g.AddLinuxUIDMapping(uint32(0), uint32(0), uint32(1)) - g.AddLinuxGIDMapping(uint32(0), uint32(0), uint32(1)) - } - - if s.IDMappings != nil { - if (len(s.IDMappings.UIDMap) > 0 || len(s.IDMappings.GIDMap) > 0) && !s.UserNS.IsHost() { - if err := g.AddOrReplaceLinuxNamespace(string(spec.UserNamespace), ""); err != nil { - return err - } - } - for _, uidmap := range s.IDMappings.UIDMap { - g.AddLinuxUIDMapping(uint32(uidmap.HostID), uint32(uidmap.ContainerID), uint32(uidmap.Size)) - } - for _, gidmap := range s.IDMappings.GIDMap { - g.AddLinuxGIDMapping(uint32(gidmap.HostID), uint32(gidmap.ContainerID), uint32(gidmap.Size)) - } - } - return nil -} - // GetNamespaceOptions transforms a slice of kernel namespaces // into a slice of pod create options. Currently, not all // kernel namespaces are supported, and they will be returned in an error diff --git a/pkg/specgen/generate/oci.go b/pkg/specgen/generate/oci.go index 0ed091f9a..8ca95016e 100644 --- a/pkg/specgen/generate/oci.go +++ b/pkg/specgen/generate/oci.go @@ -12,6 +12,42 @@ import ( "github.com/opencontainers/runtime-tools/generate" ) +func addRlimits(s *specgen.SpecGenerator, g *generate.Generator) error { + var ( + kernelMax uint64 = 1048576 + isRootless = rootless.IsRootless() + nofileSet = false + nprocSet = false + ) + + if s.Rlimits == nil { + g.Config.Process.Rlimits = nil + return nil + } + + for _, u := range s.Rlimits { + name := "RLIMIT_" + strings.ToUpper(u.Type) + if name == "RLIMIT_NOFILE" { + nofileSet = true + } else if name == "RLIMIT_NPROC" { + nprocSet = true + } + g.AddProcessRlimits(name, u.Hard, u.Soft) + } + + // If not explicitly overridden by the user, default number of open + // files and number of processes to the maximum they can be set to + // (without overriding a sysctl) + if !nofileSet && !isRootless { + g.AddProcessRlimits("RLIMIT_NOFILE", kernelMax, kernelMax) + } + if !nprocSet && !isRootless { + g.AddProcessRlimits("RLIMIT_NPROC", kernelMax, kernelMax) + } + + return nil +} + func SpecGenToOCI(s *specgen.SpecGenerator, rt *libpod.Runtime, newImage *image.Image) (*spec.Spec, error) { var ( inUserNS bool @@ -176,35 +212,12 @@ func SpecGenToOCI(s *specgen.SpecGenerator, rt *libpod.Runtime, newImage *image. g.AddProcessEnv(name, val) } - // TODO rlimits and ulimits needs further refinement by someone more - // familiar with the code. - //if err := addRlimits(config, &g); err != nil { - // return nil, err - //} - - // NAMESPACES - - if err := pidConfigureGenerator(s, &g); err != nil { - return nil, err - } - - if err := userConfigureGenerator(s, &g); err != nil { - return nil, err - } - - if err := networkConfigureGenerator(s, &g); err != nil { + if err := addRlimits(s, &g); err != nil { return nil, err } - if err := utsConfigureGenerator(s, &g, rt); err != nil { - return nil, err - } - - if err := ipcConfigureGenerator(s, &g); err != nil { - return nil, err - } - - if err := cgroupConfigureGenerator(s, &g); err != nil { + // NAMESPACES + if err := specConfigureNamespaces(s, &g, rt); err != nil { return nil, err } configSpec := g.Config diff --git a/pkg/specgen/namespaces.go b/pkg/specgen/namespaces.go index 2e7f80fe8..4f35b31bf 100644 --- a/pkg/specgen/namespaces.go +++ b/pkg/specgen/namespaces.go @@ -1,6 +1,8 @@ package specgen import ( + "strings" + "github.com/pkg/errors" ) @@ -39,6 +41,12 @@ type Namespace struct { Value string `json:"string,omitempty"` } +// IsDefault returns whether the namespace is set to the default setting (which +// also includes the empty string). +func (n *Namespace) IsDefault() bool { + return n.NSMode == Default || n.NSMode == "" +} + // IsHost returns a bool if the namespace is host based func (n *Namespace) IsHost() bool { return n.NSMode == Host @@ -69,11 +77,24 @@ func validateNetNS(n *Namespace) error { return nil } switch n.NSMode { - case Host, Path, FromContainer, FromPod, Private, NoNetwork, Bridge, Slirp: + case "", Default, Host, Path, FromContainer, FromPod, Private, NoNetwork, Bridge, Slirp: break default: return errors.Errorf("invalid network %q", n.NSMode) } + + // Path and From Container MUST have a string value set + if n.NSMode == Path || n.NSMode == FromContainer { + if len(n.Value) < 1 { + return errors.Errorf("namespace mode %s requires a value", n.NSMode) + } + } else { + // All others must NOT set a string value + if len(n.Value) > 0 { + return errors.Errorf("namespace value %s cannot be provided with namespace mode %s", n.Value, n.NSMode) + } + } + return nil } @@ -83,6 +104,15 @@ func (n *Namespace) validate() error { if n == nil { return nil } + switch n.NSMode { + case "", Default, Host, Path, FromContainer, FromPod, Private: + // Valid, do nothing + case NoNetwork, Bridge, Slirp: + return errors.Errorf("cannot use network modes with non-network namespace") + default: + return errors.Errorf("invalid namespace type %s specified", n.NSMode) + } + // Path and From Container MUST have a string value set if n.NSMode == Path || n.NSMode == FromContainer { if len(n.Value) < 1 { @@ -96,3 +126,73 @@ func (n *Namespace) validate() error { } return nil } + +// ParseNamespace parses a namespace in string form. +// This is not intended for the network namespace, which has a separate +// function. +func ParseNamespace(ns string) (Namespace, error) { + toReturn := Namespace{} + switch { + case ns == "host": + toReturn.NSMode = Host + case ns == "private": + toReturn.NSMode = Private + case strings.HasPrefix(ns, "ns:"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, errors.Errorf("must provide a path to a namespace when specifying ns:") + } + toReturn.NSMode = Path + toReturn.Value = split[1] + case strings.HasPrefix(ns, "container:"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, errors.Errorf("must provide name or ID or a container when specifying container:") + } + toReturn.NSMode = FromContainer + toReturn.Value = split[1] + default: + return toReturn, errors.Errorf("unrecognized namespace mode %s passed", ns) + } + + return toReturn, nil +} + +// ParseNetworkNamespace parses a network namespace specification in string +// form. +// Returns a namespace and (optionally) a list of CNI networks to join. +func ParseNetworkNamespace(ns string) (Namespace, []string, error) { + toReturn := Namespace{} + var cniNetworks []string + switch { + case ns == "bridge": + toReturn.NSMode = Bridge + case ns == "none": + toReturn.NSMode = NoNetwork + case ns == "host": + toReturn.NSMode = Host + case ns == "private": + toReturn.NSMode = Private + case strings.HasPrefix(ns, "ns:"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, nil, errors.Errorf("must provide a path to a namespace when specifying ns:") + } + toReturn.NSMode = Path + toReturn.Value = split[1] + case strings.HasPrefix(ns, "container:"): + split := strings.SplitN(ns, ":", 2) + if len(split) != 2 { + return toReturn, nil, errors.Errorf("must provide name or ID or a container when specifying container:") + } + toReturn.NSMode = FromContainer + toReturn.Value = split[1] + default: + // Assume we have been given a list of CNI networks. + // Which only works in bridge mode, so set that. + cniNetworks = strings.Split(ns, ",") + toReturn.NSMode = Bridge + } + + return toReturn, cniNetworks, nil +} diff --git a/pkg/specgen/pod_validate.go b/pkg/specgen/pod_validate.go index 9e9659fa9..f2f90e58d 100644 --- a/pkg/specgen/pod_validate.go +++ b/pkg/specgen/pod_validate.go @@ -1,14 +1,16 @@ package specgen import ( - "github.com/containers/libpod/libpod/define" "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/util" "github.com/pkg/errors" ) var ( // ErrInvalidPodSpecConfig describes an error given when the podspecgenerator is invalid ErrInvalidPodSpecConfig error = errors.New("invalid pod spec") + // containerConfig has the default configurations defined in containers.conf + containerConfig = util.DefaultContainerConfig() ) func exclusivePodOptions(opt1, opt2 string) error { @@ -96,10 +98,10 @@ func (p *PodSpecGenerator) Validate() error { } } if len(p.InfraImage) < 1 { - p.InfraImage = define.DefaultInfraImage + p.InfraImage = containerConfig.Engine.InfraImage } if len(p.InfraCommand) < 1 { - p.InfraCommand = []string{define.DefaultInfraCommand} + p.InfraCommand = []string{containerConfig.Engine.InfraCommand} } return nil } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 1a05733f9..e102a3234 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -5,7 +5,6 @@ import ( "syscall" "github.com/containers/image/v5/manifest" - "github.com/containers/libpod/pkg/rootless" "github.com/containers/storage" "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" @@ -283,25 +282,20 @@ type ContainerNetworkConfig struct { // namespace. // Mandatory. NetNS Namespace `json:"netns,omitempty"` - // ConfigureNetNS is whether Libpod will configure the container's - // network namespace to send and receive traffic. - // Only available is NetNS is private - conflicts with other NetNS - // modes. - ConfigureNetNS bool `json:"configure_netns,omitempty"` // StaticIP is the a IPv4 address of the container. - // Only available if ConfigureNetNS is true. + // Only available if NetNS is set to Bridge. // Optional. StaticIP *net.IP `json:"static_ip,omitempty"` // StaticIPv6 is a static IPv6 address to set in the container. - // Only available if ConfigureNetNS is true. + // Only available if NetNS is set to Bridge. // Optional. StaticIPv6 *net.IP `json:"static_ipv6,omitempty"` // StaticMAC is a static MAC address to set in the container. - // Only available if ConfigureNetNS is true. + // Only available if NetNS is set to bridge. // Optional. StaticMAC *net.HardwareAddr `json:"static_mac,omitempty"` // PortBindings is a set of ports to map into the container. - // Only available if ConfigureNetNS is true. + // Only available if NetNS is set to bridge or slirp. // Optional. PortMappings []ocicni.PortMapping `json:"portmappings,omitempty"` // PublishImagePorts will publish ports specified in the image to random @@ -312,31 +306,31 @@ type ContainerNetworkConfig struct { // If this list is empty, the default CNI network will be joined // instead. If at least one entry is present, we will not join the // default network (unless it is part of this list). - // Only available if ConfigureNetNS is true. + // Only available if NetNS is set to bridge. // Optional. CNINetworks []string `json:"cni_networks,omitempty"` // UseImageResolvConf indicates that resolv.conf should not be managed // by Podman, but instead sourced from the image. // Conflicts with DNSServer, DNSSearch, DNSOption. UseImageResolvConf bool `json:"use_image_resolve_conf,omitempty"` - // DNSServer is a set of DNS servers that will be used in the + // DNSServers is a set of DNS servers that will be used in the // container's resolv.conf, replacing the host's DNS Servers which are // used by default. // Conflicts with UseImageResolvConf. // Optional. - DNSServer []net.IP `json:"dns_server,omitempty"` + DNSServers []net.IP `json:"dns_server,omitempty"` // DNSSearch is a set of DNS search domains that will be used in the // container's resolv.conf, replacing the host's DNS search domains // which are used by default. // Conflicts with UseImageResolvConf. // Optional. DNSSearch []string `json:"dns_search,omitempty"` - // DNSOption is a set of DNS options that will be used in the + // DNSOptions is a set of DNS options that will be used in the // container's resolv.conf, replacing the host's DNS options which are // used by default. // Conflicts with UseImageResolvConf. // Optional. - DNSOption []string `json:"dns_option,omitempty"` + DNSOptions []string `json:"dns_option,omitempty"` // UseImageHosts indicates that /etc/hosts should not be managed by // Podman, and instead sourced from the image. // Conflicts with HostAdd. @@ -402,18 +396,9 @@ type Volumes struct { // NewSpecGenerator returns a SpecGenerator struct given one of two mandatory inputs func NewSpecGenerator(image string) *SpecGenerator { - networkConfig := ContainerNetworkConfig{ - NetNS: Namespace{ - NSMode: Bridge, - }, - } csc := ContainerStorageConfig{Image: image} - if rootless.IsRootless() { - networkConfig.NetNS.NSMode = Slirp - } return &SpecGenerator{ ContainerStorageConfig: csc, - ContainerNetworkConfig: networkConfig, } } diff --git a/pkg/sysinfo/README.md b/pkg/sysinfo/README.md deleted file mode 100644 index c1530cef0..000000000 --- a/pkg/sysinfo/README.md +++ /dev/null @@ -1 +0,0 @@ -SysInfo stores information about which features a kernel supports. diff --git a/pkg/sysinfo/numcpu.go b/pkg/sysinfo/numcpu.go deleted file mode 100644 index aeb1a3a80..000000000 --- a/pkg/sysinfo/numcpu.go +++ /dev/null @@ -1,12 +0,0 @@ -// +build !linux,!windows - -package sysinfo - -import ( - "runtime" -) - -// NumCPU returns the number of CPUs -func NumCPU() int { - return runtime.NumCPU() -} diff --git a/pkg/sysinfo/numcpu_linux.go b/pkg/sysinfo/numcpu_linux.go deleted file mode 100644 index f1d2d9db3..000000000 --- a/pkg/sysinfo/numcpu_linux.go +++ /dev/null @@ -1,44 +0,0 @@ -// +build linux - -package sysinfo - -import ( - "runtime" - "unsafe" - - "golang.org/x/sys/unix" -) - -// numCPU queries the system for the count of threads available -// for use to this process. -// -// Issues two syscalls. -// Returns 0 on errors. Use |runtime.NumCPU| in that case. -func numCPU() int { - // Gets the affinity mask for a process: The very one invoking this function. - pid, _, _ := unix.RawSyscall(unix.SYS_GETPID, 0, 0, 0) - - var mask [1024 / 64]uintptr - _, _, err := unix.RawSyscall(unix.SYS_SCHED_GETAFFINITY, pid, uintptr(len(mask)*8), uintptr(unsafe.Pointer(&mask[0]))) - if err != 0 { - return 0 - } - - // For every available thread a bit is set in the mask. - ncpu := 0 - for _, e := range mask { - if e == 0 { - continue - } - ncpu += int(popcnt(uint64(e))) - } - return ncpu -} - -// NumCPU returns the number of CPUs which are currently online -func NumCPU() int { - if ncpu := numCPU(); ncpu > 0 { - return ncpu - } - return runtime.NumCPU() -} diff --git a/pkg/sysinfo/numcpu_windows.go b/pkg/sysinfo/numcpu_windows.go deleted file mode 100644 index 1d89dd550..000000000 --- a/pkg/sysinfo/numcpu_windows.go +++ /dev/null @@ -1,37 +0,0 @@ -// +build windows - -package sysinfo - -import ( - "runtime" - "unsafe" - - "golang.org/x/sys/windows" -) - -var ( - kernel32 = windows.NewLazySystemDLL("kernel32.dll") - getCurrentProcess = kernel32.NewProc("GetCurrentProcess") - getProcessAffinityMask = kernel32.NewProc("GetProcessAffinityMask") -) - -func numCPU() int { - // Gets the affinity mask for a process - var mask, sysmask uintptr - currentProcess, _, _ := getCurrentProcess.Call() - ret, _, _ := getProcessAffinityMask.Call(currentProcess, uintptr(unsafe.Pointer(&mask)), uintptr(unsafe.Pointer(&sysmask))) - if ret == 0 { - return 0 - } - // For every available thread a bit is set in the mask. - ncpu := int(popcnt(uint64(mask))) - return ncpu -} - -// NumCPU returns the number of CPUs which are currently online -func NumCPU() int { - if ncpu := numCPU(); ncpu > 0 { - return ncpu - } - return runtime.NumCPU() -} diff --git a/pkg/sysinfo/sysinfo.go b/pkg/sysinfo/sysinfo.go deleted file mode 100644 index 686f66ce5..000000000 --- a/pkg/sysinfo/sysinfo.go +++ /dev/null @@ -1,153 +0,0 @@ -package sysinfo - -import "github.com/docker/docker/pkg/parsers" - -// SysInfo stores information about which features a kernel supports. -// TODO Windows: Factor out platform specific capabilities. -type SysInfo struct { - // Whether the kernel supports AppArmor or not - AppArmor bool - // Whether the kernel supports Seccomp or not - Seccomp bool - - cgroupMemInfo - cgroupCPUInfo - cgroupBlkioInfo - cgroupCpusetInfo - cgroupPids - - // Whether IPv4 forwarding is supported or not, if this was disabled, networking will not work - IPv4ForwardingDisabled bool - - // Whether bridge-nf-call-iptables is supported or not - BridgeNFCallIPTablesDisabled bool - - // Whether bridge-nf-call-ip6tables is supported or not - BridgeNFCallIP6TablesDisabled bool - - // Whether the cgroup has the mountpoint of "devices" or not - CgroupDevicesEnabled bool -} - -type cgroupMemInfo struct { - // Whether memory limit is supported or not - MemoryLimit bool - - // Whether swap limit is supported or not - SwapLimit bool - - // Whether soft limit is supported or not - MemoryReservation bool - - // Whether OOM killer disable is supported or not - OomKillDisable bool - - // Whether memory swappiness is supported or not - MemorySwappiness bool - - // Whether kernel memory limit is supported or not - KernelMemory bool -} - -type cgroupCPUInfo struct { - // Whether CPU shares is supported or not - CPUShares bool - - // Whether CPU CFS(Completely Fair Scheduler) period is supported or not - CPUCfsPeriod bool - - // Whether CPU CFS(Completely Fair Scheduler) quota is supported or not - CPUCfsQuota bool - - // Whether CPU real-time period is supported or not - CPURealtimePeriod bool - - // Whether CPU real-time runtime is supported or not - CPURealtimeRuntime bool -} - -type cgroupBlkioInfo struct { - // Whether Block IO weight is supported or not - BlkioWeight bool - - // Whether Block IO weight_device is supported or not - BlkioWeightDevice bool - - // Whether Block IO read limit in bytes per second is supported or not - BlkioReadBpsDevice bool - - // Whether Block IO write limit in bytes per second is supported or not - BlkioWriteBpsDevice bool - - // Whether Block IO read limit in IO per second is supported or not - BlkioReadIOpsDevice bool - - // Whether Block IO write limit in IO per second is supported or not - BlkioWriteIOpsDevice bool -} - -type cgroupCpusetInfo struct { - // Whether Cpuset is supported or not - Cpuset bool - - // Available Cpuset's cpus - Cpus string - - // Available Cpuset's memory nodes - Mems string -} - -type cgroupPids struct { - // Whether Pids Limit is supported or not - PidsLimit bool -} - -// IsCpusetCpusAvailable returns `true` if the provided string set is contained -// in cgroup's cpuset.cpus set, `false` otherwise. -// If error is not nil a parsing error occurred. -func (c cgroupCpusetInfo) IsCpusetCpusAvailable(provided string) (bool, error) { - return isCpusetListAvailable(provided, c.Cpus) -} - -// IsCpusetMemsAvailable returns `true` if the provided string set is contained -// in cgroup's cpuset.mems set, `false` otherwise. -// If error is not nil a parsing error occurred. -func (c cgroupCpusetInfo) IsCpusetMemsAvailable(provided string) (bool, error) { - return isCpusetListAvailable(provided, c.Mems) -} - -func isCpusetListAvailable(provided, available string) (bool, error) { - parsedProvided, err := parsers.ParseUintList(provided) - if err != nil { - return false, err - } - parsedAvailable, err := parsers.ParseUintList(available) - if err != nil { - return false, err - } - for k := range parsedProvided { - if !parsedAvailable[k] { - return false, nil - } - } - return true, nil -} - -// Returns bit count of 1, used by NumCPU -func popcnt(x uint64) (n byte) { - x -= (x >> 1) & 0x5555555555555555 - x = (x>>2)&0x3333333333333333 + x&0x3333333333333333 - x += x >> 4 - x &= 0x0f0f0f0f0f0f0f0f - x *= 0x0101010101010101 - return byte(x >> 56) -} - -// GetDefaultPidsLimit returns the default pids limit to run containers with -func GetDefaultPidsLimit() int64 { - sysInfo := New(true) - if !sysInfo.PidsLimit { - return 0 - } - return 4096 -} diff --git a/pkg/sysinfo/sysinfo_linux.go b/pkg/sysinfo/sysinfo_linux.go deleted file mode 100644 index 76bda23c6..000000000 --- a/pkg/sysinfo/sysinfo_linux.go +++ /dev/null @@ -1,261 +0,0 @@ -package sysinfo - -import ( - "fmt" - "io/ioutil" - "os" - "path" - "strings" - - cg "github.com/containers/libpod/pkg/cgroups" - "github.com/opencontainers/runc/libcontainer/cgroups" - "github.com/sirupsen/logrus" - "golang.org/x/sys/unix" -) - -func findCgroupMountpoints() (map[string]string, error) { - cgMounts, err := cgroups.GetCgroupMounts(false) - if err != nil { - return nil, fmt.Errorf("failed to parse cgroup information: %v", err) - } - mps := make(map[string]string) - for _, m := range cgMounts { - for _, ss := range m.Subsystems { - mps[ss] = m.Mountpoint - } - } - return mps, nil -} - -// New returns a new SysInfo, using the filesystem to detect which features -// the kernel supports. If `quiet` is `false` warnings are printed in logs -// whenever an error occurs or misconfigurations are present. -func New(quiet bool) *SysInfo { - sysInfo := &SysInfo{} - cgMounts, err := findCgroupMountpoints() - if err != nil { - logrus.Warnf("Failed to parse cgroup information: %v", err) - } else { - sysInfo.cgroupMemInfo = checkCgroupMem(cgMounts, quiet) - sysInfo.cgroupCPUInfo = checkCgroupCPU(cgMounts, quiet) - sysInfo.cgroupBlkioInfo = checkCgroupBlkioInfo(cgMounts, quiet) - sysInfo.cgroupCpusetInfo = checkCgroupCpusetInfo(cgMounts, quiet) - sysInfo.cgroupPids = checkCgroupPids(quiet) - } - - _, ok := cgMounts["devices"] - sysInfo.CgroupDevicesEnabled = ok - - sysInfo.IPv4ForwardingDisabled = !readProcBool("/proc/sys/net/ipv4/ip_forward") - sysInfo.BridgeNFCallIPTablesDisabled = !readProcBool("/proc/sys/net/bridge/bridge-nf-call-iptables") - sysInfo.BridgeNFCallIP6TablesDisabled = !readProcBool("/proc/sys/net/bridge/bridge-nf-call-ip6tables") - - // Check if AppArmor is supported. - if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) { - sysInfo.AppArmor = true - } - - // Check if Seccomp is supported, via CONFIG_SECCOMP. - if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL { - // Make sure the kernel has CONFIG_SECCOMP_FILTER. - if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL { - sysInfo.Seccomp = true - } - } - - return sysInfo -} - -// checkCgroupMem reads the memory information from the memory cgroup mount point. -func checkCgroupMem(cgMounts map[string]string, quiet bool) cgroupMemInfo { - mountPoint, ok := cgMounts["memory"] - if !ok { - if !quiet { - logrus.Warn("Your kernel does not support cgroup memory limit") - } - return cgroupMemInfo{} - } - - swapLimit := cgroupEnabled(mountPoint, "memory.memsw.limit_in_bytes") - if !quiet && !swapLimit { - logrus.Warn("Your kernel does not support swap memory limit") - } - memoryReservation := cgroupEnabled(mountPoint, "memory.soft_limit_in_bytes") - if !quiet && !memoryReservation { - logrus.Warn("Your kernel does not support memory reservation") - } - oomKillDisable := cgroupEnabled(mountPoint, "memory.oom_control") - if !quiet && !oomKillDisable { - logrus.Warn("Your kernel does not support oom control") - } - memorySwappiness := cgroupEnabled(mountPoint, "memory.swappiness") - if !quiet && !memorySwappiness { - logrus.Warn("Your kernel does not support memory swappiness") - } - kernelMemory := cgroupEnabled(mountPoint, "memory.kmem.limit_in_bytes") - if !quiet && !kernelMemory { - logrus.Warn("Your kernel does not support kernel memory limit") - } - - return cgroupMemInfo{ - MemoryLimit: true, - SwapLimit: swapLimit, - MemoryReservation: memoryReservation, - OomKillDisable: oomKillDisable, - MemorySwappiness: memorySwappiness, - KernelMemory: kernelMemory, - } -} - -// checkCgroupCPU reads the cpu information from the cpu cgroup mount point. -func checkCgroupCPU(cgMounts map[string]string, quiet bool) cgroupCPUInfo { - mountPoint, ok := cgMounts["cpu"] - if !ok { - if !quiet { - logrus.Warn("Unable to find cpu cgroup in mounts") - } - return cgroupCPUInfo{} - } - - cpuShares := cgroupEnabled(mountPoint, "cpu.shares") - if !quiet && !cpuShares { - logrus.Warn("Your kernel does not support cgroup cpu shares") - } - - cpuCfsPeriod := cgroupEnabled(mountPoint, "cpu.cfs_period_us") - if !quiet && !cpuCfsPeriod { - logrus.Warn("Your kernel does not support cgroup cfs period") - } - - cpuCfsQuota := cgroupEnabled(mountPoint, "cpu.cfs_quota_us") - if !quiet && !cpuCfsQuota { - logrus.Warn("Your kernel does not support cgroup cfs quotas") - } - - cpuRealtimePeriod := cgroupEnabled(mountPoint, "cpu.rt_period_us") - if !quiet && !cpuRealtimePeriod { - logrus.Warn("Your kernel does not support cgroup rt period") - } - - cpuRealtimeRuntime := cgroupEnabled(mountPoint, "cpu.rt_runtime_us") - if !quiet && !cpuRealtimeRuntime { - logrus.Warn("Your kernel does not support cgroup rt runtime") - } - - return cgroupCPUInfo{ - CPUShares: cpuShares, - CPUCfsPeriod: cpuCfsPeriod, - CPUCfsQuota: cpuCfsQuota, - CPURealtimePeriod: cpuRealtimePeriod, - CPURealtimeRuntime: cpuRealtimeRuntime, - } -} - -// checkCgroupBlkioInfo reads the blkio information from the blkio cgroup mount point. -func checkCgroupBlkioInfo(cgMounts map[string]string, quiet bool) cgroupBlkioInfo { - mountPoint, ok := cgMounts["blkio"] - if !ok { - if !quiet { - logrus.Warn("Unable to find blkio cgroup in mounts") - } - return cgroupBlkioInfo{} - } - - weight := cgroupEnabled(mountPoint, "blkio.weight") - if !quiet && !weight { - logrus.Warn("Your kernel does not support cgroup blkio weight") - } - - weightDevice := cgroupEnabled(mountPoint, "blkio.weight_device") - if !quiet && !weightDevice { - logrus.Warn("Your kernel does not support cgroup blkio weight_device") - } - - readBpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.read_bps_device") - if !quiet && !readBpsDevice { - logrus.Warn("Your kernel does not support cgroup blkio throttle.read_bps_device") - } - - writeBpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.write_bps_device") - if !quiet && !writeBpsDevice { - logrus.Warn("Your kernel does not support cgroup blkio throttle.write_bps_device") - } - readIOpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.read_iops_device") - if !quiet && !readIOpsDevice { - logrus.Warn("Your kernel does not support cgroup blkio throttle.read_iops_device") - } - - writeIOpsDevice := cgroupEnabled(mountPoint, "blkio.throttle.write_iops_device") - if !quiet && !writeIOpsDevice { - logrus.Warn("Your kernel does not support cgroup blkio throttle.write_iops_device") - } - return cgroupBlkioInfo{ - BlkioWeight: weight, - BlkioWeightDevice: weightDevice, - BlkioReadBpsDevice: readBpsDevice, - BlkioWriteBpsDevice: writeBpsDevice, - BlkioReadIOpsDevice: readIOpsDevice, - BlkioWriteIOpsDevice: writeIOpsDevice, - } -} - -// checkCgroupCpusetInfo reads the cpuset information from the cpuset cgroup mount point. -func checkCgroupCpusetInfo(cgMounts map[string]string, quiet bool) cgroupCpusetInfo { - mountPoint, ok := cgMounts["cpuset"] - if !ok { - if !quiet { - logrus.Warn("Unable to find cpuset cgroup in mounts") - } - return cgroupCpusetInfo{} - } - - cpus, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.cpus")) - if err != nil { - return cgroupCpusetInfo{} - } - - mems, err := ioutil.ReadFile(path.Join(mountPoint, "cpuset.mems")) - if err != nil { - return cgroupCpusetInfo{} - } - - return cgroupCpusetInfo{ - Cpuset: true, - Cpus: strings.TrimSpace(string(cpus)), - Mems: strings.TrimSpace(string(mems)), - } -} - -// checkCgroupPids reads the pids information from the pids cgroup mount point. -func checkCgroupPids(quiet bool) cgroupPids { - cgroup2, err := cg.IsCgroup2UnifiedMode() - if err != nil { - logrus.Errorf("Failed to check cgroups version: %v", err) - } - if !cgroup2 { - _, err := cgroups.FindCgroupMountpoint("", "pids") - if err != nil { - if !quiet { - logrus.Warn(err) - } - return cgroupPids{} - } - } - - return cgroupPids{ - PidsLimit: true, - } -} - -func cgroupEnabled(mountPoint, name string) bool { - _, err := os.Stat(path.Join(mountPoint, name)) - return err == nil -} - -func readProcBool(path string) bool { - val, err := ioutil.ReadFile(path) - if err != nil { - return false - } - return strings.TrimSpace(string(val)) == "1" -} diff --git a/pkg/sysinfo/sysinfo_linux_test.go b/pkg/sysinfo/sysinfo_linux_test.go deleted file mode 100644 index 860784f2a..000000000 --- a/pkg/sysinfo/sysinfo_linux_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package sysinfo - -import ( - "io/ioutil" - "os" - "path" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - "golang.org/x/sys/unix" -) - -func TestReadProcBool(t *testing.T) { - tmpDir, err := ioutil.TempDir("", "test-sysinfo-proc") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - procFile := filepath.Join(tmpDir, "read-proc-bool") - err = ioutil.WriteFile(procFile, []byte("1"), 0644) - require.NoError(t, err) - - if !readProcBool(procFile) { - t.Fatal("expected proc bool to be true, got false") - } - - if err := ioutil.WriteFile(procFile, []byte("0"), 0644); err != nil { - t.Fatal(err) - } - if readProcBool(procFile) { - t.Fatal("expected proc bool to be false, got true") - } - - if readProcBool(path.Join(tmpDir, "no-exist")) { - t.Fatal("should be false for non-existent entry") - } - -} - -func TestCgroupEnabled(t *testing.T) { - cgroupDir, err := ioutil.TempDir("", "cgroup-test") - require.NoError(t, err) - defer os.RemoveAll(cgroupDir) - - if cgroupEnabled(cgroupDir, "test") { - t.Fatal("cgroupEnabled should be false") - } - - err = ioutil.WriteFile(path.Join(cgroupDir, "test"), []byte{}, 0644) - require.NoError(t, err) - - if !cgroupEnabled(cgroupDir, "test") { - t.Fatal("cgroupEnabled should be true") - } -} - -func TestNew(t *testing.T) { - sysInfo := New(false) - require.NotNil(t, sysInfo) - checkSysInfo(t, sysInfo) - - sysInfo = New(true) - require.NotNil(t, sysInfo) - checkSysInfo(t, sysInfo) -} - -func checkSysInfo(t *testing.T, sysInfo *SysInfo) { - // Check if Seccomp is supported, via CONFIG_SECCOMP.then sysInfo.Seccomp must be TRUE , else FALSE - if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL { - // Make sure the kernel has CONFIG_SECCOMP_FILTER. - if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL { - require.True(t, sysInfo.Seccomp) - } - } else { - require.False(t, sysInfo.Seccomp) - } -} - -func TestNewAppArmorEnabled(t *testing.T) { - // Check if AppArmor is supported. then it must be TRUE , else FALSE - if _, err := os.Stat("/sys/kernel/security/apparmor"); err != nil { - t.Skip("App Armor Must be Enabled") - } - - sysInfo := New(true) - require.True(t, sysInfo.AppArmor) -} - -func TestNewAppArmorDisabled(t *testing.T) { - // Check if AppArmor is supported. then it must be TRUE , else FALSE - if _, err := os.Stat("/sys/kernel/security/apparmor"); !os.IsNotExist(err) { - t.Skip("App Armor Must be Disabled") - } - - sysInfo := New(true) - require.False(t, sysInfo.AppArmor) -} - -func TestNumCPU(t *testing.T) { - cpuNumbers := NumCPU() - if cpuNumbers <= 0 { - t.Fatal("CPU returned must be greater than zero") - } -} diff --git a/pkg/sysinfo/sysinfo_solaris.go b/pkg/sysinfo/sysinfo_solaris.go deleted file mode 100644 index 7463cdd8f..000000000 --- a/pkg/sysinfo/sysinfo_solaris.go +++ /dev/null @@ -1,122 +0,0 @@ -// +build solaris,cgo - -package sysinfo - -import ( - "bytes" - "os/exec" - "strconv" - "strings" -) - -/* -#cgo LDFLAGS: -llgrp -#cgo CFLAGS: -Wall -Werror -#include <unistd.h> -#include <stdlib.h> -#include <sys/lgrp_user.h> -int getLgrpCount() { - lgrp_cookie_t lgrpcookie = LGRP_COOKIE_NONE; - uint_t nlgrps; - - if ((lgrpcookie = lgrp_init(LGRP_VIEW_OS)) == LGRP_COOKIE_NONE) { - return -1; - } - nlgrps = lgrp_nlgrps(lgrpcookie); - return nlgrps; -} -*/ -import "C" - -// IsCPUSharesAvailable returns whether CPUShares setting is supported. -// We need FSS to be set as default scheduling class to support CPU Shares -func IsCPUSharesAvailable() bool { - cmd := exec.Command("/usr/sbin/dispadmin", "-d") - outBuf := new(bytes.Buffer) - errBuf := new(bytes.Buffer) - cmd.Stderr = errBuf - cmd.Stdout = outBuf - - if err := cmd.Run(); err != nil { - return false - } - return (strings.Contains(outBuf.String(), "FSS")) -} - -// New returns a new SysInfo, using the filesystem to detect which features -// the kernel supports. -//NOTE Solaris: If we change the below capabilities be sure -// to update verifyPlatformContainerSettings() in daemon_solaris.go -func New(quiet bool) *SysInfo { - sysInfo := &SysInfo{} - sysInfo.cgroupMemInfo = setCgroupMem(quiet) - sysInfo.cgroupCPUInfo = setCgroupCPU(quiet) - sysInfo.cgroupBlkioInfo = setCgroupBlkioInfo(quiet) - sysInfo.cgroupCpusetInfo = setCgroupCPUsetInfo(quiet) - - sysInfo.IPv4ForwardingDisabled = false - - sysInfo.AppArmor = false - - return sysInfo -} - -// setCgroupMem reads the memory information for Solaris. -func setCgroupMem(quiet bool) cgroupMemInfo { - - return cgroupMemInfo{ - MemoryLimit: true, - SwapLimit: true, - MemoryReservation: false, - OomKillDisable: false, - MemorySwappiness: false, - KernelMemory: false, - } -} - -// setCgroupCPU reads the cpu information for Solaris. -func setCgroupCPU(quiet bool) cgroupCPUInfo { - - return cgroupCPUInfo{ - CPUShares: true, - CPUCfsPeriod: false, - CPUCfsQuota: true, - CPURealtimePeriod: false, - CPURealtimeRuntime: false, - } -} - -// blkio switches are not supported in Solaris. -func setCgroupBlkioInfo(quiet bool) cgroupBlkioInfo { - - return cgroupBlkioInfo{ - BlkioWeight: false, - BlkioWeightDevice: false, - } -} - -// setCgroupCPUsetInfo reads the cpuset information for Solaris. -func setCgroupCPUsetInfo(quiet bool) cgroupCpusetInfo { - - return cgroupCpusetInfo{ - Cpuset: true, - Cpus: getCPUCount(), - Mems: getLgrpCount(), - } -} - -func getCPUCount() string { - ncpus := C.sysconf(C._SC_NPROCESSORS_ONLN) - if ncpus <= 0 { - return "" - } - return strconv.FormatInt(int64(ncpus), 16) -} - -func getLgrpCount() string { - nlgrps := C.getLgrpCount() - if nlgrps <= 0 { - return "" - } - return strconv.FormatInt(int64(nlgrps), 16) -} diff --git a/pkg/sysinfo/sysinfo_test.go b/pkg/sysinfo/sysinfo_test.go deleted file mode 100644 index 895828f26..000000000 --- a/pkg/sysinfo/sysinfo_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package sysinfo - -import "testing" - -func TestIsCpusetListAvailable(t *testing.T) { - cases := []struct { - provided string - available string - res bool - err bool - }{ - {"1", "0-4", true, false}, - {"01,3", "0-4", true, false}, - {"", "0-7", true, false}, - {"1--42", "0-7", false, true}, - {"1-42", "00-1,8,,9", false, true}, - {"1,41-42", "43,45", false, false}, - {"0-3", "", false, false}, - } - for _, c := range cases { - r, err := isCpusetListAvailable(c.provided, c.available) - if (c.err && err == nil) && r != c.res { - t.Fatalf("Expected pair: %v, %v for %s, %s. Got %v, %v instead", c.res, c.err, c.provided, c.available, c.err && err == nil, r) - } - } -} diff --git a/pkg/sysinfo/sysinfo_unix.go b/pkg/sysinfo/sysinfo_unix.go deleted file mode 100644 index 45f3ef1c6..000000000 --- a/pkg/sysinfo/sysinfo_unix.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build !linux,!solaris,!windows - -package sysinfo - -// New returns an empty SysInfo for non linux nor solaris for now. -func New(quiet bool) *SysInfo { - sysInfo := &SysInfo{} - return sysInfo -} diff --git a/pkg/sysinfo/sysinfo_windows.go b/pkg/sysinfo/sysinfo_windows.go deleted file mode 100644 index 4e6255bc5..000000000 --- a/pkg/sysinfo/sysinfo_windows.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build windows - -package sysinfo - -// New returns an empty SysInfo for windows for now. -func New(quiet bool) *SysInfo { - sysInfo := &SysInfo{} - return sysInfo -} diff --git a/pkg/util/utils.go b/pkg/util/utils.go index babf7dfc9..55e775d7a 100644 --- a/pkg/util/utils.go +++ b/pkg/util/utils.go @@ -13,6 +13,7 @@ import ( "time" "github.com/BurntSushi/toml" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/types" "github.com/containers/libpod/pkg/errorhandling" "github.com/containers/libpod/pkg/namespaces" @@ -27,6 +28,17 @@ import ( "golang.org/x/crypto/ssh/terminal" ) +var containerConfig *config.Config + +func init() { + var err error + containerConfig, err = config.Default() + if err != nil { + logrus.Error(err) + os.Exit(1) + } +} + // Helper function to determine the username/password passed // in the creds string. It could be either or both. func parseCreds(creds string) (string, string) { @@ -669,3 +681,7 @@ func swapSELinuxLabel(cLabel, processLabel string) (string, error) { dcon["type"] = scon["type"] return dcon.Get(), nil } + +func DefaultContainerConfig() *config.Config { + return containerConfig +} diff --git a/pkg/varlinkapi/create.go b/pkg/varlinkapi/create.go index 63d5072c6..571ce6115 100644 --- a/pkg/varlinkapi/create.go +++ b/pkg/varlinkapi/create.go @@ -13,6 +13,7 @@ import ( "syscall" "time" + "github.com/containers/common/pkg/sysinfo" "github.com/containers/image/v5/manifest" "github.com/containers/libpod/cmd/podman/parse" "github.com/containers/libpod/libpod" @@ -28,7 +29,6 @@ import ( "github.com/containers/libpod/pkg/rootless" "github.com/containers/libpod/pkg/seccomp" cc "github.com/containers/libpod/pkg/spec" - "github.com/containers/libpod/pkg/sysinfo" systemdGen "github.com/containers/libpod/pkg/systemd/generate" "github.com/containers/libpod/pkg/util" "github.com/docker/go-connections/nat" diff --git a/pkg/varlinkapi/intermediate_varlink.go b/pkg/varlinkapi/intermediate_varlink.go index 21c57d4f4..bd0c45b33 100644 --- a/pkg/varlinkapi/intermediate_varlink.go +++ b/pkg/varlinkapi/intermediate_varlink.go @@ -331,7 +331,7 @@ func intFromVarlink(v *int64, flagName string, defaultValue *int) CRInt { // structure. func VarlinkCreateToGeneric(opts iopodman.Create) GenericCLIResults { // FIXME this will need to be fixed!!!!! With containers conf - //defaultContainerConfig := cliconfig.GetDefaultConfig() + //containerConfig := cliconfig.GetDefaultConfig() // TODO | WARN // We do not get a default network over varlink. Unlike the other default values for some cli // elements, it seems it gets set to the default anyway. |