diff options
author | Ashley Cui <acui@redhat.com> | 2021-01-15 01:27:23 -0500 |
---|---|---|
committer | Ashley Cui <acui@redhat.com> | 2021-02-09 09:13:21 -0500 |
commit | 832a69b0bee6ec289521fbd59ddd480372493ee3 (patch) | |
tree | 4c8a14b7fad879dc454c37f8b59120cf74ceafd1 | |
parent | 2aaf631586e82192e6b7b992e6b5c8717eb792d7 (diff) | |
download | podman-832a69b0bee6ec289521fbd59ddd480372493ee3.tar.gz podman-832a69b0bee6ec289521fbd59ddd480372493ee3.tar.bz2 podman-832a69b0bee6ec289521fbd59ddd480372493ee3.zip |
Implement Secrets
Implement podman secret create, inspect, ls, rm
Implement podman run/create --secret
Secrets are blobs of data that are sensitive.
Currently, the only secret driver supported is filedriver, which means creating a secret stores it in base64 unencrypted in a file.
After creating a secret, a user can use the --secret flag to expose the secret inside the container at /run/secrets/[secretname]
This secret will not be commited to an image on a podman commit
Signed-off-by: Ashley Cui <acui@redhat.com>
58 files changed, 2962 insertions, 7 deletions
diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 09dd74e20..bddea524d 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -212,6 +212,28 @@ func getImages(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellComp return suggestions, cobra.ShellCompDirectiveNoFileComp } +func getSecrets(cmd *cobra.Command, toComplete string) ([]string, cobra.ShellCompDirective) { + suggestions := []string{} + + engine, err := setupContainerEngine(cmd) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveNoFileComp + } + secrets, err := engine.SecretList(registry.GetContext()) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveNoFileComp + } + + for _, s := range secrets { + if strings.HasPrefix(s.Spec.Name, toComplete) { + suggestions = append(suggestions, s.Spec.Name) + } + } + return suggestions, cobra.ShellCompDirectiveNoFileComp +} + func getRegistries() ([]string, cobra.ShellCompDirective) { regs, err := registries.GetRegistries() if err != nil { @@ -412,6 +434,21 @@ func AutocompleteVolumes(cmd *cobra.Command, args []string, toComplete string) ( return getVolumes(cmd, toComplete) } +// AutocompleteSecrets - Autocomplete secrets. +func AutocompleteSecrets(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getSecrets(cmd, toComplete) +} + +func AutocompleteSecretCreate(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return nil, cobra.ShellCompDirectiveDefault + } + return nil, cobra.ShellCompDirectiveNoFileComp +} + // AutocompleteImages - Autocomplete images. func AutocompleteImages(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if !validCurrentCmdLine(cmd, args, toComplete) { diff --git a/cmd/podman/common/create.go b/cmd/podman/common/create.go index 915ff63b6..d8935628e 100644 --- a/cmd/podman/common/create.go +++ b/cmd/podman/common/create.go @@ -603,6 +603,14 @@ func DefineCreateFlags(cmd *cobra.Command, cf *ContainerCLIOpts) { ) _ = cmd.RegisterFlagCompletionFunc(sdnotifyFlagName, AutocompleteSDNotify) + secretFlagName := "secret" + createFlags.StringArrayVar( + &cf.Secrets, + secretFlagName, []string{}, + "Add secret to container", + ) + _ = cmd.RegisterFlagCompletionFunc(secretFlagName, AutocompleteSecrets) + securityOptFlagName := "security-opt" createFlags.StringArrayVar( &cf.SecurityOpt, diff --git a/cmd/podman/common/create_opts.go b/cmd/podman/common/create_opts.go index d86a6d364..67d40ac43 100644 --- a/cmd/podman/common/create_opts.go +++ b/cmd/podman/common/create_opts.go @@ -93,6 +93,7 @@ type ContainerCLIOpts struct { Replace bool Rm bool RootFS bool + Secrets []string SecurityOpt []string SdNotifyMode string ShmSize string diff --git a/cmd/podman/common/specgen.go b/cmd/podman/common/specgen.go index 4cc53a630..975c76fd9 100644 --- a/cmd/podman/common/specgen.go +++ b/cmd/podman/common/specgen.go @@ -642,6 +642,7 @@ func FillOutSpecGen(s *specgen.SpecGenerator, c *ContainerCLIOpts, args []string s.StopTimeout = &c.StopTimeout s.Timezone = c.Timezone s.Umask = c.Umask + s.Secrets = c.Secrets return nil } diff --git a/cmd/podman/main.go b/cmd/podman/main.go index f076d13f3..05b36295b 100644 --- a/cmd/podman/main.go +++ b/cmd/podman/main.go @@ -14,6 +14,7 @@ import ( _ "github.com/containers/podman/v2/cmd/podman/play" _ "github.com/containers/podman/v2/cmd/podman/pods" "github.com/containers/podman/v2/cmd/podman/registry" + _ "github.com/containers/podman/v2/cmd/podman/secrets" _ "github.com/containers/podman/v2/cmd/podman/system" _ "github.com/containers/podman/v2/cmd/podman/system/connection" _ "github.com/containers/podman/v2/cmd/podman/volumes" diff --git a/cmd/podman/secrets/create.go b/cmd/podman/secrets/create.go new file mode 100644 index 000000000..e58ab57cd --- /dev/null +++ b/cmd/podman/secrets/create.go @@ -0,0 +1,80 @@ +package secrets + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + createCmd = &cobra.Command{ + Use: "create [options] SECRET FILE|-", + Short: "Create a new secret", + Long: "Create a secret. Input can be a path to a file or \"-\" (read from stdin). Default driver is file (unencrypted).", + RunE: create, + Args: cobra.ExactArgs(2), + Example: `podman secret create mysecret /path/to/secret + printf "secretdata" | podman secret create mysecret -`, + ValidArgsFunction: common.AutocompleteSecretCreate, + } +) + +var ( + createOpts = entities.SecretCreateOptions{} +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: createCmd, + Parent: secretCmd, + }) + + flags := createCmd.Flags() + + driverFlagName := "driver" + flags.StringVar(&createOpts.Driver, driverFlagName, "file", "Specify secret driver") + _ = createCmd.RegisterFlagCompletionFunc(driverFlagName, completion.AutocompleteNone) +} + +func create(cmd *cobra.Command, args []string) error { + name := args[0] + + var err error + path := args[1] + + var reader io.Reader + if path == "-" || path == "/dev/stdin" { + stat, err := os.Stdin.Stat() + if err != nil { + return err + } + if (stat.Mode() & os.ModeNamedPipe) == 0 { + return errors.New("if `-` is used, data must be passed into stdin") + + } + reader = os.Stdin + } else { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + reader = file + } + + report, err := registry.ContainerEngine().SecretCreate(context.Background(), name, reader, createOpts) + if err != nil { + return err + } + fmt.Println(report.ID) + return nil +} diff --git a/cmd/podman/secrets/inspect.go b/cmd/podman/secrets/inspect.go new file mode 100644 index 000000000..f38ba7f65 --- /dev/null +++ b/cmd/podman/secrets/inspect.go @@ -0,0 +1,82 @@ +package secrets + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "os" + "text/tabwriter" + + "github.com/containers/common/pkg/report" + "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/parse" + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + inspectCmd = &cobra.Command{ + Use: "inspect [options] SECRET [SECRET...]", + Short: "Inspect a secret", + Long: "Display detail information on one or more secrets", + RunE: inspect, + Example: "podman secret inspect MYSECRET", + Args: cobra.MinimumNArgs(1), + ValidArgsFunction: common.AutocompleteSecrets, + } +) + +var format string + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: inspectCmd, + Parent: secretCmd, + }) + flags := inspectCmd.Flags() + formatFlagName := "format" + flags.StringVar(&format, formatFlagName, "", "Format volume output using Go template") + _ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat) +} + +func inspect(cmd *cobra.Command, args []string) error { + inspected, errs, _ := registry.ContainerEngine().SecretInspect(context.Background(), args) + + // always print valid list + if len(inspected) == 0 { + inspected = []*entities.SecretInfoReport{} + } + + if cmd.Flags().Changed("format") { + row := report.NormalizeFormat(format) + formatted := parse.EnforceRange(row) + + tmpl, err := template.New("inspect secret").Parse(formatted) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 12, 2, 2, ' ', 0) + defer w.Flush() + tmpl.Execute(w, inspected) + } else { + buf, err := json.MarshalIndent(inspected, "", " ") + if err != nil { + return err + } + fmt.Println(string(buf)) + } + + if len(errs) > 0 { + if len(errs) > 1 { + for _, err := range errs[1:] { + fmt.Fprintf(os.Stderr, "error inspecting secret: %v\n", err) + } + } + return errors.Errorf("error inspecting secret: %v", errs[0]) + } + return nil +} diff --git a/cmd/podman/secrets/list.go b/cmd/podman/secrets/list.go new file mode 100644 index 000000000..dff4bfdca --- /dev/null +++ b/cmd/podman/secrets/list.go @@ -0,0 +1,99 @@ +package secrets + +import ( + "context" + "html/template" + "os" + "text/tabwriter" + "time" + + "github.com/containers/common/pkg/completion" + "github.com/containers/common/pkg/report" + "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/parse" + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/validate" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/docker/go-units" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var ( + lsCmd = &cobra.Command{ + Use: "ls [options]", + Aliases: []string{"list"}, + Short: "List secrets", + RunE: ls, + Example: "podman secret ls", + Args: validate.NoArgs, + ValidArgsFunction: completion.AutocompleteNone, + } + listFlag = listFlagType{} +) + +type listFlagType struct { + format string + noHeading bool +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: lsCmd, + Parent: secretCmd, + }) + + flags := lsCmd.Flags() + formatFlagName := "format" + flags.StringVar(&listFlag.format, formatFlagName, "{{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}\t\n", "Format volume output using Go template") + _ = lsCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteJSONFormat) + +} + +func ls(cmd *cobra.Command, args []string) error { + responses, err := registry.ContainerEngine().SecretList(context.Background()) + if err != nil { + return err + } + listed := make([]*entities.SecretListReport, 0, len(responses)) + for _, response := range responses { + listed = append(listed, &entities.SecretListReport{ + ID: response.ID, + Name: response.Spec.Name, + CreatedAt: units.HumanDuration(time.Since(response.CreatedAt)) + " ago", + UpdatedAt: units.HumanDuration(time.Since(response.UpdatedAt)) + " ago", + Driver: response.Spec.Driver.Name, + }) + + } + return outputTemplate(cmd, listed) +} + +func outputTemplate(cmd *cobra.Command, responses []*entities.SecretListReport) error { + headers := report.Headers(entities.SecretListReport{}, map[string]string{ + "CreatedAt": "CREATED", + "UpdatedAt": "UPDATED", + }) + + row := report.NormalizeFormat(listFlag.format) + format := parse.EnforceRange(row) + + tmpl, err := template.New("list secret").Parse(format) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 12, 2, 2, ' ', 0) + defer w.Flush() + + if cmd.Flags().Changed("format") && !parse.HasTable(listFlag.format) { + listFlag.noHeading = true + } + + if !listFlag.noHeading { + if err := tmpl.Execute(w, headers); err != nil { + return errors.Wrapf(err, "failed to write report column headers") + } + } + return tmpl.Execute(w, responses) +} diff --git a/cmd/podman/secrets/rm.go b/cmd/podman/secrets/rm.go new file mode 100644 index 000000000..c72a3c171 --- /dev/null +++ b/cmd/podman/secrets/rm.go @@ -0,0 +1,58 @@ +package secrets + +import ( + "context" + "errors" + "fmt" + + "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/utils" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + rmCmd = &cobra.Command{ + Use: "rm [options] SECRET [SECRET...]", + Short: "Remove one or more secrets", + RunE: rm, + ValidArgsFunction: common.AutocompleteSecrets, + Example: "podman secret rm mysecret1 mysecret2", + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: rmCmd, + Parent: secretCmd, + }) + flags := rmCmd.Flags() + flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all secrets") +} + +var ( + rmOptions = entities.SecretRmOptions{} +) + +func rm(cmd *cobra.Command, args []string) error { + var ( + errs utils.OutputErrors + ) + if (len(args) > 0 && rmOptions.All) || (len(args) < 1 && !rmOptions.All) { + return errors.New("`podman secret rm` requires one argument, or the --all flag") + } + responses, err := registry.ContainerEngine().SecretRm(context.Background(), args, rmOptions) + if err != nil { + return err + } + for _, r := range responses { + if r.Err == nil { + fmt.Println(r.ID) + } else { + errs = append(errs, r.Err) + } + } + return errs.PrintErrors() +} diff --git a/cmd/podman/secrets/secret.go b/cmd/podman/secrets/secret.go new file mode 100644 index 000000000..4e6b6ec7b --- /dev/null +++ b/cmd/podman/secrets/secret.go @@ -0,0 +1,25 @@ +package secrets + +import ( + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/cmd/podman/validate" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + // Command: podman _secret_ + secretCmd = &cobra.Command{ + Use: "secret", + Short: "Manage secrets", + Long: "Manage secrets", + RunE: validate.SubCommandExists, + } +) + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: secretCmd, + }) +} diff --git a/commands-demo.md b/commands-demo.md index e91609b0f..ececf0a22 100644 --- a/commands-demo.md +++ b/commands-demo.md @@ -80,6 +80,11 @@ | [podman-run(1)](https://podman.readthedocs.io/en/latest/markdown/podman-run.1.html) | Run a command in a new container | | [podman-save(1)](https://podman.readthedocs.io/en/latest/markdown/podman-save.1.html) | Save an image to a container archive | | [podman-search(1)](https://podman.readthedocs.io/en/latest/markdown/podman-search.1.html) | Search a registry for an image | +| [podman-secret(1)](https://podman.readthedocs.io/en/latest/markdown/podman-secret.1.html) | Manage podman secrets | +| [podman-secret-create(1)](https://podman.readthedocs.io/en/latest/markdown/podman-secret-create.1.html) | Create a new secret | +| [podman-secret-inspect(1)](https://podman.readthedocs.io/en/latest/markdown/podman-secret-inspect.1.html) | Display detailed information on one or more secrets | +| [podman--secret-ls(1)](https://podman.readthedocs.io/en/latest/markdown/podman-secret-ls.1.html) | List all the available secrets | +| [podman-secret-rm(1)](https://podman.readthedocs.io/en/latest/markdown/podman-secret-rm.1.html) | Remove one or more secrets | | [podman-start(1)](https://podman.readthedocs.io/en/latest/markdown/podman-start.1.html) | Start one or more containers | | [podman-stats(1)](https://podman.readthedocs.io/en/latest/markdown/podman-stats.1.html) | Display a live stream of one or more container's resource usage statistics | | [podman-stop(1)](https://podman.readthedocs.io/en/latest/markdown/podman-stop.1.html) | Stops one or more running containers | diff --git a/docs/source/Commands.rst b/docs/source/Commands.rst index 563462377..0bb23f71b 100644 --- a/docs/source/Commands.rst +++ b/docs/source/Commands.rst @@ -89,6 +89,8 @@ Commands :doc:`search <markdown/podman-search.1>` Search registry for image +:doc:`secret <markdown/podman-secret.1>` Manage podman secrets + :doc:`start <markdown/podman-start.1>` Start one or more containers :doc:`stats <markdown/podman-stats.1>` Display a live stream of container resource usage statistics diff --git a/docs/source/markdown/podman-create.1.md b/docs/source/markdown/podman-create.1.md index 1d72db065..7782949a9 100644 --- a/docs/source/markdown/podman-create.1.md +++ b/docs/source/markdown/podman-create.1.md @@ -825,6 +825,16 @@ Specify the policy to select the seccomp profile. If set to *image*, Podman will Note that this feature is experimental and may change in the future. +#### **--secret**=*secret* + +Give the container access to a secret. Can be specified multiple times. + +A secret is a blob of sensitive data which a container needs at runtime but +should not be stored in the image or in source control, such as usernames and passwords, +TLS certificates and keys, SSH keys or other important generic strings or binary content (up to 500 kb in size). + +Secrets are managed using the `podman secret` command. + #### **--security-opt**=*option* Security Options @@ -1277,7 +1287,7 @@ b NOTE: Use the environment variable `TMPDIR` to change the temporary storage location of downloaded container images. Podman defaults to use `/var/tmp`. ## SEE ALSO -**podman**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-kill**(1), **podman-stop**(1), +**podman**(1), **podman-secret**(1), **podman-save**(1), **podman-ps**(1), **podman-attach**(1), **podman-pod-create**(1), **podman-port**(1), **podman-kill**(1), **podman-stop**(1), **podman-generate-systemd**(1) **podman-rm**(1), **subgid**(5), **subuid**(5), **containers.conf**(5), **systemd.unit**(5), **setsebool**(8), **slirp4netns**(1), **fuse-overlayfs**(1), **proc**(5)**. ## HISTORY diff --git a/docs/source/markdown/podman-run.1.md b/docs/source/markdown/podman-run.1.md index 0838dd546..49b45f4f8 100644 --- a/docs/source/markdown/podman-run.1.md +++ b/docs/source/markdown/podman-run.1.md @@ -877,6 +877,16 @@ Specify the policy to select the seccomp profile. If set to *image*, Podman will Note that this feature is experimental and may change in the future. +#### **--secret**=*secret* + +Give the container access to a secret. Can be specified multiple times. + +A secret is a blob of sensitive data which a container needs at runtime but +should not be stored in the image or in source control, such as usernames and passwords, +TLS certificates and keys, SSH keys or other important generic strings or binary content (up to 500 kb in size). + +Secrets are managed using the `podman secret` command + #### **--security-opt**=*option* Security Options diff --git a/docs/source/markdown/podman-secret-create.1.md b/docs/source/markdown/podman-secret-create.1.md new file mode 100644 index 000000000..af4dc1d97 --- /dev/null +++ b/docs/source/markdown/podman-secret-create.1.md @@ -0,0 +1,43 @@ +% podman-secret-create(1) + +## NAME +podman\-secret\-create - Create a new secret + +## SYNOPSIS +**podman secret create** [*options*] *name* *file|-* + +## DESCRIPTION + +Creates a secret using standard input or from a file for the secret content. + +Create accepts a path to a file, or `-`, which tells podman to read the secret from stdin + +A secret is a blob of sensitive data which a container needs at runtime but +should not be stored in the image or in source control, such as usernames and passwords, +TLS certificates and keys, SSH keys or other important generic strings or binary content (up to 500 kb in size). + +Secrets will not be commited to an image with `podman commit`, and will not be in the archive created by a `podman export` + +## OPTIONS + +#### **--driver**=*driver* + +Specify the secret driver (default **file**, which is unencrypted). + +#### **--help** + +Print usage statement. + +## EXAMPLES + +``` +$ podman secret create my_secret ./secret.json +$ podman secret create --driver=file my_secret ./secret.json +$ printf <secret> | podman secret create my_secret - +``` + +## SEE ALSO +podman-secret (1) + +## HISTORY +January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret-inspect.1.md b/docs/source/markdown/podman-secret-inspect.1.md new file mode 100644 index 000000000..383db8375 --- /dev/null +++ b/docs/source/markdown/podman-secret-inspect.1.md @@ -0,0 +1,38 @@ +% podman-secret-inspect(1) + +## NAME +podman\-secret\-inspect - Display detailed information on one or more secrets + +## SYNOPSIS +**podman secret inspect** [*options*] *secret* [...] + +## DESCRIPTION + +Inspects the specified secret. + +By default, this renders all results in a JSON array. If a format is specified, the given template will be executed for each result. +Secrets can be queried individually by providing their full name or a unique partial name. + +## OPTIONS + +#### **--format**=*format* + +Format secret output using Go template. + +#### **--help** + +Print usage statement. + + +## EXAMPLES + +``` +$ podman secret inspect mysecret +$ podman secret inspect --format "{{.Name} {{.Scope}}" mysecret +``` + +## SEE ALSO +podman-secret(1) + +## HISTORY +January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret-ls.1.md b/docs/source/markdown/podman-secret-ls.1.md new file mode 100644 index 000000000..688784e05 --- /dev/null +++ b/docs/source/markdown/podman-secret-ls.1.md @@ -0,0 +1,30 @@ +% podman-secret-ls(1) + +## NAME +podman\-secret\-ls - List all available secrets + +## SYNOPSIS +**podman secret ls** [*options*] + +## DESCRIPTION + +Lists all the secrets that exist. The output can be formatted to a Go template using the **--format** option. + +## OPTIONS + +#### **--format**=*format* + +Format secret output using Go template. + +## EXAMPLES + +``` +$ podman secret ls +$ podman secret ls --format "{{.Name}}" +``` + +## SEE ALSO +podman-secret(1) + +## HISTORY +January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret-rm.1.md b/docs/source/markdown/podman-secret-rm.1.md new file mode 100644 index 000000000..5169626dc --- /dev/null +++ b/docs/source/markdown/podman-secret-rm.1.md @@ -0,0 +1,33 @@ +% podman-secret-rm(1) + +## NAME +podman\-secret\-rm - Remove one or more secrets + +## SYNOPSIS +**podman secret rm** [*options*] *secret* [...] + +## DESCRIPTION + +Removes one or more secrets. + +## OPTIONS + +#### **--all**, **-a** + +Remove all existing secrets. + +#### **--help** + +Print usage statement. + +## EXAMPLES + +``` +$ podman secret rm mysecret1 mysecret2 +``` + +## SEE ALSO +podman-secret(1) + +## HISTORY +January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman-secret.1.md b/docs/source/markdown/podman-secret.1.md new file mode 100644 index 000000000..125888cff --- /dev/null +++ b/docs/source/markdown/podman-secret.1.md @@ -0,0 +1,25 @@ +% podman-secret(1) + +## NAME +podman\-secret - Manage podman secrets + +## SYNOPSIS +**podman secret** *subcommand* + +## DESCRIPTION +podman secret is a set of subcommands that manage secrets. + +## SUBCOMMANDS + +| Command | Man Page | Description | +| ------- | ------------------------------------------------------ | ------------------------------------------------------ | +| create | [podman-secret-create(1)](podman-secret-create.1.md) | Create a new secret | +| inspect | [podman-secret-inspect(1)](podman-secret-inspect.1.md) | Display detailed information on one or more secrets | +| ls | [podman-secret-ls(1)](podman-secret-ls.1.md) | List all available secrets | +| rm | [podman-secret-rm(1)](podman-secret-rm.1.md) | Remove one or more secrets | + +## SEE ALSO +podman(1) + +## HISTORY +January 2021, Originally compiled by Ashley Cui <acui@redhat.com> diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 0bb8e387b..6f9e705c2 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -254,6 +254,7 @@ the exit codes follow the `chroot` standard, see below: | [podman-run(1)](podman-run.1.md) | Run a command in a new container. | | [podman-save(1)](podman-save.1.md) | Save image(s) to an archive. | | [podman-search(1)](podman-search.1.md) | Search a registry for an image. | +| [podman-secret(1)](podman-secret.1.md) | Manage podman secrets. | | [podman-start(1)](podman-start.1.md) | Start one or more containers. | | [podman-stats(1)](podman-stats.1.md) | Display a live stream of one or more container's resource usage statistics. | | [podman-stop(1)](podman-stop.1.md) | Stop one or more running containers. | diff --git a/docs/source/secret.rst b/docs/source/secret.rst new file mode 100644 index 000000000..3825ad1df --- /dev/null +++ b/docs/source/secret.rst @@ -0,0 +1,9 @@ +Secret +====== +:doc:`create <markdown/podman-secret-create.1>` Create a new secert + +:doc:`inspect <markdown/podman-secret-inspect.1>` Display detailed information on one or more secrets + +:doc:`ls <markdown/podman-secret-ls.1>` List secrets + +:doc:`rm <markdown/podman-secret-rm.1>` Remove one or more secrets diff --git a/libpod/container.go b/libpod/container.go index ed7535bc8..e667cd991 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -10,6 +10,7 @@ import ( "github.com/containernetworking/cni/pkg/types" cnitypes "github.com/containernetworking/cni/pkg/types/current" + "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/manifest" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/lock" @@ -1133,6 +1134,11 @@ func (c *Container) Umask() string { return c.config.Umask } +//Secrets return the secrets in the container +func (c *Container) Secrets() []*secrets.Secret { + return c.config.Secrets +} + // Networks gets all the networks this container is connected to. // Please do NOT use ctr.config.Networks, as this can be changed from those // values at runtime via network connect and disconnect. diff --git a/libpod/container_config.go b/libpod/container_config.go index 93ac8807d..5d7e65f2b 100644 --- a/libpod/container_config.go +++ b/libpod/container_config.go @@ -4,6 +4,7 @@ import ( "net" "time" + "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/manifest" "github.com/containers/podman/v2/pkg/namespaces" "github.com/containers/storage" @@ -146,6 +147,10 @@ type ContainerRootFSConfig struct { // working directory if it does not exist. Some OCI runtimes do this by // default, but others do not. CreateWorkingDir bool `json:"createWorkingDir,omitempty"` + // Secrets lists secrets to mount into the container + Secrets []*secrets.Secret `json:"secrets,omitempty"` + // SecretPath is the secrets location in storage + SecretsPath string `json:"secretsPath"` } // ContainerSecurityConfig is an embedded sub-config providing security configuration diff --git a/libpod/container_inspect.go b/libpod/container_inspect.go index ac7eae56b..f50c7dbfe 100644 --- a/libpod/container_inspect.go +++ b/libpod/container_inspect.go @@ -340,6 +340,13 @@ func (c *Container) generateInspectContainerConfig(spec *spec.Spec) *define.Insp ctrConfig.Timezone = c.config.Timezone + for _, secret := range c.config.Secrets { + newSec := define.InspectSecret{} + newSec.Name = secret.Name + newSec.ID = secret.ID + ctrConfig.Secrets = append(ctrConfig.Secrets, &newSec) + } + // Pad Umask to 4 characters if len(c.config.Umask) < 4 { pad := strings.Repeat("0", 4-len(c.config.Umask)) diff --git a/libpod/container_internal.go b/libpod/container_internal.go index 5a61f7fe6..b280e79d1 100644 --- a/libpod/container_internal.go +++ b/libpod/container_internal.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/containers/common/pkg/secrets" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/events" "github.com/containers/podman/v2/pkg/cgroups" @@ -29,6 +30,7 @@ import ( securejoin "github.com/cyphar/filepath-securejoin" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-tools/generate" + "github.com/opencontainers/selinux/go-selinux/label" "github.com/opentracing/opentracing-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -2212,3 +2214,25 @@ func (c *Container) hasNamespace(namespace spec.LinuxNamespaceType) bool { } return false } + +// extractSecretToStorage copies a secret's data from the secrets manager to the container's static dir +func (c *Container) extractSecretToCtrStorage(name string) error { + manager, err := secrets.NewManager(c.runtime.GetSecretsStorageDir()) + if err != nil { + return err + } + secr, data, err := manager.LookupSecretData(name) + if err != nil { + return err + } + secretFile := filepath.Join(c.config.SecretsPath, secr.Name) + + err = ioutil.WriteFile(secretFile, data, 0644) + if err != nil { + return errors.Wrapf(err, "unable to create %s", secretFile) + } + if err := label.Relabel(secretFile, c.config.MountLabel, false); err != nil { + return err + } + return nil +} diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go index ba85a1f47..3583f8fdd 100644 --- a/libpod/container_internal_linux.go +++ b/libpod/container_internal_linux.go @@ -25,6 +25,7 @@ import ( "github.com/containers/common/pkg/apparmor" "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/subscriptions" + "github.com/containers/common/pkg/umask" "github.com/containers/podman/v2/libpod/define" "github.com/containers/podman/v2/libpod/events" "github.com/containers/podman/v2/pkg/annotations" @@ -1643,14 +1644,30 @@ rootless=%d c.state.BindMounts["/run/.containerenv"] = containerenvPath } - // Add Secret Mounts - secretMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false) - for _, mount := range secretMounts { + // Add Subscription Mounts + subscriptionMounts := subscriptions.MountsWithUIDGID(c.config.MountLabel, c.state.RunDir, c.runtime.config.Containers.DefaultMountsFile, c.state.Mountpoint, c.RootUID(), c.RootGID(), rootless.IsRootless(), false) + for _, mount := range subscriptionMounts { if _, ok := c.state.BindMounts[mount.Destination]; !ok { c.state.BindMounts[mount.Destination] = mount.Source } } + // Secrets are mounted by getting the secret data from the secrets manager, + // copying the data into the container's static dir, + // then mounting the copied dir into /run/secrets. + // The secrets mounting must come after subscription mounts, since subscription mounts + // creates the /run/secrets dir in the container where we mount as well. + if len(c.Secrets()) > 0 { + // create /run/secrets if subscriptions did not create + if err := c.createSecretMountDir(); err != nil { + return errors.Wrapf(err, "error creating secrets mount") + } + for _, secret := range c.Secrets() { + src := filepath.Join(c.config.SecretsPath, secret.Name) + dest := filepath.Join("/run/secrets", secret.Name) + c.state.BindMounts[dest] = src + } + } return nil } @@ -2368,3 +2385,27 @@ func (c *Container) checkFileExistsInRootfs(file string) (bool, error) { } return true, nil } + +// Creates and mounts an empty dir to mount secrets into, if it does not already exist +func (c *Container) createSecretMountDir() error { + src := filepath.Join(c.state.RunDir, "/run/secrets") + _, err := os.Stat(src) + if os.IsNotExist(err) { + oldUmask := umask.Set(0) + defer umask.Set(oldUmask) + + if err := os.MkdirAll(src, 0644); err != nil { + return err + } + if err := label.Relabel(src, c.config.MountLabel, false); err != nil { + return err + } + if err := os.Chown(src, c.RootUID(), c.RootGID()); err != nil { + return err + } + c.state.BindMounts["/run/secrets"] = src + return nil + } + + return err +} diff --git a/libpod/define/container_inspect.go b/libpod/define/container_inspect.go index 9a93e2ffd..2cdd53cbc 100644 --- a/libpod/define/container_inspect.go +++ b/libpod/define/container_inspect.go @@ -62,6 +62,8 @@ type InspectContainerConfig struct { SystemdMode bool `json:"SystemdMode,omitempty"` // Umask is the umask inside the container. Umask string `json:"Umask,omitempty"` + // Secrets are the secrets mounted in the container + Secrets []*InspectSecret `json:"Secrets,omitempty"` } // InspectRestartPolicy holds information about the container's restart policy. @@ -705,3 +707,14 @@ type DriverData struct { Name string `json:"Name"` Data map[string]string `json:"Data"` } + +// InspectHostPort provides information on a port on the host that a container's +// port is bound to. +type InspectSecret struct { + // IP on the host we are bound to. "" if not specified (binding to all + // IPs). + Name string `json:"Name"` + // Port on the host we are bound to. No special formatting - just an + // integer stuffed into a string. + ID string `json:"ID"` +} diff --git a/libpod/options.go b/libpod/options.go index 20f62ee37..74ee60fef 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -8,6 +8,7 @@ import ( "syscall" "github.com/containers/common/pkg/config" + "github.com/containers/common/pkg/secrets" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/types" "github.com/containers/podman/v2/libpod/define" @@ -1687,6 +1688,28 @@ func WithUmask(umask string) CtrCreateOption { } } +// WithSecrets adds secrets to the container +func WithSecrets(secretNames []string) CtrCreateOption { + return func(ctr *Container) error { + if ctr.valid { + return define.ErrCtrFinalized + } + manager, err := secrets.NewManager(ctr.runtime.GetSecretsStorageDir()) + if err != nil { + return err + } + for _, name := range secretNames { + secr, err := manager.Lookup(name) + if err != nil { + return err + } + ctr.config.Secrets = append(ctr.config.Secrets, secr) + } + + return nil + } +} + // Pod Creation Options // WithInfraImage sets the infra image for libpod. diff --git a/libpod/runtime.go b/libpod/runtime.go index 0dc220b52..1ad39fe2f 100644 --- a/libpod/runtime.go +++ b/libpod/runtime.go @@ -904,3 +904,8 @@ func (r *Runtime) getVolumePlugin(name string) (*plugin.VolumePlugin, error) { return plugin.GetVolumePlugin(name, pluginPath) } + +// GetSecretsStoreageDir returns the directory that the secrets manager should take +func (r *Runtime) GetSecretsStorageDir() string { + return filepath.Join(r.store.GraphRoot(), "secrets") +} diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go index d2bcd8db3..49cf42626 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -422,6 +422,18 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai } }() + ctr.config.SecretsPath = filepath.Join(ctr.config.StaticDir, "secrets") + err = os.MkdirAll(ctr.config.SecretsPath, 0644) + if err != nil { + return nil, err + } + for _, secr := range ctr.config.Secrets { + err = ctr.extractSecretToCtrStorage(secr.Name) + if err != nil { + return nil, err + } + } + if ctr.config.ConmonPidFile == "" { ctr.config.ConmonPidFile = filepath.Join(ctr.state.RunDir, "conmon.pid") } @@ -492,7 +504,6 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai toLock.lock.Lock() defer toLock.lock.Unlock() } - // Add the container to the state // TODO: May be worth looking into recovering from name/ID collisions here if ctr.config.Pod != "" { diff --git a/pkg/api/handlers/compat/secrets.go b/pkg/api/handlers/compat/secrets.go new file mode 100644 index 000000000..ea2dfc707 --- /dev/null +++ b/pkg/api/handlers/compat/secrets.go @@ -0,0 +1,121 @@ +package compat + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/pkg/api/handlers/utils" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/domain/infra/abi" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func ListSecrets(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + query := struct { + Filters map[string][]string `schema:"filters"` + }{} + + 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 + } + if len(query.Filters) > 0 { + utils.Error(w, "filters not supported", http.StatusBadRequest, errors.New("bad parameter")) + } + ic := abi.ContainerEngine{Libpod: runtime} + reports, err := ic.SecretList(r.Context()) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, reports) +} + +func InspectSecret(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + ) + name := utils.GetName(r) + names := []string{name} + ic := abi.ContainerEngine{Libpod: runtime} + reports, errs, err := ic.SecretInspect(r.Context(), names) + if err != nil { + utils.InternalServerError(w, err) + return + } + if len(errs) > 0 { + utils.SecretNotFound(w, name, errs[0]) + return + } + utils.WriteResponse(w, http.StatusOK, reports[0]) + +} + +func RemoveSecret(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + ) + + opts := entities.SecretRmOptions{} + name := utils.GetName(r) + ic := abi.ContainerEngine{Libpod: runtime} + reports, err := ic.SecretRm(r.Context(), []string{name}, opts) + if err != nil { + utils.InternalServerError(w, err) + return + } + if reports[0].Err != nil { + utils.SecretNotFound(w, name, reports[0].Err) + return + } + utils.WriteResponse(w, http.StatusNoContent, nil) +} + +func CreateSecret(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + ) + opts := entities.SecretCreateOptions{} + createParams := struct { + *entities.SecretCreateRequest + Labels map[string]string `schema:"labels"` + }{} + + if err := json.NewDecoder(r.Body).Decode(&createParams); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + if len(createParams.Labels) > 0 { + utils.Error(w, "labels not supported", http.StatusBadRequest, errors.New("bad parameter")) + } + + decoded, _ := base64.StdEncoding.DecodeString(createParams.Data) + reader := bytes.NewReader(decoded) + opts.Driver = createParams.Driver.Name + + ic := abi.ContainerEngine{Libpod: runtime} + report, err := ic.SecretCreate(r.Context(), createParams.Name, reader, opts) + if err != nil { + if errors.Cause(err).Error() == "secret name in use" { + utils.Error(w, "name conflicts with an existing object", http.StatusConflict, err) + return + } + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, report) +} + +func UpdateSecret(w http.ResponseWriter, r *http.Request) { + utils.Error(w, fmt.Sprintf("unsupported endpoint: %v", r.Method), http.StatusNotImplemented, errors.New("update is not supported")) +} diff --git a/pkg/api/handlers/libpod/secrets.go b/pkg/api/handlers/libpod/secrets.go new file mode 100644 index 000000000..447a5d021 --- /dev/null +++ b/pkg/api/handlers/libpod/secrets.go @@ -0,0 +1,40 @@ +package libpod + +import ( + "net/http" + + "github.com/containers/podman/v2/libpod" + "github.com/containers/podman/v2/pkg/api/handlers/utils" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/domain/infra/abi" + "github.com/gorilla/schema" + "github.com/pkg/errors" +) + +func CreateSecret(w http.ResponseWriter, r *http.Request) { + var ( + runtime = r.Context().Value("runtime").(*libpod.Runtime) + decoder = r.Context().Value("decoder").(*schema.Decoder) + ) + query := struct { + Name string `schema:"name"` + Driver string `schema:"driver"` + }{ + // override any golang type defaults + } + opts := entities.SecretCreateOptions{} + 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.Driver = query.Driver + + ic := abi.ContainerEngine{Libpod: runtime} + report, err := ic.SecretCreate(r.Context(), query.Name, r.Body, opts) + if err != nil { + utils.InternalServerError(w, err) + return + } + utils.WriteResponse(w, http.StatusOK, report) +} diff --git a/pkg/api/handlers/utils/errors.go b/pkg/api/handlers/utils/errors.go index e2c287c45..c8785fb89 100644 --- a/pkg/api/handlers/utils/errors.go +++ b/pkg/api/handlers/utils/errors.go @@ -80,6 +80,14 @@ func SessionNotFound(w http.ResponseWriter, name string, err error) { Error(w, msg, http.StatusNotFound, err) } +func SecretNotFound(w http.ResponseWriter, nameOrID string, err error) { + if errors.Cause(err).Error() != "no such secret" { + InternalServerError(w, err) + } + msg := fmt.Sprintf("No such secret: %s", nameOrID) + Error(w, msg, http.StatusNotFound, err) +} + func ContainerNotRunning(w http.ResponseWriter, containerID string, err error) { msg := fmt.Sprintf("Container %s is not running", containerID) Error(w, msg, http.StatusConflict, err) diff --git a/pkg/api/server/register_secrets.go b/pkg/api/server/register_secrets.go new file mode 100644 index 000000000..95abf83e8 --- /dev/null +++ b/pkg/api/server/register_secrets.go @@ -0,0 +1,194 @@ +package server + +import ( + "net/http" + + "github.com/containers/podman/v2/pkg/api/handlers/compat" + "github.com/containers/podman/v2/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerSecretHandlers(r *mux.Router) error { + // swagger:operation POST /libpod/secrets/create libpod libpodCreateSecret + // --- + // tags: + // - secrets + // summary: Create a secret + // parameters: + // - in: query + // name: name + // type: string + // description: User-defined name of the secret. + // required: true + // - in: query + // name: driver + // type: string + // description: Secret driver + // default: "file" + // - in: body + // name: request + // description: Secret + // schema: + // type: string + // produces: + // - application/json + // responses: + // '201': + // $ref: "#/responses/SecretCreateResponse" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/secrets/create"), s.APIHandler(libpod.CreateSecret)).Methods(http.MethodPost) + // swagger:operation GET /libpod/secrets/json libpod libpodListSecret + // --- + // tags: + // - secrets + // summary: List secrets + // description: Returns a list of secrets + // produces: + // - application/json + // parameters: + // responses: + // '200': + // "$ref": "#/responses/SecretListResponse" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/secrets/json"), s.APIHandler(compat.ListSecrets)).Methods(http.MethodGet) + // swagger:operation GET /libpod/secrets/{name}/json libpod libpodInspectSecret + // --- + // tags: + // - secrets + // summary: Inspect secret + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the secret + // produces: + // - application/json + // responses: + // '200': + // "$ref": "#/responses/SecretInspectResponse" + // '404': + // "$ref": "#/responses/NoSuchSecret" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/secrets/{name}/json"), s.APIHandler(compat.InspectSecret)).Methods(http.MethodGet) + // swagger:operation DELETE /libpod/secrets/{name} libpod libpodRemoveSecret + // --- + // tags: + // - secrets + // summary: Remove secret + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the secret + // - in: query + // name: all + // type: boolean + // description: Remove all secrets + // default: false + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchSecret" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/libpod/secrets/{name}"), s.APIHandler(compat.RemoveSecret)).Methods(http.MethodDelete) + + /* + * Docker compatibility endpoints + */ + // swagger:operation GET /secrets compat ListSecret + // --- + // tags: + // - secrets (compat) + // summary: List secrets + // description: Returns a list of secrets + // produces: + // - application/json + // parameters: + // responses: + // '200': + // "$ref": "#/responses/SecretListResponse" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/secrets"), s.APIHandler(compat.ListSecrets)).Methods(http.MethodGet) + r.Handle("/secrets", s.APIHandler(compat.ListSecrets)).Methods(http.MethodGet) + // swagger:operation POST /secrets/create compat CreateSecret + // --- + // tags: + // - secrets (compat) + // summary: Create a secret + // parameters: + // - in: body + // name: create + // description: | + // attributes for creating a secret + // schema: + // $ref: "#/definitions/SecretCreate" + // produces: + // - application/json + // responses: + // '201': + // $ref: "#/responses/SecretCreateResponse" + // '409': + // "$ref": "#/responses/SecretInUse" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/secrets/create"), s.APIHandler(compat.CreateSecret)).Methods(http.MethodPost) + r.Handle("/secrets/create", s.APIHandler(compat.CreateSecret)).Methods(http.MethodPost) + // swagger:operation GET /secrets/{name} compat InspectSecret + // --- + // tags: + // - secrets (compat) + // summary: Inspect secret + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the secret + // produces: + // - application/json + // responses: + // '200': + // "$ref": "#/responses/SecretInspectResponse" + // '404': + // "$ref": "#/responses/NoSuchSecret" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/secrets/{name}"), s.APIHandler(compat.InspectSecret)).Methods(http.MethodGet) + r.Handle("/secrets/{name}", s.APIHandler(compat.InspectSecret)).Methods(http.MethodGet) + // swagger:operation DELETE /secrets/{name} compat RemoveSecret + // --- + // tags: + // - secrets (compat) + // summary: Remove secret + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name or ID of the secret + // produces: + // - application/json + // responses: + // '204': + // description: no error + // '404': + // "$ref": "#/responses/NoSuchSecret" + // '500': + // "$ref": "#/responses/InternalError" + r.Handle(VersionedPath("/secrets/{name}"), s.APIHandler(compat.RemoveSecret)).Methods(http.MethodDelete) + r.Handle("/secret/{name}", s.APIHandler(compat.RemoveSecret)).Methods(http.MethodDelete) + + r.Handle(VersionedPath("/secrets/{name}/update"), s.APIHandler(compat.UpdateSecret)).Methods(http.MethodPost) + r.Handle("/secrets/{name}/update", s.APIHandler(compat.UpdateSecret)).Methods(http.MethodPost) + return nil +} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index d612041f6..6926eda62 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -124,6 +124,7 @@ func newServer(runtime *libpod.Runtime, duration time.Duration, listener *net.Li server.registerPlayHandlers, server.registerPluginsHandlers, server.registerPodsHandlers, + server.registerSecretHandlers, server.RegisterSwaggerHandlers, server.registerSwarmHandlers, server.registerSystemHandlers, diff --git a/pkg/api/tags.yaml b/pkg/api/tags.yaml index 0cfb3f440..bb56098eb 100644 --- a/pkg/api/tags.yaml +++ b/pkg/api/tags.yaml @@ -13,6 +13,8 @@ tags: description: Actions related to pods - name: volumes description: Actions related to volumes + - name: secrets + description: Actions related to secrets - name: system description: Actions related to Podman engine - name: containers (compat) @@ -25,5 +27,7 @@ tags: description: Actions related to compatibility networks - name: volumes (compat) description: Actions related to volumes for the compatibility endpoints + - name: secrets (compat) + description: Actions related to secrets for the compatibility endpoints - name: system (compat) description: Actions related to Podman and compatibility engines diff --git a/pkg/bindings/secrets/secrets.go b/pkg/bindings/secrets/secrets.go new file mode 100644 index 000000000..3fd70dcad --- /dev/null +++ b/pkg/bindings/secrets/secrets.go @@ -0,0 +1,78 @@ +package secrets + +import ( + "context" + "io" + "net/http" + + "github.com/containers/podman/v2/pkg/bindings" + "github.com/containers/podman/v2/pkg/domain/entities" +) + +// List returns information about existing secrets in the form of a slice. +func List(ctx context.Context, options *ListOptions) ([]*entities.SecretInfoReport, error) { + var ( + secrs []*entities.SecretInfoReport + ) + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/secrets/json", nil, nil) + if err != nil { + return secrs, err + } + return secrs, response.Process(&secrs) +} + +// Inspect returns low-level information about a secret. +func Inspect(ctx context.Context, nameOrID string, options *InspectOptions) (*entities.SecretInfoReport, error) { + var ( + inspect *entities.SecretInfoReport + ) + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + response, err := conn.DoRequest(nil, http.MethodGet, "/secrets/%s/json", nil, nil, nameOrID) + if err != nil { + return inspect, err + } + return inspect, response.Process(&inspect) +} + +// Remove removes a secret from storage +func Remove(ctx context.Context, nameOrID string) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + + response, err := conn.DoRequest(nil, http.MethodDelete, "/secrets/%s", nil, nil, nameOrID) + if err != nil { + return err + } + return response.Process(nil) +} + +// Create creates a secret given some data +func Create(ctx context.Context, reader io.Reader, options *CreateOptions) (*entities.SecretCreateReport, error) { + var ( + create *entities.SecretCreateReport + ) + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, err + } + + params, err := options.ToParams() + if err != nil { + return nil, err + } + + response, err := conn.DoRequest(reader, http.MethodPost, "/secrets/create", params, nil) + if err != nil { + return nil, err + } + return create, response.Process(&create) +} diff --git a/pkg/bindings/secrets/types.go b/pkg/bindings/secrets/types.go new file mode 100644 index 000000000..a98e894dc --- /dev/null +++ b/pkg/bindings/secrets/types.go @@ -0,0 +1,23 @@ +package secrets + +//go:generate go run ../generator/generator.go ListOptions +// ListOptions are optional options for inspecting secrets +type ListOptions struct { +} + +//go:generate go run ../generator/generator.go InspectOptions +// InspectOptions are optional options for inspecting secrets +type InspectOptions struct { +} + +//go:generate go run ../generator/generator.go RemoveOptions +// RemoveOptions are optional options for removing secrets +type RemoveOptions struct { +} + +//go:generate go run ../generator/generator.go CreateOptions +// CreateOptions are optional options for Creating secrets +type CreateOptions struct { + Driver *string + Name *string +} diff --git a/pkg/bindings/secrets/types_create_options.go b/pkg/bindings/secrets/types_create_options.go new file mode 100644 index 000000000..84cf38fa3 --- /dev/null +++ b/pkg/bindings/secrets/types_create_options.go @@ -0,0 +1,107 @@ +package secrets + +import ( + "net/url" + "reflect" + "strings" + + "github.com/containers/podman/v2/pkg/bindings/util" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +/* +This file is generated automatically by go generate. Do not edit. +*/ + +// Changed +func (o *CreateOptions) Changed(fieldName string) bool { + r := reflect.ValueOf(o) + value := reflect.Indirect(r).FieldByName(fieldName) + return !value.IsNil() +} + +// ToParams +func (o *CreateOptions) ToParams() (url.Values, error) { + params := url.Values{} + if o == nil { + return params, nil + } + json := jsoniter.ConfigCompatibleWithStandardLibrary + s := reflect.ValueOf(o) + if reflect.Ptr == s.Kind() { + s = s.Elem() + } + sType := s.Type() + for i := 0; i < s.NumField(); i++ { + fieldName := sType.Field(i).Name + if !o.Changed(fieldName) { + continue + } + fieldName = strings.ToLower(fieldName) + f := s.Field(i) + if reflect.Ptr == f.Kind() { + f = f.Elem() + } + switch { + case util.IsSimpleType(f): + params.Set(fieldName, util.SimpleTypeToParam(f)) + case f.Kind() == reflect.Slice: + for i := 0; i < f.Len(); i++ { + elem := f.Index(i) + if util.IsSimpleType(elem) { + params.Add(fieldName, util.SimpleTypeToParam(elem)) + } else { + return nil, errors.New("slices must contain only simple types") + } + } + case f.Kind() == reflect.Map: + lowerCaseKeys := make(map[string][]string) + iter := f.MapRange() + for iter.Next() { + lowerCaseKeys[iter.Key().Interface().(string)] = iter.Value().Interface().([]string) + + } + s, err := json.MarshalToString(lowerCaseKeys) + if err != nil { + return nil, err + } + + params.Set(fieldName, s) + } + + } + return params, nil +} + +// WithDriver +func (o *CreateOptions) WithDriver(value string) *CreateOptions { + v := &value + o.Driver = v + return o +} + +// GetDriver +func (o *CreateOptions) GetDriver() string { + var driver string + if o.Driver == nil { + return driver + } + return *o.Driver +} + +// WithName +func (o *CreateOptions) WithName(value string) *CreateOptions { + v := &value + o.Name = v + return o +} + +// GetName +func (o *CreateOptions) GetName() string { + var name string + if o.Name == nil { + return name + } + return *o.Name +} diff --git a/pkg/bindings/secrets/types_inspect_options.go b/pkg/bindings/secrets/types_inspect_options.go new file mode 100644 index 000000000..cd36b0531 --- /dev/null +++ b/pkg/bindings/secrets/types_inspect_options.go @@ -0,0 +1,75 @@ +package secrets + +import ( + "net/url" + "reflect" + "strings" + + "github.com/containers/podman/v2/pkg/bindings/util" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +/* +This file is generated automatically by go generate. Do not edit. +*/ + +// Changed +func (o *InspectOptions) Changed(fieldName string) bool { + r := reflect.ValueOf(o) + value := reflect.Indirect(r).FieldByName(fieldName) + return !value.IsNil() +} + +// ToParams +func (o *InspectOptions) ToParams() (url.Values, error) { + params := url.Values{} + if o == nil { + return params, nil + } + json := jsoniter.ConfigCompatibleWithStandardLibrary + s := reflect.ValueOf(o) + if reflect.Ptr == s.Kind() { + s = s.Elem() + } + sType := s.Type() + for i := 0; i < s.NumField(); i++ { + fieldName := sType.Field(i).Name + if !o.Changed(fieldName) { + continue + } + fieldName = strings.ToLower(fieldName) + f := s.Field(i) + if reflect.Ptr == f.Kind() { + f = f.Elem() + } + switch { + case util.IsSimpleType(f): + params.Set(fieldName, util.SimpleTypeToParam(f)) + case f.Kind() == reflect.Slice: + for i := 0; i < f.Len(); i++ { + elem := f.Index(i) + if util.IsSimpleType(elem) { + params.Add(fieldName, util.SimpleTypeToParam(elem)) + } else { + return nil, errors.New("slices must contain only simple types") + } + } + case f.Kind() == reflect.Map: + lowerCaseKeys := make(map[string][]string) + iter := f.MapRange() + for iter.Next() { + lowerCaseKeys[iter.Key().Interface().(string)] = iter.Value().Interface().([]string) + + } + s, err := json.MarshalToString(lowerCaseKeys) + if err != nil { + return nil, err + } + + params.Set(fieldName, s) + } + + } + return params, nil +} diff --git a/pkg/bindings/secrets/types_list_options.go b/pkg/bindings/secrets/types_list_options.go new file mode 100644 index 000000000..d313d8f73 --- /dev/null +++ b/pkg/bindings/secrets/types_list_options.go @@ -0,0 +1,75 @@ +package secrets + +import ( + "net/url" + "reflect" + "strings" + + "github.com/containers/podman/v2/pkg/bindings/util" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +/* +This file is generated automatically by go generate. Do not edit. +*/ + +// Changed +func (o *ListOptions) Changed(fieldName string) bool { + r := reflect.ValueOf(o) + value := reflect.Indirect(r).FieldByName(fieldName) + return !value.IsNil() +} + +// ToParams +func (o *ListOptions) ToParams() (url.Values, error) { + params := url.Values{} + if o == nil { + return params, nil + } + json := jsoniter.ConfigCompatibleWithStandardLibrary + s := reflect.ValueOf(o) + if reflect.Ptr == s.Kind() { + s = s.Elem() + } + sType := s.Type() + for i := 0; i < s.NumField(); i++ { + fieldName := sType.Field(i).Name + if !o.Changed(fieldName) { + continue + } + fieldName = strings.ToLower(fieldName) + f := s.Field(i) + if reflect.Ptr == f.Kind() { + f = f.Elem() + } + switch { + case util.IsSimpleType(f): + params.Set(fieldName, util.SimpleTypeToParam(f)) + case f.Kind() == reflect.Slice: + for i := 0; i < f.Len(); i++ { + elem := f.Index(i) + if util.IsSimpleType(elem) { + params.Add(fieldName, util.SimpleTypeToParam(elem)) + } else { + return nil, errors.New("slices must contain only simple types") + } + } + case f.Kind() == reflect.Map: + lowerCaseKeys := make(map[string][]string) + iter := f.MapRange() + for iter.Next() { + lowerCaseKeys[iter.Key().Interface().(string)] = iter.Value().Interface().([]string) + + } + s, err := json.MarshalToString(lowerCaseKeys) + if err != nil { + return nil, err + } + + params.Set(fieldName, s) + } + + } + return params, nil +} diff --git a/pkg/bindings/secrets/types_remove_options.go b/pkg/bindings/secrets/types_remove_options.go new file mode 100644 index 000000000..ca970e30e --- /dev/null +++ b/pkg/bindings/secrets/types_remove_options.go @@ -0,0 +1,75 @@ +package secrets + +import ( + "net/url" + "reflect" + "strings" + + "github.com/containers/podman/v2/pkg/bindings/util" + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +/* +This file is generated automatically by go generate. Do not edit. +*/ + +// Changed +func (o *RemoveOptions) Changed(fieldName string) bool { + r := reflect.ValueOf(o) + value := reflect.Indirect(r).FieldByName(fieldName) + return !value.IsNil() +} + +// ToParams +func (o *RemoveOptions) ToParams() (url.Values, error) { + params := url.Values{} + if o == nil { + return params, nil + } + json := jsoniter.ConfigCompatibleWithStandardLibrary + s := reflect.ValueOf(o) + if reflect.Ptr == s.Kind() { + s = s.Elem() + } + sType := s.Type() + for i := 0; i < s.NumField(); i++ { + fieldName := sType.Field(i).Name + if !o.Changed(fieldName) { + continue + } + fieldName = strings.ToLower(fieldName) + f := s.Field(i) + if reflect.Ptr == f.Kind() { + f = f.Elem() + } + switch { + case util.IsSimpleType(f): + params.Set(fieldName, util.SimpleTypeToParam(f)) + case f.Kind() == reflect.Slice: + for i := 0; i < f.Len(); i++ { + elem := f.Index(i) + if util.IsSimpleType(elem) { + params.Add(fieldName, util.SimpleTypeToParam(elem)) + } else { + return nil, errors.New("slices must contain only simple types") + } + } + case f.Kind() == reflect.Map: + lowerCaseKeys := make(map[string][]string) + iter := f.MapRange() + for iter.Next() { + lowerCaseKeys[iter.Key().Interface().(string)] = iter.Value().Interface().([]string) + + } + s, err := json.MarshalToString(lowerCaseKeys) + if err != nil { + return nil, err + } + + params.Set(fieldName, s) + } + + } + return params, nil +} diff --git a/pkg/bindings/test/secrets_test.go b/pkg/bindings/test/secrets_test.go new file mode 100644 index 000000000..17c043e4b --- /dev/null +++ b/pkg/bindings/test/secrets_test.go @@ -0,0 +1,133 @@ +package test_bindings + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/containers/podman/v2/pkg/bindings" + "github.com/containers/podman/v2/pkg/bindings/secrets" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Podman secrets", func() { + var ( + bt *bindingTest + s *gexec.Session + connText context.Context + err error + ) + + BeforeEach(func() { + bt = newBindingTest() + bt.RestoreImagesFromCache() + s = bt.startAPIService() + time.Sleep(1 * time.Second) + connText, err = bindings.NewConnection(context.Background(), bt.sock) + Expect(err).To(BeNil()) + }) + + AfterEach(func() { + + s.Kill() + bt.cleanup() + }) + + It("create secret", func() { + r := strings.NewReader("mysecret") + name := "mysecret" + opts := &secrets.CreateOptions{ + Name: &name, + } + _, err := secrets.Create(connText, r, opts) + Expect(err).To(BeNil()) + + // should not be allowed to create duplicate secret name + _, err = secrets.Create(connText, r, opts) + Expect(err).To(Not(BeNil())) + }) + + It("inspect secret", func() { + r := strings.NewReader("mysecret") + name := "mysecret" + opts := &secrets.CreateOptions{ + Name: &name, + } + _, err := secrets.Create(connText, r, opts) + Expect(err).To(BeNil()) + + data, err := secrets.Inspect(connText, name, nil) + Expect(err).To(BeNil()) + Expect(data.Spec.Name).To(Equal(name)) + + // inspecting non-existent secret should fail + data, err = secrets.Inspect(connText, "notasecret", nil) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + }) + + It("list secret", func() { + r := strings.NewReader("mysecret") + name := "mysecret" + opts := &secrets.CreateOptions{ + Name: &name, + } + _, err := secrets.Create(connText, r, opts) + Expect(err).To(BeNil()) + + data, err := secrets.List(connText, nil) + Expect(err).To(BeNil()) + Expect(data[0].Spec.Name).To(Equal(name)) + }) + + It("list multiple secret", func() { + r := strings.NewReader("mysecret") + name := "mysecret" + opts := &secrets.CreateOptions{ + Name: &name, + } + _, err := secrets.Create(connText, r, opts) + Expect(err).To(BeNil()) + + r2 := strings.NewReader("mysecret2") + name2 := "mysecret2" + opts2 := &secrets.CreateOptions{ + Name: &name2, + } + _, err = secrets.Create(connText, r2, opts2) + Expect(err).To(BeNil()) + + data, err := secrets.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(data)).To(Equal(2)) + }) + + It("list no secrets", func() { + data, err := secrets.List(connText, nil) + Expect(err).To(BeNil()) + Expect(len(data)).To(Equal(0)) + }) + + It("remove secret", func() { + r := strings.NewReader("mysecret") + name := "mysecret" + opts := &secrets.CreateOptions{ + Name: &name, + } + _, err := secrets.Create(connText, r, opts) + Expect(err).To(BeNil()) + + err = secrets.Remove(connText, name) + Expect(err).To(BeNil()) + + // removing non-existent secret should fail + err = secrets.Remove(connText, "nosecret") + Expect(err).To(Not(BeNil())) + code, _ := bindings.CheckResponseCode(err) + Expect(code).To(BeNumerically("==", http.StatusNotFound)) + }) + +}) diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 39bda1d72..9ff1714e7 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -81,6 +81,10 @@ type ContainerEngine interface { PodTop(ctx context.Context, options PodTopOptions) (*StringSliceReport, error) PodUnpause(ctx context.Context, namesOrIds []string, options PodunpauseOptions) ([]*PodUnpauseReport, error) SetupRootless(ctx context.Context, cmd *cobra.Command) error + SecretCreate(ctx context.Context, name string, reader io.Reader, options SecretCreateOptions) (*SecretCreateReport, error) + SecretInspect(ctx context.Context, nameOrIDs []string) ([]*SecretInfoReport, []error, error) + SecretList(ctx context.Context) ([]*SecretInfoReport, error) + SecretRm(ctx context.Context, nameOrID []string, opts SecretRmOptions) ([]*SecretRmReport, error) Shutdown(ctx context.Context) SystemDf(ctx context.Context, options SystemDfOptions) (*SystemDfReport, error) Unshare(ctx context.Context, args []string) error diff --git a/pkg/domain/entities/secrets.go b/pkg/domain/entities/secrets.go new file mode 100644 index 000000000..3cad4c099 --- /dev/null +++ b/pkg/domain/entities/secrets.go @@ -0,0 +1,104 @@ +package entities + +import ( + "time" + + "github.com/containers/podman/v2/pkg/errorhandling" +) + +type SecretCreateReport struct { + ID string +} + +type SecretCreateOptions struct { + Driver string +} + +type SecretListRequest struct { + Filters map[string]string +} + +type SecretListReport struct { + ID string + Name string + Driver string + CreatedAt string + UpdatedAt string +} + +type SecretRmOptions struct { + All bool +} + +type SecretRmReport struct { + ID string + Err error +} + +type SecretInfoReport struct { + ID string + CreatedAt time.Time + UpdatedAt time.Time + Spec SecretSpec +} + +type SecretSpec struct { + Name string + Driver SecretDriverSpec +} + +type SecretDriverSpec struct { + Name string + Options map[string]string +} + +// swagger:model SecretCreate +type SecretCreateRequest struct { + // User-defined name of the secret. + Name string + // Base64-url-safe-encoded (RFC 4648) data to store as secret. + Data string + // Driver represents a driver (default "file") + Driver SecretDriverSpec +} + +// Secret create response +// swagger:response SecretCreateResponse +type SwagSecretCreateResponse struct { + // in:body + Body struct { + SecretCreateReport + } +} + +// Secret list response +// swagger:response SecretListResponse +type SwagSecretListResponse struct { + // in:body + Body []*SecretInfoReport +} + +// Secret inspect response +// swagger:response SecretInspectResponse +type SwagSecretInspectResponse struct { + // in:body + Body SecretInfoReport +} + +// No such secret +// swagger:response NoSuchSecret +type SwagErrNoSuchSecret struct { + // in:body + Body struct { + errorhandling.ErrorModel + } +} + +// Secret in use +// swagger:response SecretInUse +type SwagErrSecretInUse struct { + // in:body + Body struct { + errorhandling.ErrorModel + } +} diff --git a/pkg/domain/infra/abi/secrets.go b/pkg/domain/infra/abi/secrets.go new file mode 100644 index 000000000..b1fe60e01 --- /dev/null +++ b/pkg/domain/infra/abi/secrets.go @@ -0,0 +1,138 @@ +package abi + +import ( + "context" + "io" + "io/ioutil" + "path/filepath" + + "github.com/containers/common/pkg/secrets" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/pkg/errors" +) + +func (ic *ContainerEngine) SecretCreate(ctx context.Context, name string, reader io.Reader, options entities.SecretCreateOptions) (*entities.SecretCreateReport, error) { + data, _ := ioutil.ReadAll(reader) + secretsPath := ic.Libpod.GetSecretsStorageDir() + manager, err := secrets.NewManager(secretsPath) + if err != nil { + return nil, err + } + driverOptions := make(map[string]string) + + if options.Driver == "" { + options.Driver = "file" + } + if options.Driver == "file" { + driverOptions["path"] = filepath.Join(secretsPath, "filedriver") + } + secretID, err := manager.Store(name, data, options.Driver, driverOptions) + if err != nil { + return nil, err + } + return &entities.SecretCreateReport{ + ID: secretID, + }, nil +} + +func (ic *ContainerEngine) SecretInspect(ctx context.Context, nameOrIDs []string) ([]*entities.SecretInfoReport, []error, error) { + secretsPath := ic.Libpod.GetSecretsStorageDir() + manager, err := secrets.NewManager(secretsPath) + if err != nil { + return nil, nil, err + } + errs := make([]error, 0, len(nameOrIDs)) + reports := make([]*entities.SecretInfoReport, 0, len(nameOrIDs)) + for _, nameOrID := range nameOrIDs { + secret, err := manager.Lookup(nameOrID) + if err != nil { + if errors.Cause(err).Error() == "no such secret" { + errs = append(errs, err) + continue + } else { + return nil, nil, errors.Wrapf(err, "error inspecting secret %s", nameOrID) + } + } + report := &entities.SecretInfoReport{ + ID: secret.ID, + CreatedAt: secret.CreatedAt, + UpdatedAt: secret.CreatedAt, + Spec: entities.SecretSpec{ + Name: secret.Name, + Driver: entities.SecretDriverSpec{ + Name: secret.Driver, + }, + }, + } + reports = append(reports, report) + + } + + return reports, errs, nil +} + +func (ic *ContainerEngine) SecretList(ctx context.Context) ([]*entities.SecretInfoReport, error) { + secretsPath := ic.Libpod.GetSecretsStorageDir() + manager, err := secrets.NewManager(secretsPath) + if err != nil { + return nil, err + } + secretList, err := manager.List() + if err != nil { + return nil, err + } + report := make([]*entities.SecretInfoReport, 0, len(secretList)) + for _, secret := range secretList { + reportItem := entities.SecretInfoReport{ + ID: secret.ID, + CreatedAt: secret.CreatedAt, + UpdatedAt: secret.CreatedAt, + Spec: entities.SecretSpec{ + Name: secret.Name, + Driver: entities.SecretDriverSpec{ + Name: secret.Driver, + Options: secret.DriverOptions, + }, + }, + } + report = append(report, &reportItem) + } + return report, nil +} + +func (ic *ContainerEngine) SecretRm(ctx context.Context, nameOrIDs []string, options entities.SecretRmOptions) ([]*entities.SecretRmReport, error) { + var ( + err error + toRemove []string + reports = []*entities.SecretRmReport{} + ) + secretsPath := ic.Libpod.GetSecretsStorageDir() + manager, err := secrets.NewManager(secretsPath) + if err != nil { + return nil, err + } + toRemove = nameOrIDs + if options.All { + allSecrs, err := manager.List() + if err != nil { + return nil, err + } + for _, secr := range allSecrs { + toRemove = append(toRemove, secr.ID) + } + } + for _, nameOrID := range toRemove { + deletedID, err := manager.Delete(nameOrID) + if err == nil || errors.Cause(err).Error() == "no such secret" { + reports = append(reports, &entities.SecretRmReport{ + Err: err, + ID: deletedID, + }) + continue + } else { + return nil, err + } + } + + return reports, nil +} diff --git a/pkg/domain/infra/tunnel/secrets.go b/pkg/domain/infra/tunnel/secrets.go new file mode 100644 index 000000000..f7c0f7d13 --- /dev/null +++ b/pkg/domain/infra/tunnel/secrets.go @@ -0,0 +1,82 @@ +package tunnel + +import ( + "context" + "io" + + "github.com/containers/podman/v2/pkg/bindings/secrets" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/containers/podman/v2/pkg/errorhandling" + "github.com/pkg/errors" +) + +func (ic *ContainerEngine) SecretCreate(ctx context.Context, name string, reader io.Reader, options entities.SecretCreateOptions) (*entities.SecretCreateReport, error) { + opts := new(secrets.CreateOptions).WithDriver(options.Driver).WithName(name) + created, _ := secrets.Create(ic.ClientCtx, reader, opts) + return created, nil +} + +func (ic *ContainerEngine) SecretInspect(ctx context.Context, nameOrIDs []string) ([]*entities.SecretInfoReport, []error, error) { + allInspect := make([]*entities.SecretInfoReport, 0, len(nameOrIDs)) + errs := make([]error, 0, len(nameOrIDs)) + for _, name := range nameOrIDs { + inspected, err := secrets.Inspect(ic.ClientCtx, name, nil) + if err != nil { + errModel, ok := err.(errorhandling.ErrorModel) + if !ok { + return nil, nil, err + } + if errModel.ResponseCode == 404 { + errs = append(errs, errors.Errorf("no such secret %q", name)) + continue + } + return nil, nil, err + } + allInspect = append(allInspect, inspected) + } + return allInspect, errs, nil +} + +func (ic *ContainerEngine) SecretList(ctx context.Context) ([]*entities.SecretInfoReport, error) { + secrs, _ := secrets.List(ic.ClientCtx, nil) + return secrs, nil +} + +func (ic *ContainerEngine) SecretRm(ctx context.Context, nameOrIDs []string, options entities.SecretRmOptions) ([]*entities.SecretRmReport, error) { + allRm := make([]*entities.SecretRmReport, 0, len(nameOrIDs)) + if options.All { + allSecrets, err := secrets.List(ic.ClientCtx, nil) + if err != nil { + return nil, err + } + for _, secret := range allSecrets { + allRm = append(allRm, &entities.SecretRmReport{ + Err: secrets.Remove(ic.ClientCtx, secret.ID), + ID: secret.ID, + }) + } + return allRm, nil + } + for _, name := range nameOrIDs { + secret, err := secrets.Inspect(ic.ClientCtx, name, nil) + if err != nil { + errModel, ok := err.(errorhandling.ErrorModel) + if !ok { + return nil, err + } + if errModel.ResponseCode == 404 { + allRm = append(allRm, &entities.SecretRmReport{ + Err: errors.Errorf("no secret with name or id %q: no such secret ", name), + ID: "", + }) + continue + } + } + allRm = append(allRm, &entities.SecretRmReport{ + Err: secrets.Remove(ic.ClientCtx, name), + ID: secret.ID, + }) + + } + return allRm, nil +} diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 1bc050b00..74291325c 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -359,6 +359,10 @@ func createContainerOptions(ctx context.Context, rt *libpod.Runtime, s *specgen. options = append(options, libpod.WithHealthCheck(s.ContainerHealthCheckConfig.HealthConfig)) logrus.Debugf("New container has a health check") } + + if len(s.Secrets) != 0 { + options = append(options, libpod.WithSecrets(s.Secrets)) + } return options, nil } diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index a6cc0a730..732579bf0 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -237,6 +237,9 @@ type ContainerStorageConfig struct { // If not set, the default of rslave will be used. // Optional. RootfsPropagation string `json:"rootfs_propagation,omitempty"` + // Secrets are the secrets that will be added to the container + // Optional. + Secrets []string `json:"secrets,omitempty"` } // ContainerSecurityConfig is a container's security features, including diff --git a/test/apiv2/50-secrets.at b/test/apiv2/50-secrets.at new file mode 100644 index 000000000..1ef43381a --- /dev/null +++ b/test/apiv2/50-secrets.at @@ -0,0 +1,36 @@ +# -*- sh -*- +# +# secret-related tests +# + +# secret create +t POST secrets/create '"Name":"mysecret","Data":"c2VjcmV0"' 200\ + .ID~.* \ + +# secret create unsupported labels +t POST secrets/create '"Name":"mysecret","Data":"c2VjcmV0","Labels":{"fail":"fail"}' 400 + +# secret create name already in use +t POST secrets/create '"Name":"mysecret","Data":"c2VjcmV0"' 409 + +# secret inspect +t GET secrets/mysecret 200\ + .Spec.Name=mysecret + +# secret inspect non-existent secret +t GET secrets/bogus 404 + +# secret list +t GET secrets 200\ + length=1 + +# secret list unsupported filters +t GET secrets?filters=%7B%22name%22%3A%5B%22foo1%22%5D%7D 400 + +# secret rm +t DELETE secrets/mysecret 204 +# secret rm non-existent secret +t DELETE secrets/bogus 404 + +# secret update not implemented +t POST secrets/mysecret/update "" 501 diff --git a/test/e2e/commit_test.go b/test/e2e/commit_test.go index 3c7bbca66..8760978fd 100644 --- a/test/e2e/commit_test.go +++ b/test/e2e/commit_test.go @@ -279,4 +279,29 @@ var _ = Describe("Podman commit", func() { data := check.InspectImageJSON() Expect(data[0].ID).To(Equal(string(id))) }) + + It("podman commit should not commit secret", func() { + secretsString := "somesecretdata" + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte(secretsString), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "mysecret", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "--secret", "mysecret", "--name", "secr", ALPINE, "cat", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(secretsString)) + + session = podmanTest.Podman([]string{"commit", "secr", "foobar.com/test1-image:latest"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "foobar.com/test1-image:latest", "cat", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + + }) }) diff --git a/test/e2e/common_test.go b/test/e2e/common_test.go index 54d801e12..53810d882 100644 --- a/test/e2e/common_test.go +++ b/test/e2e/common_test.go @@ -491,6 +491,21 @@ func (p *PodmanTestIntegration) CleanupVolume() { p.Cleanup() } +// CleanupSecret cleans up the temporary store +func (p *PodmanTestIntegration) CleanupSecrets() { + // Remove all containers + session := p.Podman([]string{"secret", "rm", "-a"}) + session.Wait(90) + + // Stop remove service on secret cleanup + p.StopRemoteService() + + // Nuke tempdir + if err := os.RemoveAll(p.TempDir); err != nil { + fmt.Printf("%q\n", err) + } +} + // InspectContainerToJSON takes the session output of an inspect // container and returns json func (s *PodmanSessionIntegration) InspectContainerToJSON() []define.InspectContainerData { diff --git a/test/e2e/run_test.go b/test/e2e/run_test.go index caeaf190e..76d362288 100644 --- a/test/e2e/run_test.go +++ b/test/e2e/run_test.go @@ -668,8 +668,8 @@ USER bin` Expect(session.ExitCode()).To(Equal(0)) }) - It("podman run with secrets", func() { - SkipIfRemote("--default-mounts-file option is not supported in podman-remote") + It("podman run with subscription secrets", func() { + SkipIfRemote("--default-mount-file option is not supported in podman-remote") containersDir := filepath.Join(podmanTest.TempDir, "containers") err := os.MkdirAll(containersDir, 0755) Expect(err).To(BeNil()) @@ -1448,4 +1448,26 @@ WORKDIR /madethis` Expect(session.ExitCode()).To(Equal(0)) Expect(session.OutputToString()).To(ContainSubstring(hostnameEnv)) }) + + It("podman run --secret", func() { + secretsString := "somesecretdata" + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte(secretsString), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "mysecret", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"run", "--secret", "mysecret", "--name", "secr", ALPINE, "cat", "/run/secrets/mysecret"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(Equal(secretsString)) + + session = podmanTest.Podman([]string{"inspect", "secr", "--format", " {{(index .Config.Secrets 0).Name}}"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(session.OutputToString()).To(ContainSubstring("mysecret")) + + }) }) diff --git a/test/e2e/secret_test.go b/test/e2e/secret_test.go new file mode 100644 index 000000000..6dad605c5 --- /dev/null +++ b/test/e2e/secret_test.go @@ -0,0 +1,202 @@ +package integration + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "github.com/containers/podman/v2/test/utils" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Podman secret", func() { + var ( + tempdir string + err error + podmanTest *PodmanTestIntegration + ) + + BeforeEach(func() { + tempdir, err = CreateTempDirInTempDir() + if err != nil { + os.Exit(1) + } + podmanTest = PodmanTestCreate(tempdir) + podmanTest.Setup() + podmanTest.SeedImages() + }) + + AfterEach(func() { + podmanTest.CleanupSecrets() + f := CurrentGinkgoTestDescription() + processTestResult(f) + + }) + + It("podman secret create", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + secrID := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"secret", "inspect", "--format", "{{.ID}}", secrID}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.OutputToString()).To(Equal(secrID)) + }) + + It("podman secret create bad name should fail", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "?!", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Not(Equal(0))) + }) + + It("podman secret inspect", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + secrID := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"secret", "inspect", secrID}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.IsJSONOutputValid()).To(BeTrue()) + }) + + It("podman secret inspect with --format", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + secrID := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"secret", "inspect", "--format", "{{.ID}}", secrID}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.OutputToString()).To(Equal(secrID)) + }) + + It("podman secret inspect multiple secrets", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + secrID := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + session2 := podmanTest.Podman([]string{"secret", "create", "b", secretFilePath}) + session2.WaitWithDefaultTimeout() + secrID2 := session2.OutputToString() + Expect(session2.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"secret", "inspect", secrID, secrID2}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.IsJSONOutputValid()).To(BeTrue()) + }) + + It("podman secret inspect bogus", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + inspect := podmanTest.Podman([]string{"secret", "inspect", "bogus"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Not(Equal(0))) + + }) + + It("podman secret ls", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + list := podmanTest.Podman([]string{"secret", "ls"}) + list.WaitWithDefaultTimeout() + Expect(list.ExitCode()).To(Equal(0)) + Expect(len(list.OutputToStringArray())).To(Equal(2)) + + }) + + It("podman secret ls with Go template", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + list := podmanTest.Podman([]string{"secret", "ls", "--format", "table {{.Name}}"}) + list.WaitWithDefaultTimeout() + + Expect(list.ExitCode()).To(Equal(0)) + Expect(len(list.OutputToStringArray())).To(Equal(2), list.OutputToString()) + }) + + It("podman secret rm", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + secrID := session.OutputToString() + Expect(session.ExitCode()).To(Equal(0)) + + removed := podmanTest.Podman([]string{"secret", "rm", "a"}) + removed.WaitWithDefaultTimeout() + Expect(removed.ExitCode()).To(Equal(0)) + Expect(removed.OutputToString()).To(Equal(secrID)) + + session = podmanTest.Podman([]string{"secret", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(1)) + }) + + It("podman secret rm --all", func() { + secretFilePath := filepath.Join(podmanTest.TempDir, "secret") + err := ioutil.WriteFile(secretFilePath, []byte("mysecret"), 0755) + Expect(err).To(BeNil()) + + session := podmanTest.Podman([]string{"secret", "create", "a", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + session = podmanTest.Podman([]string{"secret", "create", "b", secretFilePath}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + removed := podmanTest.Podman([]string{"secret", "rm", "-a"}) + removed.WaitWithDefaultTimeout() + Expect(removed.ExitCode()).To(Equal(0)) + + session = podmanTest.Podman([]string{"secret", "ls"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + Expect(len(session.OutputToStringArray())).To(Equal(1)) + }) + +}) diff --git a/vendor/github.com/containers/common/pkg/secrets/filedriver/filedriver.go b/vendor/github.com/containers/common/pkg/secrets/filedriver/filedriver.go new file mode 100644 index 000000000..37edc16be --- /dev/null +++ b/vendor/github.com/containers/common/pkg/secrets/filedriver/filedriver.go @@ -0,0 +1,158 @@ +package filedriver + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + "sort" + + "github.com/containers/storage/pkg/lockfile" + "github.com/pkg/errors" +) + +// secretsDataFile is the file where secrets data/payload will be stored +var secretsDataFile = "secretsdata.json" + +// errNoSecretData indicates that there is not data associated with an id +var errNoSecretData = errors.New("no secret data with ID") + +// errNoSecretData indicates that there is secret data already associated with an id +var errSecretIDExists = errors.New("secret data with ID already exists") + +// Driver is the filedriver object +type Driver struct { + // secretsDataFilePath is the path to the secretsfile + secretsDataFilePath string + // lockfile is the filedriver lockfile + lockfile lockfile.Locker +} + +// NewDriver creates a new file driver. +// rootPath is the directory where the secrets data file resides. +func NewDriver(rootPath string) (*Driver, error) { + fileDriver := new(Driver) + fileDriver.secretsDataFilePath = filepath.Join(rootPath, secretsDataFile) + // the lockfile functions requre that the rootPath dir is executable + if err := os.MkdirAll(rootPath, 0700); err != nil { + return nil, err + } + + lock, err := lockfile.GetLockfile(filepath.Join(rootPath, "secretsdata.lock")) + if err != nil { + return nil, err + } + fileDriver.lockfile = lock + + return fileDriver, nil +} + +// List returns all secret IDs +func (d *Driver) List() ([]string, error) { + d.lockfile.Lock() + defer d.lockfile.Unlock() + secretData, err := d.getAllData() + if err != nil { + return nil, err + } + var allID []string + for k := range secretData { + allID = append(allID, k) + } + sort.Strings(allID) + return allID, err +} + +// Lookup returns the bytes associated with a secret ID +func (d *Driver) Lookup(id string) ([]byte, error) { + d.lockfile.Lock() + defer d.lockfile.Unlock() + + secretData, err := d.getAllData() + if err != nil { + return nil, err + } + if data, ok := secretData[id]; ok { + return data, nil + } + return nil, errors.Wrapf(errNoSecretData, "%s", id) +} + +// Store stores the bytes associated with an ID. An error is returned if the ID arleady exists +func (d *Driver) Store(id string, data []byte) error { + d.lockfile.Lock() + defer d.lockfile.Unlock() + + secretData, err := d.getAllData() + if err != nil { + return err + } + if _, ok := secretData[id]; ok { + return errors.Wrapf(errSecretIDExists, "%s", id) + } + secretData[id] = data + marshalled, err := json.MarshalIndent(secretData, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile(d.secretsDataFilePath, marshalled, 0600) + if err != nil { + return err + } + return nil +} + +// Delete deletes the secret associated with the specified ID. An error is returned if no matching secret is found. +func (d *Driver) Delete(id string) error { + d.lockfile.Lock() + defer d.lockfile.Unlock() + secretData, err := d.getAllData() + if err != nil { + return err + } + if _, ok := secretData[id]; ok { + delete(secretData, id) + } else { + return errors.Wrap(errNoSecretData, id) + } + marshalled, err := json.MarshalIndent(secretData, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile(d.secretsDataFilePath, marshalled, 0600) + if err != nil { + return err + } + return nil +} + +// getAllData reads the data file and returns all data +func (d *Driver) getAllData() (map[string][]byte, error) { + // check if the db file exists + _, err := os.Stat(d.secretsDataFilePath) + if err != nil { + if os.IsNotExist(err) { + // the file will be created later on a store() + return make(map[string][]byte), nil + } else { + return nil, err + } + } + + file, err := os.Open(d.secretsDataFilePath) + if err != nil { + return nil, err + } + defer file.Close() + + byteValue, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + secretData := new(map[string][]byte) + err = json.Unmarshal([]byte(byteValue), secretData) + if err != nil { + return nil, err + } + return *secretData, nil +} diff --git a/vendor/github.com/containers/common/pkg/secrets/secrets.go b/vendor/github.com/containers/common/pkg/secrets/secrets.go new file mode 100644 index 000000000..5e0fb3e9d --- /dev/null +++ b/vendor/github.com/containers/common/pkg/secrets/secrets.go @@ -0,0 +1,282 @@ +package secrets + +import ( + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/containers/common/pkg/secrets/filedriver" + "github.com/containers/storage/pkg/lockfile" + "github.com/containers/storage/pkg/stringid" + "github.com/pkg/errors" +) + +// maxSecretSize is the max size for secret data - 512kB +const maxSecretSize = 512000 + +// secretIDLength is the character length of a secret ID - 25 +const secretIDLength = 25 + +// errInvalidPath indicates that the secrets path is invalid +var errInvalidPath = errors.New("invalid secrets path") + +// errNoSuchSecret indicates that the secret does not exist +var errNoSuchSecret = errors.New("no such secret") + +// errSecretNameInUse indicates that the secret name is already in use +var errSecretNameInUse = errors.New("secret name in use") + +// errInvalidSecretName indicates that the secret name is invalid +var errInvalidSecretName = errors.New("invalid secret name") + +// errInvalidDriver indicates that the driver type is invalid +var errInvalidDriver = errors.New("invalid driver") + +// errInvalidDriverOpt indicates that a driver option is invalid +var errInvalidDriverOpt = errors.New("invalid driver option") + +// errAmbiguous indicates that a secret is ambiguous +var errAmbiguous = errors.New("secret is ambiguous") + +// errDataSize indicates that the secret data is too large or too small +var errDataSize = errors.New("secret data must be larger than 0 and less than 512000 bytes") + +// secretsFile is the name of the file that the secrets database will be stored in +var secretsFile = "secrets.json" + +// secretNameRegexp matches valid secret names +// Allowed: 64 [a-zA-Z0-9-_.] characters, and the start and end character must be [a-zA-Z0-9] +var secretNameRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`) + +// SecretsManager holds information on handling secrets +type SecretsManager struct { + // secretsPath is the path to the db file where secrets are stored + secretsDBPath string + // lockfile is the locker for the secrets file + lockfile lockfile.Locker + // db is an in-memory cache of the database of secrets + db *db +} + +// Secret defines a secret +type Secret struct { + // Name is the name of the secret + Name string `json:"name"` + // ID is the unique secret ID + ID string `json:"id"` + // Metadata stores other metadata on the secret + Metadata map[string]string `json:"metadata,omitempty"` + // CreatedAt is when the secret was created + CreatedAt time.Time `json:"createdAt"` + // Driver is the driver used to store secret data + Driver string `json:"driver"` + // DriverOptions is other metadata needed to use the driver + DriverOptions map[string]string `json:"driverOptions"` +} + +// SecretsDriver interfaces with the secrets data store. +// The driver stores the actual bytes of secret data, as opposed to +// the secret metadata. +// Currently only the unencrypted filedriver is implemented. +type SecretsDriver interface { + // List lists all secret ids in the secrets data store + List() ([]string, error) + // Lookup gets the secret's data bytes + Lookup(id string) ([]byte, error) + // Store stores the secret's data bytes + Store(id string, data []byte) error + // Delete deletes a secret's data from the driver + Delete(id string) error +} + +// NewManager creates a new secrets manager +// rootPath is the directory where the secrets data file resides +func NewManager(rootPath string) (*SecretsManager, error) { + manager := new(SecretsManager) + + if !filepath.IsAbs(rootPath) { + return nil, errors.Wrapf(errInvalidPath, "path must be absolute: %s", rootPath) + } + // the lockfile functions requre that the rootPath dir is executable + if err := os.MkdirAll(rootPath, 0700); err != nil { + return nil, err + } + + lock, err := lockfile.GetLockfile(filepath.Join(rootPath, "secrets.lock")) + if err != nil { + return nil, err + } + manager.lockfile = lock + manager.secretsDBPath = filepath.Join(rootPath, secretsFile) + manager.db = new(db) + manager.db.Secrets = make(map[string]Secret) + manager.db.NameToID = make(map[string]string) + manager.db.IDToName = make(map[string]string) + return manager, nil +} + +// Store takes a name, creates a secret and stores the secret metadata and the secret payload. +// It returns a generated ID that is associated with the secret. +// The max size for secret data is 512kB. +func (s *SecretsManager) Store(name string, data []byte, driverType string, driverOpts map[string]string) (string, error) { + err := validateSecretName(name) + if err != nil { + return "", err + } + + if !(len(data) > 0 && len(data) < maxSecretSize) { + return "", errDataSize + } + + s.lockfile.Lock() + defer s.lockfile.Unlock() + + exist, err := s.exactSecretExists(name) + if err != nil { + return "", err + } + if exist { + return "", errors.Wrapf(errSecretNameInUse, name) + } + + secr := new(Secret) + secr.Name = name + + for { + newID := stringid.GenerateNonCryptoID() + // GenerateNonCryptoID() gives 64 characters, so we truncate to correct length + newID = newID[0:secretIDLength] + _, err := s.lookupSecret(newID) + if err != nil { + if errors.Cause(err) == errNoSuchSecret { + secr.ID = newID + break + } else { + return "", err + } + } + } + + secr.Driver = driverType + secr.Metadata = make(map[string]string) + secr.CreatedAt = time.Now() + secr.DriverOptions = driverOpts + + driver, err := getDriver(driverType, driverOpts) + if err != nil { + return "", err + } + err = driver.Store(secr.ID, data) + if err != nil { + return "", errors.Wrapf(err, "error creating secret %s", name) + } + + err = s.store(secr) + if err != nil { + return "", errors.Wrapf(err, "error creating secret %s", name) + } + + return secr.ID, nil +} + +// Delete removes all secret metadata and secret data associated with the specified secret. +// Delete takes a name, ID, or partial ID. +func (s *SecretsManager) Delete(nameOrID string) (string, error) { + err := validateSecretName(nameOrID) + if err != nil { + return "", err + } + + s.lockfile.Lock() + defer s.lockfile.Unlock() + + secret, err := s.lookupSecret(nameOrID) + if err != nil { + return "", err + } + secretID := secret.ID + + driver, err := getDriver(secret.Driver, secret.DriverOptions) + if err != nil { + return "", err + } + + err = driver.Delete(secretID) + if err != nil { + return "", errors.Wrapf(err, "error deleting secret %s", nameOrID) + } + + err = s.delete(secretID) + if err != nil { + return "", errors.Wrapf(err, "error deleting secret %s", nameOrID) + } + return secretID, nil +} + +// Lookup gives a secret's metadata given its name, ID, or partial ID. +func (s *SecretsManager) Lookup(nameOrID string) (*Secret, error) { + s.lockfile.Lock() + defer s.lockfile.Unlock() + + return s.lookupSecret(nameOrID) +} + +// List lists all secrets. +func (s *SecretsManager) List() ([]Secret, error) { + s.lockfile.Lock() + defer s.lockfile.Unlock() + + secrets, err := s.lookupAll() + if err != nil { + return nil, err + } + var ls []Secret + for _, v := range secrets { + ls = append(ls, v) + + } + return ls, nil +} + +// LookupSecretData returns secret metadata as well as secret data in bytes. +// The secret data can be looked up using its name, ID, or partial ID. +func (s *SecretsManager) LookupSecretData(nameOrID string) (*Secret, []byte, error) { + s.lockfile.Lock() + defer s.lockfile.Unlock() + + secret, err := s.lookupSecret(nameOrID) + if err != nil { + return nil, nil, err + } + driver, err := getDriver(secret.Driver, secret.DriverOptions) + if err != nil { + return nil, nil, err + } + data, err := driver.Lookup(secret.ID) + if err != nil { + return nil, nil, err + } + return secret, data, nil +} + +// validateSecretName checks if the secret name is valid. +func validateSecretName(name string) error { + if !secretNameRegexp.MatchString(name) || len(name) > 64 || strings.HasSuffix(name, "-") || strings.HasSuffix(name, ".") { + return errors.Wrapf(errInvalidSecretName, "only 64 [a-zA-Z0-9-_.] characters allowed, and the start and end character must be [a-zA-Z0-9]: %s", name) + } + return nil +} + +// getDriver creates a new driver. +func getDriver(name string, opts map[string]string) (SecretsDriver, error) { + if name == "file" { + if path, ok := opts["path"]; ok { + return filedriver.NewDriver(path) + } else { + return nil, errors.Wrap(errInvalidDriverOpt, "need path for filedriver") + } + } + return nil, errInvalidDriver +} diff --git a/vendor/github.com/containers/common/pkg/secrets/secretsdb.go b/vendor/github.com/containers/common/pkg/secrets/secretsdb.go new file mode 100644 index 000000000..22db97c12 --- /dev/null +++ b/vendor/github.com/containers/common/pkg/secrets/secretsdb.go @@ -0,0 +1,211 @@ +package secrets + +import ( + "encoding/json" + "io/ioutil" + "os" + "strings" + "time" + + "github.com/pkg/errors" +) + +type db struct { + // Secrets maps a secret id to secret metadata + Secrets map[string]Secret `json:"secrets"` + // NameToID maps a secret name to a secret id + NameToID map[string]string `json:"nameToID"` + // IDToName maps a secret id to a secret name + IDToName map[string]string `json:"idToName"` + // lastModified is the time when the database was last modified on the file system + lastModified time.Time +} + +// loadDB loads database data into the in-memory cache if it has been modified +func (s *SecretsManager) loadDB() error { + // check if the db file exists + fileInfo, err := os.Stat(s.secretsDBPath) + if err != nil { + if !os.IsExist(err) { + // If the file doesn't exist, then there's no reason to update the db cache, + // the db cache will show no entries anyway. + // The file will be created later on a store() + return nil + } else { + return err + } + } + + // We check if the file has been modified after the last time it was loaded into the cache. + // If the file has been modified, then we know that our cache is not up-to-date, so we load + // the db into the cache. + if s.db.lastModified.Equal(fileInfo.ModTime()) { + return nil + } + + file, err := os.Open(s.secretsDBPath) + if err != nil { + return err + } + defer file.Close() + if err != nil { + return err + } + + byteValue, err := ioutil.ReadAll(file) + if err != nil { + return err + } + unmarshalled := new(db) + if err := json.Unmarshal(byteValue, unmarshalled); err != nil { + return err + } + s.db = unmarshalled + s.db.lastModified = fileInfo.ModTime() + + return nil +} + +// getNameAndID takes a secret's name, ID, or partial ID, and returns both its name and full ID. +func (s *SecretsManager) getNameAndID(nameOrID string) (name, id string, err error) { + name, id, err = s.getExactNameAndID(nameOrID) + if err == nil { + return name, id, nil + } else if errors.Cause(err) != errNoSuchSecret { + return "", "", err + } + + // ID prefix may have been given, iterate through all IDs. + // ID and partial ID has a max lenth of 25, so we return if its greater than that. + if len(nameOrID) > secretIDLength { + return "", "", errors.Wrapf(errNoSuchSecret, "no secret with name or id %q", nameOrID) + } + exists := false + var foundID, foundName string + for id, name := range s.db.IDToName { + if strings.HasPrefix(id, nameOrID) { + if exists { + return "", "", errors.Wrapf(errAmbiguous, "more than one result secret with prefix %s", nameOrID) + } + exists = true + foundID = id + foundName = name + } + } + + if exists { + return foundName, foundID, nil + } + return "", "", errors.Wrapf(errNoSuchSecret, "no secret with name or id %q", nameOrID) +} + +// getExactNameAndID takes a secret's name or ID and returns both its name and full ID. +func (s *SecretsManager) getExactNameAndID(nameOrID string) (name, id string, err error) { + err = s.loadDB() + if err != nil { + return "", "", err + } + if name, ok := s.db.IDToName[nameOrID]; ok { + id := nameOrID + return name, id, nil + } + + if id, ok := s.db.NameToID[nameOrID]; ok { + name := nameOrID + return name, id, nil + } + + return "", "", errors.Wrapf(errNoSuchSecret, "no secret with name or id %q", nameOrID) +} + +// exactSecretExists checks if the secret exists, given a name or ID +// Does not match partial name or IDs +func (s *SecretsManager) exactSecretExists(nameOrID string) (bool, error) { + _, _, err := s.getExactNameAndID(nameOrID) + if err != nil { + if errors.Cause(err) == errNoSuchSecret { + return false, nil + } + return false, err + } + return true, nil +} + +// lookupAll gets all secrets stored. +func (s *SecretsManager) lookupAll() (map[string]Secret, error) { + err := s.loadDB() + if err != nil { + return nil, err + } + return s.db.Secrets, nil +} + +// lookupSecret returns a secret with the given name, ID, or partial ID. +func (s *SecretsManager) lookupSecret(nameOrID string) (*Secret, error) { + err := s.loadDB() + if err != nil { + return nil, err + } + _, id, err := s.getNameAndID(nameOrID) + if err != nil { + return nil, err + } + allSecrets, err := s.lookupAll() + if err != nil { + return nil, err + } + if secret, ok := allSecrets[id]; ok { + return &secret, nil + } + + return nil, errors.Wrapf(errNoSuchSecret, "no secret with name or id %q", nameOrID) +} + +// Store creates a new secret in the secrets database. +// It deals with only storing metadata, not data payload. +func (s *SecretsManager) store(entry *Secret) error { + err := s.loadDB() + if err != nil { + return err + } + + s.db.Secrets[entry.ID] = *entry + s.db.NameToID[entry.Name] = entry.ID + s.db.IDToName[entry.ID] = entry.Name + + marshalled, err := json.MarshalIndent(s.db, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile(s.secretsDBPath, marshalled, 0600) + if err != nil { + return err + } + + return nil +} + +// delete deletes a secret from the secrets database, given a name, ID, or partial ID. +// It deals with only deleting metadata, not data payload. +func (s *SecretsManager) delete(nameOrID string) error { + name, id, err := s.getNameAndID(nameOrID) + if err != nil { + return err + } + err = s.loadDB() + if err != nil { + return err + } + delete(s.db.Secrets, id) + delete(s.db.NameToID, name) + delete(s.db.IDToName, id) + marshalled, err := json.MarshalIndent(s.db, "", " ") + if err != nil { + return err + } + err = ioutil.WriteFile(s.secretsDBPath, marshalled, 0600) + if err != nil { + return err + } + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index e8b5edf8c..6c425ac06 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -102,6 +102,8 @@ github.com/containers/common/pkg/report github.com/containers/common/pkg/report/camelcase github.com/containers/common/pkg/retry github.com/containers/common/pkg/seccomp +github.com/containers/common/pkg/secrets +github.com/containers/common/pkg/secrets/filedriver github.com/containers/common/pkg/subscriptions github.com/containers/common/pkg/sysinfo github.com/containers/common/pkg/umask |