summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClark Laughlin <clark.laughlin@linaro.org>2015-11-21 21:31:44 -0500
committerClark Laughlin <clark.laughlin@linaro.org>2015-11-21 21:31:44 -0500
commit1abea08f55c557f0f129641fba4d130080d8a0e9 (patch)
treec216750cb5824496fcd45f8e281c5058e592daa6
parentdce3b0308d037b68e5d1ca20bc1c7d99df565cbd (diff)
initial addition of registry browser
-rw-r--r--registry-browser/Dockerfile32
-rwxr-xr-xregistry-browser/docker-build-image3
-rwxr-xr-xregistry-browser/docker-test-image7
-rw-r--r--registry-browser/server.go220
-rw-r--r--registry-browser/static/index.html205
-rw-r--r--registry-browser/static/util.js31
6 files changed, 498 insertions, 0 deletions
diff --git a/registry-browser/Dockerfile b/registry-browser/Dockerfile
new file mode 100644
index 0000000..0e4453c
--- /dev/null
+++ b/registry-browser/Dockerfile
@@ -0,0 +1,32 @@
+FROM ubuntu:14.04
+RUN apt-get update
+RUN apt-get install -y build-essential git wget curl mercurial
+
+RUN mkdir /goproj
+
+ENV GOPATH=/goproj
+ENV PATH=/goproj/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
+
+# install golang from source
+RUN wget -qO- https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | tar -C /usr/local -xzf -
+
+# create gopath directories
+RUN mkdir -p /goproj
+RUN mkdir -p /goproj/bin
+RUN mkdir -p /goproj/pkg
+RUN mkdir -p /goproj/src/linaro.org/docker-registry-browser
+
+# get dependencies
+RUN go get github.com/gorilla/mux
+
+# Copy the local package files to the container's workspace.
+ADD . /goproj/src/linaro.org/docker-registry-browser
+
+WORKDIR /goproj/src/linaro.org/docker-registry-browser
+RUN go install .
+
+# Run the golang server app when the container starts.
+ENTRYPOINT /goproj/bin/docker-registry-browser -type=quay.io -namespace=linaro -registry=https://quay.io
+
+# Document that the service listens on port 80
+EXPOSE 80
diff --git a/registry-browser/docker-build-image b/registry-browser/docker-build-image
new file mode 100755
index 0000000..49ce80e
--- /dev/null
+++ b/registry-browser/docker-build-image
@@ -0,0 +1,3 @@
+#!/bin/bash
+docker build -t docker-registry-browser .
+
diff --git a/registry-browser/docker-test-image b/registry-browser/docker-test-image
new file mode 100755
index 0000000..a1ded08
--- /dev/null
+++ b/registry-browser/docker-test-image
@@ -0,0 +1,7 @@
+#!/bin/bash
+docker run \
+ --name test \
+ -p 9090:80 \
+ -it \
+ --rm \
+ docker-registry-browser
diff --git a/registry-browser/server.go b/registry-browser/server.go
new file mode 100644
index 0000000..9f9c5da
--- /dev/null
+++ b/registry-browser/server.go
@@ -0,0 +1,220 @@
+package main
+
+import (
+ "regexp"
+ "flag"
+ "fmt"
+ "log"
+ "encoding/base64"
+ "encoding/json"
+ "io/ioutil"
+ "github.com/gorilla/mux"
+ "net/http"
+ "crypto/tls"
+)
+
+
+var (
+ ignoreCertErrors = flag.Bool("ignoreCertErrors", true, "Ignore certificate errors")
+ listenAddr = flag.String("listen", ":80", "HTTP service address")
+ namespace = flag.String("namespace", "", "Registry namespace (if needed)")
+ registryServer = flag.String("registry", "https://registry", "Docker registry endpoint")
+ user = flag.String("user", "portus", "User to authenticate with the registry")
+ password = flag.String("password", "", "Password to authenticate with the registry")
+)
+
+
+type repositoryEntry struct {
+ Namespace string `json:"namespsace"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Link string `json:"link"`
+ Tags []string `json:"tags"`
+}
+
+
+func MakeHttpRequest(url string, headersToAdd map[string]string, ignoreCertErrors bool) ([]byte, http.Header, error) {
+ // get an HTTP client and optionally allow bad certificates
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: ignoreCertErrors},
+ }
+ client := &http.Client{Transport: tr}
+
+ req, err := http.NewRequest("GET", url, nil)
+
+ if headersToAdd != nil {
+ for k, v := range headersToAdd {
+ req.Header.Add(k, v)
+ }
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ defer resp.Body.Close()
+ content, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return content, resp.Header, nil
+}
+
+func authenticateWithRegistry(url, user, password string) (string, error) {
+
+ _, responseHeader, err := MakeHttpRequest(url, nil, *ignoreCertErrors)
+ if err != nil {
+ log.Fatal(err)
+ return "", err
+ }
+
+ wwwAuthenticateHeader := responseHeader.Get("WWW-Authenticate")
+
+ log.Println(wwwAuthenticateHeader)
+
+ re := regexp.MustCompile(`realm="(?P<realm>[\-\.\:/0-9A-Za-z]*)",service="(?P<service>[\-\.\:/0-9A-Za-z]*)",scope="(?P<scope>[\-\*\.\:/0-9A-Za-z]*)"`)
+ captures := re.FindAllStringSubmatch(wwwAuthenticateHeader, -1)
+ realm := captures[0][1]
+ service := captures[0][2]
+ scope := captures[0][3]
+
+ authURL := fmt.Sprintf("%s?service=%s&scope=%s", realm, service, scope);
+
+ authorizationHeader := make(map[string]string)
+ passwordString := fmt.Sprintf("%s:%s", user, password)
+ authorizationHeader["Authorization"] = fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(passwordString)))
+
+ responseBody, responseHeader, err := MakeHttpRequest(authURL, authorizationHeader, *ignoreCertErrors)
+ if err != nil {
+ log.Fatal(err)
+ return "", err
+ }
+
+ var tokenData map[string]string
+ err = json.Unmarshal([]byte(responseBody), &tokenData)
+
+ if err != nil {
+ log.Fatal(err)
+ return "", err
+ }
+
+ return tokenData["token"], nil
+}
+
+func retrieveRegistryCatalog(url, user, password string) (string, error) {
+
+ token, err := authenticateWithRegistry(url, user, password)
+ if err != nil {
+ log.Fatal(err)
+ return "", err
+ }
+
+ tokenHeader := make(map[string]string)
+ tokenHeader["Authorization"] = fmt.Sprintf("Bearer %s", token)
+
+ body, _, err := MakeHttpRequest(url, tokenHeader, *ignoreCertErrors)
+ if err != nil {
+ log.Fatal(err)
+ return "", err
+ }
+
+ return string(body), nil
+}
+
+
+
+func Handle_Distribution_AllRepos(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+
+ responseData := struct{
+ Repositories []repositoryEntry `json:"repositories"`
+ }{}
+
+ //
+ // get all of the repositories
+ //
+
+ url1 := fmt.Sprintf("%s/v2/_catalog", *registryServer)
+ content, err := retrieveRegistryCatalog(url1, *user, *password)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ repositories := struct{
+ Repositories []string `json:"repositories"`
+ }{}
+
+ err = json.Unmarshal([]byte(content), &repositories)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ //
+ // now, for each repository, get the available tags and add
+ // everything to the response data map
+ //
+
+ for _, each := range repositories.Repositories {
+ url2 := fmt.Sprintf("%s/v2/%s/tags/list", *registryServer, each)
+ content, err := MakeHttpRequest(url2)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ tags := struct{
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+ }{}
+
+ err = json.Unmarshal([]byte(content), &tags)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ //
+ // add to the response
+ //
+
+ responseData.Repositories = append(responseData.Repositories,
+ repositoryEntry{ "", tags.Name, "", "", tags.Tags })
+ }
+
+ enc := json.NewEncoder(w)
+ enc.Encode(responseData)
+}
+
+
+func main() {
+
+ flag.Parse()
+ log.Println("Listening on:", *listenAddr)
+ log.Println("Registry server:", *registryServer)
+ log.Println("Ignore certificate errors:", *ignoreCertErrors)
+
+ //
+ // handle routes for access to data
+ //
+
+ r := mux.NewRouter()
+ s := r.Methods("GET").PathPrefix("/registry").Subrouter()
+
+ // combined list of repositories and available tags
+ s.HandleFunc("/all", Handle_Distribution_AllRepos)
+
+ http.Handle("/registry/", r)
+
+ //
+ // handle static content
+ //
+
+ http.Handle("/", http.FileServer(http.Dir("./static/")))
+
+ // GO!
+
+ err := http.ListenAndServe(*listenAddr, nil)
+ if err != nil {
+ log.Fatal("http.ListenAndServe: ", err)
+ }
+}
diff --git a/registry-browser/static/index.html b/registry-browser/static/index.html
new file mode 100644
index 0000000..d6a2871
--- /dev/null
+++ b/registry-browser/static/index.html
@@ -0,0 +1,205 @@
+<html>
+<head>
+ <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
+ <META HTTP-EQUIV="EXPIRES" CONTENT="0">
+ <META HTTP-EQUIV="PRAGMA" CONTENT="NO-CACHE">
+
+ <link href='https://fonts.googleapis.com/css?family=Merriweather+Sans:200,700' rel='stylesheet' type='text/css'>
+ <link href='https://code.jquery.com/ui/1.11.4/themes/overcast/jquery-ui.css' rel='stylesheet' type='text/css'>
+
+ <script type='text/javascript' src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
+ <script type='text/javascript' src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/list.js/1.1.1/list.min.js"></script>
+
+ <style>
+ h1, h2, h3 {
+ font-family: "Merriweather Sans", Arial, serif;
+ font-weight: 700;
+ }
+
+ a, a:visited {
+ color: black;
+ text-decoration: none;
+ }
+
+ hr {
+ border: none;
+ height: 1px;
+ color: #99c747;
+ background-color: #99c747;
+ }
+
+ a:hover {
+ color: #99c747;
+ text-decoration: none;
+ }
+
+ body {
+ font-family: "Merriweather Sans", Arial, serif;
+ font-weight: 200;
+ }
+
+ blockquote {
+ font-family: courier;
+ margin-left: 20px;
+ padding: 10px;
+ background-color: #eeeeee;
+ border-left: 3px solid #99c747;
+ }
+
+ .list {
+ margin: 0;
+ padding: 20px 0 0;
+ }
+
+ .list > li {
+ display: inline-block;
+ width: 33%;
+ background-color: white;
+ border: 3px solid #99c747;
+ padding: 10px;
+ margin: 5px;
+ border-radius: 10px;
+ box-shadow: inset 0 1px 0 #fff;
+ }
+
+ .description {
+ font-famiy: sans-serif;
+ font-size: 70%;
+ }
+
+ .tags-header {
+ padding-right: 5px;
+ font-size: 90%;
+ }
+
+ .formatted-tags {
+ font-weight: normal;
+ white-space: normal;
+ }
+
+ .formatted-tags > li {
+ display: inline-block;
+ font-family: sans-serif;
+ font-size: 90%;
+ background-color: #99c747;
+ border-radius: 5px;
+ padding: 2px;
+ white-space: nowrap;
+ margin-top: 1px;
+ margin-bottom: 1px;
+ margin-left: 3px;
+ margin-right: 3px;
+ color: white;
+ }
+
+ h4 {
+ font-size: 1.5em;
+ margin: 0 0 0.3rem;
+ font-weight: bold;
+ }
+
+ input {
+ border: solid 1px #ccc;
+ border-radius: 5px;
+ padding: 7px 14px;
+ margin-bottom: 10px
+ }
+
+ input:focus {
+ outline: none;
+ border-color: #aaa;
+ }
+</style>
+
+<script type="text/javascript" src="util.js"></script>
+<script type="text/javascript">
+
+$(function() {
+ // get the top-level list of available repositories
+ $.getJSON("/registry/all", function(repositoryData) {
+ items = repositoryData["repositories"];
+ if(items === null)
+ return;
+
+ $.each(items, function(i, e) {
+ // force 'latest' to be the first tag displayed
+ sorted_tags = e['tags'];
+ latest_idx = sorted_tags.indexOf('latest');
+ if(latest_idx > 0) {
+ sorted_tags.splice(latest_idx, 1);
+ sorted_tags.unshift('latest');
+ }
+
+ e['formatted-name'] = '<a href="' + e['link'] + '">' + e['name'] + '</a>';
+ e['formatted-tags'] = '<li>' + sorted_tags.join('</li><li>') + '</li>';
+ });
+
+ options = {
+ valueNames : ['name', 'tags'],
+ item: 'repo-item'
+ };
+
+ var repoList = new List('repo-list', options, items);
+ });
+});
+
+</script>
+
+</head>
+<body>
+<h1><img style="vertical-align: middle; width: 150px" src="https://www.linaro.org/app/images/linaro-logo-web.png"/>
+Aarch64 Docker Registry</h1>
+
+<h2>Available Images</h2>
+
+<div id="repo-list">
+<input class="search" placeholder="Search"/>
+<ul class="list"></ul>
+</div>
+
+<div style="display:none;">
+ <!-- A template element is needed when list is empty, TODO: needs a better solution -->
+ <li id="repo-item">
+ <h4 class="formatted-name"></h4>
+ <span class="tags-header">Tags:</span><span class="formatted-tags"></span>
+ <hr/>
+ <span class="description"></span>
+ </li>
+</div>
+
+<br/><br/>
+
+<h2>Overview and Instructions</h2>
+
+<p>This is a privately-run docker registry containing repositories of aarch64 images for use with Docker on aarch64 platforms hosted on CoreOS quay.io. Details, including instructions for pulling images, can be found by clicking on each image name.
+
+
+<p>
+Images may be pulled from this using the <code>docker pull</code> command:<br/><br/>
+<code>docker pull quay.io/linaro/<b><i>repository:tag</i></b></code><br/><br/>
+i.e.<br/><br/>
+<code>docker pull quay.io/linaro/<b><i>centos:latest</i></b></code>
+</p>
+
+<!--
+<br/>
+<br/>
+
+<p>You may receive an error from your Docker client regarding an unknown CA certificate:
+<blockquote>
+Error response from daemon:<br/>
+v2 ping attempt failed with error: Get https://docker-registry.linaro.org/v2/: x509: certificate signed by unknown authority<br/>
+v1 ping attempt failed with error: Get https://docker-registry.linaro.org/v1/_ping: x509: certificate signed by unknown authority. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `-insecure-registry docker-registry.linaro.org` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/docker-registry.linaro.org/ca.crt
+</blockquote>
+To resolve this issue:
+<ol>
+ <li>Retrieve the "GoDaddy Certificate Bundles - G2" from this page <a target="_blank" href="https://certs.godaddy.com/repository">https://certs.godaddy.com/repository</a> (<a href="https://certs.godaddy.com/repository/gd_bundle-g2.crt">direct link</a>)
+ <li>Place it in a file named <code>/etc/docker/certs.d/docker-registry.linaro.org/ca.crt</code> on your system
+ <li>Restart the Docker daemon
+</ol>
+</p>
+-->
+
+</body>
+</html>
diff --git a/registry-browser/static/util.js b/registry-browser/static/util.js
new file mode 100644
index 0000000..8c4e948
--- /dev/null
+++ b/registry-browser/static/util.js
@@ -0,0 +1,31 @@
+
+function parse_query(s) {
+ var searchStr = s;
+ var section = location.hash.substring(1,location.hash.length);
+ var searchArray = new Array();
+
+ while (searchStr!='') {
+ var name, value;
+ // strip off leading ? or &
+ if ((searchStr.charAt(0)=='?')||(searchStr.charAt(0)=='&')) searchStr = searchStr.substring(1,searchStr.length);
+ // find name
+ name = searchStr.substring(0,searchStr.indexOf('='));
+ // find value
+ if (searchStr.indexOf('&')!=-1)
+ value = searchStr.substring(searchStr.indexOf('=')+1,searchStr.indexOf('&'));
+ else
+ value = searchStr.substring(searchStr.indexOf('=')+1,searchStr.length);
+ // add pair to an associative array
+ searchArray[name] = value;
+ // cut first pair from string
+ if (searchStr.indexOf('&')!=-1)
+ searchStr = searchStr.substring(searchStr.indexOf('&')+1,searchStr.length);
+ else
+ searchStr = '';
+ // debug step
+ //document.write(name + ': ' + value + ': ' + searchStr + '<br>');
+ }
+
+ return searchArray;
+}
+