refactor: Split up code for readability and add docs
This commit is contained in:
parent
1c2d087ec4
commit
fa43472a5d
4 changed files with 242 additions and 159 deletions
12
const.go
Normal file
12
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"
|
158
image.go
158
image.go
|
@ -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 {
|
func getCurrentBinary() []byte {
|
||||||
Cmd []string
|
path, _ := os.Executable()
|
||||||
Env []string
|
file, _ := ioutil.ReadFile(path)
|
||||||
|
return file
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
func createTarArchive(files *map[string][]byte) []byte {
|
||||||
Created time.Time `json:"created"`
|
buf := new(bytes.Buffer)
|
||||||
Author string `json:"author"`
|
w := tar.NewWriter(buf)
|
||||||
Architecture string `json:"architecture"`
|
|
||||||
Os string `json:"os"`
|
for name, file := range *files {
|
||||||
Config *ImageConfig `json:"config"`
|
hdr := &tar.Header{
|
||||||
RootFs RootFs `json:"rootfs"`
|
Name: name,
|
||||||
History []History `json:"history"`
|
// 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
152
main.go
|
@ -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
79
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