refactor: Split up code for readability and add docs

This commit is contained in:
Vincent Ambo 2017-03-16 14:06:21 +01:00
parent 1c2d087ec4
commit fa43472a5d
4 changed files with 242 additions and 159 deletions

12
const.go Normal file
View 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"

158
image.go
View file

@ -1,28 +1,150 @@
// The code in this file creates a Docker image layer containing the binary of the
// application itself.
package main package main
import "time" import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"time"
)
type RootFs struct { // This function creates a Docker-image digest (i.e. SHA256 hash with
DiffIds []string `json:"diff_ids"` // algorithm-specification prefix)
Type string `json:"type"` func Digest(b []byte) string {
hash := sha256.New()
hash.Write(b)
return fmt.Sprintf("sha256:%x", hash.Sum(nil))
} }
type History struct { func GetImageOfCurrentExecutable() Image {
Created time.Time `json:"created"` binary := getCurrentBinary()
CreatedBy string `json:"created_by"` 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),
} }
type ImageConfig struct {
Cmd []string
Env []string
} }
type Config struct { func getCurrentBinary() []byte {
Created time.Time `json:"created"` path, _ := os.Executable()
Author string `json:"author"` file, _ := ioutil.ReadFile(path)
Architecture string `json:"architecture"` return file
Os string `json:"os"` }
Config *ImageConfig `json:"config"`
RootFs RootFs `json:"rootfs"` func createTarArchive(files *map[string][]byte) []byte {
History []History `json:"history"` 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,
}
} }

152
main.go
View file

@ -1,92 +1,18 @@
package main package main
import ( import (
"archive/tar"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os"
"time"
) )
const ImageContentType string = "application/vnd.docker.container.image.v1+json"
const ManifestContentType string = "application/vnd.docker.distribution.manifest.v2+json"
const LayerContentType string = "application/vnd.docker.image.rootfs.diff.tar.gzip"
const DigestHeader string = "Docker-Content-Digest"
type Element struct {
MediaType string `json:"mediaType"`
Size int `json:"size"`
Digest string `json:"digest"`
}
type Manifest struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
Config Element `json:"config"`
Layers []Element `json:"layers"`
}
// A really "dumb" representation of an image, with a data blob (tar.gz image) and its hash as the type expected
// in the manifest.
type Image struct {
Data []byte
TarDigest string
GzipDigest string
}
func main() { func main() {
log.Println("Starting quinistry") log.Println("Starting quinistry")
img := getImage() image := GetImageOfCurrentExecutable()
now := time.Now()
config := Config{ layerUri := fmt.Sprintf("/v2/quinistry/blobs/%s", image.LayerDigest)
Created: now, configUri := fmt.Sprintf("/v2/quinistry/blobs/%s", image.ConfigDigest)
Author: "tazjin",
Architecture: "amd64",
Os: "linux",
Config: &ImageConfig{
Cmd: []string{"main"},
Env: []string{"PATH=/"},
},
RootFs: RootFs{
DiffIds: []string{
img.TarDigest,
},
Type: "layers",
},
History: []History{
{
Created: now,
CreatedBy: "quinistry magic",
},
},
}
configJson, _ := json.Marshal(config)
manifest := Manifest{
SchemaVersion: 2,
MediaType: ManifestContentType,
Config: Element{
MediaType: ImageContentType,
Size: len(configJson),
Digest: digest(configJson),
},
Layers: []Element{
{
MediaType: LayerContentType,
Size: len(img.Data),
Digest: img.GzipDigest,
},
},
}
log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Acknowledge that we speak V2 // Acknowledge that we speak V2
@ -99,29 +25,26 @@ func main() {
// Serve manifest // Serve manifest
if r.RequestURI == "/v2/quinistry/manifests/latest" { if r.RequestURI == "/v2/quinistry/manifests/latest" {
logRequest("Serving manifest", r) logRequest("Serving manifest", r)
w.Header().Add("Content-Type", ManifestContentType) w.Header().Set(ContentType, ManifestMediaType)
resp, _ := json.Marshal(manifest) w.Header().Add(DigestHeader, image.ManifestDigest)
w.Header().Add(DigestHeader, digest(resp)) w.Write(image.Manifest)
w.Write(resp)
return return
} }
// Serve actual image layer // Serve actual image layer
layerUri := fmt.Sprintf("/v2/quinistry/blobs/%s", img.GzipDigest)
if r.RequestURI == layerUri { if r.RequestURI == layerUri {
logRequest("Serving image layer blob", r) logRequest("Serving image layer blob", r)
w.Header().Add(DigestHeader, img.GzipDigest) w.Header().Add(DigestHeader, image.LayerDigest)
w.Write(img.Data) w.Write(image.Layer)
return return
} }
// Serve image config // Serve image config
configUri := fmt.Sprintf("/v2/quinistry/blobs/%s", digest(configJson))
if r.RequestURI == configUri { if r.RequestURI == configUri {
logRequest("Serving config", r) logRequest("Serving config", r)
w.Header().Set("Content-Type", ImageContentType) w.Header().Set("Content-Type", ImageConfigMediaType)
w.Header().Set(DigestHeader, digest(configJson)) w.Header().Set(DigestHeader, image.ConfigDigest)
w.Write(configJson) w.Write(image.Config)
return return
} }
@ -132,56 +55,3 @@ func main() {
func logRequest(msg string, r *http.Request) { func logRequest(msg string, r *http.Request) {
log.Printf("%s: %s %s\n", msg, r.Method, r.RequestURI) log.Printf("%s: %s %s\n", msg, r.Method, r.RequestURI)
} }
func digest(b []byte) string {
hash := sha256.New()
hash.Write(b)
return fmt.Sprintf("sha256:%x", hash.Sum(nil))
}
// Creates an image of the currently running binary (spooky!)
func getImage() *Image {
// Current binary, imagine this is some other output or whatever
path, _ := os.Executable()
// don't care about error! :O
file, _ := ioutil.ReadFile(path)
// First create tar archive
tarBuf := new(bytes.Buffer)
tarW := tar.NewWriter(tarBuf)
hdr := &tar.Header{
Name: "/main",
Mode: 0755,
Size: int64(len(file)),
}
tarW.WriteHeader(hdr)
tarW.Write(file)
if err := tarW.Close(); err != nil {
log.Fatalln(err)
os.Exit(1)
}
tarBytes := tarBuf.Bytes()
// Then GZIP it
zBuf := new(bytes.Buffer)
zw := gzip.NewWriter(zBuf)
zw.Name = "Docker registry fake test"
zw.Write(tarBytes)
if err := zw.Close(); err != nil {
log.Fatal(err)
os.Exit(1)
}
gzipData := zBuf.Bytes()
return &Image{
TarDigest: digest(tarBytes),
GzipDigest: digest(gzipData),
Data: gzipData,
}
}

79
types.go Normal file
View 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
}