summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAshley Cui <acui@redhat.com>2021-01-15 01:27:23 -0500
committerAshley Cui <acui@redhat.com>2021-02-09 09:13:21 -0500
commit832a69b0bee6ec289521fbd59ddd480372493ee3 (patch)
tree4c8a14b7fad879dc454c37f8b59120cf74ceafd1
parent2aaf631586e82192e6b7b992e6b5c8717eb792d7 (diff)
downloadpodman-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>
-rw-r--r--cmd/podman/common/completion.go37
-rw-r--r--cmd/podman/common/create.go8
-rw-r--r--cmd/podman/common/create_opts.go1
-rw-r--r--cmd/podman/common/specgen.go1
-rw-r--r--cmd/podman/main.go1
-rw-r--r--cmd/podman/secrets/create.go80
-rw-r--r--cmd/podman/secrets/inspect.go82
-rw-r--r--cmd/podman/secrets/list.go99
-rw-r--r--cmd/podman/secrets/rm.go58
-rw-r--r--cmd/podman/secrets/secret.go25
-rw-r--r--commands-demo.md5
-rw-r--r--docs/source/Commands.rst2
-rw-r--r--docs/source/markdown/podman-create.1.md12
-rw-r--r--docs/source/markdown/podman-run.1.md10
-rw-r--r--docs/source/markdown/podman-secret-create.1.md43
-rw-r--r--docs/source/markdown/podman-secret-inspect.1.md38
-rw-r--r--docs/source/markdown/podman-secret-ls.1.md30
-rw-r--r--docs/source/markdown/podman-secret-rm.1.md33
-rw-r--r--docs/source/markdown/podman-secret.1.md25
-rw-r--r--docs/source/markdown/podman.1.md1
-rw-r--r--docs/source/secret.rst9
-rw-r--r--libpod/container.go6
-rw-r--r--libpod/container_config.go5
-rw-r--r--libpod/container_inspect.go7
-rw-r--r--libpod/container_internal.go24
-rw-r--r--libpod/container_internal_linux.go47
-rw-r--r--libpod/define/container_inspect.go13
-rw-r--r--libpod/options.go23
-rw-r--r--libpod/runtime.go5
-rw-r--r--libpod/runtime_ctr.go13
-rw-r--r--pkg/api/handlers/compat/secrets.go121
-rw-r--r--pkg/api/handlers/libpod/secrets.go40
-rw-r--r--pkg/api/handlers/utils/errors.go8
-rw-r--r--pkg/api/server/register_secrets.go194
-rw-r--r--pkg/api/server/server.go1
-rw-r--r--pkg/api/tags.yaml4
-rw-r--r--pkg/bindings/secrets/secrets.go78
-rw-r--r--pkg/bindings/secrets/types.go23
-rw-r--r--pkg/bindings/secrets/types_create_options.go107
-rw-r--r--pkg/bindings/secrets/types_inspect_options.go75
-rw-r--r--pkg/bindings/secrets/types_list_options.go75
-rw-r--r--pkg/bindings/secrets/types_remove_options.go75
-rw-r--r--pkg/bindings/test/secrets_test.go133
-rw-r--r--pkg/domain/entities/engine_container.go4
-rw-r--r--pkg/domain/entities/secrets.go104
-rw-r--r--pkg/domain/infra/abi/secrets.go138
-rw-r--r--pkg/domain/infra/tunnel/secrets.go82
-rw-r--r--pkg/specgen/generate/container_create.go4
-rw-r--r--pkg/specgen/specgen.go3
-rw-r--r--test/apiv2/50-secrets.at36
-rw-r--r--test/e2e/commit_test.go25
-rw-r--r--test/e2e/common_test.go15
-rw-r--r--test/e2e/run_test.go26
-rw-r--r--test/e2e/secret_test.go202
-rw-r--r--vendor/github.com/containers/common/pkg/secrets/filedriver/filedriver.go158
-rw-r--r--vendor/github.com/containers/common/pkg/secrets/secrets.go282
-rw-r--r--vendor/github.com/containers/common/pkg/secrets/secretsdb.go211
-rw-r--r--vendor/modules.txt2
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