Skip to content

Commit

Permalink
feat(build, docker, buildah, stapel): add secrets support (#6446)
Browse files Browse the repository at this point in the history
Signed-off-by: Yaroslav Pershin <62902094+iapershin@users.noreply.github.com>
  • Loading branch information
iapershin authored Nov 29, 2024
1 parent 9934891 commit bf39dac
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 49 deletions.
25 changes: 22 additions & 3 deletions pkg/build/builder/ansible.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ import (
)

type Ansible struct {
config *config.Ansible
extra *Extra
config *config.Ansible
extra *Extra
secrets []config.Secret
}

type Extra struct {
ContainerWerfPath string
TmpPath string
}

func NewAnsibleBuilder(config *config.Ansible, extra *Extra) *Ansible {
func NewAnsibleBuilder(config *config.Ansible, extra *Extra, secrets []config.Secret) *Ansible {
return &Ansible{config: config, extra: extra}
}

Expand Down Expand Up @@ -133,6 +134,13 @@ func (b *Ansible) stage(ctx context.Context, cr container_backend.ContainerBacke
command := strings.Join(commandParts, " ")
container.AddServiceRunCommands(command)

err = b.addBuildSecretsVolumes(stageHostTmpDir, func(secretPath string) {
container.AddVolume(secretPath)
})
if err != nil {
return fmt.Errorf("unable to add volumes: %w", err)
}

return nil
} else {
return fmt.Errorf("ansible builder is not supported when using buildah backend, please use shell builder instead")
Expand Down Expand Up @@ -233,3 +241,14 @@ func (b *Ansible) stageHostWorkDir(userStageName string) (string, error) {

return p, nil
}

func (b *Ansible) addBuildSecretsVolumes(stageHostTmpDir string, fn func(string)) error {
for _, s := range b.secrets {
secretPath, err := s.GetMountPath(stageHostTmpDir)
if err != nil {
return err
}
fn(secretPath)
}
return nil
}
44 changes: 35 additions & 9 deletions pkg/build/builder/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import (
const scriptFileName = "script.sh"

type Shell struct {
config *config.Shell
extra *Extra
config *config.Shell
extra *Extra
secrets []config.Secret
}

func NewShellBuilder(config *config.Shell, extra *Extra) *Shell {
return &Shell{config: config, extra: extra}
func NewShellBuilder(config *config.Shell, extra *Extra, secrets []config.Secret) *Shell {
return &Shell{config: config, extra: extra, secrets: secrets}
}

func (b *Shell) IsBeforeInstallEmpty(ctx context.Context) bool {
Expand Down Expand Up @@ -67,14 +68,13 @@ func (b *Shell) isEmptyStage(ctx context.Context, userStageName string) bool {
}

func (b *Shell) stage(cr container_backend.ContainerBackend, stageBuilder stage_builder.StageBuilderInterface, useLegacyStapelBuilder bool, userStageName string) error {
stageHostTmpDir, err := b.stageHostTmpDir(userStageName)
if err != nil {
return err
}
if useLegacyStapelBuilder {
container := stageBuilder.LegacyStapelStageBuilder().BuilderContainer()

stageHostTmpDir, err := b.stageHostTmpDir(userStageName)
if err != nil {
return err
}

container.AddVolume(
fmt.Sprintf("%s:%s:rw", stageHostTmpDir, b.containerTmpDir()),
)
Expand All @@ -86,9 +86,24 @@ func (b *Shell) stage(cr container_backend.ContainerBackend, stageBuilder stage_
return err
}

err := b.addBuildSecretsVolumes(stageHostTmpDir, func(secretPath string) {
container.AddVolume(secretPath)
})
if err != nil {
return fmt.Errorf("unable to add volumes: %w", err)
}

container.AddServiceRunCommands(containerTmpScriptFilePath)

} else {
stageBuilder.StapelStageBuilder().AddCommands(b.stageCommands(userStageName)...)

err = b.addBuildSecretsVolumes(stageHostTmpDir, func(secretPath string) {
stageBuilder.StapelStageBuilder().AddBuildVolumes(secretPath)
})
if err != nil {
return fmt.Errorf("unable to add volumes: %w", err)
}
}

return nil
Expand Down Expand Up @@ -179,3 +194,14 @@ func (b *Shell) stageHostTmpDir(userStageName string) (string, error) {
func (b *Shell) containerTmpDir() string {
return path.Join(b.extra.ContainerWerfPath, "shell")
}

func (b *Shell) addBuildSecretsVolumes(stageHostTmpDir string, fn func(string)) error {
for _, s := range b.secrets {
secretPath, err := s.GetMountPath(stageHostTmpDir)
if err != nil {
return err
}
fn(secretPath)
}
return nil
}
4 changes: 2 additions & 2 deletions pkg/build/stage/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ func getBuilder(imageBaseConfig *config.StapelImageBase, baseStageOptions *BaseS
var b builder.Builder
extra := &builder.Extra{ContainerWerfPath: baseStageOptions.ContainerWerfDir, TmpPath: baseStageOptions.ImageTmpDir}
if imageBaseConfig.Shell != nil {
b = builder.NewShellBuilder(imageBaseConfig.Shell, extra)
b = builder.NewShellBuilder(imageBaseConfig.Shell, extra, imageBaseConfig.Secrets)
} else if imageBaseConfig.Ansible != nil {
b = builder.NewAnsibleBuilder(imageBaseConfig.Ansible, extra)
b = builder.NewAnsibleBuilder(imageBaseConfig.Ansible, extra, imageBaseConfig.Secrets)
}

return b
Expand Down
30 changes: 10 additions & 20 deletions pkg/config/raw_image_from_dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,33 +146,23 @@ func (c *rawImageFromDockerfile) toImageFromDockerfileDirective(giterminismManag
if len(c.RawSecrets) > 0 && image.Staged {
return nil, fmt.Errorf("secrets are not supported for staged build yet")
}
secretIds := make(map[string]struct{})
for _, rawSecrets := range c.RawSecrets {
secret, err := rawSecrets.toDirective()
if err != nil {
return nil, err
}

secretId := secret.GetSecretId()
if v, ok := secretIds[secretId]; !ok {
secretIds[secretId] = struct{}{}
} else {
return nil, fmt.Errorf("duplicated secret id %s", v)
}

err = secret.InspectByGiterminism(giterminismManager)
if err != nil {
return nil, newDetailedConfigError(err.Error(), nil, c.doc)
}
secrets, err := GetValidatedSecrets(c.RawSecrets, giterminismManager, c.doc)
if err != nil {
return nil, err
}

secretArg, err := secret.GetSecretStringArg()
secretsArgs := make([]string, 0, len(secrets))
for _, s := range secrets {
secret, err := s.GetSecretStringArg()
if err != nil {
return nil, err
}

image.Secrets = append(image.Secrets, secretArg)
secretsArgs = append(secretsArgs, secret)
}

image.Secrets = secretsArgs

if err := image.validate(giterminismManager); err != nil {
return nil, err
}
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/raw_stapel_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type rawStapelImage struct {
RawImport []*rawImport `yaml:"import,omitempty"`
RawDependencies []*rawDependency `yaml:"dependencies,omitempty"`
Platform []string `yaml:"platform,omitempty"`
RawSecrets []*rawSecret `yaml:"secrets,omitempty"`

doc *doc `yaml:"-"` // parent

Expand Down Expand Up @@ -295,6 +296,13 @@ func (c *rawStapelImage) toStapelImageBaseDirective(giterminismManager gitermini
imageBase.Dependencies = append(imageBase.Dependencies, dependencyDirective)
}

secrets, err := GetValidatedSecrets(c.RawSecrets, giterminismManager, c.doc)
if err != nil {
return nil, err
}

imageBase.Secrets = secrets

if err := c.validateStapelImageBaseDirective(giterminismManager, imageBase); err != nil {
return nil, err
}
Expand Down
92 changes: 83 additions & 9 deletions pkg/config/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package config
import (
"errors"
"fmt"
"math"
"math/rand/v2"
"os"
"path/filepath"
"time"

"github.com/werf/werf/v2/pkg/giterminism_manager"
)
Expand All @@ -14,6 +15,7 @@ type Secret interface {
GetSecretStringArg() (string, error)
GetSecretId() string
InspectByGiterminism(giterminismManager giterminism_manager.Interface) error
GetMountPath(stageHostTmpDir string) (string, error)
}

type SecretFromEnv struct {
Expand All @@ -32,6 +34,9 @@ type SecretFromPlainValue struct {
}

func newSecretFromEnv(s *rawSecret) (*SecretFromEnv, error) {
if _, exists := os.LookupEnv(s.Env); !exists {
return nil, fmt.Errorf("specified env variable doesn't exist")
}
if s.Id == "" {
s.Id = s.Env
}
Expand All @@ -42,6 +47,9 @@ func newSecretFromEnv(s *rawSecret) (*SecretFromEnv, error) {
}

func newSecretFromSrc(s *rawSecret) (*SecretFromSrc, error) {
if _, err := os.Stat(s.Src); errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("path %s doesn't exist", s.Src)
}
if s.Id == "" {
s.Id = filepath.Base(s.Src)
}
Expand All @@ -62,16 +70,10 @@ func newSecretFromPlainValue(s *rawSecret) (*SecretFromPlainValue, error) {
}

func (s *SecretFromEnv) GetSecretStringArg() (string, error) {
if _, exists := os.LookupEnv(s.Value); !exists {
return "", fmt.Errorf("specified secret env %q doesn't exist", s.Value)
}
return fmt.Sprintf("id=%s,env=%s", s.Id, s.Value), nil
}

func (s *SecretFromSrc) GetSecretStringArg() (string, error) {
if _, err := os.Stat(s.Value); errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("specified secret path %s doesn't exist", s.Value)
}
return fmt.Sprintf("id=%s,src=%s", s.Id, s.Value), nil
}

Expand All @@ -84,8 +86,7 @@ func (s *SecretFromPlainValue) GetSecretStringArg() (string, error) {
}

func (s *SecretFromPlainValue) setPlainValueAsEnv() (*SecretFromEnv, error) {
t := time.Now().Unix()
envKey := fmt.Sprintf("tmpbuild%d_%s", t, s.Id) // generate unique value
envKey := fmt.Sprintf("tmpbuild%d_%s", rand.IntN(math.MaxInt32), s.Id) // generate unique value
if _, e := os.LookupEnv(envKey); e {
return nil, fmt.Errorf("can't set secret %s: id is not unique", s.Id) // should never be here
}
Expand Down Expand Up @@ -124,3 +125,76 @@ func (s *SecretFromSrc) InspectByGiterminism(giterminismManager giterminism_mana
func (s *SecretFromPlainValue) InspectByGiterminism(giterminismManager giterminism_manager.Interface) error {
return nil
}

func GetValidatedSecrets(rawSecrets []*rawSecret, giterminismManager giterminism_manager.Interface, doc *doc) ([]Secret, error) {
secretIds := make(map[string]struct{})
secrets := make([]Secret, 0, len(rawSecrets))

for _, s := range rawSecrets {
secret, err := s.toDirective()
if err != nil {
return nil, err
}

secretId := secret.GetSecretId()
if _, ok := secretIds[secretId]; !ok {
secretIds[secretId] = struct{}{}
} else {
return nil, newDetailedConfigError("duplicated secret %s", secretId, s.doc)
}

err = secret.InspectByGiterminism(giterminismManager)
if err != nil {
return nil, err
}

secrets = append(secrets, secret)
}

return secrets, nil
}

func (s *SecretFromEnv) GetMountPath(stageHostTmpDir string) (string, error) {
data := []byte(os.Getenv(s.Value))
return getMountPath(s.Id, stageHostTmpDir, data)
}

func (s *SecretFromSrc) GetMountPath(stageHostTmpDir string) (string, error) {
if abs, err := filepath.Abs(s.Value); err != nil {
return "", fmt.Errorf("unable to set mount: %w", err)
} else { // TODO: (iapershin) create fix to use abs path everywhere
return generateMountPath(s.Id, abs), nil
}
}

func (s *SecretFromPlainValue) GetMountPath(stageHostTmpDir string) (string, error) {
return getMountPath(s.Id, stageHostTmpDir, []byte(s.Value))
}

func getMountPath(secretId, stageHostTmpDir string, data []byte) (string, error) {
tmpFile, err := writeToTmpFile(stageHostTmpDir, data)
if err != nil {
return "", fmt.Errorf("unable to mount secret: %w", err)
}
return generateMountPath(secretId, tmpFile), nil
}

func writeToTmpFile(stageHostTmpDir string, data []byte) (string, error) {
tmpFile, err := os.CreateTemp(stageHostTmpDir, "stapel*")
if err != nil {
return "", err
}

tmpFilePath := tmpFile.Name()

if err := os.WriteFile(tmpFilePath, data, 0o400); err != nil {
return "", err
}

return tmpFilePath, nil
}

func generateMountPath(id, filepath string) string {
containerPath := fmt.Sprintf("/run/secrets/%s", id)
return fmt.Sprintf("%s:%s:ro", filepath, containerPath)
}
1 change: 1 addition & 0 deletions pkg/config/stapel_image_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type StapelImageBase struct {
Mount []*Mount
Import []*Import
Dependencies []*Dependency
Secrets []Secret

final bool
platform []string
Expand Down
18 changes: 12 additions & 6 deletions pkg/container_backend/buildah_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -850,19 +850,24 @@ func (backend *BuildahBackend) RemoveHostDirs(ctx context.Context, mountDir stri
})
}

func parseVolume(volume string) (string, string, error) {
volumeParts := strings.SplitN(volume, ":", 2)
if len(volumeParts) != 2 {
return "", "", fmt.Errorf("expected SOURCE:DESTINATION format")
func parseVolume(volume string) (string, string, string, error) {
volumeParts := strings.Split(volume, ":")

switch len(volumeParts) {
case 2:
return volumeParts[0], volumeParts[1], "", nil
case 3:
return volumeParts[0], volumeParts[1], volumeParts[2], nil
default:
return "", "", "", fmt.Errorf("expected SOURCE:DESTINATION[:OPTIONS] format")
}
return volumeParts[0], volumeParts[1], nil
}

func makeBuildahMounts(volumes []string) ([]*specs.Mount, error) {
var mounts []*specs.Mount

for _, volume := range volumes {
from, to, err := parseVolume(volume)
from, to, mode, err := parseVolume(volume)
if err != nil {
return nil, fmt.Errorf("invalid volume %q: %w", volume, err)
}
Expand All @@ -871,6 +876,7 @@ func makeBuildahMounts(volumes []string) ([]*specs.Mount, error) {
Type: "bind",
Source: from,
Destination: to,
Options: []string{mode},
})
}

Expand Down
Loading

0 comments on commit bf39dac

Please sign in to comment.