server things

Signed-off-by: Jess Frazelle <acidburn@google.com>
This commit is contained in:
Jess Frazelle 2016-12-19 18:04:40 -08:00
parent 1252fed54f
commit 5b848b5b4f
No known key found for this signature in database
GPG key ID: 18F3685C0022BFF3
10 changed files with 933 additions and 0 deletions

1
.gitignore vendored
View file

@ -45,3 +45,4 @@ Icon
.Trashes
reg
server/server

29
server/Dockerfile Normal file
View file

@ -0,0 +1,29 @@
FROM alpine:latest
MAINTAINER Jessica Frazelle <jess@linux.com>
ENV PATH /go/bin:/usr/local/go/bin:$PATH
ENV GOPATH /go
RUN apk add --no-cache \
ca-certificates
COPY static /src/static
COPY templates /src/templates
RUN set -x \
&& apk add --no-cache --virtual .build-deps \
go \
git \
gcc \
libc-dev \
libgcc \
&& go get -v github.com/jessfraz/reg \
&& cd /go/src/github.com/jessfraz/reg \
&& go build -o /usr/bin/reg-server ./server \
&& apk del .build-deps \
&& rm -rf /go \
&& echo "Build complete."
WORKDIR /src
ENTRYPOINT [ "reg-server" ]

286
server/server.go Normal file
View file

@ -0,0 +1,286 @@
package main
import (
"errors"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/cliconfig"
"github.com/docker/engine-api/types"
"github.com/jessfraz/reg/registry"
"github.com/urfave/cli"
)
const (
// VERSION is the binary version.
VERSION = "v0.1.0"
dockerConfigPath = ".docker/config.json"
)
var (
updating = false
)
// preload initializes any global options and configuration
// before the main or sub commands are run.
func preload(c *cli.Context) (err error) {
if c.GlobalBool("debug") {
logrus.SetLevel(logrus.DebugLevel)
}
return nil
}
func main() {
app := cli.NewApp()
app.Name = "reg-server"
app.Version = VERSION
app.Author = "@jessfraz"
app.Email = "no-reply@butts.com"
app.Usage = "Docker registry v2 static UI server."
app.Before = preload
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug, d",
Usage: "run in debug mode",
},
cli.StringFlag{
Name: "username, u",
Usage: "username for the registry",
},
cli.StringFlag{
Name: "password, p",
Usage: "password for the registry",
},
cli.StringFlag{
Name: "registry, r",
Usage: "URL to the provate registry (ex. r.j3ss.co)",
},
cli.StringFlag{
Name: "port",
Value: "8080",
Usage: "port for server to run on",
},
cli.StringFlag{
Name: "cert",
Usage: "path to ssl cert",
},
cli.StringFlag{
Name: "key",
Usage: "path to ssl key",
},
cli.StringFlag{
Name: "interval",
Value: "5m",
Usage: "interval to generate new index.html's at",
},
}
app.Action = func(c *cli.Context) error {
auth, err := getAuthConfig(c)
if err != nil {
return err
}
// create the registry client
r, err := registry.New(auth, c.GlobalBool("debug"))
if err != nil {
return err
}
// get the path to the static directory
wd, err := os.Getwd()
if err != nil {
return err
}
staticDir := filepath.Join(wd, "static")
// create the initial index
if err := createStaticIndex(r, staticDir); err != nil {
return err
}
// parse the duration
dur, err := time.ParseDuration(c.String("interval"))
if err != nil {
logrus.Fatalf("parsing %s as duration failed: %v", c.String("interval"), err)
}
ticker := time.NewTicker(dur)
go func() {
// create more indexes every X minutes based off interval
for range ticker.C {
if !updating {
if err := createStaticIndex(r, staticDir); err != nil {
logrus.Warnf("creating static index failed: %v", err)
}
}
}
}()
// create mux server
mux := http.NewServeMux()
// static files handler
staticHandler := http.FileServer(http.Dir(staticDir))
mux.Handle("/", staticHandler)
// TODO: add handler for individual repos
// set up the server
port := c.String("port")
server := &http.Server{
Addr: ":" + port,
Handler: mux,
}
logrus.Infof("Starting server on port %q", port)
if c.String("cert") != "" && c.String("key") != "" {
logrus.Fatal(server.ListenAndServeTLS(c.String("cert"), c.String("key")))
} else {
logrus.Fatal(server.ListenAndServe())
}
return nil
}
app.Run(os.Args)
}
func getAuthConfig(c *cli.Context) (types.AuthConfig, error) {
if c.GlobalString("username") != "" && c.GlobalString("password") != "" && c.GlobalString("registry") != "" {
return types.AuthConfig{
Username: c.GlobalString("username"),
Password: c.GlobalString("password"),
ServerAddress: c.GlobalString("registry"),
}, nil
}
dcfg, err := cliconfig.Load(cliconfig.ConfigDir())
if err != nil {
return types.AuthConfig{}, fmt.Errorf("Loading config file failed: %v", err)
}
// return error early if there are no auths saved
if !dcfg.ContainsAuth() {
if c.GlobalString("registry") != "" {
return types.AuthConfig{
ServerAddress: c.GlobalString("registry"),
}, nil
}
return types.AuthConfig{}, fmt.Errorf("No auth was present in %s, please pass a registry, username, and password", cliconfig.ConfigDir())
}
// if they passed a specific registry, return those creds _if_ they exist
if c.GlobalString("registry") != "" {
if creds, ok := dcfg.AuthConfigs[c.GlobalString("registry")]; ok {
return creds, nil
}
return types.AuthConfig{}, fmt.Errorf("No authentication credentials exist for %s", c.GlobalString("registry"))
}
// set the auth config as the registryURL, username and Password
for _, creds := range dcfg.AuthConfigs {
return creds, nil
}
return types.AuthConfig{}, fmt.Errorf("Could not find any authentication credentials")
}
func getRepoAndRef(c *cli.Context) (repo, ref string, err error) {
if len(c.Args()) < 1 {
return "", "", errors.New("pass the name of the repository")
}
arg := c.Args()[0]
parts := []string{}
if strings.Contains(arg, "@") {
parts = strings.Split(c.Args()[0], "@")
} else if strings.Contains(arg, ":") {
parts = strings.Split(c.Args()[0], ":")
} else {
parts = []string{arg}
}
repo = parts[0]
ref = "latest"
if len(parts) > 1 {
ref = parts[1]
}
return
}
type data struct {
RegistryURL string
LastUpdated string
Repos []repository
}
type repository struct {
Name string
Tags string
RegistryURL string
// TODO: add date last uploaded
}
func createStaticIndex(r *registry.Registry, staticDir string) error {
updating = true
logrus.Info("fetching catalog")
repoList, err := r.Catalog()
if err != nil {
return fmt.Errorf("getting catalog failed: %v", err)
}
logrus.Info("fetching tags")
var repos []repository
for _, repo := range repoList {
tags, err := r.Tags(repo)
if err != nil {
return fmt.Errorf("getting tags for %s failed: %v", repo, err)
}
repos = append(repos, repository{
Name: repo,
Tags: strings.Join(tags, "<br/>"),
RegistryURL: r.URL,
})
}
// create temporoary file to save template to
logrus.Info("creating temporary file for template")
f, err := ioutil.TempFile("", "reg-server")
if err != nil {
return fmt.Errorf("creating temp file failed: %v", err)
}
defer f.Close()
defer os.Remove(f.Name())
// parse & execute the template
logrus.Info("parsing and executing the template")
templateDir := filepath.Join(staticDir, "../templates")
lp := filepath.Join(templateDir, "layout.html")
d := data{
RegistryURL: r.URL,
Repos: repos,
LastUpdated: time.Now().String(),
}
tmpl := template.Must(template.New("").ParseFiles(lp))
if err := tmpl.ExecuteTemplate(f, "layout", d); err != nil {
return fmt.Errorf("execute template failed: %v", err)
}
f.Close()
logrus.Info("renaming the temporary file to index.html")
index := filepath.Join(staticDir, "index.html")
if err := os.Rename(f.Name(), index); err != nil {
return fmt.Errorf("renaming result from %s to %s failed: %v", f.Name(), index, err)
}
updating = false
return nil
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="32px" height="32px" viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
<g id="search_1_">
<path fill="#CCCCCC" d="M20,0.005c-6.627,0-12,5.373-12,12c0,2.026,0.507,3.933,1.395,5.608l-8.344,8.342l0.007,0.006
C0.406,26.602,0,27.49,0,28.477c0,1.949,1.58,3.529,3.529,3.529c0.985,0,1.874-0.406,2.515-1.059l-0.002-0.002l8.341-8.34
c1.676,0.891,3.586,1.4,5.617,1.4c6.627,0,12-5.373,12-12S26.627,0.005,20,0.005z M4.795,29.697
c-0.322,0.334-0.768,0.543-1.266,0.543c-0.975,0-1.765-0.789-1.765-1.764c0-0.498,0.21-0.943,0.543-1.266l-0.009-0.008l8.066-8.066
c0.705,0.951,1.545,1.791,2.494,2.498L4.795,29.697z M20,22.006c-5.522,0-10-4.479-10-10c0-5.522,4.478-10,10-10
c5.521,0,10,4.478,10,10C30,17.527,25.521,22.006,20,22.006z"/>
<path fill="#CCCCCC" d="M20,5.005c-3.867,0-7,3.134-7,7c0,0.276,0.224,0.5,0.5,0.5s0.5-0.224,0.5-0.5c0-3.313,2.686-6,6-6
c0.275,0,0.5-0.224,0.5-0.5S20.275,5.005,20,5.005z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,220 @@
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,300');
/* Have to use @import for the font, as you can only specify a single stylesheet */
* {
margin: 0;
padding: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
html {
min-height: 100%;
border-top: 10px solid #ECEEF1;
border-bottom: 10px solid #ECEEF1;
color: #61666c;
font-weight: 300;
font-size: 1em;
font-family: 'Open Sans', sans-serif;
line-height: 2em;
}
body {
padding: 20px;
-webkit-backface-visibility: hidden;
}
code {
font-family: Inconsolata,monospace;
}
a {
color: #61666c;
text-decoration: none;
}
a:hover {
color: #2a2a2a;
}
/*------------------------------------*\
Wrapper
\*------------------------------------*/
.wrapper {
margin: 0 auto;
padding-top: 20px;
max-width: 800px;
}
/*------------------------------------*\
Demo block
\*------------------------------------*/
.block {
font-size: .875em;
margin: 20px 0;
padding: 20px;
color: #9099A3;
}
h1 {
font-weight: 200;
text-align: center;
font-size: 1.4em;
line-height: 3em;
font-family: 'Museo Slab', 'Open Sans', monospace;
}
form {
text-align: center;
}
input {
margin: 0 auto;
font-size: 100%;
vertical-align: middle;
*overflow: visible;
line-height: normal;
font-family: 'Open Sans', sans-serif;
font-size: 12px;
font-weight: 300;
line-height: 18px;
display: inline-block;
height: 20px;
padding: 4px 32px 4px 6px;
margin-bottom: 9px;
font-size: 14px;
line-height: 20px;
color: #555555;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
width: 196px;
background-color: #ffffff;
border: 1px solid #cccccc;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-moz-transition: border linear 0.2s, box-shadow linear 0.2s;
-o-transition: border linear 0.2s, box-shadow linear 0.2s;
transition: border linear 0.2s, box-shadow linear 0.2s;
background: url('search.svg') no-repeat 211px center;
background-size: auto 20px;
}
input:focus {
border-color: rgba(82, 168, 236, 0.8);
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
input::-moz-focus-inner {
padding: 0;
border: 0;
}
input[type="search"] {
margin-top: 20px;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
-webkit-appearance: textfield;
-webkit-transition: all 300ms ease-in;
-moz-transition: all 300ms ease-in;
-ms-transition: all 300ms ease-in;
-o-transition: all 300ms ease-in;
transition: all 300ms ease-in;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
a.clear,
a.clear:link,
a.clear:visited {
color: #666;
padding: 2px 0 2px 0;
font-weight: 400;
font-size: 14px;
margin: 0px 0 0 20px;
line-height: 14px;
display: inline-block;
border-bottom: transparent 1px solid;
vertical-align: -10px;
-webkit-transition: all 300ms ease-in;
-moz-transition: all 300ms ease-in;
-ms-transition: all 300ms ease-in;
-o-transition: all 300ms ease-in;
transition: all 300ms ease-in;
}
a.clear:hover {
text-decoration: none;
color: #333;
cursor: pointer;
}
/*------------------------------------*\
Table (directory listing)
\*------------------------------------*/
table {
border-collapse: collapse;
font-size: .875em;
max-width: 100%;
margin: 20px auto 0px auto;
}
tr {
outline: 0;
border: 0;
}
tr:hover td {
background: #f6f6f6;
}
th {
text-align: left;
font-size: .75em;
padding-right: 20px;
}
/* 2nd Column: Filename */
th + th {
width: 65%;
}
/* 3rd Column: Last Modified */
/* 4th Column: Size */
th + th + th + th {
width: 5%;
}
tr td:first-of-type {
padding-left: 10px;
padding-right: 10px;
}
td {
padding: 5px 0;
outline: 0;
border: 0;
border-bottom: 1px solid #edf1f5;
vertical-align: middle;
text-align: left;
-webkit-transition: background 300ms ease-in;
-moz-transition: background 300ms ease-in;
-ms-transition: background 300ms ease-in;
-o-transition: background 300ms ease-in;
transition: background 300ms ease-in;
}
td:last-child,
th:last-child {
text-align: right;
padding-right: 0px;
}
td a {
display: block;
}
tr.parent a {
color: #9099A3;
}
.parent a:hover {
color: #2a2a2a;
}
/*------------------------------------*\
Footer
\*------------------------------------*/
.footer {
text-align: center;
font-size: .75em;
margin-top: 50px;
}
img {
outline: none;
border: none;
max-height: 16px;
}

View file

@ -0,0 +1,226 @@
@import url('http://fonts.googleapis.com/css?family=Open+Sans:400,300');
/* Have to use @import for the font, as you can only specify a single stylesheet */
* {
margin:0;
padding:0;
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing: border-box;
}
html {
min-height:100%;
border-top:10px solid #ECEEF1;
border-bottom:10px solid #ECEEF1;
color:#61666c;
font-weight:300;
font-size:1em;
font-family:'Open Sans', sans-serif;
line-height:2em;
}
body {
padding:20px;
-webkit-backface-visibility:hidden;
}
code {
font-family:Inconsolata,monospace;
}
a {
color:#61666c;
text-decoration:none;
}
a:hover {
color:#2a2a2a;
}
/*------------------------------------*\
Wrapper
\*------------------------------------*/
.wrapper {
margin:0 auto;
padding-top:20px;
max-width:800px;
}
/*------------------------------------*\
Demo block
\*------------------------------------*/
.block {
font-size:.875em;
margin:20px 0;
padding:20px;
color:#9099A3;
}
h1 {
font-weight:200;
text-align:center;
font-size:1.4em;
line-height:3em;
font-family:'Museo Slab','Open Sans',monospace;
}
form {
text-align:center;
}
input {
margin: 0 auto;
font-size: 100%;
vertical-align: middle;
*overflow: visible;
line-height: normal;
font-family:'Open Sans', sans-serif;
font-size: 12px;
font-weight: 300;
line-height: 18px;
color:#555555;
display: inline-block;
height: 20px;
padding: 4px 32px 4px 6px;
margin-bottom: 9px;
font-size: 14px;
line-height: 20px;
color: #555555;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
width: 196px;
background-color: #ffffff;
border: 1px solid #cccccc;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border linear .2s, box-shadow linear .2s;
-moz-transition: border linear .2s, box-shadow linear .2s;
-o-transition: border linear .2s, box-shadow linear .2s;
transition: border linear .2s, box-shadow linear .2s;
background: url('search.svg') no-repeat 211px center;
background-size:auto 20px;
}
input:focus {
border-color: rgba(82, 168, 236, 0.8);
outline: 0;
outline: thin dotted \9;
/* IE6-9 */
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
-moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
input::-moz-focus-inner {
padding: 0;
border: 0;
}
input[type="search"] {
margin-top: 20px;
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
-webkit-appearance: textfield;
-webkit-transition:all 300ms ease-in;
-moz-transition:all 300ms ease-in;
-ms-transition:all 300ms ease-in;
-o-transition:all 300ms ease-in;
transition:all 300ms ease-in;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
a.clear, a.clear:link, a.clear:visited {
color:#666;
padding:2px 0 2px 0;
font-weight: 400;
font-size: 14px;
margin:0px 0 0 20px;
line-height: 14px;
display: inline-block;
border-bottom: transparent 1px solid;
vertical-align: -10px;
-webkit-transition:all 300ms ease-in;
-moz-transition:all 300ms ease-in;
-ms-transition:all 300ms ease-in;
-o-transition:all 300ms ease-in;
transition:all 300ms ease-in;
}
a.clear:hover {
text-decoration: none;
color:#333;
cursor: pointer;
}
/*------------------------------------*\
Table (directory listing)
\*------------------------------------*/
table {
border-collapse:collapse;
font-size:.875em;
max-width:100%;
margin:20px auto 0px auto;
}
tr {
outline:0;
border:0;
}
tr:hover td {
background:#f6f6f6;
}
th {
text-align:left;
font-size:.75em;
padding-right:20px;
}
/* 2nd Column: Filename */
th + th {
width:65%;
}
/* 3rd Column: Last Modified */
th + th + th {
}
/* 4th Column: Size */
th + th + th + th {
width:5%;
}
tr td:first-of-type {
padding-left:10px;
padding-right:10px;
}
td {
padding:5px 0;
outline:0;
border:0;
border-bottom:1px solid #edf1f5;
vertical-align:middle;
text-align:left;
-webkit-transition:background 300ms ease-in;
-moz-transition:background 300ms ease-in;
-ms-transition:background 300ms ease-in;
-o-transition:background 300ms ease-in;
transition:background 300ms ease-in;
}
td:last-child, th:last-child {
text-align:right;
padding-right:0px;
}
td a{
display: block;
}
tr.parent a {
color:#9099A3;
}
.parent a:hover {
color:#2a2a2a;
}
/*------------------------------------*\
Footer
\*------------------------------------*/
.footer {
text-align:center;
font-size:.75em;
margin-top:50px;
}
img {
outline:none;
border:none;
max-height:16px;
}

BIN
server/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,91 @@
// pretty date function
function prettyDate(time){
var date = new Date((time || "").replace(/-/g,"/").replace(/[TZ]/g," ")),
diff = (((new Date()).getTime() - date.getTime()) / 1000),
day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0)
return;
return day_diff == 0 && (
diff < 60 && "just now" ||
diff < 120 && "1 minute ago" ||
diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
diff < 7200 && "1 hour ago" ||
diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
day_diff == 1 && "Yesterday" ||
day_diff < 7 && day_diff + " days ago" ||
day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago" ||
day_diff > 31 && Math.round(day_diff / 31) + " months ago";
}
// search function
function search(search_val){
var suche = search_val.toLowerCase();
var table = document.getElementById("directory");
var cellNr = 1;
var ele;
for (var r = 1; r < table.rows.length; r++){
ele = table.rows[r].cells[cellNr].innerHTML.replace(/<[^>]+>/g,"");
if (ele.toLowerCase().indexOf(suche)>=0 ) {
table.rows[r].style.display = '';
} else {
table.rows[r].style.display = 'none';
}
}
}
var el = document.querySelectorAll('tr:nth-child(2)')[0].querySelectorAll('td:nth-child(2)')[0];
if (el.textContent == 'Parent Directory'){
var parent_row = document.querySelectorAll('tr:nth-child(2)')[0];
if (parent_row.classList){
parent_row.classList.add('parent');
} else {
parent_row.className += ' ' + 'parent';
}
}
// var rows = document.querySelectorAll('tr:not(.parent)');
// Array.prototype.forEach.call(rows, function(item, index){
// if (index !== 0) {
// var date_holder = item.querySelectorAll('td:nth-child(3)')[0];
// var date = date_holder.textContent;
// date = prettyDate(date);
// date_holder.innerHTML = date;
// }
// });
var cells = document.querySelectorAll('td a');
Array.prototype.forEach.call(cells, function(item, index){
var link = item.getAttribute('href');
link = link.replace('.html', '');
item.setAttribute('href', link);
});
var our_table = document.querySelectorAll('table')[0];
our_table.setAttribute('id', 'directory');
// search script
var search_input = document.querySelectorAll('input[name="filter"]')[0];
var clear_button = document.querySelectorAll('a.clear')[0];
if (search_input.value !== ''){
search(search_input.value);
}
search_input.addEventListener('keyup', function(e){
e.preventDefault();
search(search_input.value);
});
search_input.addEventListener('keypress', function(e){
if ( e.which == 13 ) {
e.preventDefault();
}
});
clear_button.addEventListener('click', function(e){
search_input.value = '';
search('');
});

View file

@ -0,0 +1,59 @@
{{define "layout"}}
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<base href="/" >
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>{{ .RegistryURL }}</title>
<link rel="icon" type="image/ico" href="/favicon.ico">
<link rel="stylesheet" href="/css/styles.css" />
</head>
<body>
<h1>{{ .RegistryURL }}<</h1>
<form>
<input name="filter" type="search"><a class="clear">clear</a>
</form>
<div class="wrapper">
<table>
<tr>
<th>Name</th>
<th>Tags</th>
<th>Pull Command</th>
</tr>
{{ range $key, $value := .Repos }}
<tr>
<td valign="top">
{{ $value.Name }}
</td>
<td>
{{ $value.Tags }}
</td>
<td align="right">
<code>docker pull {{ $value.RegistryURL }}/{{ $value.Name }}</code>
</td>
</tr>
{{ end }}
</table>
</div>
<div class="footer">
<a href="https://twitter.com/jessfraz">@jessfraz</a>
<p>Last Updated: {{ .LastUpdated }}</p>
</div><!--/.footer-->
<script src="/js/scripts.js"></script>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-29404280-12', 'jessfraz.com');
ga('send', 'pageview');
</script>
</body>
</html>
{{end}}