diff options
Diffstat (limited to 'libpod')
-rw-r--r-- | libpod/container.go | 12 | ||||
-rw-r--r-- | libpod/container_api.go | 11 | ||||
-rw-r--r-- | libpod/container_internal.go | 228 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 277 | ||||
-rw-r--r-- | libpod/image/errors.go | 15 | ||||
-rw-r--r-- | libpod/image/image.go | 5 | ||||
-rw-r--r-- | libpod/kube.go | 270 | ||||
-rw-r--r-- | libpod/mounts_linux.go | 18 | ||||
-rw-r--r-- | libpod/oci.go | 15 | ||||
-rw-r--r-- | libpod/options.go | 11 | ||||
-rw-r--r-- | libpod/pod.go | 4 | ||||
-rw-r--r-- | libpod/pod_easyjson.go | 128 | ||||
-rw-r--r-- | libpod/runtime_pod_infra_linux.go | 4 |
13 files changed, 753 insertions, 245 deletions
diff --git a/libpod/container.go b/libpod/container.go index 7bb5b2687..16f61d021 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -996,3 +996,15 @@ func (c *Container) IsInfra() bool { func (c *Container) IsReadOnly() bool { return c.config.Spec.Root.Readonly } + +// NetworkDisabled returns whether the container is running with a disabled network +func (c *Container) NetworkDisabled() bool { + if !c.config.PostConfigureNetNS { + for _, ns := range c.config.Spec.Linux.Namespaces { + if ns.Type == spec.NetworkNamespace { + return ns.Path == "" + } + } + } + return false +} diff --git a/libpod/container_api.go b/libpod/container_api.go index 390987394..df6b6e962 100644 --- a/libpod/container_api.go +++ b/libpod/container_api.go @@ -830,8 +830,15 @@ func (c *Container) Refresh(ctx context.Context) error { return nil } +// ContainerCheckpointOptions is a struct used to pass the parameters +// for checkpointing to corresponding functions +type ContainerCheckpointOptions struct { + Keep bool + KeepRunning bool +} + // Checkpoint checkpoints a container -func (c *Container) Checkpoint(ctx context.Context, keep bool) error { +func (c *Container) Checkpoint(ctx context.Context, options ContainerCheckpointOptions) error { logrus.Debugf("Trying to checkpoint container %s", c) if !c.batched { c.lock.Lock() @@ -842,7 +849,7 @@ func (c *Container) Checkpoint(ctx context.Context, keep bool) error { } } - return c.checkpoint(ctx, keep) + return c.checkpoint(ctx, options) } // Restore restores a container diff --git a/libpod/container_internal.go b/libpod/container_internal.go index d2f48d661..051e0aeb7 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -19,14 +19,11 @@ import ( "github.com/containers/libpod/pkg/hooks" "github.com/containers/libpod/pkg/hooks/exec" "github.com/containers/libpod/pkg/lookup" - "github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/rootless" - "github.com/containers/libpod/pkg/secrets" "github.com/containers/storage" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chrootarchive" "github.com/containers/storage/pkg/mount" - "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" @@ -583,7 +580,7 @@ func (c *Container) checkDependenciesRunningLocked(depCtrs map[string]*Container } func (c *Container) completeNetworkSetup() error { - if !c.config.PostConfigureNetNS { + if !c.config.PostConfigureNetNS || c.NetworkDisabled() { return nil } if err := c.syncContainer(); err != nil { @@ -597,10 +594,6 @@ func (c *Container) completeNetworkSetup() error { // Initialize a container, creating it in the runtime func (c *Container) init(ctx context.Context) error { - if err := c.makeBindMounts(); err != nil { - return err - } - // Generate the OCI spec spec, err := c.generateSpec(ctx) if err != nil { @@ -987,86 +980,6 @@ func (c *Container) postDeleteHooks(ctx context.Context) (err error) { return nil } -// Make standard bind mounts to include in the container -func (c *Container) makeBindMounts() error { - if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil { - return errors.Wrapf(err, "cannot chown run directory %s", c.state.RunDir) - } - - if c.state.BindMounts == nil { - c.state.BindMounts = make(map[string]string) - } - - // SHM is always added when we mount the container - c.state.BindMounts["/dev/shm"] = c.config.ShmDir - - // Make /etc/resolv.conf - if _, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { - // If it already exists, delete so we can recreate - delete(c.state.BindMounts, "/etc/resolv.conf") - } - newResolv, err := c.generateResolvConf() - if err != nil { - return errors.Wrapf(err, "error creating resolv.conf for container %s", c.ID()) - } - c.state.BindMounts["/etc/resolv.conf"] = newResolv - - newPasswd, err := c.generatePasswd() - if err != nil { - return errors.Wrapf(err, "error creating temporary passwd file for container %s", c.ID()) - } - if newPasswd != "" { - // Make /etc/passwd - if _, ok := c.state.BindMounts["/etc/passwd"]; ok { - // If it already exists, delete so we can recreate - delete(c.state.BindMounts, "/etc/passwd") - } - logrus.Debugf("adding entry to /etc/passwd for non existent default user") - c.state.BindMounts["/etc/passwd"] = newPasswd - } - // Make /etc/hosts - if _, ok := c.state.BindMounts["/etc/hosts"]; ok { - // If it already exists, delete so we can recreate - delete(c.state.BindMounts, "/etc/hosts") - } - newHosts, err := c.generateHosts() - if err != nil { - return errors.Wrapf(err, "error creating hosts file for container %s", c.ID()) - } - c.state.BindMounts["/etc/hosts"] = newHosts - - // Make /etc/hostname - // This should never change, so no need to recreate if it exists - if _, ok := c.state.BindMounts["/etc/hostname"]; !ok { - hostnamePath, err := c.writeStringToRundir("hostname", c.Hostname()) - if err != nil { - return errors.Wrapf(err, "error creating hostname file for container %s", c.ID()) - } - c.state.BindMounts["/etc/hostname"] = hostnamePath - } - - // Make .containerenv - // Empty file, so no need to recreate if it exists - if _, ok := c.state.BindMounts["/run/.containerenv"]; !ok { - // Empty string for now, but we may consider populating this later - containerenvPath, err := c.writeStringToRundir(".containerenv", "") - if err != nil { - return errors.Wrapf(err, "error creating containerenv file for container %s", c.ID()) - } - c.state.BindMounts["/run/.containerenv"] = containerenvPath - } - - // Add Secret Mounts - secretMounts := secrets.SecretMountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.DefaultMountsFile, c.state.DestinationRunDir, c.RootUID(), c.RootGID()) - for _, mount := range secretMounts { - if _, ok := c.state.BindMounts[mount.Destination]; !ok { - c.state.BindMounts[mount.Destination] = mount.Source - } - } - - return nil -} - // writeStringToRundir copies the provided file to the runtimedir func (c *Container) writeStringToRundir(destFile, output string) (string, error) { destFileName := filepath.Join(c.state.RunDir, destFile) @@ -1095,145 +1008,6 @@ func (c *Container) writeStringToRundir(destFile, output string) (string, error) return filepath.Join(c.state.DestinationRunDir, destFile), nil } -// generatePasswd generates a container specific passwd file, -// iff g.config.User is a number -func (c *Container) generatePasswd() (string, error) { - var ( - groupspec string - group *user.Group - gid int - ) - if c.config.User == "" { - return "", nil - } - spec := strings.SplitN(c.config.User, ":", 2) - userspec := spec[0] - if len(spec) > 1 { - groupspec = spec[1] - } - // If a non numeric User, then don't generate passwd - uid, err := strconv.ParseUint(userspec, 10, 32) - if err != nil { - return "", nil - } - // Lookup the user to see if it exists in the container image - _, err = lookup.GetUser(c.state.Mountpoint, userspec) - if err != nil && err != user.ErrNoPasswdEntries { - return "", err - } - if err == nil { - return "", nil - } - if groupspec != "" { - if !c.state.Mounted { - return "", errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate group field for passwd record", c.ID()) - } - group, err = lookup.GetGroup(c.state.Mountpoint, groupspec) - if err != nil { - if err == user.ErrNoGroupEntries { - return "", errors.Wrapf(err, "unable to get gid %s from group file", groupspec) - } - return "", err - } - gid = group.Gid - } - originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd") - orig, err := ioutil.ReadFile(originPasswdFile) - if err != nil && !os.IsNotExist(err) { - return "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile) - } - - pwd := fmt.Sprintf("%s%d:x:%d:%d:container user:%s:/bin/sh\n", orig, uid, uid, gid, c.WorkingDir()) - passwdFile, err := c.writeStringToRundir("passwd", pwd) - if err != nil { - return "", errors.Wrapf(err, "failed to create temporary passwd file") - } - if os.Chmod(passwdFile, 0644); err != nil { - return "", err - } - return passwdFile, nil -} - -// generateResolvConf generates a containers resolv.conf -func (c *Container) generateResolvConf() (string, error) { - // Determine the endpoint for resolv.conf in case it is a symlink - resolvPath, err := filepath.EvalSymlinks("/etc/resolv.conf") - if err != nil { - return "", err - } - - contents, err := ioutil.ReadFile(resolvPath) - if err != nil { - return "", errors.Wrapf(err, "unable to read %s", resolvPath) - } - - // Process the file to remove localhost nameservers - // TODO: set ipv6 enable bool more sanely - resolv, err := resolvconf.FilterResolvDNS(contents, true) - if err != nil { - return "", errors.Wrapf(err, "error parsing host resolv.conf") - } - - // Make a new resolv.conf - nameservers := resolvconf.GetNameservers(resolv.Content) - if len(c.config.DNSServer) > 0 { - // We store DNS servers as net.IP, so need to convert to string - nameservers = []string{} - for _, server := range c.config.DNSServer { - nameservers = append(nameservers, server.String()) - } - } - - search := resolvconf.GetSearchDomains(resolv.Content) - if len(c.config.DNSSearch) > 0 { - search = c.config.DNSSearch - } - - options := resolvconf.GetOptions(resolv.Content) - if len(c.config.DNSOption) > 0 { - options = c.config.DNSOption - } - - destPath := filepath.Join(c.state.RunDir, "resolv.conf") - - if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { - return "", errors.Wrapf(err, "error removing resolv.conf for container %s", c.ID()) - } - - // Build resolv.conf - if _, err = resolvconf.Build(destPath, nameservers, search, options); err != nil { - return "", errors.Wrapf(err, "error building resolv.conf for container %s") - } - - // Relabel resolv.conf for the container - if err := label.Relabel(destPath, c.config.MountLabel, false); err != nil { - return "", err - } - - return filepath.Join(c.state.DestinationRunDir, "resolv.conf"), nil -} - -// generateHosts creates a containers hosts file -func (c *Container) generateHosts() (string, error) { - orig, err := ioutil.ReadFile("/etc/hosts") - if err != nil { - return "", errors.Wrapf(err, "unable to read /etc/hosts") - } - hosts := string(orig) - if len(c.config.HostAdd) > 0 { - for _, host := range c.config.HostAdd { - // the host format has already been verified at this point - fields := strings.SplitN(host, ":", 2) - hosts += fmt.Sprintf("%s %s\n", fields[1], fields[0]) - } - } - if len(c.state.NetworkStatus) > 0 && len(c.state.NetworkStatus[0].IPs) > 0 { - ipAddress := strings.Split(c.state.NetworkStatus[0].IPs[0].Address.String(), "/")[0] - hosts += fmt.Sprintf("%s\t%s\n", ipAddress, c.Hostname()) - } - return c.writeStringToRundir("hosts", hosts) -} - func (c *Container) addLocalVolumes(ctx context.Context, g *generate.Generator) error { var uid, gid int mountPoint := c.state.Mountpoint diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index 66c7e8a04..ffb82cc94 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -11,6 +11,7 @@ import ( "os" "path" "path/filepath" + "strconv" "strings" "sync" "syscall" @@ -21,8 +22,11 @@ import ( crioAnnotations "github.com/containers/libpod/pkg/annotations" "github.com/containers/libpod/pkg/criu" "github.com/containers/libpod/pkg/lookup" + "github.com/containers/libpod/pkg/resolvconf" "github.com/containers/libpod/pkg/rootless" + "github.com/containers/libpod/pkg/secrets" "github.com/containers/storage/pkg/idtools" + "github.com/opencontainers/runc/libcontainer/user" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" "github.com/opencontainers/selinux/go-selinux/label" @@ -132,6 +136,9 @@ func (c *Container) prepare() (err error) { // cleanupNetwork unmounts and cleans up the container's network func (c *Container) cleanupNetwork() error { + if c.NetworkDisabled() { + return nil + } if c.state.NetNS == nil { logrus.Debugf("Network is already cleaned up, skipping...") return nil @@ -169,6 +176,11 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { g.AddOrReplaceLinuxNamespace(spec.NetworkNamespace, c.state.NetNS.Path()) } } + + if err := c.makeBindMounts(); err != nil { + return nil, err + } + // Check if the spec file mounts contain the label Relabel flags z or Z. // If they do, relabel the source directory and then remove the option. for _, m := range g.Mounts() { @@ -347,8 +359,34 @@ func (c *Container) generateSpec(ctx context.Context) (*spec.Spec, error) { // Mounts need to be sorted so paths will not cover other paths mounts := sortMounts(g.Mounts()) g.ClearMounts() + + // Determine property of RootPropagation based on volume properties. If + // a volume is shared, then keep root propagation shared. This should + // work for slave and private volumes too. + // + // For slave volumes, it can be either [r]shared/[r]slave. + // + // For private volumes any root propagation value should work. + rootPropagation := "" for _, m := range mounts { g.AddMount(m) + for _, opt := range m.Options { + switch opt { + case MountShared, MountRShared: + if rootPropagation != MountShared && rootPropagation != MountRShared { + rootPropagation = MountShared + } + case MountSlave, MountRSlave: + if rootPropagation != MountShared && rootPropagation != MountRShared && rootPropagation != MountSlave && rootPropagation != MountRSlave { + rootPropagation = MountRSlave + } + } + } + } + + if rootPropagation != "" { + logrus.Debugf("set root propagation to %q", rootPropagation) + g.SetLinuxRootPropagation(rootPropagation) } return g.Config, nil } @@ -431,7 +469,7 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr return nil } -func (c *Container) checkpoint(ctx context.Context, keep bool) (err error) { +func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointOptions) (err error) { if !criu.CheckForCriu() { return errors.Errorf("checkpointing a container requires at least CRIU %d", criu.MinCriuVersion) @@ -440,7 +478,7 @@ func (c *Container) checkpoint(ctx context.Context, keep bool) (err error) { if c.state.State != ContainerStateRunning { return errors.Wrapf(ErrCtrStateInvalid, "%q is not running, cannot checkpoint", c.state.State) } - if err := c.runtime.ociRuntime.checkpointContainer(c); err != nil { + if err := c.runtime.ociRuntime.checkpointContainer(c, options); err != nil { return err } @@ -457,14 +495,16 @@ func (c *Container) checkpoint(ctx context.Context, keep bool) (err error) { logrus.Debugf("Checkpointed container %s", c.ID()) - c.state.State = ContainerStateStopped + if !options.KeepRunning { + c.state.State = ContainerStateStopped - // Cleanup Storage and Network - if err := c.cleanup(ctx); err != nil { - return err + // Cleanup Storage and Network + if err := c.cleanup(ctx); err != nil { + return err + } } - if !keep { + if !options.Keep { // Remove log file os.Remove(filepath.Join(c.bundlePath(), "dump.log")) // Remove statistic file @@ -591,3 +631,226 @@ func (c *Container) restore(ctx context.Context, keep bool) (err error) { return c.save() } + +// Make standard bind mounts to include in the container +func (c *Container) makeBindMounts() error { + if err := os.Chown(c.state.RunDir, c.RootUID(), c.RootGID()); err != nil { + return errors.Wrapf(err, "cannot chown run directory %s", c.state.RunDir) + } + + if c.state.BindMounts == nil { + c.state.BindMounts = make(map[string]string) + } + + if !c.NetworkDisabled() { + // Make /etc/resolv.conf + if _, ok := c.state.BindMounts["/etc/resolv.conf"]; ok { + // If it already exists, delete so we can recreate + delete(c.state.BindMounts, "/etc/resolv.conf") + } + newResolv, err := c.generateResolvConf() + if err != nil { + return errors.Wrapf(err, "error creating resolv.conf for container %s", c.ID()) + } + c.state.BindMounts["/etc/resolv.conf"] = newResolv + + // Make /etc/hosts + if _, ok := c.state.BindMounts["/etc/hosts"]; ok { + // If it already exists, delete so we can recreate + delete(c.state.BindMounts, "/etc/hosts") + } + newHosts, err := c.generateHosts() + if err != nil { + return errors.Wrapf(err, "error creating hosts file for container %s", c.ID()) + } + c.state.BindMounts["/etc/hosts"] = newHosts + + } + + // SHM is always added when we mount the container + c.state.BindMounts["/dev/shm"] = c.config.ShmDir + + newPasswd, err := c.generatePasswd() + if err != nil { + return errors.Wrapf(err, "error creating temporary passwd file for container %s", c.ID()) + } + if newPasswd != "" { + // Make /etc/passwd + if _, ok := c.state.BindMounts["/etc/passwd"]; ok { + // If it already exists, delete so we can recreate + delete(c.state.BindMounts, "/etc/passwd") + } + logrus.Debugf("adding entry to /etc/passwd for non existent default user") + c.state.BindMounts["/etc/passwd"] = newPasswd + } + + // Make /etc/hostname + // This should never change, so no need to recreate if it exists + if _, ok := c.state.BindMounts["/etc/hostname"]; !ok { + hostnamePath, err := c.writeStringToRundir("hostname", c.Hostname()) + if err != nil { + return errors.Wrapf(err, "error creating hostname file for container %s", c.ID()) + } + c.state.BindMounts["/etc/hostname"] = hostnamePath + } + + // Make .containerenv + // Empty file, so no need to recreate if it exists + if _, ok := c.state.BindMounts["/run/.containerenv"]; !ok { + // Empty string for now, but we may consider populating this later + containerenvPath, err := c.writeStringToRundir(".containerenv", "") + if err != nil { + return errors.Wrapf(err, "error creating containerenv file for container %s", c.ID()) + } + c.state.BindMounts["/run/.containerenv"] = containerenvPath + } + + // Add Secret Mounts + secretMounts := secrets.SecretMountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.DefaultMountsFile, c.state.DestinationRunDir, c.RootUID(), c.RootGID()) + for _, mount := range secretMounts { + if _, ok := c.state.BindMounts[mount.Destination]; !ok { + c.state.BindMounts[mount.Destination] = mount.Source + } + } + + return nil +} + +// generateResolvConf generates a containers resolv.conf +func (c *Container) generateResolvConf() (string, error) { + // Determine the endpoint for resolv.conf in case it is a symlink + resolvPath, err := filepath.EvalSymlinks("/etc/resolv.conf") + if err != nil { + return "", err + } + + contents, err := ioutil.ReadFile(resolvPath) + if err != nil { + return "", errors.Wrapf(err, "unable to read %s", resolvPath) + } + + // Process the file to remove localhost nameservers + // TODO: set ipv6 enable bool more sanely + resolv, err := resolvconf.FilterResolvDNS(contents, true) + if err != nil { + return "", errors.Wrapf(err, "error parsing host resolv.conf") + } + + // Make a new resolv.conf + nameservers := resolvconf.GetNameservers(resolv.Content) + if len(c.config.DNSServer) > 0 { + // We store DNS servers as net.IP, so need to convert to string + nameservers = []string{} + for _, server := range c.config.DNSServer { + nameservers = append(nameservers, server.String()) + } + } + + search := resolvconf.GetSearchDomains(resolv.Content) + if len(c.config.DNSSearch) > 0 { + search = c.config.DNSSearch + } + + options := resolvconf.GetOptions(resolv.Content) + if len(c.config.DNSOption) > 0 { + options = c.config.DNSOption + } + + destPath := filepath.Join(c.state.RunDir, "resolv.conf") + + if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { + return "", errors.Wrapf(err, "error removing resolv.conf for container %s", c.ID()) + } + + // Build resolv.conf + if _, err = resolvconf.Build(destPath, nameservers, search, options); err != nil { + return "", errors.Wrapf(err, "error building resolv.conf for container %s") + } + + // Relabel resolv.conf for the container + if err := label.Relabel(destPath, c.config.MountLabel, false); err != nil { + return "", err + } + + return filepath.Join(c.state.DestinationRunDir, "resolv.conf"), nil +} + +// generateHosts creates a containers hosts file +func (c *Container) generateHosts() (string, error) { + orig, err := ioutil.ReadFile("/etc/hosts") + if err != nil { + return "", errors.Wrapf(err, "unable to read /etc/hosts") + } + hosts := string(orig) + if len(c.config.HostAdd) > 0 { + for _, host := range c.config.HostAdd { + // the host format has already been verified at this point + fields := strings.SplitN(host, ":", 2) + hosts += fmt.Sprintf("%s %s\n", fields[1], fields[0]) + } + } + if len(c.state.NetworkStatus) > 0 && len(c.state.NetworkStatus[0].IPs) > 0 { + ipAddress := strings.Split(c.state.NetworkStatus[0].IPs[0].Address.String(), "/")[0] + hosts += fmt.Sprintf("%s\t%s\n", ipAddress, c.Hostname()) + } + return c.writeStringToRundir("hosts", hosts) +} + +// generatePasswd generates a container specific passwd file, +// iff g.config.User is a number +func (c *Container) generatePasswd() (string, error) { + var ( + groupspec string + group *user.Group + gid int + ) + if c.config.User == "" { + return "", nil + } + spec := strings.SplitN(c.config.User, ":", 2) + userspec := spec[0] + if len(spec) > 1 { + groupspec = spec[1] + } + // If a non numeric User, then don't generate passwd + uid, err := strconv.ParseUint(userspec, 10, 32) + if err != nil { + return "", nil + } + // Lookup the user to see if it exists in the container image + _, err = lookup.GetUser(c.state.Mountpoint, userspec) + if err != nil && err != user.ErrNoPasswdEntries { + return "", err + } + if err == nil { + return "", nil + } + if groupspec != "" { + if !c.state.Mounted { + return "", errors.Wrapf(ErrCtrStateInvalid, "container %s must be mounted in order to translate group field for passwd record", c.ID()) + } + group, err = lookup.GetGroup(c.state.Mountpoint, groupspec) + if err != nil { + if err == user.ErrNoGroupEntries { + return "", errors.Wrapf(err, "unable to get gid %s from group file", groupspec) + } + return "", err + } + gid = group.Gid + } + originPasswdFile := filepath.Join(c.state.Mountpoint, "/etc/passwd") + orig, err := ioutil.ReadFile(originPasswdFile) + if err != nil && !os.IsNotExist(err) { + return "", errors.Wrapf(err, "unable to read passwd file %s", originPasswdFile) + } + + pwd := fmt.Sprintf("%s%d:x:%d:%d:container user:%s:/bin/sh\n", orig, uid, uid, gid, c.WorkingDir()) + passwdFile, err := c.writeStringToRundir("passwd", pwd) + if err != nil { + return "", errors.Wrapf(err, "failed to create temporary passwd file") + } + if os.Chmod(passwdFile, 0644); err != nil { + return "", err + } + return passwdFile, nil +} diff --git a/libpod/image/errors.go b/libpod/image/errors.go new file mode 100644 index 000000000..4088946cb --- /dev/null +++ b/libpod/image/errors.go @@ -0,0 +1,15 @@ +package image + +import ( + "errors" +) + +// Copied directly from libpod errors to avoid circular imports +var ( + // ErrNoSuchCtr indicates the requested container does not exist + ErrNoSuchCtr = errors.New("no such container") + // ErrNoSuchPod indicates the requested pod does not exist + ErrNoSuchPod = errors.New("no such pod") + // ErrNoSuchImage indicates the requested image does not exist + ErrNoSuchImage = errors.New("no such image") +) diff --git a/libpod/image/image.go b/libpod/image/image.go index 7e520d97e..434f9031e 100644 --- a/libpod/image/image.go +++ b/libpod/image/image.go @@ -252,7 +252,7 @@ func (i *Image) getLocalImage() (*storage.Image, error) { // The image has a registry name in it and we made sure we looked for it locally // with a tag. It cannot be local. if decomposedImage.hasRegistry { - return nil, errors.Errorf("%s", imageError) + return nil, errors.Wrapf(ErrNoSuchImage, imageError) } @@ -275,7 +275,7 @@ func (i *Image) getLocalImage() (*storage.Image, error) { return repoImage, nil } - return nil, errors.Wrapf(err, imageError) + return nil, errors.Wrapf(ErrNoSuchImage, err.Error()) } // ID returns the image ID as a string @@ -869,6 +869,7 @@ func (i *Image) Inspect(ctx context.Context) (*inspect.ImageData, error) { GraphDriver: driver, ManifestType: manifestType, User: ociv1Img.Config.User, + History: ociv1Img.History, } return data, nil } diff --git a/libpod/kube.go b/libpod/kube.go new file mode 100644 index 000000000..00db0033b --- /dev/null +++ b/libpod/kube.go @@ -0,0 +1,270 @@ +package libpod + +import ( + "fmt" + "strings" + + "github.com/containers/libpod/pkg/lookup" + "github.com/containers/libpod/pkg/util" + "github.com/cri-o/ocicni/pkg/ocicni" + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InspectForKube takes a slice of libpod containers and generates +// one v1.Pod description that includes just a single container. +func (c *Container) InspectForKube() (*v1.Pod, error) { + // Generate the v1.Pod yaml description + return simplePodWithV1Container(c) +} + +// simplePodWithV1Container is a function used by inspect when kube yaml needs to be generated +// for a single container. we "insert" that container description in a pod. +func simplePodWithV1Container(ctr *Container) (*v1.Pod, error) { + var containers []v1.Container + result, err := containerToV1Container(ctr) + if err != nil { + return nil, err + } + containers = append(containers, result) + + tm := v12.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + } + + // Add a label called "app" with the containers name as a value + labels := make(map[string]string) + labels["app"] = removeUnderscores(ctr.Name()) + om := v12.ObjectMeta{ + // The name of the pod is container_name-libpod + Name: fmt.Sprintf("%s-libpod", removeUnderscores(ctr.Name())), + Labels: labels, + // CreationTimestamp seems to be required, so adding it; in doing so, the timestamp + // will reflect time this is run (not container create time) because the conversion + // of the container create time to v1 Time is probably not warranted nor worthwhile. + CreationTimestamp: v12.Now(), + } + ps := v1.PodSpec{ + Containers: containers, + } + p := v1.Pod{ + TypeMeta: tm, + ObjectMeta: om, + Spec: ps, + } + return &p, nil +} + +// containerToV1Container converts information we know about a libpod container +// to a V1.Container specification. +func containerToV1Container(c *Container) (v1.Container, error) { + kubeContainer := v1.Container{} + kubeSec, err := generateKubeSecurityContext(c) + if err != nil { + return kubeContainer, err + } + + if len(c.config.Spec.Linux.Devices) > 0 { + // TODO Enable when we can support devices and their names + devices, err := generateKubeVolumeDeviceFromLinuxDevice(c.Spec().Linux.Devices) + if err != nil { + return kubeContainer, err + } + kubeContainer.VolumeDevices = devices + return kubeContainer, errors.Wrapf(ErrNotImplemented, "linux devices") + } + + if len(c.config.UserVolumes) > 0 { + // TODO When we until we can resolve what the volume name should be, this is disabled + // Volume names need to be coordinated "globally" in the kube files. + volumes, err := libpodMountsToKubeVolumeMounts(c) + if err != nil { + return kubeContainer, err + } + kubeContainer.VolumeMounts = volumes + return kubeContainer, errors.Wrapf(ErrNotImplemented, "volume names") + } + + envVariables, err := libpodEnvVarsToKubeEnvVars(c.config.Spec.Process.Env) + if err != nil { + return kubeContainer, nil + } + + ports, err := ocicniPortMappingToContainerPort(c.PortMappings()) + if err != nil { + return kubeContainer, nil + } + + containerCommands := c.Command() + kubeContainer.Name = removeUnderscores(c.Name()) + + _, image := c.Image() + kubeContainer.Image = image + kubeContainer.Stdin = c.Stdin() + kubeContainer.Command = containerCommands + // TODO need to figure out how we handle command vs entry point. Kube appears to prefer entrypoint. + // right now we just take the container's command + //container.Args = args + kubeContainer.WorkingDir = c.WorkingDir() + kubeContainer.Ports = ports + // This should not be applicable + //container.EnvFromSource = + kubeContainer.Env = envVariables + // TODO enable resources when we can support naming conventions + //container.Resources + kubeContainer.SecurityContext = kubeSec + kubeContainer.StdinOnce = false + kubeContainer.TTY = c.config.Spec.Process.Terminal + + return kubeContainer, nil +} + +// ocicniPortMappingToContainerPort takes an ocicni portmapping and converts +// it to a v1.ContainerPort format for kube output +func ocicniPortMappingToContainerPort(portMappings []ocicni.PortMapping) ([]v1.ContainerPort, error) { + var containerPorts []v1.ContainerPort + for _, p := range portMappings { + var protocol v1.Protocol + switch strings.ToUpper(p.Protocol) { + case "TCP": + protocol = v1.ProtocolTCP + case "UDP": + protocol = v1.ProtocolUDP + default: + return containerPorts, errors.Errorf("unknown network protocol %s", p.Protocol) + } + cp := v1.ContainerPort{ + // Name will not be supported + HostPort: p.HostPort, + HostIP: p.HostIP, + ContainerPort: p.ContainerPort, + Protocol: protocol, + } + containerPorts = append(containerPorts, cp) + } + return containerPorts, nil +} + +// libpodEnvVarsToKubeEnvVars converts a key=value string slice to []v1.EnvVar +func libpodEnvVarsToKubeEnvVars(envs []string) ([]v1.EnvVar, error) { + var envVars []v1.EnvVar + for _, e := range envs { + splitE := strings.SplitN(e, "=", 2) + if len(splitE) != 2 { + return envVars, errors.Errorf("environment variable %s is malformed; should be key=value", e) + } + ev := v1.EnvVar{ + Name: splitE[0], + Value: splitE[1], + } + envVars = append(envVars, ev) + } + return envVars, nil +} + +// Is this worth it? +func libpodMaxAndMinToResourceList(c *Container) (v1.ResourceList, v1.ResourceList) { //nolint + // It does not appear we can properly calculate CPU resources from the information + // we know in libpod. Libpod knows CPUs by time, shares, etc. + + // We also only know about a memory limit; no memory minimum + maxResources := make(map[v1.ResourceName]resource.Quantity) + minResources := make(map[v1.ResourceName]resource.Quantity) + config := c.Config() + maxMem := config.Spec.Linux.Resources.Memory.Limit + + _ = maxMem + + return maxResources, minResources +} + +func generateKubeVolumeMount(hostSourcePath string, mounts []specs.Mount) (v1.VolumeMount, error) { + vm := v1.VolumeMount{} + for _, m := range mounts { + if m.Source == hostSourcePath { + // TODO Name is not provided and is required by Kube; therefore, this is disabled earlier + //vm.Name = + vm.MountPath = m.Source + vm.SubPath = m.Destination + if util.StringInSlice("ro", m.Options) { + vm.ReadOnly = true + } + return vm, nil + } + } + return vm, errors.New("unable to find mount source") +} + +// libpodMountsToKubeVolumeMounts converts the containers mounts to a struct kube understands +func libpodMountsToKubeVolumeMounts(c *Container) ([]v1.VolumeMount, error) { + // At this point, I dont think we can distinguish between the default + // volume mounts and user added ones. For now, we pass them all. + var vms []v1.VolumeMount + for _, hostSourcePath := range c.config.UserVolumes { + vm, err := generateKubeVolumeMount(hostSourcePath, c.config.Spec.Mounts) + if err != nil { + return vms, err + } + vms = append(vms, vm) + } + return vms, nil +} + +// generateKubeSecurityContext generates a securityContext based on the existing container +func generateKubeSecurityContext(c *Container) (*v1.SecurityContext, error) { + priv := c.Privileged() + ro := c.IsReadOnly() + allowPrivEscalation := !c.Spec().Process.NoNewPrivileges + + // TODO enable use of capabilities when we can figure out how to extract cap-add|remove + //caps := v1.Capabilities{ + // //Add: c.config.Spec.Process.Capabilities + //} + sc := v1.SecurityContext{ + // TODO enable use of capabilities when we can figure out how to extract cap-add|remove + //Capabilities: &caps, + Privileged: &priv, + // TODO How do we know if selinux were passed into podman + //SELinuxOptions: + // RunAsNonRoot is an optional parameter; our first implementations should be root only; however + // I'm leaving this as a bread-crumb for later + //RunAsNonRoot: &nonRoot, + ReadOnlyRootFilesystem: &ro, + AllowPrivilegeEscalation: &allowPrivEscalation, + } + + if c.User() != "" { + // It is *possible* that + logrus.Debug("Looking in container for user: %s", c.User()) + u, err := lookup.GetUser(c.state.Mountpoint, c.User()) + if err != nil { + return nil, err + } + user := int64(u.Uid) + sc.RunAsUser = &user + } + return &sc, nil +} + +// generateKubeVolumeDeviceFromLinuxDevice takes a list of devices and makes a VolumeDevice struct for kube +func generateKubeVolumeDeviceFromLinuxDevice(devices []specs.LinuxDevice) ([]v1.VolumeDevice, error) { + var volumeDevices []v1.VolumeDevice + for _, d := range devices { + vd := v1.VolumeDevice{ + // TBD How are we going to sync up these names + //Name: + DevicePath: d.Path, + } + volumeDevices = append(volumeDevices, vd) + } + return volumeDevices, nil +} + +func removeUnderscores(s string) string { + return strings.Replace(s, "_", "", -1) +} diff --git a/libpod/mounts_linux.go b/libpod/mounts_linux.go new file mode 100644 index 000000000..e6aa09eac --- /dev/null +++ b/libpod/mounts_linux.go @@ -0,0 +1,18 @@ +// +build linux + +package libpod + +const ( + // MountPrivate represents the private mount option. + MountPrivate = "private" + // MountRPrivate represents the rprivate mount option. + MountRPrivate = "rprivate" + // MountShared represents the shared mount option. + MountShared = "shared" + // MountRShared represents the rshared mount option. + MountRShared = "rshared" + // MountSlave represents the slave mount option. + MountSlave = "slave" + // MountRSlave represents the rslave mount option. + MountRSlave = "rslave" +) diff --git a/libpod/oci.go b/libpod/oci.go index 190a4dc7f..a7aec06e5 100644 --- a/libpod/oci.go +++ b/libpod/oci.go @@ -846,13 +846,22 @@ func (r *OCIRuntime) execStopContainer(ctr *Container, timeout uint) error { } // checkpointContainer checkpoints the given container -func (r *OCIRuntime) checkpointContainer(ctr *Container) error { +func (r *OCIRuntime) checkpointContainer(ctr *Container, options ContainerCheckpointOptions) error { // imagePath is used by CRIU to store the actual checkpoint files imagePath := ctr.CheckpointPath() // workPath will be used to store dump.log and stats-dump workPath := ctr.bundlePath() logrus.Debugf("Writing checkpoint to %s", imagePath) logrus.Debugf("Writing checkpoint logs to %s", workPath) - return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, "checkpoint", - "--image-path", imagePath, "--work-path", workPath, ctr.ID()) + args := []string{} + args = append(args, "checkpoint") + args = append(args, "--image-path") + args = append(args, imagePath) + args = append(args, "--work-path") + args = append(args, workPath) + if options.KeepRunning { + args = append(args, "--leave-running") + } + args = append(args, ctr.ID()) + return utils.ExecCmdWithStdStreams(os.Stdin, os.Stdout, os.Stderr, nil, r.path, args...) } diff --git a/libpod/options.go b/libpod/options.go index 8d044313b..507847d65 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1295,3 +1295,14 @@ func WithInfraContainer() PodCreateOption { return nil } } + +// WithInfraContainerPorts tells the pod to add port bindings to the pause container +func WithInfraContainerPorts(bindings []ocicni.PortMapping) PodCreateOption { + return func(pod *Pod) error { + if pod.valid { + return ErrPodFinalized + } + pod.config.InfraContainer.PortBindings = bindings + return nil + } +} diff --git a/libpod/pod.go b/libpod/pod.go index 8ac976f6a..07f41f5c6 100644 --- a/libpod/pod.go +++ b/libpod/pod.go @@ -4,6 +4,7 @@ import ( "time" "github.com/containers/storage" + "github.com/cri-o/ocicni/pkg/ocicni" "github.com/pkg/errors" ) @@ -96,7 +97,8 @@ type PodContainerInfo struct { // InfraContainerConfig is the configuration for the pod's infra container type InfraContainerConfig struct { - HasInfraContainer bool `json:"makeInfraContainer"` + HasInfraContainer bool `json:"makeInfraContainer"` + PortBindings []ocicni.PortMapping `json:"infraPortBindings"` } // ID retrieves the pod's ID diff --git a/libpod/pod_easyjson.go b/libpod/pod_easyjson.go index 6c1c939f3..8ea9a5e72 100644 --- a/libpod/pod_easyjson.go +++ b/libpod/pod_easyjson.go @@ -6,6 +6,7 @@ package libpod import ( json "encoding/json" + ocicni "github.com/cri-o/ocicni/pkg/ocicni" easyjson "github.com/mailru/easyjson" jlexer "github.com/mailru/easyjson/jlexer" jwriter "github.com/mailru/easyjson/jwriter" @@ -721,6 +722,29 @@ func easyjsonBe091417DecodeGithubComContainersLibpodLibpod5(in *jlexer.Lexer, ou switch key { case "makeInfraContainer": out.HasInfraContainer = bool(in.Bool()) + case "infraPortBindings": + if in.IsNull() { + in.Skip() + out.PortBindings = nil + } else { + in.Delim('[') + if out.PortBindings == nil { + if !in.IsDelim(']') { + out.PortBindings = make([]ocicni.PortMapping, 0, 1) + } else { + out.PortBindings = []ocicni.PortMapping{} + } + } else { + out.PortBindings = (out.PortBindings)[:0] + } + for !in.IsDelim(']') { + var v6 ocicni.PortMapping + easyjsonBe091417DecodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(in, &v6) + out.PortBindings = append(out.PortBindings, v6) + in.WantComma() + } + in.Delim(']') + } default: in.SkipRecursive() } @@ -745,5 +769,109 @@ func easyjsonBe091417EncodeGithubComContainersLibpodLibpod5(out *jwriter.Writer, } out.Bool(bool(in.HasInfraContainer)) } + { + const prefix string = ",\"infraPortBindings\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + if in.PortBindings == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 { + out.RawString("null") + } else { + out.RawByte('[') + for v7, v8 := range in.PortBindings { + if v7 > 0 { + out.RawByte(',') + } + easyjsonBe091417EncodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(out, v8) + } + out.RawByte(']') + } + } + out.RawByte('}') +} +func easyjsonBe091417DecodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(in *jlexer.Lexer, out *ocicni.PortMapping) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeString() + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "hostPort": + out.HostPort = int32(in.Int32()) + case "containerPort": + out.ContainerPort = int32(in.Int32()) + case "protocol": + out.Protocol = string(in.String()) + case "hostIP": + out.HostIP = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonBe091417EncodeGithubComContainersLibpodVendorGithubComCriOOcicniPkgOcicni(out *jwriter.Writer, in ocicni.PortMapping) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"hostPort\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int32(int32(in.HostPort)) + } + { + const prefix string = ",\"containerPort\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Int32(int32(in.ContainerPort)) + } + { + const prefix string = ",\"protocol\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.Protocol)) + } + { + const prefix string = ",\"hostIP\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.String(string(in.HostIP)) + } out.RawByte('}') } diff --git a/libpod/runtime_pod_infra_linux.go b/libpod/runtime_pod_infra_linux.go index fea79e994..450a2fb32 100644 --- a/libpod/runtime_pod_infra_linux.go +++ b/libpod/runtime_pod_infra_linux.go @@ -7,7 +7,6 @@ import ( "github.com/containers/libpod/libpod/image" "github.com/containers/libpod/pkg/rootless" - "github.com/cri-o/ocicni/pkg/ocicni" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" ) @@ -50,9 +49,8 @@ func (r *Runtime) makeInfraContainer(ctx context.Context, p *Pod, imgName, imgID options = append(options, withIsInfra()) // Since user namespace sharing is not implemented, we only need to check if it's rootless - portMappings := make([]ocicni.PortMapping, 0) networks := make([]string, 0) - options = append(options, WithNetNS(portMappings, isRootless, networks)) + options = append(options, WithNetNS(p.config.InfraContainer.PortBindings, isRootless, networks)) return r.newContainer(ctx, g.Config, options...) } |