Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable interception via shell per lifecycle phase execution #485

Closed
wants to merge 11 commits into from
3 changes: 3 additions & 0 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ type BuildOptions struct {
// Process type that will be used when setting container start command.
DefaultProcessType string

Intercept string

// Filter files from the application source.
// If true include file, otherwise exclude.
FileFilter func(string) bool
Expand Down Expand Up @@ -259,6 +261,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
ClearCache: opts.ClearCache,
Publish: opts.Publish,
UseCreator: false,
Intercept: opts.Intercept,
TrustBuilder: opts.TrustBuilder,
LifecycleImage: ephemeralBuilder.Name(),
HTTPProxy: proxyConfig.HTTPProxy,
Expand Down
1 change: 1 addition & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type LifecycleOptions struct {
Network string
Volumes []string
DefaultProcessType string
Intercept string
FileFilter func(string) bool
}

Expand Down
92 changes: 76 additions & 16 deletions internal/build/phase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,112 @@ package build

import (
"context"
"fmt"
"io"
"strings"

"github.com/docker/docker/api/types"
dcontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/pkg/errors"

"github.com/buildpacks/pack/internal/container"
"github.com/buildpacks/pack/internal/style"
)

type Phase struct {
name string
infoWriter io.Writer
errorWriter io.Writer
docker client.CommonAPIClient
ctrConf *dcontainer.Config
hostConf *dcontainer.HostConfig
ctr dcontainer.ContainerCreateCreatedBody
uid, gid int
appPath string
containerOps []ContainerOperation
fileFilter func(string) bool
name string
infoWriter io.Writer
errorWriter io.Writer
docker client.CommonAPIClient
ctrConf *dcontainer.Config
hostConf *dcontainer.HostConfig
createdCtrIDs []string
uid, gid int
appPath string
containerOps []ContainerOperation
intercept string
fileFilter func(string) bool
}

func (p *Phase) Run(ctx context.Context) error {
var err error
p.ctr, err = p.docker.ContainerCreate(ctx, p.ctrConf, p.hostConf, nil, "")
if p.intercept != "" {
if err := p.attemptToIntercept(ctx); err == nil {
return nil
} else {
_, _ = fmt.Fprintf(p.errorWriter, "Failed to start intercepted container: %s\n", err.Error())
_, _ = fmt.Fprintln(p.infoWriter, "Phase will run without interception")
}
}

ctrID, err := p.createContainer(ctx, p.ctrConf)
if err != nil {
return errors.Wrapf(err, "failed to create '%s' container", p.name)
}

for _, containerOp := range p.containerOps {
if err := containerOp(p.docker, ctx, p.ctr.ID, p.infoWriter, p.errorWriter); err != nil {
if err := containerOp(p.docker, ctx, ctrID, p.infoWriter, p.errorWriter); err != nil {
return err
}
}

return container.Run(
ctx,
p.docker,
p.ctr.ID,
ctrID,
p.infoWriter,
p.errorWriter,
)
}

func (p *Phase) attemptToIntercept(ctx context.Context) error {
originalCmd := p.ctrConf.Cmd

ctrConf := *p.ctrConf
ctrConf.Cmd = []string{p.intercept}
ctrConf.AttachStdin = true
ctrConf.AttachStdout = true
ctrConf.AttachStderr = true
ctrConf.Tty = true
ctrConf.OpenStdin = true

ctrID, err := p.createContainer(ctx, &ctrConf)
if err != nil {
return err
}

_, _ = fmt.Fprintf(p.infoWriter, `Attempting to intercept...
-----------
To continue to the next phase type: %s
To manually run the phase type:
%s
-----------
`, style.Symbol("exit"), style.Symbol(strings.Join(originalCmd, " ")))

return container.Start(ctx, p.docker, ctrID, types.ContainerStartOptions{})
}

func (p *Phase) createContainer(ctx context.Context, ctrConf *dcontainer.Config) (ctrID string, err error) {
ctr, err := p.docker.ContainerCreate(ctx, ctrConf, p.hostConf, nil, "")
if err != nil {
return "", errors.Wrapf(err, "failed to create '%s' container", p.name)
}

p.createdCtrIDs = append(p.createdCtrIDs, ctr.ID)

for _, containerOp := range p.containerOps {
if err := containerOp(p.docker, ctx, ctr.ID, p.infoWriter, p.errorWriter); err != nil {
return "", err
}
}

return ctr.ID, nil
}

func (p *Phase) Cleanup() error {
return p.docker.ContainerRemove(context.Background(), p.ctr.ID, types.ContainerRemoveOptions{Force: true})
var err error
for _, ctrID := range p.createdCtrIDs {
err = p.docker.ContainerRemove(context.Background(), ctrID, types.ContainerRemoveOptions{Force: true})
}
return err
}
1 change: 1 addition & 0 deletions internal/build/phase_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func (m *DefaultPhaseFactory) New(provider *PhaseConfigProvider) RunnerCleaner {
errorWriter: provider.ErrorWriter(),
uid: m.lifecycleExec.opts.Builder.UID(),
gid: m.lifecycleExec.opts.Builder.GID(),
intercept: m.lifecycleExec.opts.Intercept,
appPath: m.lifecycleExec.opts.AppPath,
containerOps: provider.containerOps,
fileFilter: m.lifecycleExec.opts.FileFilter,
Expand Down
13 changes: 11 additions & 2 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type BuildFlags struct {
EnvFiles []string
Buildpacks []string
Volumes []string
Intercept string
}

// Build an image from source code
Expand Down Expand Up @@ -97,12 +98,13 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
logger.Debugf("Builder %s is trusted", style.Symbol(flags.Builder))
} else {
logger.Debugf("Builder %s is untrusted", style.Symbol(flags.Builder))
logger.Debug("As a result, the phases of the lifecycle which require root access will be run in separate trusted ephemeral containers.")
logger.Debug("As a result, some phases will be run in separate trusted ephemeral containers.")
logger.Debug("For more information, see https://medium.com/buildpacks/faster-more-secure-builds-with-pack-0-11-0-4d0c633ca619")
}

if !trustBuilder && len(flags.Volumes) > 0 {
logger.Warn("Using untrusted builder with volume mounts. If there is sensitive data in the volumes, this may present a security vulnerability.")
logger.Warn("Using untrusted builder with volume mounts.")
logger.Warn("If there is sensitive data in the volumes, this may present a security vulnerability.")
}

pullPolicy, err := pubcfg.ParsePullPolicy(flags.Policy)
Expand All @@ -121,6 +123,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
Publish: flags.Publish,
PullPolicy: pullPolicy,
ClearCache: flags.ClearCache,
Intercept: flags.Intercept,
TrustBuilder: trustBuilder,
Buildpacks: buildpacks,
ContainerConfig: pack.ContainerConfig{
Expand Down Expand Up @@ -160,9 +163,15 @@ func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Co
cmd.Flags().StringArrayVar(&buildFlags.Volumes, "volume", nil, "Mount host volume into the build container, in the form '<host path>:<target path>[:<mode>]'."+multiValueHelp("volume"))
cmd.Flags().StringVarP(&buildFlags.DefaultProcessType, "default-process", "D", "", `Set the default process type. (default "web")`)
cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`)

// TODO: Remove --no-pull flag after v0.13.0 released. See https://github.com/buildpacks/pack/issues/775
cmd.Flags().BoolVar(&buildFlags.NoPull, "no-pull", false, "Skip pulling builder and run images before use")
cmd.Flags().MarkHidden("no-pull")

cmd.Flags().StringVarP(&buildFlags.Intercept, "intercept", "i", "", "Intercept each phase of the lifecycle with command")
if !cfg.Experimental {
cmd.Flags().MarkHidden("intercept")
}
}

func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackClient, logger logging.Logger) error {
Expand Down
142 changes: 138 additions & 4 deletions internal/container/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ import (
"context"
"fmt"
"io"
"os"
"os/signal"
"syscall"

"github.com/docker/docker/api/types"
dcontainer "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
)

func Run(ctx context.Context, docker client.CommonAPIClient, ctrID string, out, errOut io.Writer) error {
bodyChan, errChan := docker.ContainerWait(ctx, ctrID, dcontainer.WaitConditionNextExit)
func Run(ctx context.Context, docker client.CommonAPIClient, containerID string, out, errOut io.Writer) error {
bodyChan, errChan := docker.ContainerWait(ctx, containerID, dcontainer.WaitConditionNextExit)

resp, err := docker.ContainerAttach(ctx, ctrID, types.ContainerAttachOptions{
resp, err := docker.ContainerAttach(ctx, containerID, types.ContainerAttachOptions{
Stream: true,
Stdout: true,
Stderr: true,
Expand All @@ -25,7 +29,7 @@ func Run(ctx context.Context, docker client.CommonAPIClient, ctrID string, out,
}
defer resp.Close()

if err := docker.ContainerStart(ctx, ctrID, types.ContainerStartOptions{}); err != nil {
if err := docker.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil {
return errors.Wrap(err, "container start")
}

Expand Down Expand Up @@ -57,3 +61,133 @@ func optionallyCloseWriter(writer io.Writer) error {

return nil
}

func Start(ctx context.Context, client client.CommonAPIClient, containerID string, hostConfig types.ContainerStartOptions) error {
bodyChan, errChan := client.ContainerWait(ctx, containerID, dcontainer.WaitConditionNextExit)

// Attach to the container on a separate thread
attachCtx, cancelFn := context.WithCancel(ctx)
defer cancelFn()

readerCloser, err := attachToContainerReader(attachCtx, client, containerID)
if err != nil {
return errors.Wrap(err, "attach reader")
}
defer readerCloser.Close()

err = client.ContainerStart(ctx, containerID, hostConfig)
if err != nil {
return errors.Wrap(err, "starting container")
}

// Start it
writerCloser, attachErrorChan := attachToContainerWriter(attachCtx, os.Stdin, client, containerID)
defer func() {
fmt.Println("finalizing", containerID)
writerCloser.Close()
}()

// TODO: wire and verify that this works
// Make sure terminal resizes are passed on to the container
// monitorTty(ctx, client, containerID, terminalFd)

select {
case err := <-attachErrorChan:
fmt.Println("attach:err=", err)
return err
case body := <-bodyChan:
fmt.Println("await:status=", body.StatusCode)
if body.StatusCode != 0 {
return fmt.Errorf("failed with status code: %d", body.StatusCode)
}
case err := <-errChan:
fmt.Println("await:err=", err)
return err
}

return nil
}

func attachToContainerReader(ctx context.Context, client client.CommonAPIClient, containerID string) (io.Closer, error) {
attached, err := client.ContainerAttach(ctx, containerID, types.ContainerAttachOptions{
Stderr: true,
Stdout: true,
Stdin: false,
Stream: true,
})

if err != nil {
return nil, err
}

go io.Copy(os.Stdout, attached.Reader)
go io.Copy(os.Stderr, attached.Reader)

return attached.Conn, nil
}

func attachToContainerWriter(ctx context.Context, stdIn io.Reader, client client.CommonAPIClient, containerID string) (io.Closer, chan error) {
errChan := make(chan error)

attached, err := client.ContainerAttach(ctx, containerID, types.ContainerAttachOptions{
Stderr: false,
Stdout: false,
Stdin: true,
Stream: true,
})

if err != nil {
errChan <- err
return nil, errChan
}

go func(w io.WriteCloser) {
fmt.Println("scan:pre", containerID)
for ctx.Err() == nil {
fmt.Println("scan:read", containerID)
_, err := io.Copy(w, stdIn)

fmt.Println("scan:post", containerID)
if err != nil {
errChan <- err
}
}
}(attached.Conn)

return attached.Conn, errChan
}

// From https://github.com/docker/docker/blob/0d70706b4b6bf9d5a5daf46dd147ca71270d0ab7/api/client/utils.go#L222-L233
func monitorTty(ctx context.Context, client *client.Client, containerID string, terminalFd uintptr) {
resizeTty(ctx, client, containerID, terminalFd)

sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan, syscall.SIGWINCH)
go func() {
for _ = range sigchan {
resizeTty(ctx, client, containerID, terminalFd)
}
}()
}

func resizeTty(ctx context.Context, client *client.Client, containerID string, terminalFd uintptr) error {
height, width := getTtySize(terminalFd)
if height == 0 && width == 0 {
return nil
}

return client.ContainerResize(ctx, containerID, types.ResizeOptions{
Height: height,
Width: width,
})
}

// From https://github.com/docker/docker/blob/0d70706b4b6bf9d5a5daf46dd147ca71270d0ab7/api/client/utils.go#L235-L247
func getTtySize(terminalFd uintptr) (uint, uint) {
ws, err := term.GetWinsize(terminalFd)
if err != nil {
return 0, 0
}

return uint(ws.Height), uint(ws.Width)
}