From b88bf9362a798f8bc32b452755b29f6592a472f5 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Wed, 15 Mar 2017 23:45:13 +0100 Subject: [PATCH 1/7] feat: Initial testing --- main.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 main.go diff --git a/main.go b/main.go new file mode 100644 index 000000000..89935c49a --- /dev/null +++ b/main.go @@ -0,0 +1,131 @@ +package main + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" +) + +type Layer 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 map[string]string `json:"config"` + Layers []Layer `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 { + Layer Layer + Data []byte +} + +const ManifestContentType string = "application/vnd.docker.distribution.manifest.v2+json" +const LayerContentType string = "application/vnd.docker.image.rootfs.diff.tar.gzip" + +func main() { + img := getImage() + + manifest := Manifest{ + SchemaVersion: 2, + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Config: map[string]string{}, + Layers: []Layer{ + img.Layer, + }, + } + + log.Fatal(http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Acknowledge that we speak V2 + if r.RequestURI == "/v2/" { + log.Println("Acknowleding V2 API") + fmt.Fprintln(w) + return + } + + // Serve manifest + if r.RequestURI == "/v2/quinistry/manifests/latest" { + log.Printf("Serving manifest for %v\n", *r) + w.Header().Add("Content-Type", ManifestContentType) + resp, _ := json.Marshal(manifest) + w.Header().Add("Docker-Content-Digest", digest(resp)) + log.Println(digest(resp)) + fmt.Fprintln(w, string(resp)) + return + } + + // Serve actual image layer + if r.RequestURI == fmt.Sprintf("/v2/quinistry/blob/%s", img.Layer.Digest) { + fmt.Printf("Serving layer for %v\n", *r) + fmt.Fprint(w, img.Data) + return + } + + fmt.Printf("Unhandled: %v\n", *r) + }))) +} + +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) + } + + // Then GZIP it + zBuf := new(bytes.Buffer) + zw := gzip.NewWriter(zBuf) + zw.Name = "Docker registry fake test" + + zw.Write(tarBuf.Bytes()) + if err := zw.Close(); err != nil { + log.Fatal(err) + os.Exit(1) + } + + return &Image{ + Layer: Layer{ + MediaType: LayerContentType, + Size: zBuf.Len(), + Digest: digest(zBuf.Bytes()), + }, + Data: zBuf.Bytes(), + } +} From 9690defd2b718705e58c321130ddbb089123c9c1 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 16 Mar 2017 10:53:14 +0100 Subject: [PATCH 2/7] chore: Add .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..62c893550 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file From 1c2d087ec42cd6acc2967140e0f48b2119a5e51a Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 16 Mar 2017 11:43:17 +0100 Subject: [PATCH 3/7] feat: Fully working quine registry --- image.go | 28 +++++++++++++ main.go | 118 ++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 115 insertions(+), 31 deletions(-) create mode 100644 image.go diff --git a/image.go b/image.go new file mode 100644 index 000000000..e62754446 --- /dev/null +++ b/image.go @@ -0,0 +1,28 @@ +package main + +import "time" + +type RootFs struct { + DiffIds []string `json:"diff_ids"` + Type string `json:"type"` +} + +type History struct { + Created time.Time `json:"created"` + CreatedBy string `json:"created_by"` +} + +type ImageConfig struct { + Cmd []string + Env []string +} + +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"` +} diff --git a/main.go b/main.go index 89935c49a..ad1470e9e 100644 --- a/main.go +++ b/main.go @@ -11,73 +11,128 @@ import ( "log" "net/http" "os" + "time" ) -type Layer struct { +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 map[string]string `json:"config"` - Layers []Layer `json:"layers"` + 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 { - Layer Layer - Data []byte + Data []byte + TarDigest string + GzipDigest string } -const ManifestContentType string = "application/vnd.docker.distribution.manifest.v2+json" -const LayerContentType string = "application/vnd.docker.image.rootfs.diff.tar.gzip" - 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) manifest := Manifest{ SchemaVersion: 2, - MediaType: "application/vnd.docker.distribution.manifest.v2+json", - Config: map[string]string{}, - Layers: []Layer{ - img.Layer, + 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) { // Acknowledge that we speak V2 if r.RequestURI == "/v2/" { - log.Println("Acknowleding V2 API") + logRequest("Acknowleding V2 API", r) fmt.Fprintln(w) return } // Serve manifest if r.RequestURI == "/v2/quinistry/manifests/latest" { - log.Printf("Serving manifest for %v\n", *r) + logRequest("Serving manifest", r) w.Header().Add("Content-Type", ManifestContentType) resp, _ := json.Marshal(manifest) - w.Header().Add("Docker-Content-Digest", digest(resp)) - log.Println(digest(resp)) - fmt.Fprintln(w, string(resp)) + w.Header().Add(DigestHeader, digest(resp)) + w.Write(resp) return } // Serve actual image layer - if r.RequestURI == fmt.Sprintf("/v2/quinistry/blob/%s", img.Layer.Digest) { - fmt.Printf("Serving layer for %v\n", *r) - fmt.Fprint(w, img.Data) + 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) return } - fmt.Printf("Unhandled: %v\n", *r) + // 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) + 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) +} + func digest(b []byte) string { hash := sha256.New() hash.Write(b) @@ -97,7 +152,7 @@ func getImage() *Image { tarBuf := new(bytes.Buffer) tarW := tar.NewWriter(tarBuf) hdr := &tar.Header{ - Name: "main", + Name: "/main", Mode: 0755, Size: int64(len(file)), } @@ -109,23 +164,24 @@ func getImage() *Image { 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(tarBuf.Bytes()) + zw.Write(tarBytes) if err := zw.Close(); err != nil { log.Fatal(err) os.Exit(1) } + gzipData := zBuf.Bytes() + return &Image{ - Layer: Layer{ - MediaType: LayerContentType, - Size: zBuf.Len(), - Digest: digest(zBuf.Bytes()), - }, - Data: zBuf.Bytes(), + TarDigest: digest(tarBytes), + GzipDigest: digest(gzipData), + Data: gzipData, } } From fa43472a5d1cd3ffc83582ab34e61ec4598adb4b Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 16 Mar 2017 14:06:21 +0100 Subject: [PATCH 4/7] refactor: Split up code for readability and add docs --- const.go | 12 +++++ image.go | 158 ++++++++++++++++++++++++++++++++++++++++++++++++------- main.go | 152 ++++------------------------------------------------ types.go | 79 ++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 159 deletions(-) create mode 100644 const.go create mode 100644 types.go diff --git a/const.go b/const.go new file mode 100644 index 000000000..173fa9efc --- /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 e62754446..3daeac34d 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" +) -type RootFs struct { - DiffIds []string `json:"diff_ids"` - Type string `json:"type"` +// 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)) } -type History struct { - Created time.Time `json:"created"` - CreatedBy string `json:"created_by"` +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), + } + } -type ImageConfig struct { - Cmd []string - Env []string +func getCurrentBinary() []byte { + path, _ := os.Executable() + file, _ := ioutil.ReadFile(path) + return file } -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 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/main.go b/main.go index ad1470e9e..50b47418d 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() + image := GetImageOfCurrentExecutable() - 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) - - 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 000000000..498cbac2f --- /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 +} From 9a1d876c34fb930936fa2966d8ed0ab72c5ceea2 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 16 Mar 2017 14:18:50 +0100 Subject: [PATCH 5/7] docs: Add README --- .gitignore | 3 ++- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 README.md diff --git a/.gitignore b/.gitignore index 62c893550..622119552 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea/ \ No newline at end of file +.idea/ +quinistry \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..de197a219 --- /dev/null +++ b/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 From 6e7262763ca7d0d94324477907161bd3907c9c1e Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Thu, 16 Mar 2017 19:16:15 +0100 Subject: [PATCH 6/7] feat k8s: Add demo deployments inside k8s --- k8s/child.yaml | 27 +++++++++++++++++++++++++++ k8s/parent.yaml | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 k8s/child.yaml create mode 100644 k8s/parent.yaml diff --git a/k8s/child.yaml b/k8s/child.yaml new file mode 100644 index 000000000..aa2e31826 --- /dev/null +++ b/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/k8s/parent.yaml b/k8s/parent.yaml new file mode 100644 index 000000000..0db2fe300 --- /dev/null +++ b/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 From 86ff65a36ca9e9cc9aaa6b845eecb9c6c7fa488a Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 21 Dec 2019 00:48:37 +0000 Subject: [PATCH 7/7] chore(quinistry): Prepare for depot merge --- .gitignore => fun/quinistry/.gitignore | 0 README.md => fun/quinistry/README.md | 0 const.go => fun/quinistry/const.go | 0 image.go => fun/quinistry/image.go | 0 {k8s => fun/quinistry/k8s}/child.yaml | 0 {k8s => fun/quinistry/k8s}/parent.yaml | 0 main.go => fun/quinistry/main.go | 0 types.go => fun/quinistry/types.go | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename .gitignore => fun/quinistry/.gitignore (100%) rename README.md => fun/quinistry/README.md (100%) rename const.go => fun/quinistry/const.go (100%) rename image.go => fun/quinistry/image.go (100%) rename {k8s => fun/quinistry/k8s}/child.yaml (100%) rename {k8s => fun/quinistry/k8s}/parent.yaml (100%) rename main.go => fun/quinistry/main.go (100%) rename types.go => fun/quinistry/types.go (100%) diff --git a/.gitignore b/fun/quinistry/.gitignore similarity index 100% rename from .gitignore rename to fun/quinistry/.gitignore diff --git a/README.md b/fun/quinistry/README.md similarity index 100% rename from README.md rename to fun/quinistry/README.md diff --git a/const.go b/fun/quinistry/const.go similarity index 100% rename from const.go rename to fun/quinistry/const.go diff --git a/image.go b/fun/quinistry/image.go similarity index 100% rename from image.go rename to fun/quinistry/image.go diff --git a/k8s/child.yaml b/fun/quinistry/k8s/child.yaml similarity index 100% rename from k8s/child.yaml rename to fun/quinistry/k8s/child.yaml diff --git a/k8s/parent.yaml b/fun/quinistry/k8s/parent.yaml similarity index 100% rename from k8s/parent.yaml rename to fun/quinistry/k8s/parent.yaml diff --git a/main.go b/fun/quinistry/main.go similarity index 100% rename from main.go rename to fun/quinistry/main.go diff --git a/types.go b/fun/quinistry/types.go similarity index 100% rename from types.go rename to fun/quinistry/types.go