1
0
Fork 0
mirror of https://github.com/imjasonh/kontain.me synced 2026-07-07 00:32:28 +00:00
kontain.me/cmd/ko/main.go
Jason Hall c7ea6912f0 fix some tests
Signed-off-by: Jason Hall <imjasonh@gmail.com>
2026-05-13 22:19:59 -04:00

303 lines
8.9 KiB
Go

package main
import (
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"strings"
"time"
"github.com/chainguard-dev/clog/gcp"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/ko/pkg/build"
"github.com/imjasonh/kontain.me/pkg/serve"
"golang.org/x/mod/module"
"golang.org/x/mod/zip"
yaml "gopkg.in/yaml.v2"
)
func main() {
ctx := context.Background()
st, err := serve.NewStorage(ctx)
if err != nil {
slog.ErrorContext(ctx, "serve.NewStorage", "err", err)
os.Exit(1)
}
http.Handle("/v2/", gcp.WithCloudTraceContext(&server{storage: st}))
http.Handle("/", http.RedirectHandler("https://github.com/imjasonh/kontain.me/blob/main/cmd/ko", http.StatusSeeOther))
port := os.Getenv("PORT")
if port == "" {
port = "8080"
slog.InfoContext(ctx, "Defaulting port", "port", port)
}
slog.InfoContext(ctx, "Listening...", "port", port)
slog.ErrorContext(ctx, "ListenAndServe", "err", http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
type server struct{ storage *serve.Storage }
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.String(), "/v2/")
switch {
case path == "":
// API Version check.
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
return
case strings.Contains(path, "/blobs/"),
strings.Contains(path, "/manifests/sha256:"):
// Extract requested blob digest and redirect to serve it from GCS.
// If it doesn't exist, this will return 404.
parts := strings.Split(r.URL.Path, "/")
digest := parts[len(parts)-1]
serve.Blob(w, r, digest)
case strings.Contains(path, "/manifests/"):
s.serveKoManifest(w, r)
default:
http.Error(w, "Not found", http.StatusNotFound)
}
}
// ko.kontain.me/github.com/knative/build/cmd/controller -> ko build and serve
func (s *server) serveKoManifest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
path := strings.TrimPrefix(r.URL.Path, "/v2/")
path = strings.TrimPrefix(path, "ko/") // To handle legacy behavior.
parts := strings.Split(path, "/")
ip := strings.Join(parts[:len(parts)-2], "/")
// "go get" the package
tagOrDigest := parts[len(parts)-1]
// If request is for image by digest, try to serve it from GCS.
if strings.HasPrefix(tagOrDigest, "sha256:") {
desc, err := s.storage.BlobExists(ctx, tagOrDigest)
if err != nil {
slog.ErrorContext(ctx, "storage.BlobExists", "err", err)
serve.Error(w, serve.ErrNotFound)
return
}
if r.Method == http.MethodHead {
w.Header().Set("Docker-Content-Digest", tagOrDigest)
w.Header().Set("Content-Type", string(desc.MediaType))
w.Header().Set("Content-Length", fmt.Sprintf("%d", desc.Size))
return
}
serve.Blob(w, r, tagOrDigest)
return
}
// Traverse up from the importpath to find the module root, by checking
// whether the path is a module path that returns a version.
module, version, err := walkUp(ctx, ip, tagOrDigest)
if err != nil {
slog.ErrorContext(ctx, "walkUp", "err", err)
serve.Error(w, err)
return
}
// Check if we've already got a manifest for this importpath + resolved version.
ck := cacheKey(ip, version)
if _, err := s.storage.BlobExists(ctx, ck); err == nil {
slog.InfoContext(ctx, "serving cached manifest", "ck", ck)
serve.Blob(w, r, ck)
return
}
filepath := strings.TrimPrefix(ip, module)
// Pull the module source from the module proxy and build it.
br, err := s.fetchAndBuild(ctx, module, version, filepath)
if err != nil {
slog.ErrorContext(ctx, "fetchAndBuild", "err", err)
serve.Error(w, err)
return
}
if idx, ok := br.(v1.ImageIndex); ok {
if err := s.storage.ServeIndex(w, r, idx, ck); err != nil {
slog.ErrorContext(ctx, "storage.ServeIndex", "err", err)
serve.Error(w, err)
}
return
}
if img, ok := br.(v1.Image); ok {
if err := s.storage.ServeManifest(w, r, img, ck); err != nil {
slog.ErrorContext(ctx, "storage.ServeManifest", "err", err)
serve.Error(w, err)
}
return
}
serve.Error(w, errors.New("image was not image or index"))
}
func cacheKey(importpath, version string) string {
ck := []byte(fmt.Sprintf("%s-%s", importpath, version))
return fmt.Sprintf("ko-%x", md5.Sum(ck))
}
const defaultBaseImage = "gcr.io/distroless/static:nonroot"
func (s *server) getBaseImage(ctx context.Context, ip string) (name.Reference, build.Result, error) {
base := defaultBaseImage
// Assuming we're in the root of the module directory, see if we can
// find the .ko.yaml file.
f, err := os.Open(".ko.yaml")
if err == nil {
defer f.Close()
slog.InfoContext(ctx, "Found .ko.yaml")
var y struct {
DefaultBaseImage string `yaml:"defaultBaseImage"`
BaseImageOverrides map[string]string `yaml:"baseImageOverrides"`
}
if err := yaml.NewDecoder(f).Decode(&y); err != nil {
return nil, nil, err
}
if y.DefaultBaseImage != "" {
base = y.DefaultBaseImage
}
if bio := y.BaseImageOverrides[ip]; bio != "" {
base = bio
}
}
slog.InfoContext(ctx, "Using base image", "base", base, "ip", ip)
ref, err := name.ParseReference(base)
if err != nil {
return nil, nil, err
}
d, err := remote.Head(ref, remote.WithContext(ctx))
if err != nil {
return nil, nil, err
}
switch d.MediaType {
case types.DockerManifestList, types.OCIImageIndex:
slog.InfoContext(ctx, "Base image is index", "base", base)
idx, err := remote.Index(ref)
return ref, idx, err
case types.DockerManifestSchema2, types.OCIManifestSchema1:
slog.InfoContext(ctx, "Base image is image", "base", base)
img, err := remote.Image(ref)
return ref, img, err
default:
return nil, nil, fmt.Errorf("unknown media type: %s", d.MediaType)
}
}
// given an importpath e.g., github.com/google/go-containerregistry/cmd/crane,
// return its go module (github.com/google/go-containerregistry) by
// sequentially checking whether the Go module proxy has version info for it.
func walkUp(ctx context.Context, importpath, version string) (string, string, error) {
parts := strings.Split(importpath, "/")
for i := len(parts) - 1; i > 0; i-- {
check := strings.Join(parts[:i], "/")
if resolved, err := getVersion(ctx, check, version); err == nil {
return check, resolved, nil
}
}
return "", "", errors.New("no module found")
}
func getVersion(ctx context.Context, mod, version string) (string, error) {
url := fmt.Sprintf("https://proxy.golang.org/%s/@v/%s.info", mod, version)
if version == "latest" {
url = fmt.Sprintf("https://proxy.golang.org/%s/@latest", mod)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("%d: %s", resp.StatusCode, resp.Status)
}
defer resp.Body.Close()
var v module.Version
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return "", err
}
return v.Version, nil
}
func (s *server) fetchAndBuild(ctx context.Context, mod, version, filepath string) (build.Result, error) {
url := fmt.Sprintf("https://proxy.golang.org/%s/@v/%s.zip", mod, version)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
}
defer resp.Body.Close()
// Write a temp zip file.
tmpzip, err := os.CreateTemp("", "ko-*")
if err != nil {
return nil, err
}
defer os.Remove(tmpzip.Name()) // Clean up the zip file.
if _, err := io.Copy(tmpzip, resp.Body); err != nil {
return nil, err
}
tmpzip.Close()
// Create a tempdir and cd into it
// (This is only safe because concurrency=1)
tmpdir, err := os.MkdirTemp("", "ko-*")
if err != nil {
return nil, err
}
// Clean up the temp dir. If building is successful, we'll serve a
// cached manifest and not need to rebuild.
defer os.RemoveAll(tmpdir)
// Unzip and validate the module zip file.
if err := zip.Unzip(tmpdir, module.Version{
Path: mod,
Version: version,
}, tmpzip.Name()); err != nil {
return nil, err
}
// ko build the package.
g, err := build.NewGo(
ctx, tmpdir,
build.WithBaseImages(s.getBaseImage),
build.WithPlatforms("all"),
build.WithConfig(map[string]build.Config{
mod + filepath: build.Config{
// Go module proxy zips include only
// modules.txt in vendor/, so force mod mode to
// avoid go build errors.
Flags: build.FlagArray{"-mod=mod"},
},
}),
build.WithCreationTime(v1.Time{Time: time.Unix(0, 0)}),
build.WithDisabledSBOM(),
)
if err != nil {
return nil, err
}
ip := build.StrictScheme + mod + filepath
if err := g.IsSupportedReference(ip); err != nil {
return nil, err
}
slog.InfoContext(ctx, "ko build", "ip", ip)
return g.Build(ctx, ip)
}