mirror of
https://github.com/containers/podman
synced 2024-10-19 08:44:11 +00:00
Merge pull request #9125 from ashley-cui/secretswiring
Implement Secrets
This commit is contained in:
commit
f98605e0e4
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -93,6 +93,7 @@ type ContainerCLIOpts struct {
|
|||
Replace bool
|
||||
Rm bool
|
||||
RootFS bool
|
||||
Secrets []string
|
||||
SecurityOpt []string
|
||||
SdNotifyMode string
|
||||
ShmSize string
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
80
cmd/podman/secrets/create.go
Normal file
80
cmd/podman/secrets/create.go
Normal file
|
@ -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
|
||||
}
|
82
cmd/podman/secrets/inspect.go
Normal file
82
cmd/podman/secrets/inspect.go
Normal file
|
@ -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
|
||||
}
|
99
cmd/podman/secrets/list.go
Normal file
99
cmd/podman/secrets/list.go
Normal file
|
@ -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)
|
||||
}
|
58
cmd/podman/secrets/rm.go
Normal file
58
cmd/podman/secrets/rm.go
Normal file
|
@ -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()
|
||||
}
|
25
cmd/podman/secrets/secret.go
Normal file
25
cmd/podman/secrets/secret.go
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
43
docs/source/markdown/podman-secret-create.1.md
Normal file
43
docs/source/markdown/podman-secret-create.1.md
Normal file
|
@ -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>
|
38
docs/source/markdown/podman-secret-inspect.1.md
Normal file
38
docs/source/markdown/podman-secret-inspect.1.md
Normal file
|
@ -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>
|
30
docs/source/markdown/podman-secret-ls.1.md
Normal file
30
docs/source/markdown/podman-secret-ls.1.md
Normal file
|
@ -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>
|
33
docs/source/markdown/podman-secret-rm.1.md
Normal file
33
docs/source/markdown/podman-secret-rm.1.md
Normal file
|
@ -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>
|
25
docs/source/markdown/podman-secret.1.md
Normal file
25
docs/source/markdown/podman-secret.1.md
Normal file
|
@ -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>
|
|
@ -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. |
|
||||
|
|
9
docs/source/secret.rst
Normal file
9
docs/source/secret.rst
Normal file
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
121
pkg/api/handlers/compat/secrets.go
Normal file
121
pkg/api/handlers/compat/secrets.go
Normal file
|
@ -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"))
|
||||
}
|
40
pkg/api/handlers/libpod/secrets.go
Normal file
40
pkg/api/handlers/libpod/secrets.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
194
pkg/api/server/register_secrets.go
Normal file
194
pkg/api/server/register_secrets.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
78
pkg/bindings/secrets/secrets.go
Normal file
78
pkg/bindings/secrets/secrets.go
Normal file
|
@ -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)
|
||||
}
|
23
pkg/bindings/secrets/types.go
Normal file
23
pkg/bindings/secrets/types.go
Normal file
|
@ -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
|
||||
}
|
107
pkg/bindings/secrets/types_create_options.go
Normal file
107
pkg/bindings/secrets/types_create_options.go
Normal file
|
@ -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
|
||||
}
|
75
pkg/bindings/secrets/types_inspect_options.go
Normal file
75
pkg/bindings/secrets/types_inspect_options.go
Normal file
|
@ -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
|
||||
}
|
75
pkg/bindings/secrets/types_list_options.go
Normal file
75
pkg/bindings/secrets/types_list_options.go
Normal file
|
@ -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
|
||||
}
|
75
pkg/bindings/secrets/types_remove_options.go
Normal file
75
pkg/bindings/secrets/types_remove_options.go
Normal file
|
@ -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
|
||||
}
|
133
pkg/bindings/test/secrets_test.go
Normal file
133
pkg/bindings/test/secrets_test.go
Normal file
|
@ -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))
|
||||
})
|
||||
|
||||
})
|
|
@ -82,6 +82,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
|
||||
|
|
104
pkg/domain/entities/secrets.go
Normal file
104
pkg/domain/entities/secrets.go
Normal file
|
@ -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
|
||||
}
|
||||
}
|
138
pkg/domain/infra/abi/secrets.go
Normal file
138
pkg/domain/infra/abi/secrets.go
Normal file
|
@ -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
|
||||
}
|
82
pkg/domain/infra/tunnel/secrets.go
Normal file
82
pkg/domain/infra/tunnel/secrets.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
36
test/apiv2/50-secrets.at
Normal file
36
test/apiv2/50-secrets.at
Normal file
|
@ -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
|
|
@ -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)))
|
||||
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"))
|
||||
|
||||
})
|
||||
})
|
||||
|
|
202
test/e2e/secret_test.go
Normal file
202
test/e2e/secret_test.go
Normal file
|
@ -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))
|
||||
})
|
||||
|
||||
})
|
158
vendor/github.com/containers/common/pkg/secrets/filedriver/filedriver.go
generated
vendored
Normal file
158
vendor/github.com/containers/common/pkg/secrets/filedriver/filedriver.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
282
vendor/github.com/containers/common/pkg/secrets/secrets.go
generated
vendored
Normal file
282
vendor/github.com/containers/common/pkg/secrets/secrets.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
211
vendor/github.com/containers/common/pkg/secrets/secretsdb.go
generated
vendored
Normal file
211
vendor/github.com/containers/common/pkg/secrets/secretsdb.go
generated
vendored
Normal file
|
@ -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
|
||||
}
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue