forked from containers/skopeo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add new
experimental-image-proxy
hidden command
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
Showing
2 changed files
with
266 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |