diff options
-rw-r--r-- | cmd/podman/common/create.go | 5 | ||||
-rw-r--r-- | cmd/podman/common/create_opts.go | 1 | ||||
-rw-r--r-- | cmd/podman/common/specgen.go | 1 | ||||
-rw-r--r-- | completions/bash/podman | 1 | ||||
-rw-r--r-- | docs/source/markdown/podman-create.1.md | 12 | ||||
-rw-r--r-- | docs/source/markdown/podman-run.1.md | 12 | ||||
-rw-r--r-- | libpod/container.go | 9 | ||||
-rw-r--r-- | libpod/container_inspect.go | 2 | ||||
-rw-r--r-- | libpod/container_internal_linux.go | 57 | ||||
-rw-r--r-- | libpod/define/container_inspect.go | 3 | ||||
-rw-r--r-- | libpod/options.go | 24 | ||||
-rw-r--r-- | pkg/specgen/generate/container_create.go | 4 | ||||
-rw-r--r-- | pkg/specgen/specgen.go | 3 | ||||
-rw-r--r-- | test/apiv2/20-containers.at | 32 | ||||
-rw-r--r-- | test/e2e/config/containers.conf | 2 | ||||
-rw-r--r-- | test/e2e/containers_conf_test.go | 9 | ||||
-rw-r--r-- | test/e2e/create_test.go | 27 | ||||
-rw-r--r-- | test/e2e/run_test.go | 25 |
18 files changed, 229 insertions, 0 deletions
diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index c522bbffb..bbe31d19f 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -449,6 +449,11 @@ func GetCreateFlags(cf *ContainerCLIOpts) *pflag.FlagSet { "tty", "t", false, "Allocate a pseudo-TTY for container", ) + createFlags.StringVar( + &cf.Timezone, + "tz", containerConfig.TZ(), + "Set timezone in container", + ) createFlags.StringSliceVar( &cf.UIDMap, "uidmap", []string{}, diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index 8bce48d8d..3183a5cce 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -91,6 +91,7 @@ type ContainerCLIOpts struct { Systemd string TmpFS []string TTY bool + Timezone string UIDMap []string Ulimit []string User string diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index c9232654a..225370368 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -619,6 +619,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string } s.Remove = c.Rm s.StopTimeout = &c.StopTimeout + s.Timezone = c.Timezone return nil } diff --git a/completions/bash/podman b/completions/bash/podman index c0d9560ed..458090ac4 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -2120,6 +2120,7 @@ _podman_container_run() { --stop-signal --stop-timeout --tmpfs + --tz --subgidname --subuidname --sysctl diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 30ac54de2..1fc99cd87 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -815,6 +815,10 @@ interactive shell. The default is false. Note: The **-t** option is incompatible with a redirection of the Podman client standard input. +**--tz**=*timezone* + +Set timezone in container. This flag takes area-based timezones, GMT time, as well as `local`, which sets the timezone in the container to match the host machine. See `/usr/share/zoneinfo/` for valid timezones. + **--uidmap**=*container_uid:host_uid:amount* UID map for the user namespace. Using this flag will run the container with user namespace enabled. It conflicts with the `--userns` and `--subuidname` flags. @@ -1036,6 +1040,14 @@ the uids and gids from the host. $ podman create --uidmap 0:30000:7000 --gidmap 0:30000:7000 fedora echo hello ``` +### Configure timezone in a container + +``` +$ podman create --tz=local alpine date +$ podman create --tz=Asia/Shanghai alpine date +$ podman create --tz=US/Eastern alpine date +``` + ### Rootless Containers Podman runs as a non root user on most systems. This feature requires that a new enough version of shadow-utils diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 2e2adbc7e..86179e63c 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -856,6 +856,10 @@ interactive shell. The default is **false**. **NOTE**: The **-t** option is incompatible with a redirection of the Podman client standard input. +**--tz**=*timezone* + +Set timezone in container. This flag takes area-based timezones, GMT time, as well as `local`, which sets the timezone in the container to match the host machine. See `/usr/share/zoneinfo/` for valid timezones. + **--uidmap**=*container_uid*:*host_uid*:*amount* Run the container in a new user namespace using the supplied mapping. This option conflicts @@ -1319,6 +1323,14 @@ using global options. podman --log-level=debug --storage-driver overlay --storage-opt "overlay.mount_program=/usr/bin/fuse-overlayfs" run busybox /bin/sh ``` +### Configure timezone in a container + +``` +$ podman run --tz=local alpine date +$ podman run --tz=Asia/Shanghai alpine date +$ podman run --tz=US/Eastern alpine date +``` + ### Rootless Containers Podman runs as a non root user on most systems. This feature requires that a new enough version of **shadow-utils** diff --git a/libpod/container.go b/libpod/container.go index 66e444c51..a71692dd8 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -424,6 +424,10 @@ type ContainerConfig struct { // to 0, 1, 2) that will be passed to the executed process. The total FDs // passed will be 3 + PreserveFDs. PreserveFDs uint `json:"preserveFds,omitempty"` + + // Timezone is the timezone inside the container. + // Local means it has the same timezone as the host machine + Timezone string `json:"timezone,omitempty"` } // ContainerNamedVolume is a named volume that will be mounted into the @@ -1248,3 +1252,8 @@ func (c *Container) AutoRemove() bool { } return c.Spec().Annotations[define.InspectAnnotationAutoremove] == define.InspectResponseTrue } + +func (c *Container) Timezone() string { + return c.config.Timezone + +} diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index 03684ddec..94d5dc93b 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -322,6 +322,8 @@ func (c *Container) generateInspectContainerConfig(spec *spec.Spec) *define.Insp ctrConfig.CreateCommand = c.config.CreateCommand + ctrConfig.Timezone = c.config.Timezone + return ctrConfig } diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index c58350d18..8bf6092c3 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -1241,6 +1241,31 @@ func (c *Container) makeBindMounts() error { c.state.BindMounts["/etc/hostname"] = hostnamePath } + // Make /etc/localtime + if c.Timezone() != "" { + if _, ok := c.state.BindMounts["/etc/localtime"]; !ok { + var zonePath string + if c.Timezone() == "local" { + zonePath, err = filepath.EvalSymlinks("/etc/localtime") + if err != nil { + return errors.Wrapf(err, "error finding local timezone for container %s", c.ID()) + } + } else { + zone := filepath.Join("/usr/share/zoneinfo", c.Timezone()) + zonePath, err = filepath.EvalSymlinks(zone) + if err != nil { + return errors.Wrapf(err, "error setting timezone for container %s", c.ID()) + } + } + localtimePath, err := c.copyTimezoneFile(zonePath) + if err != nil { + return errors.Wrapf(err, "error setting timezone for container %s", c.ID()) + } + c.state.BindMounts["/etc/localtime"] = localtimePath + + } + } + // Make .containerenv // Empty file, so no need to recreate if it exists if _, ok := c.state.BindMounts["/run/.containerenv"]; !ok { @@ -1533,3 +1558,35 @@ func (c *Container) getOCICgroupPath() (string, error) { return "", errors.Wrapf(define.ErrInvalidArg, "invalid cgroup manager %s requested", c.runtime.config.Engine.CgroupManager) } } + +func (c *Container) copyTimezoneFile(zonePath string) (string, error) { + var localtimeCopy string = filepath.Join(c.state.RunDir, "localtime") + file, err := os.Stat(zonePath) + if err != nil { + return "", err + } + if file.IsDir() { + return "", errors.New("Invalid timezone: is a directory") + } + src, err := os.Open(zonePath) + if err != nil { + return "", err + } + defer src.Close() + dest, err := os.Create(localtimeCopy) + if err != nil { + return "", err + } + defer dest.Close() + _, err = io.Copy(dest, src) + if err != nil { + return "", err + } + if err := label.Relabel(localtimeCopy, c.config.MountLabel, false); err != nil { + return "", err + } + if err := dest.Chown(c.RootUID(), c.RootGID()); err != nil { + return "", err + } + return localtimeCopy, err +} diff --git a/libpod/define/container_inspect.go b/libpod/define/container_inspect.go index a3cf4304f..614882467 100644 --- a/libpod/define/container_inspect.go +++ b/libpod/define/container_inspect.go @@ -54,6 +54,9 @@ type InspectContainerConfig struct { // CreateCommand is the full command plus arguments of the process the // container has been created with. CreateCommand []string `json:"CreateCommand,omitempty"` + // Timezone is the timezone inside the container. + // Local means it has the same timezone as the host machine + Timezone string `json:"Timezone,omitempty"` } // InspectRestartPolicy holds information about the container's restart policy. diff --git a/libpod/options.go b/libpod/options.go index 4038be9b8..c1a8fdbe1 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1525,6 +1525,30 @@ func withSetAnon() VolumeCreateOption { } } +// WithTimezone sets the timezone in the container +func WithTimezone(path string) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + if path != "local" { + zone := filepath.Join("/usr/share/zoneinfo", path) + + file, err := os.Stat(zone) + if err != nil { + return err + } + //We don't want to mount a timezone directory + if file.IsDir() { + return errors.New("Invalid timezone: is a directory") + } + } + + ctr.config.Timezone = path + return nil + } +} + // Pod Creation Options // WithPodName sets the name of the pod. diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 2b84aa8c5..1ab576869 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -135,6 +135,10 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. options = append(options, libpod.WithStdin()) } + if s.Timezone != "" { + options = append(options, libpod.WithTimezone(s.Timezone)) + } + useSystemd := false switch s.Systemd { case "always": diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index 361f09379..fe735bc1f 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -135,6 +135,9 @@ type ContainerBasicConfig struct { // passed will be 3 + PreserveFDs. // set tags as `json:"-"` for not supported remote PreserveFDs uint `json:"-"` + // Timezone is the timezone inside the container. + // Local means it has the same timezone as the host machine + Timezone string `json:"timezone,omitempty"` } // ContainerStorageConfig contains information on the storage configuration of a diff --git a/test/apiv2/20-containers.at b/test/apiv2/20-containers.at index 9efebfeb9..25843e61c 100644 --- a/test/apiv2/20-containers.at +++ b/test/apiv2/20-containers.at @@ -98,9 +98,41 @@ t GET libpod/images/newrepo:v2/json 200 \ .Comment=bar \ .Config.Cmd[-1]="/bin/foo" +# Create a container for testing the container initializing later +podman create -t -i --name myctr $IMAGE ls + +# Check configuration before initializing +t GET libpod/containers/myctr/json 200 \ + .Id~[0-9a-f]\\{64\\} \ + .State.Status="configured" \ + .State.Pid=0 \ + .ResolvConfPath="" \ + .HostnamePath="" \ + .HostsPath="" \ + .NetworkSettings.SandboxKey="" + +cpid_file=$(jq -r '.ConmonPidFile' <<<"$output") +userdata_path=$(dirname $cpid_file) + +# Initializing the container +t POST libpod/containers/myctr/init '' 204 + +# Check configuration after initializing +t GET libpod/containers/myctr/json 200 \ + .Id~[0-9a-f]\\{64\\} \ + .State.Status="created" \ + .State.Pid~[0-9]\\{1\,8\\} \ + .ResolvConfPath=$userdata_path/resolv.conf \ + .HostnamePath=$userdata_path/hostname \ + .HostsPath=$userdata_path/hosts \ + .NetworkSettings.SandboxKey~.*/netns/cni- \ + .OCIConfigPath~.*config\.json \ + .GraphDriver.Data.MergedDir~.*merged + t DELETE images/localhost/newrepo:latest?force=true 200 t DELETE images/localhost/newrepo:v1?force=true 200 t DELETE images/localhost/newrepo:v2?force=true 200 t DELETE libpod/containers/$cid 204 +t DELETE libpod/containers/myctr 204 # vim: filetype=sh diff --git a/test/e2e/config/containers.conf b/test/e2e/config/containers.conf index a3bdde786..0a07676c4 100644 --- a/test/e2e/config/containers.conf +++ b/test/e2e/config/containers.conf @@ -48,3 +48,5 @@ default_sysctls = [ dns_searches=[ "foobar.com", ] dns_servers=[ "1.2.3.4", ] dns_options=[ "debug", ] + +tz = "Pacific/Honolulu" diff --git a/test/e2e/containers_conf_test.go b/test/e2e/containers_conf_test.go index 26da9486d..d8e5f2e69 100644 --- a/test/e2e/containers_conf_test.go +++ b/test/e2e/containers_conf_test.go @@ -211,4 +211,13 @@ var _ = Describe("Podman run", func() { Expect(session.ExitCode()).To(Equal(0)) Expect(session.LineInOuputStartsWith("search")).To(BeFalse()) }) + + It("podman run containers.conf timezone", func() { + //containers.conf timezone set to Pacific/Honolulu + session := podmanTest.Podman([]string{"run", ALPINE, "date", "+'%H %Z'"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("HST")) + + }) }) diff --git a/test/e2e/create_test.go b/test/e2e/create_test.go index 5f96f96b1..1e33be013 100644 --- a/test/e2e/create_test.go +++ b/test/e2e/create_test.go @@ -471,4 +471,31 @@ var _ = Describe("Podman create", func() { Expect(len(data)).To(Equal(1)) Expect(data[0].Config.StopSignal).To(Equal(uint(15))) }) + + It("podman create --tz", func() { + session := podmanTest.Podman([]string{"create", "--tz", "foo", "--name", "bad", ALPINE, "date"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + session = podmanTest.Podman([]string{"create", "--tz", "America", "--name", "dir", ALPINE, "date"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + session = podmanTest.Podman([]string{"create", "--tz", "Pacific/Honolulu", "--name", "zone", ALPINE, "date"}) + session.WaitWithDefaultTimeout() + inspect := podmanTest.Podman([]string{"inspect", "zone"}) + inspect.WaitWithDefaultTimeout() + data := inspect.InspectContainerToJSON() + Expect(len(data)).To(Equal(1)) + Expect(data[0].Config.Timezone).To(Equal("Pacific/Honolulu")) + + session = podmanTest.Podman([]string{"create", "--tz", "local", "--name", "lcl", ALPINE, "date"}) + session.WaitWithDefaultTimeout() + inspect = podmanTest.Podman([]string{"inspect", "lcl"}) + inspect.WaitWithDefaultTimeout() + data = inspect.InspectContainerToJSON() + Expect(len(data)).To(Equal(1)) + Expect(data[0].Config.Timezone).To(Equal("local")) + }) + }) diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index e7b68c60f..f49770727 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -1047,4 +1047,29 @@ USER mail` Expect(session.ExitCode()).To(Equal(0)) Expect(strings.Contains(session.OutputToString(), groupName)).To(BeTrue()) }) + + It("podman run --tz", func() { + session := podmanTest.Podman([]string{"run", "--tz", "foo", "--rm", ALPINE, "date"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + session = podmanTest.Podman([]string{"run", "--tz", "America", "--rm", ALPINE, "date"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + session = podmanTest.Podman([]string{"run", "--tz", "Pacific/Honolulu", "--rm", ALPINE, "date", "+'%H %Z'"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("HST")) + + session = podmanTest.Podman([]string{"run", "--tz", "local", "--rm", ALPINE, "date", "+'%H %Z'"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + t := time.Now() + z, _ := t.Zone() + h := strconv.Itoa(t.Hour()) + Expect(session.OutputToString()).To(ContainSubstring(z)) + Expect(session.OutputToString()).To(ContainSubstring(h)) + + }) }) |