package main
import (
"context"
"flag"
"fmt"
"io"
"os"
"strings"
"github.com/containerd/containerd"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/remotes/docker"
"github.com/openeuler/Conch/internal/image"
"github.com/openeuler/Conch/pkg/ulog"
)
func printPullHelp(out io.Writer) {
fmt.Fprintln(out, "Usage:")
fmt.Fprintln(out, " conch pull [options] <image-name>")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Description:")
fmt.Fprintln(out, " Pull a Conch native image into containerd content store, then unpack")
fmt.Fprintln(out, " all child images and link snapshot labels.")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Options:")
fmt.Fprintln(out, " -n, --namespace string")
fmt.Fprintln(out, " containerd namespace (default: config containerd.default_namespace or default)")
fmt.Fprintln(out, " -address string")
fmt.Fprintf(out, " containerd socket address (default: config containerd.socket or %s)\n", defaultContainerdAddress)
fmt.Fprintln(out, " -config string")
fmt.Fprintln(out, " config file path (default: auto-detect common config paths)")
fmt.Fprintln(out, " --plain-http")
fmt.Fprintln(out, " allow plain HTTP / disable TLS verification for source image pulls")
fmt.Fprintln(out, " --user string")
fmt.Fprintln(out, " registry credentials in username:password format for source image pulls")
fmt.Fprintln(out, " --kernel-plain-http")
fmt.Fprintln(out, " allow plain HTTP / disable TLS verification for default kernel image pulls")
fmt.Fprintln(out, " --kernel-user string")
fmt.Fprintln(out, " registry credentials in username:password format for default kernel image pulls")
fmt.Fprintln(out, "")
fmt.Fprintln(out, "Example:")
fmt.Fprintln(out, " conch pull -n default hub.oepkgs.net/conch/sandbox-snapshot:latest")
fmt.Fprintln(out, " conch pull --kernel-plain-http --kernel-user example-user:example-password docker.io/library/nginx:latest")
}
func runPull(ctx context.Context, args []string) error {
if err := initUnpackLogger(); err != nil {
return err
}
defer func() {
logger := ulog.GetLogger()
if closer, ok := logger.(interface{ Close() error }); ok {
_ = closer.Close()
}
}()
fs := flag.NewFlagSet("pull", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
addr := fs.String("address", "", "containerd socket address")
namespace := fs.String("namespace", "", "containerd namespace")
configPath := fs.String("config", "", "config file path")
plainHTTP := fs.Bool("plain-http", false, "allow plain HTTP / disable TLS verification for source image pulls")
user := fs.String("user", "", "registry credentials in username:password format for source image pulls")
kernelPlainHTTP := fs.Bool("kernel-plain-http", false, "allow plain HTTP / disable TLS verification for default kernel image pulls")
kernelUser := fs.String("kernel-user", "", "registry credentials in username:password format for default kernel image pulls")
fs.StringVar(namespace, "n", "", "containerd namespace")
fs.Usage = func() { printPullHelp(os.Stderr) }
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() != 1 {
fs.Usage()
return fmt.Errorf("conch pull: exactly one image name is required")
}
imageName := fs.Arg(0)
cfg, err := loadConchConfig(*configPath)
if err != nil {
return fmt.Errorf("conch pull: load config: %w", err)
}
containerdAddr, ns := resolveContainerdRuntime(cfg, *addr, *namespace)
username, password, err := parseRegistryUser(*user)
if err != nil {
return fmt.Errorf("conch pull: %w", err)
}
kernelUsername, kernelPassword, err := parseRegistryUser(*kernelUser)
if err != nil {
return fmt.Errorf("conch pull: %w", err)
}
client, err := containerd.New(containerdAddr)
if err != nil {
return fmt.Errorf("connect to containerd: %w", err)
}
defer client.Close()
pullCtx := namespaces.WithNamespace(ctx, ns)
fmt.Println("------------------------------------------------------------")
fmt.Printf("Pulling image: %s\n", imageName)
resolver := docker.NewResolver(docker.ResolverOptions{
PlainHTTP: *plainHTTP,
Credentials: func(string) (string, string, error) {
return username, password, nil
},
})
pullOpts := []containerd.RemoteOpt{
containerd.WithResolver(resolver),
}
if _, err := client.Pull(pullCtx, imageName, pullOpts...); err != nil {
return fmt.Errorf("conch pull: pull image %s: %w", imageName, err)
}
if err := image.ValidateConchImageIndex(pullCtx, client, imageName); err != nil {
results, convErr := image.PullAndUnpackOCIImage(pullCtx, client, image.PullOCIImageOptions{
SourceImage: imageName,
DefaultKernelImage: cfg.Image.DefaultKernelImage,
SourcePlainHTTP: *plainHTTP,
SourceRegistryUsername: username,
SourceRegistryPassword: password,
KernelPlainHTTP: *kernelPlainHTTP,
KernelRegistryUsername: kernelUsername,
KernelRegistryPassword: kernelPassword,
})
if convErr != nil {
return fmt.Errorf("conch pull: image %s is not a supported Conch image and OCI conversion failed: %w", imageName, convErr)
}
printUnpackSummary(results)
return nil
}
if _, err := client.Fetch(pullCtx, imageName, containerd.WithResolver(resolver)); err != nil {
return fmt.Errorf("conch pull: fetch all Conch image content: %w", err)
}
results, err := image.UnpackAllSubImages(pullCtx, client, imageName)
if err != nil {
return fmt.Errorf("conch pull: unpack pulled image: %w", err)
}
printUnpackSummary(results)
return nil
}
func parseRegistryUser(user string) (string, string, error) {
if user == "" {
return "", "", nil
}
idx := strings.IndexByte(user, ':')
if idx <= 0 || idx == len(user)-1 {
return "", "", fmt.Errorf("invalid --user value %q, want username:password", user)
}
return user[:idx], user[idx+1:], nil
}