diff options
-rw-r--r-- | const.go | 12 | ||||
-rw-r--r-- | image.go | 158 | ||||
-rw-r--r-- | main.go | 152 | ||||
-rw-r--r-- | types.go | 79 |
4 files changed, 242 insertions, 159 deletions
diff --git a/const.go b/const.go new file mode 100644 index 000000000000..173fa9efc390 --- /dev/null +++ b/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/image.go b/image.go index e627544462e5..3daeac34d6bb 100644 --- a/image.go +++ b/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 -import "time" +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) -type RootFs struct { - DiffIds []string `json:"diff_ids"` - Type string `json:"type"` + 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() } -type History struct { - Created time.Time `json:"created"` - CreatedBy string `json:"created_by"` +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() } -type ImageConfig struct { - Cmd []string - Env []string +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 } -type Config struct { - Created time.Time `json:"created"` - Author string `json:"author"` - Architecture string `json:"architecture"` - Os string `json:"os"` - Config *ImageConfig `json:"config"` - RootFs RootFs `json:"rootfs"` - History []History `json:"history"` +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/main.go b/main.go index ad1470e9eb5f..50b47418d1a8 100644 --- a/main.go +++ b/main.go @@ -1,92 +1,18 @@ package main import ( - "archive/tar" - "bytes" - "compress/gzip" - "crypto/sha256" - "encoding/json" "fmt" - "io/ioutil" "log" "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() { log.Println("Starting quinistry") - img := getImage() - now := time.Now() - - config := Config{ - Created: now, - 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) + image := GetImageOfCurrentExecutable() - 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, - }, - }, - } + 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 @@ -99,29 +25,26 @@ func main() { // Serve manifest if r.RequestURI == "/v2/quinistry/manifests/latest" { logRequest("Serving manifest", r) - w.Header().Add("Content-Type", ManifestContentType) - resp, _ := json.Marshal(manifest) - w.Header().Add(DigestHeader, digest(resp)) - w.Write(resp) + w.Header().Set(ContentType, ManifestMediaType) + w.Header().Add(DigestHeader, image.ManifestDigest) + w.Write(image.Manifest) return } // Serve actual image layer - layerUri := fmt.Sprintf("/v2/quinistry/blobs/%s", img.GzipDigest) if r.RequestURI == layerUri { logRequest("Serving image layer blob", r) - w.Header().Add(DigestHeader, img.GzipDigest) - w.Write(img.Data) + w.Header().Add(DigestHeader, image.LayerDigest) + w.Write(image.Layer) return } // Serve image config - configUri := fmt.Sprintf("/v2/quinistry/blobs/%s", digest(configJson)) if r.RequestURI == configUri { logRequest("Serving config", r) - w.Header().Set("Content-Type", ImageContentType) - w.Header().Set(DigestHeader, digest(configJson)) - w.Write(configJson) + w.Header().Set("Content-Type", ImageConfigMediaType) + w.Header().Set(DigestHeader, image.ConfigDigest) + w.Write(image.Config) return } @@ -132,56 +55,3 @@ func main() { func logRequest(msg string, r *http.Request) { 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, - } -} diff --git a/types.go b/types.go new file mode 100644 index 000000000000..498cbac2f2ab --- /dev/null +++ b/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 +} |