diff --git a/cmd/podman/generate/systemd.go b/cmd/podman/generate/systemd.go index e40416534a6e..a7cdd9bfee4f 100644 --- a/cmd/podman/generate/systemd.go +++ b/cmd/podman/generate/systemd.go @@ -13,6 +13,7 @@ import ( "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/cmd/podman/utils" "github.com/containers/podman/v4/pkg/domain/entities" + envLib "github.com/containers/podman/v4/pkg/env" systemDefine "github.com/containers/podman/v4/pkg/systemd/define" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -28,9 +29,11 @@ const ( wantsFlagName = "wants" afterFlagName = "after" requiresFlagName = "requires" + envFlagName = "env" ) var ( + envInput []string files bool format string systemdRestart string @@ -109,6 +112,9 @@ func init() { flags.StringArrayVar(&systemdOptions.Requires, requiresFlagName, nil, "Similar to wants, but declares stronger requirement dependencies") _ = systemdCmd.RegisterFlagCompletionFunc(requiresFlagName, completion.AutocompleteNone) + flags.StringArrayVarP(&envInput, envFlagName, "e", nil, "Set environment variables to the systemd unit files") + _ = systemdCmd.RegisterFlagCompletionFunc(envFlagName, completion.AutocompleteNone) + flags.SetNormalizeFunc(utils.TimeoutAliasFlags) } @@ -141,6 +147,13 @@ func systemd(cmd *cobra.Command, args []string) error { if cmd.Flags().Changed(stopTimeoutCompatFlagName) { setStopTimeout++ } + if cmd.Flags().Changed(envFlagName) { + cliEnv, err := envLib.ParseSlice(envInput) + if err != nil { + return fmt.Errorf("error parsing environment variables: %w", err) + } + systemdOptions.AdditionalEnvVariables = envLib.Slice(cliEnv) + } switch setStopTimeout { case 1: systemdOptions.StopTimeout = &stopTimeout diff --git a/docs/source/markdown/podman-generate-systemd.1.md b/docs/source/markdown/podman-generate-systemd.1.md index 88dff2a4568c..1ada6bd3feed 100644 --- a/docs/source/markdown/podman-generate-systemd.1.md +++ b/docs/source/markdown/podman-generate-systemd.1.md @@ -44,6 +44,12 @@ User-defined dependencies will be appended to the generated unit file, but any e Set the systemd unit name prefix for containers. The default is *container*. +#### **--env**, **-e**=*env* + +Set environment variables to the systemd unit files. + +If an environment variable is specified without a value, Podman will check the host environment for a value and set the variable only if it is set on the host. As a special case, if an environment variable ending in __*__ is specified without a value, Podman will search the host environment for variables starting with the prefix and will add those variables to the systemd unit files. + #### **--files**, **-f** Generate files instead of printing to stdout. The generated files are named {container,pod}-{ID,name}.service and will be placed in the current working directory. diff --git a/pkg/api/handlers/libpod/generate.go b/pkg/api/handlers/libpod/generate.go index 48c4c59e1da3..431927ac5dc9 100644 --- a/pkg/api/handlers/libpod/generate.go +++ b/pkg/api/handlers/libpod/generate.go @@ -17,20 +17,21 @@ func GenerateSystemd(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Name bool `schema:"useName"` - New bool `schema:"new"` - NoHeader bool `schema:"noHeader"` - TemplateUnitFile bool `schema:"templateUnitFile"` - RestartPolicy *string `schema:"restartPolicy"` - RestartSec uint `schema:"restartSec"` - StopTimeout uint `schema:"stopTimeout"` - StartTimeout uint `schema:"startTimeout"` - ContainerPrefix *string `schema:"containerPrefix"` - PodPrefix *string `schema:"podPrefix"` - Separator *string `schema:"separator"` - Wants []string `schema:"wants"` - After []string `schema:"after"` - Requires []string `schema:"requires"` + Name bool `schema:"useName"` + New bool `schema:"new"` + NoHeader bool `schema:"noHeader"` + TemplateUnitFile bool `schema:"templateUnitFile"` + RestartPolicy *string `schema:"restartPolicy"` + RestartSec uint `schema:"restartSec"` + StopTimeout uint `schema:"stopTimeout"` + StartTimeout uint `schema:"startTimeout"` + ContainerPrefix *string `schema:"containerPrefix"` + PodPrefix *string `schema:"podPrefix"` + Separator *string `schema:"separator"` + Wants []string `schema:"wants"` + After []string `schema:"after"` + Requires []string `schema:"requires"` + AdditionalEnvVariables []string `schema:"additionalEnvVariables"` }{ StartTimeout: 0, StopTimeout: util.DefaultContainerConfig().Engine.StopTimeout, @@ -58,20 +59,21 @@ func GenerateSystemd(w http.ResponseWriter, r *http.Request) { containerEngine := abi.ContainerEngine{Libpod: runtime} options := entities.GenerateSystemdOptions{ - Name: query.Name, - New: query.New, - NoHeader: query.NoHeader, - TemplateUnitFile: query.TemplateUnitFile, - RestartPolicy: query.RestartPolicy, - StartTimeout: &query.StartTimeout, - StopTimeout: &query.StopTimeout, - ContainerPrefix: ContainerPrefix, - PodPrefix: PodPrefix, - Separator: Separator, - RestartSec: &query.RestartSec, - Wants: query.Wants, - After: query.After, - Requires: query.Requires, + Name: query.Name, + New: query.New, + NoHeader: query.NoHeader, + TemplateUnitFile: query.TemplateUnitFile, + RestartPolicy: query.RestartPolicy, + StartTimeout: &query.StartTimeout, + StopTimeout: &query.StopTimeout, + ContainerPrefix: ContainerPrefix, + PodPrefix: PodPrefix, + Separator: Separator, + RestartSec: &query.RestartSec, + Wants: query.Wants, + After: query.After, + Requires: query.Requires, + AdditionalEnvVariables: query.AdditionalEnvVariables, } report, err := containerEngine.GenerateSystemd(r.Context(), utils.GetName(r), options) diff --git a/pkg/api/server/register_generate.go b/pkg/api/server/register_generate.go index 82fbe3d0959e..ac2818db05da 100644 --- a/pkg/api/server/register_generate.go +++ b/pkg/api/server/register_generate.go @@ -93,6 +93,13 @@ func (s *APIServer) registerGenerateHandlers(r *mux.Router) error { // type: string // default: [] // description: Systemd Requires list for the container or pods. + // - in: query + // name: additionalEnvVariables + // type: array + // items: + // type: string + // default: [] + // description: Set environment variables to the systemd unit files. // produces: // - application/json // responses: diff --git a/pkg/bindings/generate/types.go b/pkg/bindings/generate/types.go index 25c398c8b93b..31b43897cb40 100644 --- a/pkg/bindings/generate/types.go +++ b/pkg/bindings/generate/types.go @@ -38,4 +38,6 @@ type SystemdOptions struct { After *[]string // Requires - systemd requires list for the container or pods Requires *[]string + // AdditionalEnvVariables - Sets environment variables to a systemd unit file + AdditionalEnvVariables *[]string } diff --git a/pkg/bindings/generate/types_systemd_options.go b/pkg/bindings/generate/types_systemd_options.go index 4d436945b4d3..3aec33a5441a 100644 --- a/pkg/bindings/generate/types_systemd_options.go +++ b/pkg/bindings/generate/types_systemd_options.go @@ -226,3 +226,18 @@ func (o *SystemdOptions) GetRequires() []string { } return *o.Requires } + +// WithAdditionalEnvVariables set field AdditionalEnvVariables to given value +func (o *SystemdOptions) WithAdditionalEnvVariables(value []string) *SystemdOptions { + o.AdditionalEnvVariables = &value + return o +} + +// GetAdditionalEnvVariables returns value of field AdditionalEnvVariables +func (o *SystemdOptions) GetAdditionalEnvVariables() []string { + if o.AdditionalEnvVariables == nil { + var z []string + return z + } + return *o.AdditionalEnvVariables +} diff --git a/pkg/domain/entities/generate.go b/pkg/domain/entities/generate.go index f18e79b472f0..314996497a2e 100644 --- a/pkg/domain/entities/generate.go +++ b/pkg/domain/entities/generate.go @@ -4,34 +4,21 @@ import "io" // GenerateSystemdOptions control the generation of systemd unit files. type GenerateSystemdOptions struct { - // Name - use container/pod name instead of its ID. - Name bool - // New - create a new container instead of starting a new one. - New bool - // RestartPolicy - systemd restart policy. - RestartPolicy *string - // RestartSec - systemd service restartsec. Configures the time to sleep before restarting a service. - RestartSec *uint - // StartTimeout - time when starting the container. - StartTimeout *uint - // StopTimeout - time when stopping the container. - StopTimeout *uint - // ContainerPrefix - systemd unit name prefix for containers - ContainerPrefix string - // PodPrefix - systemd unit name prefix for pods - PodPrefix string - // Separator - systemd unit name separator between name/id and prefix - Separator string - // NoHeader - skip header generation - NoHeader bool - // TemplateUnitFile - make use of %i and %I to differentiate between the different instances of the unit - TemplateUnitFile bool - // Wants - systemd wants list for the container or pods - Wants []string - // After - systemd after list for the container or pods - After []string - // Requires - systemd requires list for the container or pods - Requires []string + Name bool + New bool + RestartPolicy *string + RestartSec *uint + StartTimeout *uint + StopTimeout *uint + ContainerPrefix string + PodPrefix string + Separator string + NoHeader bool + TemplateUnitFile bool + Wants []string + After []string + Requires []string + AdditionalEnvVariables []string } // GenerateSystemdReport diff --git a/pkg/domain/infra/tunnel/generate.go b/pkg/domain/infra/tunnel/generate.go index ed63d363a148..d3c3638cb364 100644 --- a/pkg/domain/infra/tunnel/generate.go +++ b/pkg/domain/infra/tunnel/generate.go @@ -19,7 +19,8 @@ func (ic *ContainerEngine) GenerateSystemd(ctx context.Context, nameOrID string, WithSeparator(opts.Separator). WithWants(opts.Wants). WithAfter(opts.After). - WithRequires(opts.Requires) + WithRequires(opts.Requires). + WithAdditionalEnvVariables(opts.AdditionalEnvVariables) if opts.StartTimeout != nil { options.WithStartTimeout(*opts.StartTimeout) diff --git a/pkg/systemd/generate/containers.go b/pkg/systemd/generate/containers.go index 1f8c519b7998..6f546b0ab103 100644 --- a/pkg/systemd/generate/containers.go +++ b/pkg/systemd/generate/containers.go @@ -22,85 +22,40 @@ import ( // containerInfo contains data required for generating a container's systemd // unit file. type containerInfo struct { - // ServiceName of the systemd service. - ServiceName string - // Name or ID of the container. - ContainerNameOrID string - // Type of the unit. - Type string - // NotifyAccess of the unit. - NotifyAccess string - // StopTimeout sets the timeout Podman waits before killing the container - // during service stop. - StopTimeout uint - // RestartPolicy of the systemd unit (e.g., no, on-failure, always). - RestartPolicy string - // Custom number of restart attempts. - StartLimitBurst string - // PIDFile of the service. Required for forking services. Must point to the - // PID of the associated conmon process. - PIDFile string - // ContainerIDFile to be used in the unit. - ContainerIDFile string - // GenerateTimestamp, if set the generated unit file has a time stamp. - GenerateTimestamp bool - // BoundToServices are the services this service binds to. Note that this - // service runs after them. - BoundToServices []string - // PodmanVersion for the header. Will be set internally. Will be auto-filled - // if left empty. - PodmanVersion string - // Executable is the path to the podman executable. Will be auto-filled if - // left empty. - Executable string - // RootFlags contains the root flags which were used to create the container - // Only used with --new - RootFlags string - // TimeStamp at the time of creating the unit file. Will be set internally. - TimeStamp string - // CreateCommand is the full command plus arguments of the process the - // container has been created with. - CreateCommand []string - // containerEnv stores the container environment variables - containerEnv []string - // ExtraEnvs contains the container environment variables referenced - // by only the key in the container create command, e.g. --env FOO. - // This is only used with --new - ExtraEnvs []string - // EnvVariable is generate.EnvVariable and must not be set. - EnvVariable string - // ExecStartPre of the unit. - ExecStartPre string - // ExecStart of the unit. - ExecStart string - // TimeoutStartSec of the unit. - TimeoutStartSec uint - // TimeoutStopSec of the unit. - TimeoutStopSec uint - // ExecStop of the unit. - ExecStop string - // ExecStopPost of the unit. - ExecStopPost string - // Removes autogenerated by Podman and timestamp if set to true - GenerateNoHeader bool - // If not nil, the container is part of the pod. We can use the - // podInfo to extract the relevant data. - Pod *podInfo - // Location of the GraphRoot for the container. Required for ensuring the - // volume has finished mounting when coming online at boot. - GraphRoot string - // Location of the RunRoot for the container. Required for ensuring the tmpfs - // or volume exists and is mounted when coming online at boot. - RunRoot string - // Add %i and %I to description and execute parts - IdentifySpecifier bool - // Wants are the list of services that this service is (weak) dependent on. This - // option does not influence the order in which services are started or stopped. - Wants []string - // After ordering dependencies between the list of services and this service. - After []string - // Similar to Wants, but declares a stronger requirement dependency. - Requires []string + ServiceName string + ContainerNameOrID string + Type string + NotifyAccess string + StopTimeout uint + RestartPolicy string + StartLimitBurst string + PIDFile string + ContainerIDFile string + GenerateTimestamp bool + BoundToServices []string + PodmanVersion string + Executable string + RootFlags string + TimeStamp string + CreateCommand []string + containerEnv []string + ExtraEnvs []string + EnvVariable string + AdditionalEnvVariables []string + ExecStartPre string + ExecStart string + TimeoutStartSec uint + TimeoutStopSec uint + ExecStop string + ExecStopPost string + GenerateNoHeader bool + Pod *podInfo + GraphRoot string + RunRoot string + IdentifySpecifier bool + Wants []string + After []string + Requires []string } const containerTemplate = headerTemplate + ` @@ -127,6 +82,10 @@ Environment={{{{.EnvVariable}}}}=%n{{{{- if (eq .IdentifySpecifier true) }}}}-%i {{{{- if .ExtraEnvs}}}} Environment={{{{- range $index, $value := .ExtraEnvs -}}}}{{{{if $index}}}} {{{{end}}}}{{{{ $value }}}}{{{{end}}}} {{{{- end}}}} +{{{{- if .AdditionalEnvVariables}}}} +{{{{- range $index, $value := .AdditionalEnvVariables -}}}}{{{{if $index}}}}{{{{end}}}} +Environment={{{{ $value }}}}{{{{end}}}} +{{{{- end}}}} Restart={{{{.RestartPolicy}}}} {{{{- if .StartLimitBurst}}}} StartLimitBurst={{{{.StartLimitBurst}}}} @@ -211,19 +170,20 @@ func generateContainerInfo(ctr *libpod.Container, options entities.GenerateSyste envs := config.Spec.Process.Env info := containerInfo{ - ServiceName: serviceName, - ContainerNameOrID: nameOrID, - RestartPolicy: define.DefaultRestartPolicy, - PIDFile: conmonPidFile, - TimeoutStartSec: startTimeout, - StopTimeout: stopTimeout, - GenerateTimestamp: true, - CreateCommand: createCommand, - RunRoot: runRoot, - containerEnv: envs, - Wants: options.Wants, - After: options.After, - Requires: options.Requires, + ServiceName: serviceName, + ContainerNameOrID: nameOrID, + RestartPolicy: define.DefaultRestartPolicy, + PIDFile: conmonPidFile, + TimeoutStartSec: startTimeout, + StopTimeout: stopTimeout, + GenerateTimestamp: true, + CreateCommand: createCommand, + RunRoot: runRoot, + containerEnv: envs, + Wants: options.Wants, + After: options.After, + Requires: options.Requires, + AdditionalEnvVariables: options.AdditionalEnvVariables, } return &info, nil @@ -324,6 +284,9 @@ func executeContainerTemplate(info *containerInfo, options entities.GenerateSyst info.ExecStart = "{{{{.Executable}}}} start {{{{.ContainerNameOrID}}}}" info.ExecStop = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}" info.ExecStopPost = "{{{{.Executable}}}} stop {{{{if (ge .StopTimeout 0)}}}}-t {{{{.StopTimeout}}}}{{{{end}}}} {{{{.ContainerNameOrID}}}}" + for i, env := range info.AdditionalEnvVariables { + info.AdditionalEnvVariables[i] = escapeSystemdArg(env) + } // Assemble the ExecStart command when creating a new container. // diff --git a/pkg/systemd/generate/containers_test.go b/pkg/systemd/generate/containers_test.go index 873cbfbb3098..7f92e75b8f9c 100644 --- a/pkg/systemd/generate/containers_test.go +++ b/pkg/systemd/generate/containers_test.go @@ -780,6 +780,33 @@ ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id Type=notify NotifyAccess=all +[Install] +WantedBy=default.target +` + + goodEnvironment := `# container-foobar.service +# autogenerated by Podman CI + +[Unit] +Description=Podman container-foobar.service +Documentation=man:podman-generate-systemd(1) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/var/run/containers/storage + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Environment=FOO=abc +Environment="BAR=my test" +Environment=USER=%%a +Restart=on-failure +TimeoutStopSec=70 +ExecStart=/usr/bin/podman start foobar +ExecStop=/usr/bin/podman stop -t 10 foobar +ExecStopPost=/usr/bin/podman stop -t 10 foobar +PIDFile=/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid +Type=forking + [Install] WantedBy=default.target ` @@ -1424,7 +1451,7 @@ WantedBy=default.target false, false, }, - {"good with environment variables", + {"good with container environment variables", containerInfo{ Executable: "/usr/bin/podman", ServiceName: "jadda-jadda", @@ -1444,6 +1471,25 @@ WantedBy=default.target false, false, }, + {"good with systemd environment variables", + containerInfo{ + Executable: "/usr/bin/podman", + ServiceName: "container-foobar", + ContainerNameOrID: "foobar", + PIDFile: "/run/containers/storage/overlay-containers/639c53578af4d84b8800b4635fa4e680ee80fd67e0e6a2d4eea48d1e3230f401/userdata/conmon.pid", + StopTimeout: 10, + PodmanVersion: "CI", + GraphRoot: "/var/lib/containers/storage", + RunRoot: "/var/run/containers/storage", + EnvVariable: define.EnvVariable, + AdditionalEnvVariables: []string{"FOO=abc", "BAR=my test", "USER=%a"}, + }, + goodEnvironment, + false, + false, + false, + false, + }, {"good with restart policy", containerInfo{ Executable: "/usr/bin/podman", diff --git a/test/e2e/generate_systemd_test.go b/test/e2e/generate_systemd_test.go index 45a2f1f86359..fb78924941af 100644 --- a/test/e2e/generate_systemd_test.go +++ b/test/e2e/generate_systemd_test.go @@ -600,4 +600,62 @@ var _ = Describe("Podman generate systemd", func() { Expect(session).Should(Exit(0)) Expect(session.OutputToString()).To(ContainSubstring(" --label key={{someval}}")) }) + + It("podman generate systemd --env", func() { + session := podmanTest.RunTopContainer("test") + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"generate", "systemd", "--env", "foo=bar", "-e", "hoge=fuga", "test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(ContainSubstring("Environment=foo=bar")) + Expect(session.OutputToString()).To(ContainSubstring("Environment=hoge=fuga")) + + session = podmanTest.Podman([]string{"generate", "systemd", "--env", "=bar", "-e", "hoge=fuga", "test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(125)) + Expect(session.ErrorToString()).To(ContainSubstring("invalid environment variable")) + + // Use -e/--env option with --new option + session = podmanTest.Podman([]string{"generate", "systemd", "--env", "foo=bar", "-e", "hoge=fuga", "--new", "test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(ContainSubstring("Environment=foo=bar")) + Expect(session.OutputToString()).To(ContainSubstring("Environment=hoge=fuga")) + + session = podmanTest.Podman([]string{"generate", "systemd", "--env", "foo=bar", "-e", "=fuga", "--new", "test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(125)) + Expect(session.ErrorToString()).To(ContainSubstring("invalid environment variable")) + + // Escape systemd arguments + session = podmanTest.Podman([]string{"generate", "systemd", "--env", "BAR=my test", "-e", "USER=%a", "test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(ContainSubstring("\"BAR=my test\"")) + Expect(session.OutputToString()).To(ContainSubstring("USER=%%a")) + + // Specify the environment variables without a value + os.Setenv("FOO1", "BAR1") + os.Setenv("FOO2", "BAR2") + os.Setenv("FOO3", "BAR3") + defer os.Unsetenv("FOO1") + defer os.Unsetenv("FOO2") + defer os.Unsetenv("FOO3") + + session = podmanTest.Podman([]string{"generate", "systemd", "--env", "FOO1", "test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(ContainSubstring("BAR1")) + Expect(session.OutputToString()).NotTo(ContainSubstring("BAR2")) + Expect(session.OutputToString()).NotTo(ContainSubstring("BAR3")) + + session = podmanTest.Podman([]string{"generate", "systemd", "--env", "FOO*", "test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(ContainSubstring("BAR1")) + Expect(session.OutputToString()).To(ContainSubstring("BAR2")) + Expect(session.OutputToString()).To(ContainSubstring("BAR3")) + }) })