merge(quinistry): Integrate at //fun/quinistry
This is too historically interesting for me to lose it.
This commit is contained in:
commit
cdf25193a4
8 changed files with 417 additions and 0 deletions
2
fun/quinistry/.gitignore
vendored
Normal file
2
fun/quinistry/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.idea/
|
||||
quinistry
|
63
fun/quinistry/README.md
Normal file
63
fun/quinistry/README.md
Normal file
|
@ -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)
|
12
fun/quinistry/const.go
Normal file
12
fun/quinistry/const.go
Normal file
|
@ -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"
|
150
fun/quinistry/image.go
Normal file
150
fun/quinistry/image.go
Normal file
|
@ -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,
|
||||
}
|
||||
}
|
27
fun/quinistry/k8s/child.yaml
Normal file
27
fun/quinistry/k8s/child.yaml
Normal file
|
@ -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
|
27
fun/quinistry/k8s/parent.yaml
Normal file
27
fun/quinistry/k8s/parent.yaml
Normal file
|
@ -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
|
57
fun/quinistry/main.go
Normal file
57
fun/quinistry/main.go
Normal file
|
@ -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)
|
||||
}
|
79
fun/quinistry/types.go
Normal file
79
fun/quinistry/types.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Reference in a new issue