Skip to content

Commit

Permalink
Add new experimental-image-proxy hidden command
Browse files Browse the repository at this point in the history
This imports the code from https://github.com/cgwalters/container-image-proxy

First, assume one is operating on a codebase that isn't Go, but wants
to interact with container images - we can't just include the Go containers/image
library.

The primary intended use case of this is for things like
[ostree-containers](ostreedev/ostree-rs-ext#18)
where we're using container images to encapsulate host operating system
updates, but we don't want to involve the [containers/image](github.com/containers/image/)
storage layer.

Vendoring the containers/image stack in another project is a large lift; the stripped
binary for this proxy standalone weighs in at 16M (I'm sure the lack
of LTO and the overall simplicity of the Go compiler is a large factor).
Anyways, I'd like to avoid shipping another copy.

This command is marked as experimental, and hidden.  The goal is
just to use it from the ostree stack for now, ideally shipping at least
in CentOS 9 Stream relatively soon.   We can (and IMO should)
change and improve it later.

A lot more discussion in cgwalters/container-image-proxy#1
  • Loading branch information
cgwalters committed Oct 6, 2021
1 parent fc81803 commit 88c692d
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/skopeo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func createApp() (*cobra.Command, *globalOptions) {
copyCmd(&opts),
deleteCmd(&opts),
inspectCmd(&opts),
proxyCmd(&opts),
layersCmd(&opts),
loginCmd(&opts),
logoutCmd(&opts),
Expand Down
265 changes: 265 additions & 0 deletions cmd/skopeo/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package main

import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"

"github.com/containers/image/v5/image"
"github.com/containers/image/v5/manifest"
"github.com/containers/image/v5/pkg/blobinfocache"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/opencontainers/go-digest"
"github.com/spf13/cobra"
)

type proxyHandler struct {
imageref string
sysctx *types.SystemContext
cache types.BlobInfoCache
imgsrc *types.ImageSource
img *types.Image
shutdown bool
}

func (h *proxyHandler) ensureImage() error {
if h.img != nil {
return nil
}
imgRef, err := alltransports.ParseImageName(h.imageref)
if err != nil {
return err
}
imgsrc, err := imgRef.NewImageSource(context.Background(), h.sysctx)
if err != nil {
return err
}
img, err := image.FromUnparsedImage(context.Background(), h.sysctx, image.UnparsedInstance(imgsrc, nil))
if err != nil {
return fmt.Errorf("failed to load image: %w", err)
}
h.img = &img
h.imgsrc = &imgsrc
return nil
}

func (h *proxyHandler) implManifest(w http.ResponseWriter, r *http.Request) error {
if err := h.ensureImage(); err != nil {
return err
}

_, err := io.Copy(io.Discard, r.Body)
if err != nil {
return err
}
ctx := context.TODO()
rawManifest, _, err := (*h.img).Manifest(ctx)
if err != nil {
return err
}
digest, err := manifest.Digest(rawManifest)
if err != nil {
return err
}
w.Header().Add("Manifest-Digest", digest.String())

ociManifest, err := manifest.OCI1FromManifest(rawManifest)
if err != nil {
return err
}
ociSerialized, err := ociManifest.Serialize()
if err != nil {
return err
}

w.Header().Set("Content-Length", fmt.Sprintf("%d", len(ociSerialized)))
w.WriteHeader(200)
_, err = io.Copy(w, bytes.NewReader(ociSerialized))
if err != nil {
return err
}
return nil
}

func (h *proxyHandler) implBlob(w http.ResponseWriter, r *http.Request, digestStr string) error {
if err := h.ensureImage(); err != nil {
return err
}

_, err := io.Copy(io.Discard, r.Body)
if err != nil {
return err
}

ctx := context.TODO()
d, err := digest.Parse(digestStr)
if err != nil {
return err
}
blobr, blobSize, err := (*h.imgsrc).GetBlob(ctx, types.BlobInfo{Digest: d, Size: -1}, h.cache)
if err != nil {
return err
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", blobSize))
w.Header().Set("Content-Type", "application/octet-stream")
w.WriteHeader(200)
verifier := d.Verifier()
tr := io.TeeReader(blobr, verifier)
_, err = io.Copy(w, tr)
if err != nil {
return err
}
if !verifier.Verified() {
return fmt.Errorf("Corrupted blob, expecting %s", d.String())
}
return nil
}

// ServeHTTP handles two requests:
//
// GET /manifest
// GET /blobs/<digest>
// POST /quit
func (h *proxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
if r.URL.Path == "/quit" {
w.Header().Set("Content-Length", "0")
w.WriteHeader(200)
h.shutdown = true
return
}
}

if r.Method != http.MethodGet {
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

if r.URL.Path == "" || !strings.HasPrefix(r.URL.Path, "/") {
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusBadRequest)
return
}

var err error
if err != nil {

}

if r.URL.Path == "/manifest" {
err = h.implManifest(w, r)
} else if strings.HasPrefix(r.URL.Path, "/blobs/") {
blob := filepath.Base(r.URL.Path)
err = h.implBlob(w, r, blob)
} else {
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusBadRequest)
return
}
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
}

type sockResponseWriter struct {
out io.Writer
headers http.Header
}

func (rw sockResponseWriter) Header() http.Header {
return rw.headers
}

func (rw sockResponseWriter) Write(buf []byte) (int, error) {
return rw.out.Write(buf)
}

func (rw sockResponseWriter) WriteHeader(statusCode int) {
rw.out.Write([]byte(fmt.Sprintf("HTTP/1.1 %d OK\r\n", statusCode)))
rw.headers.Write(rw.out)
rw.out.Write([]byte("\r\n"))
}

type proxyOptions struct {
global *globalOptions
quiet bool
sockFd int
portNum int
}

func proxyCmd(global *globalOptions) *cobra.Command {
opts := proxyOptions{global: global}
cmd := &cobra.Command{
Use: "experimental-image-proxy [command options] IMAGE",
Short: "Interactive proxy for fetching container images (EXPERIMENTAL)",
Long: `Run skopeo as a proxy, supporting HTTP requests to fetch manifests and blobs.`,
RunE: commandAction(opts.run),
Args: cobra.ExactArgs(1),
// Not stabilized yet
Hidden: true,
Example: `skopeo proxy --sockfd 3`,
}
adjustUsage(cmd)
flags := cmd.Flags()
flags.IntVar(&opts.sockFd, "sockfd", -1, "Serve on opened socket pair")
return cmd
}

// Implementation of podman experimental-image-proxy
func (opts *proxyOptions) run(args []string, stdout io.Writer) error {
sysCtx := opts.global.newSystemContext()

handler := &proxyHandler{
imageref: args[0],
sysctx: sysCtx,
cache: blobinfocache.DefaultCache(sysCtx),
}
var buf *bufio.ReadWriter
if opts.sockFd != -1 {
fd := os.NewFile(uintptr(opts.sockFd), "sock")
buf = bufio.NewReadWriter(bufio.NewReader(fd), bufio.NewWriter(fd))
} else {
buf = bufio.NewReadWriter(bufio.NewReader(os.Stdin), bufio.NewWriter(os.Stdout))
}

for {
req, err := http.ReadRequest(buf.Reader)
if err != nil {
if err == io.EOF {
return nil
}
return err
}
resp := sockResponseWriter{
out: buf,
headers: make(map[string][]string),
}
handler.ServeHTTP(resp, req)
err = buf.Flush()
if err != nil {
return err
}

if handler.shutdown {
break
}
}

if handler.img != nil {
if err := (*handler.imgsrc).Close(); err != nil {
return err
}
}

return nil
}

0 comments on commit 88c692d

Please sign in to comment.