mirror of
https://github.com/89luca89/distrobox.git
synced 2026-08-19 01:14:49 -05:00
feat(create): pull on create (#39)
* feat(containermanager): add ExistsImage function * feat(containermanager): add PullImage function * feat(containermanager): pipe command output to stdout and stderr * feat(create): allow image pull on distrobox create * feat(ephemeral): allow image pulling * feat(assemble): allow image pulling * refactor(manifest): use AlwaysPull instead of Pull to explicit intent
This commit is contained in:
committed by
Alessio Biancalana
parent
8261771ab1
commit
1e1681e90a
@@ -208,6 +208,8 @@ func createAction(ctx context.Context, cmd *cli.Command) error {
|
||||
DryRun: cmd.Bool("dry-run"),
|
||||
GenerateEntry: !cmd.Bool("no-entry"),
|
||||
Rootful: cmd.Bool("root"),
|
||||
ContainerAlwaysPull: cmd.Bool("pull"),
|
||||
NonInteractive: cmd.Bool("yes"),
|
||||
}
|
||||
|
||||
progress := ui.NewProgress(os.Stderr)
|
||||
@@ -220,6 +222,11 @@ func createAction(ctx context.Context, cmd *cli.Command) error {
|
||||
printContainerAlreadyExists(progress, containerAlreadyExistsErr.ContainerName, opts.Rootful)
|
||||
}
|
||||
|
||||
if errors.Is(err, commands.ErrImagePullAbortedByUser) {
|
||||
progress.Finalize("next time, pull the image first")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("create command failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -134,11 +134,10 @@ func (ac *AssembleCommand) createItem(ctx context.Context, item manifest.Item, d
|
||||
GenerateEntry: item.Entry,
|
||||
Rootful: item.Root,
|
||||
DryRun: dryRun,
|
||||
NonInteractive: true,
|
||||
ContainerAlwaysPull: item.AlwaysPull,
|
||||
}
|
||||
|
||||
// TODO: pull image if needed
|
||||
// https://github.com/89luca89/distrobox/blob/main/distrobox-create#L1016
|
||||
|
||||
err := ac.createCmd.Execute(ctx, opts)
|
||||
if err != nil {
|
||||
ac.progress.Fail()
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
)
|
||||
|
||||
var ErrHostnameTooLong = fmt.Errorf("hostname too long, must be less than %d characters", maxHostnameLength)
|
||||
var ErrImagePullAbortedByUser = errors.New("image pull operation aborted by user")
|
||||
|
||||
type ContainerAlreadyExistsError struct {
|
||||
ContainerName string
|
||||
@@ -36,6 +37,7 @@ type CreateCommand struct {
|
||||
containerManager containermanager.ContainerManager
|
||||
generateEntryCmd *GenerateEntryCommand
|
||||
progress *ui.Progress
|
||||
prompter *ui.Prompter
|
||||
}
|
||||
|
||||
type CreateOptions struct {
|
||||
@@ -77,6 +79,9 @@ type CreateOptions struct {
|
||||
|
||||
GenerateEntry bool
|
||||
Rootful bool
|
||||
|
||||
ContainerAlwaysPull bool
|
||||
NonInteractive bool
|
||||
}
|
||||
|
||||
func NewCreateCommand(cm containermanager.ContainerManager, progress *ui.Progress) *CreateCommand {
|
||||
@@ -109,8 +114,9 @@ func (c *CreateCommand) Execute(ctx context.Context, opts CreateOptions) error {
|
||||
containerImage = cloneImage
|
||||
}
|
||||
|
||||
// TODO: pull image if needed
|
||||
// https://github.com/89luca89/distrobox/blob/main/distrobox-create#L1016
|
||||
if err := c.askPullImage(ctx, containerImage, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.progress.Next("Creating '%s' using image %s", containerName, containerImage)
|
||||
|
||||
@@ -268,3 +274,23 @@ func (c *CreateCommand) clone(ctx context.Context, containerName string) (string
|
||||
|
||||
return commitTag, nil
|
||||
}
|
||||
|
||||
func (c *CreateCommand) askPullImage(ctx context.Context, containerImage string, opts CreateOptions) error {
|
||||
if opts.ContainerAlwaysPull || !c.containerManager.ImageExists(ctx, containerImage) {
|
||||
skipConfirm := opts.NonInteractive || opts.ContainerAlwaysPull
|
||||
if !skipConfirm {
|
||||
msg := fmt.Sprintf("Image '%s' not found.\n. Do you want to pull the image now?", containerImage)
|
||||
answer := c.prompter.Prompt(msg, true)
|
||||
if !answer {
|
||||
return ErrImagePullAbortedByUser
|
||||
}
|
||||
}
|
||||
|
||||
err := c.containerManager.PullImage(ctx, containerImage, opts.ContainerPlatform)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull image '%s': %w", containerImage, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -47,9 +47,7 @@ func (c *EphemeralCommand) Execute(ctx context.Context, opts EphemeralOptions) e
|
||||
// override options not relevant for creating ephemeral containers
|
||||
createOpts.GenerateEntry = false
|
||||
createOpts.DryRun = opts.DryRun
|
||||
// TODO: pull image if needed
|
||||
// The feature is still a todo in the CreateCommand. When implemented,
|
||||
// remember to set it here as well.
|
||||
createOpts.NonInteractive = true
|
||||
if err := c.createCmd.Execute(ctx, createOpts); err != nil {
|
||||
return fmt.Errorf("failed to create ephemeral container: %w", err)
|
||||
}
|
||||
|
||||
@@ -82,7 +82,9 @@ type ContainerManager interface {
|
||||
Create(ctx context.Context, opts CreateOptions) error
|
||||
Remove(ctx context.Context, containerName string, opts RmOptions) error
|
||||
Exists(ctx context.Context, containerName string) bool
|
||||
ImageExists(ctx context.Context, imageName string) bool
|
||||
Stop(ctx context.Context, containerNames []string) error
|
||||
InspectContainer(ctx context.Context, containerName string) (*InspectResult, error)
|
||||
Commit(ctx context.Context, containerID string, tag string) error
|
||||
PullImage(ctx context.Context, imageName string, platform string) error
|
||||
Commit(ctx context.Context, containerID string, imageTag string) error
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -57,6 +58,7 @@ type dockerContainer struct {
|
||||
type runOptions struct {
|
||||
DryRun bool
|
||||
Interactive bool
|
||||
TailLogs bool
|
||||
}
|
||||
|
||||
type inspectOutput struct {
|
||||
@@ -70,6 +72,11 @@ type inspectOutput struct {
|
||||
} `json:"Config"`
|
||||
}
|
||||
|
||||
type InspectImageOutput struct {
|
||||
ID string `json:"ID"`
|
||||
Architecture string `json:"Architecture"`
|
||||
}
|
||||
|
||||
func (d *Docker) ListContainers(ctx context.Context) ([]containermanager.Container, error) {
|
||||
args := []string{"ps", "-a", "--no-trunc", "--format", "json"}
|
||||
out, err := d.run(ctx, args, runOptions{})
|
||||
@@ -473,6 +480,11 @@ func (d *Docker) run(ctx context.Context, args []string, opts runOptions) (strin
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if opts.TailLogs {
|
||||
cmd.Stdout = io.MultiWriter(&stdout, os.Stdout)
|
||||
cmd.Stderr = io.MultiWriter(&stderr, os.Stderr)
|
||||
}
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
captured := strings.TrimSpace(stderr.String())
|
||||
@@ -683,6 +695,36 @@ func (d *Docker) InspectContainer(ctx context.Context, containerName string) (*c
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (d *Docker) ImageExists(ctx context.Context, imageName string) bool {
|
||||
args := []string{"inspect", "--type", "image", "--format", "json", imageName}
|
||||
output, err := d.run(ctx, args, runOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var inspects []inspectOutput
|
||||
if err := json.Unmarshal([]byte(output), &inspects); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(inspects) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Docker) PullImage(ctx context.Context, imageName string, platform string) error {
|
||||
var args []string
|
||||
if platform != "" {
|
||||
args = []string{"pull", "--platform", platform, imageName}
|
||||
} else {
|
||||
args = []string{"pull", imageName}
|
||||
}
|
||||
_, err := d.run(ctx, args, runOptions{TailLogs: true})
|
||||
return err
|
||||
}
|
||||
|
||||
func buildContainerPath(cleanPath bool, hostPath string, cfg *containermanager.InspectResult) string {
|
||||
standardPaths := []string{"/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"}
|
||||
|
||||
|
||||
@@ -522,6 +522,36 @@ func (p *Podman) Enter(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Podman) ImageExists(ctx context.Context, imageName string) bool {
|
||||
args := []string{"inspect", "--type", "image", "--format", "json", imageName}
|
||||
output, err := p.run(ctx, args, runOptions{})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var inspects []inspectOutput
|
||||
if err := json.Unmarshal([]byte(output), &inspects); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(inspects) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Podman) PullImage(ctx context.Context, imageName string, platform string) error {
|
||||
var args []string
|
||||
if platform != "" {
|
||||
args = []string{"pull", "--platform", platform, imageName}
|
||||
} else {
|
||||
args = []string{"pull", imageName}
|
||||
}
|
||||
_, err := p.run(ctx, args, runOptions{TailLogs: true})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Podman) Remove(
|
||||
ctx context.Context,
|
||||
containerName string,
|
||||
|
||||
@@ -28,7 +28,7 @@ type Item struct {
|
||||
Nvidia bool
|
||||
InitHooks []string
|
||||
PreInitHooks []string
|
||||
Pull bool
|
||||
AlwaysPull bool
|
||||
Root bool
|
||||
StartNow bool
|
||||
UnshareGroups bool
|
||||
@@ -163,7 +163,7 @@ func sectionToItem(section *ini.Section) Item { //nolint:funlen // Function leng
|
||||
case "entry":
|
||||
item.Entry = parseBool(last)
|
||||
case "pull":
|
||||
item.Pull = parseBool(last)
|
||||
item.AlwaysPull = parseBool(last)
|
||||
case "root":
|
||||
item.Root = parseBool(last)
|
||||
case "start_now":
|
||||
|
||||
@@ -35,7 +35,7 @@ start_now=true
|
||||
|
||||
assert.Equal(t, "distrodev", parsed[0].Name)
|
||||
assert.Equal(t, "ubuntu:24.04", parsed[0].Image)
|
||||
assert.True(t, parsed[0].Pull)
|
||||
assert.True(t, parsed[0].AlwaysPull)
|
||||
assert.False(t, parsed[0].Init)
|
||||
assert.True(t, parsed[0].StartNow)
|
||||
}
|
||||
@@ -158,7 +158,7 @@ nvidia=true
|
||||
distrodev := parsed[0]
|
||||
assert.Equal(t, "distrodev", distrodev.Name)
|
||||
assert.Equal(t, "ubuntu:24.04", distrodev.Image)
|
||||
assert.True(t, distrodev.Pull)
|
||||
assert.True(t, distrodev.AlwaysPull)
|
||||
assert.False(t, distrodev.Init)
|
||||
assert.True(t, distrodev.StartNow)
|
||||
|
||||
@@ -203,14 +203,14 @@ image=ubuntu:22.04 # this will override the included image
|
||||
ubuntu22 := parsed[0]
|
||||
assert.Equal(t, "ubuntu22", ubuntu22.Name)
|
||||
assert.Equal(t, "ubuntu:22.04", ubuntu22.Image)
|
||||
assert.True(t, ubuntu22.Pull)
|
||||
assert.True(t, ubuntu22.AlwaysPull)
|
||||
assert.True(t, ubuntu22.Root)
|
||||
|
||||
// Check ubuntu24
|
||||
ubuntu24 := parsed[1]
|
||||
assert.Equal(t, "ubuntu24", ubuntu24.Name)
|
||||
assert.Equal(t, "ubuntu:22.04", ubuntu24.Image)
|
||||
assert.True(t, ubuntu24.Pull)
|
||||
assert.True(t, ubuntu24.AlwaysPull)
|
||||
assert.True(t, ubuntu24.Root)
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ start_now=true
|
||||
|
||||
assert.Equal(t, "distrodev", parsed[0].Name)
|
||||
assert.Equal(t, "ubuntu:24.04", parsed[0].Image)
|
||||
assert.True(t, parsed[0].Pull)
|
||||
assert.True(t, parsed[0].AlwaysPull)
|
||||
assert.False(t, parsed[0].Init)
|
||||
assert.True(t, parsed[0].StartNow)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user