Merge pull request #9125 from ashley-cui/secretswiring

Implement Secrets
This commit is contained in:
OpenShift Merge Robot 2021-02-09 17:51:08 +01:00 committed by GitHub
commit f98605e0e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 2962 additions and 7 deletions

View file

@ -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) {

View file

@ -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,

View file

@ -93,6 +93,7 @@ type ContainerCLIOpts struct {
Replace bool
Rm bool
RootFS bool
Secrets []string
SecurityOpt []string
SdNotifyMode string
ShmSize string

View file

@ -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
}

View file

@ -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"

View 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
}

View 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
}

View 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
View 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()
}

View 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,
})
}

View file

@ -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 |

View file

@ -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

View file

@ -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

View file

@ -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

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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
View 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

View file

@ -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.

View file

@ -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

View file

@ -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))

View file

@ -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
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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.

View file

@ -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")
}

View file

@ -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 != "" {

View 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"))
}

View 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)
}

View file

@ -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)

View 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
}

View file

@ -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,

View file

@ -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

View 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)
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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))
})
})

View file

@ -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

View 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
}
}

View 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
}

View 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
}

View file

@ -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
}

View file

@ -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
View 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

View file

@ -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)))
})
})

View file

@ -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 {

View file

@ -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
View 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))
})
})

View 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
}

View 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
}

View 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
View file

@ -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