diff options
Diffstat (limited to 'fun/quinistry')
-rw-r--r-- | fun/quinistry/.gitignore | 2 | ||||
-rw-r--r-- | fun/quinistry/README.md | 63 | ||||
-rw-r--r-- | fun/quinistry/const.go | 12 | ||||
-rw-r--r-- | fun/quinistry/default.nix | 11 | ||||
-rw-r--r-- | fun/quinistry/image.go | 150 | ||||
-rw-r--r-- | fun/quinistry/k8s/child.yaml | 27 | ||||
-rw-r--r-- | fun/quinistry/k8s/parent.yaml | 27 | ||||
-rw-r--r-- | fun/quinistry/main.go | 57 | ||||
-rw-r--r-- | fun/quinistry/types.go | 79 |
9 files changed, 428 insertions, 0 deletions
diff --git a/fun/quinistry/.gitignore b/fun/quinistry/.gitignore new file mode 100644 index 000000000000..622119552e47 --- /dev/null +++ b/fun/quinistry/.gitignore @@ -0,0 +1,2 @@ +.idea/ +quinistry \ No newline at end of file diff --git a/fun/quinistry/README.md b/fun/quinistry/README.md new file mode 100644 index 000000000000..de197a219ebf --- /dev/null +++ b/fun/quinistry/README.md @@ -0,0 +1,63 @@ +Quinistry +========= + +*A simple Docker registry quine.* + +## What? + +This is an example project for a from-scratch implementation of an HTTP server compatible with the [Docker Registry V2][] +protocol. + +It serves a single image called `quinistry:latest` which is a Docker image that runs quinistry itself, therefore it is a +sort of Docker registry [quine][]. + +The official documentation does not contain enough information to actually implement this protocol (which I assume is +intentional), but a bit of trial&error lead there anyways. I've added comments to parts of the code to clear up things +that may be helpful to other developers in the future. + +## Example + +``` +# Run quinistry: +vincent@urdhva ~/go/src/github.com/tazjin/quinistry (git)-[master] % ./quinistry +2017/03/16 14:11:56 Starting quinistry + +# Pull the quinistry image from itself: +vincent@urdhva ~ % docker pull localhost:8080/quinistry +Using default tag: latest +latest: Pulling from quinistry +7bf1a8b18466: Already exists +Digest: sha256:d5cd4490901ef04b4e28e4ccc03a1d25fe3461200cf4d7166aab86fcd495e22e +Status: Downloaded newer image for localhost:8080/quinistry:latest + +# Quinistry will log: +2017/03/16 14:14:03 Acknowleding V2 API: GET /v2/ +2017/03/16 14:14:03 Serving manifest: GET /v2/quinistry/manifests/latest +2017/03/16 14:14:03 Serving config: GET /v2/quinistry/blobs/sha256:fbb165c48849de16017aa398aa9bb08fd1c00eaa7c150b6c2af37312913db279 + +# Run the downloaded image: +vincent@urdhva ~ % docker run -p 8090:8080 localhost:8080/quinistry +2017/03/16 13:15:18 Starting quinistry + +# And download it again from itself: +vincent@urdhva ~ % docker pull localhost:8090/quinistry +Using default tag: latest +latest: Pulling from quinistry +7bf1a8b18466: Already exists +Digest: sha256:11141d95ddce0bac9ffa32ab1e8bc94748ed923e87762c68858dc41d11a46d3f +Status: Downloaded newer image for localhost:8090/quinistry:latest +``` + +## Building + +Quinistry creates a Docker image that only contains a statically linked `main` binary. As this package makes use of +`net/http`, Go will (by default) link against `libc` for DNS resolution and create a dynamic binary instead. + +To disable this, `build` the project with `-tags netgo`: + +``` +go build -tags netgo +``` + +[Docker Registry V2]: https://docs.docker.com/registry/spec/api/ +[quine]: https://en.wikipedia.org/wiki/Quine_(computing) \ No newline at end of file diff --git a/fun/quinistry/const.go b/fun/quinistry/const.go new file mode 100644 index 000000000000..173fa9efc390 --- /dev/null +++ b/fun/quinistry/const.go @@ -0,0 +1,12 @@ +package main + +// HTTP content types + +const ImageConfigMediaType string = "application/vnd.docker.container.image.v1+json" +const ManifestMediaType string = "application/vnd.docker.distribution.manifest.v2+json" +const LayerMediaType string = "application/vnd.docker.image.rootfs.diff.tar.gzip" + +// HTTP header names + +const ContentType string = "Content-Type" +const DigestHeader string = "Docker-Content-Digest" diff --git a/fun/quinistry/default.nix b/fun/quinistry/default.nix new file mode 100644 index 000000000000..8b005da8f67e --- /dev/null +++ b/fun/quinistry/default.nix @@ -0,0 +1,11 @@ +{ pkgs, ... }: + +pkgs.buildGo.program { + name = "quinistry"; + srcs = [ + ./const.go + ./image.go + ./main.go + ./types.go + ]; +} diff --git a/fun/quinistry/image.go b/fun/quinistry/image.go new file mode 100644 index 000000000000..3daeac34d6bb --- /dev/null +++ b/fun/quinistry/image.go @@ -0,0 +1,150 @@ +// The code in this file creates a Docker image layer containing the binary of the +// application itself. + +package main + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "time" +) + +// This function creates a Docker-image digest (i.e. SHA256 hash with +// algorithm-specification prefix) +func Digest(b []byte) string { + hash := sha256.New() + hash.Write(b) + + return fmt.Sprintf("sha256:%x", hash.Sum(nil)) +} + +func GetImageOfCurrentExecutable() Image { + binary := getCurrentBinary() + tarArchive := createTarArchive(&map[string][]byte{ + "/main": binary, + }) + + configJson, configElem := createConfig([]string{Digest(tarArchive)}) + compressed := gzipArchive("Quinistry image", tarArchive) + manifest := createManifest(&configElem, &compressed) + manifestJson, _ := json.Marshal(manifest) + + return Image{ + Layer: compressed, + LayerDigest: Digest(compressed), + Manifest: manifestJson, + ManifestDigest: Digest(manifestJson), + Config: configJson, + ConfigDigest: Digest(configJson), + } + +} + +func getCurrentBinary() []byte { + path, _ := os.Executable() + file, _ := ioutil.ReadFile(path) + return file +} + +func createTarArchive(files *map[string][]byte) []byte { + buf := new(bytes.Buffer) + w := tar.NewWriter(buf) + + for name, file := range *files { + hdr := &tar.Header{ + Name: name, + // Everything is executable \o/ + Mode: 0755, + Size: int64(len(file)), + } + w.WriteHeader(hdr) + w.Write(file) + } + + if err := w.Close(); err != nil { + log.Fatalln(err) + os.Exit(1) + } + + return buf.Bytes() +} + +func gzipArchive(name string, archive []byte) []byte { + buf := new(bytes.Buffer) + w := gzip.NewWriter(buf) + w.Name = name + w.Write(archive) + + if err := w.Close(); err != nil { + log.Fatalln(err) + os.Exit(1) + } + + return buf.Bytes() +} + +func createConfig(layerDigests []string) (configJson []byte, elem Element) { + now := time.Now() + + imageConfig := &ImageConfig{ + Cmd: []string{"/main"}, + Env: []string{"PATH=/"}, + } + + rootFs := RootFs{ + DiffIds: layerDigests, + Type: "layers", + } + + history := []History{ + { + Created: now, + CreatedBy: "Quinistry magic", + }, + } + + config := Config{ + Created: now, + Author: "tazjin", + Architecture: "amd64", + Os: "linux", + Config: imageConfig, + RootFs: rootFs, + History: history, + } + + configJson, _ = json.Marshal(config) + + elem = Element{ + MediaType: ImageConfigMediaType, + Size: len(configJson), + Digest: Digest(configJson), + } + + return +} + +func createManifest(config *Element, layer *[]byte) Manifest { + layers := []Element{ + { + MediaType: LayerMediaType, + Size: len(*layer), + // Layers must contain the digest of the *gzipped* layer. + Digest: Digest(*layer), + }, + } + + return Manifest{ + SchemaVersion: 2, + MediaType: ManifestMediaType, + Config: *config, + Layers: layers, + } +} diff --git a/fun/quinistry/k8s/child.yaml b/fun/quinistry/k8s/child.yaml new file mode 100644 index 000000000000..aa2e318262cd --- /dev/null +++ b/fun/quinistry/k8s/child.yaml @@ -0,0 +1,27 @@ +# This is a child quinistry, running via an image served off the parent. +--- +apiVersion: extensions/v1beta1 +kind: DaemonSet +metadata: + name: quinistry-gen2 + labels: + k8s-app: quinistry + quinistry/role: child + quinistry/generation: '2' +spec: + template: + metadata: + labels: + k8s-app: quinistry + quinistry/role: child + quinistry/generation: '2' + spec: + containers: + - name: quinistry + # Bootstrap via Docker Hub (or any other registry) + image: localhost:5000/quinistry + ports: + - name: registry + containerPort: 8080 + # Incremented hostPort, + hostPort: 5001 diff --git a/fun/quinistry/k8s/parent.yaml b/fun/quinistry/k8s/parent.yaml new file mode 100644 index 000000000000..0db2fe300e1e --- /dev/null +++ b/fun/quinistry/k8s/parent.yaml @@ -0,0 +1,27 @@ +# This is a bootstrapped Quinistry DaemonSet. The initial image +# comes from Docker Hub +--- +apiVersion: extensions/v1beta1 +kind: DaemonSet +metadata: + name: quinistry + labels: + k8s-app: quinistry + quinistry/role: parent + quinistry/generation: '1' +spec: + template: + metadata: + labels: + k8s-app: quinistry + quinistry/role: parent + quinistry/generation: '1' + spec: + containers: + - name: quinistry + # Bootstrap via Docker Hub (or any other registry) + image: tazjin/quinistry + ports: + - name: registry + containerPort: 8080 + hostPort: 5000 diff --git a/fun/quinistry/main.go b/fun/quinistry/main.go new file mode 100644 index 000000000000..50b47418d1a8 --- /dev/null +++ b/fun/quinistry/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + log.Println("Starting quinistry") + + image := GetImageOfCurrentExecutable() + + layerUri := fmt.Sprintf("/v2/quinistry/blobs/%s", image.LayerDigest) + configUri := fmt.Sprintf("/v2/quinistry/blobs/%s", image.ConfigDigest) + + log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Acknowledge that we speak V2 + if r.RequestURI == "/v2/" { + logRequest("Acknowleding V2 API", r) + fmt.Fprintln(w) + return + } + + // Serve manifest + if r.RequestURI == "/v2/quinistry/manifests/latest" { + logRequest("Serving manifest", r) + w.Header().Set(ContentType, ManifestMediaType) + w.Header().Add(DigestHeader, image.ManifestDigest) + w.Write(image.Manifest) + return + } + + // Serve actual image layer + if r.RequestURI == layerUri { + logRequest("Serving image layer blob", r) + w.Header().Add(DigestHeader, image.LayerDigest) + w.Write(image.Layer) + return + } + + // Serve image config + if r.RequestURI == configUri { + logRequest("Serving config", r) + w.Header().Set("Content-Type", ImageConfigMediaType) + w.Header().Set(DigestHeader, image.ConfigDigest) + w.Write(image.Config) + return + } + + log.Printf("Unhandled request: %v\n", *r) + }))) +} + +func logRequest(msg string, r *http.Request) { + log.Printf("%s: %s %s\n", msg, r.Method, r.RequestURI) +} diff --git a/fun/quinistry/types.go b/fun/quinistry/types.go new file mode 100644 index 000000000000..498cbac2f2ab --- /dev/null +++ b/fun/quinistry/types.go @@ -0,0 +1,79 @@ +package main + +import "time" + +// This type represents the rootfs-key of the Docker image config. +// It specifies the digest (i.e. usually SHA256 hash) of the tar'ed, but NOT +// compressed image layers. +type RootFs struct { + // The digests of the non-compressed FS layers. + DiffIds []string `json:"diff_ids"` + + // Type should always be set to "layers" + Type string `json:"type"` +} + +// This type represents an entry in the Docker image config's history key. +// Every history element "belongs" to a filesystem layer. +type History struct { + Created time.Time `json:"created"` + CreatedBy string `json:"created_by"` +} + +// This type represents runtime-configuration for the Docker image. +// A lot of possible keys are omitted here, see: +// https://github.com/docker/docker/blob/master/image/spec/v1.2.md#image-json-description +type ImageConfig struct { + Cmd []string + Env []string +} + +// This type represents the Docker image configuration +type Config struct { + Created time.Time `json:"created"` + Author string `json:"author"` + + // Architecture should be "amd64" + Architecture string `json:"architecture"` + + // OS should be "linux" + Os string `json:"os"` + + // Configuration can be set to 'nil', in which case all options have to be + // supplied at container launch time. + Config *ImageConfig `json:"config"` + + // Filesystem layers and history elements have to be in the same order. + RootFs RootFs `json:"rootfs"` + History []History `json:"history"` +} + +// This type represents any manifest +type Element struct { + MediaType string `json:"mediaType"` + Size int `json:"size"` + Digest string `json:"digest"` +} + +// This type represents a Docker image manifest as used by the registry +// protocol V2. +type Manifest struct { + SchemaVersion int `json:"schemaVersion"` // Must be 2 + MediaType string `json:"mediaType"` // Use ManifestMediaType const + Config Element `json:"config"` + Layers []Element `json:"layers"` +} + +// A really "dumb" representation of an image, with its data blob and related +// metadata. +// Note: This is not a registry API type. +type Image struct { + Layer []byte + LayerDigest string + + Manifest []byte + ManifestDigest string + + Config []byte + ConfigDigest string +} |