// Package registry fetches images from an OCI registry into the store. // The registry side (auth, manifest lists, blobs) is crane's, // github.com/google/go-containerregistry, an unprivileged single // binary; werkdock runs it to a `docker save` tarball and loads that // the way `load -i` does (RFC 0002 level 2, RFC 0004). The surface is // werkdock's, so an in-process client can replace crane later without // a caller noticing. package registry import ( "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "werkdock/internal/store" ) // DefaultPlatform is what a pull asks for when the caller does not say. func DefaultPlatform() string { return "linux/" + runtime.GOARCH } // Pull fetches reference into st and returns the names it was stored // under. Progress from crane goes to progress, which may be nil. // An image that already exists is refused before anything is // downloaded, as `load` would refuse it afterwards. func Pull(st store.Store, reference, platform string, progress io.Writer) ([]string, error) { ref, err := store.ParseReference(reference) if err != nil { return nil, err } if platform == "" { platform = DefaultPlatform() } if _, err := st.Dir(ref.String()); err == nil { return nil, fmt.Errorf("image %q already exists (werkdock rmi %s to replace it)", ref.Short(), ref.Short()) } crane, err := FindCrane(os.Getenv("WERKDOCK_CRANE"), os.Executable) if err != nil { return nil, err } if err := os.MkdirAll(st.Root, 0o755); err != nil { return nil, err } tarball, err := os.CreateTemp(st.Root, ".pull-*.tar") if err != nil { return nil, err } tarball.Close() defer os.Remove(tarball.Name()) if progress == nil { progress = io.Discard } cmd := exec.Command(crane, "pull", "--platform", platform, ref.String(), tarball.Name()) cmd.Stdout, cmd.Stderr = progress, progress if err := cmd.Run(); err != nil { return nil, fmt.Errorf("crane pull %s: %w", ref.Short(), err) } return st.Load(tarball.Name(), ref.String()) } // FindCrane locates the crane binary: WERKDOCK_CRANE if set, else PATH, // else beside the werkdock binary — the managed webspaces have no // package for it, so a deployment ships it next to werkdock. func FindCrane(configured string, executable func() (string, error)) (string, error) { if configured != "" { if _, err := os.Stat(configured); err != nil { return "", fmt.Errorf("WERKDOCK_CRANE: %w", err) } return configured, nil } if path, err := exec.LookPath("crane"); err == nil { return path, nil } if self, err := executable(); err == nil { beside := filepath.Join(filepath.Dir(self), "crane") if info, err := os.Stat(beside); err == nil && !info.IsDir() { return beside, nil } } return "", errors.New("pull needs the crane binary (github.com/google/go-containerregistry): " + "put it in PATH or beside werkdock, or set WERKDOCK_CRANE to its path") }