Use react single page app for browsing registry

pull/3/head
Mike Heijmans 2016-05-12 23:01:19 -07:00
parent cb3a66ea50
commit b64247eb22
No known key found for this signature in database
GPG Key ID: AD75EF5BA031CA8B
44 changed files with 843 additions and 189 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
.env
node_modules
bower
public/bundle.js

View File

@ -6,7 +6,7 @@ FROM niche/ruby-base:0.1
MAINTAINER Mike Heijmans <parabuzzle@gmail.com>
# Add env variables
ENV PORT 4567
ENV PORT 80
ENV REGISTRY_HOST localhost
ENV REGISTRY_PORT=5000
ENV REGISTRY_PROTO=https
@ -25,8 +25,5 @@ WORKDIR $APP_HOME
# Add the app
ADD . $APP_HOME
# Expose needed ports
EXPOSE 4567
# Run the app
CMD bundle exec foreman start

View File

@ -26,3 +26,5 @@ gem 'httparty'
gem 'pry'
gem 'memoist'
gem "sinatra-cross_origin", "~> 0.3.1"

View File

@ -28,6 +28,7 @@ GEM
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
sinatra-cross_origin (0.3.2)
slim (3.0.6)
temple (~> 0.7.3)
tilt (>= 1.3.3, < 2.1)
@ -56,9 +57,10 @@ DEPENDENCIES
rack (~> 1.6.0)
rake
sinatra
sinatra-cross_origin (~> 0.3.1)
slim
thin
unicorn
BUNDLED WITH
1.10.6
1.11.2

View File

@ -1,23 +1,29 @@
# CraneOperator
Just as crane operators can see where all the containers are in the shipyard, CraneOp gives you a simple web interface for browsing around a Docker Registry running version 2.0+
Just as crane operators can see where all the containers that are in the shipyard, CraneOp gives you a simple web interface for browsing around a Docker Registry running version 2.0+
[![Circle CI](https://circleci.com/gh/parabuzzle/craneoperator.svg?style=svg)](https://circleci.com/gh/parabuzzle/craneoperator)
![screenshots/demo.gif](screenshots/demo.gif)
## Why Crane Operator?
When you run your own internal docker registry, it can be challenging to find out what has been saved there. I wanted to create a simple and lightweight frontend for browsing my registry. Most solutions that exist are built for registry v1 and don't work with the newer registry v2. (to be honest, its hard enough to even get registry v2 working... browsing it shouldn't be)
## How do I run it?
```
docker run -d -p 4567:4567 parabuzzle/craneoperator:latest
docker run -d -p 80:80 parabuzzle/craneoperator:latest
```
## Customizing the Crane
## How do I configure it?
```
docker run -d \
-p 4567:4567 \
-p 80:80 \
-e REGISTRY_HOST=registry.yourdomain.com \
-e REGISTRY_PORT=443 \
-e REGISTRY_PROTO=https \
-e REGISTRY_SSL_VERIFY=false \
parabuzzle/craneoperator:latest
```
![https://raw.githubusercontent.com/parabuzzle/craneoperator/master/screenshots/image_info.png](https://raw.githubusercontent.com/parabuzzle/craneoperator/master/screenshots/image_info.png)
![screenshots/Crane_Operator.jpg](screenshots/Crane_Operator.jpg)

View File

@ -37,6 +37,9 @@ task :push => :tag do
end
task :build do
sh "npm install"
sh "npm install webpack"
sh "node_modules/webpack/bin/webpack.js"
sh "docker build -t parabuzzle/craneoperator:latest ."
end

View File

@ -1 +1 @@
1.0
2.0

1
apiserver.rb Normal file
View File

@ -0,0 +1 @@
apiserver.rb

8
app/App.js Normal file
View File

@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import RepoBrowser from './components/RepoBrowser'
ReactDOM.render(
<RepoBrowser />,
document.getElementById('app')
);

View File

@ -0,0 +1,52 @@
import React from 'react';
import Footer from './sections/Footer';
import Header from './sections/Header';
import Repos from './Repos';
import Tags from './Tags';
import TagInfo from './TagInfo';
class RepoBrowser extends React.Component {
constructor(props){
super(props);
this.state = {
repo: undefined,
tag: undefined,
getinfo: false
}
}
handleSetTag(name){
this.setState({
tag: name,
getinfo: true
})
}
handleSetRepo(name){
this.setState({
getinfo: false,
repo: name
})
}
render(){
return(
<div className="main-container">
<Header />
<div className="container">
<div className="col-sm-3">
<Repos repo={this.state.repo} setRepo={(name) => this.handleSetRepo(name)}/>
</div>
<div className="col-sm-3">
<Tags repo={this.state.repo} setTag={(name) => this.handleSetTag(name)}/>
</div>
<div className="col-sm-6 col-left-border">
<TagInfo tag={this.state.tag} repo={this.state.repo} getinfo={this.state.getinfo}/>
</div>
</div>
<Footer />
</div>
)};
}
export default RepoBrowser;

15
app/components/Repos.js Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
import List from './Repos/List';
class Repos extends React.Component {
render(){
return(
<div>
<h3>Repos</h3>
<List setRepo={this.props.setRepo}/>
</div>
)
}
}
export default Repos;

View File

@ -0,0 +1,69 @@
import React from 'react';
import axios from 'axios';
import Loader from 'react-loader';
import { Button } from 'react-bootstrap';
export default class List extends React.Component {
constructor(props){
super(props);
this.state = {
repos: [],
repo: undefined,
loaded: false,
error: undefined
}
}
init(){
this.props.setRepo;
this.getReposList()
.then(function(data){
this.setState({
repos: data,
loaded: true,
repo: this.props.repo
})
}.bind(this));
}
componentDidMount(){
this.init();
}
getReposList(){
return axios.get(`/containers.json`)
.then(function (response) {
return(response.data);
})
.catch(function (response){
this.setState({
loaded: true,
error: response
})
});
}
handleClick(name){
this.props.setRepo(name);
this.setState({
repo: name
})
}
render(){
return(
<ul className="list-group">
<Loader loaded={this.state.loaded} color="red" scale={0.75}>
{this.state.error && "Error Fetching Repos"}
{this.state.repos.map((repo, index) => (
this.state.repo === repo ?
<Button bsClass="list-group-item active" key={index} onClick={() => this.handleClick(repo)}>{repo}</Button>
:
<Button bsClass="list-group-item" key={index} onClick={() => this.handleClick(repo)}>{repo}</Button>
))}
</Loader>
</ul>
)
}
}

View File

@ -0,0 +1,66 @@
import React from 'react';
export default class RepoConfig extends React.Component {
render() {
return (
<div>
<h3>Configuration</h3>
<div className="row">
<div className="col-md-3">
<b>Entrypoint:</b>
</div>
<div className="col-md-9">
{this.props.config.Entrypoint && this.props.config.Entrypoint.map((point, index) => (
<span key={index}>{point} </span>
))}
</div>
</div>
<hr/>
<div className="row">
<div className="col-md-3">
<b>CMD:</b>
</div>
<div className="col-md-9">
{this.props.config.Cmd && this.props.config.Cmd.map((cmd, index) => (
<span key={index}>{cmd} </span>
))}
</div>
</div>
<hr/>
<div className="row">
<div className="col-md-3">
<b>ENV:</b>
</div>
<div className="col-md-9">
{this.props.config.Env && this.props.config.Env.map((env, index) => (
<div key={index}>{env}</div>
))}
</div>
</div>
<hr/>
<div className="row">
<div className="col-md-3">
<b>Exposed Ports:</b>
</div>
<div className="col-md-9">
{this.props.config.ExposedPorts && Object.keys(this.props.config.ExposedPorts).map((port, index) => (
<div key={index}>{port}</div>
))}
</div>
</div>
<hr/>
<div className="row">
<div className="col-md-3">
<b>Volumes:</b>
</div>
<div className="col-md-9">
{this.props.config.Volumes && Object.keys(this.props.config.Volumes).map((volume, index) => (
<div key={index}>{volume}</div>
))}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,48 @@
import React from 'react';
import Time from 'react-time';
import Loader from 'react-loader';
import RepoConfig from './RepoConfig';
require('react-datetime');
export default class RepoTagInfo extends React.Component {
render(){
console.log(this.props.info)
return(
<div>
<div className="row">
<div className="col-md-3"><b>Architecture:</b></div>
<div className="col-md-7">{this.props.info.architecture}</div>
</div>
<div className="row">
<div className="col-md-3"><b>OS:</b></div>
<div className="col-md-7">{this.props.info.information && this.props.info.information.os}</div>
</div>
<div className="row">
<div className="col-md-3"><b>Created:</b></div>
<div className="col-md-7">
{this.props.info.information && <Time value={this.props.info.information.created_millis} format="MM/DD/YYYY hh:mma" />} UTC
<span className='small text-muted'> ({this.props.info.information && <Time value={this.props.info.information.created_millis} titleFormat="YYYY/MM/DD HH:mm" relative />})</span></div>
</div>
<div className="row">
<div className="col-md-3"><b>Author:</b></div>
<div className="col-md-7">{this.props.info.information && this.props.info.information.author}</div>
</div>
<div className="row">
<div className="col-md-3"><b>ID:</b></div>
<div className="col-md-7">{this.props.info.information && this.props.info.information.id}</div>
</div>
<div className="row">
<div className="col-md-3"><b>Container:</b></div>
<div className="col-md-7">{this.props.info.information && this.props.info.information.container}</div>
</div>
<div className="row">
<div className="col-md-3"><b>Docker Version:</b></div>
<div className="col-md-7">{this.props.info.information && this.props.info.information.docker_version}</div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,79 @@
import React from 'react';
import Time from 'react-time';
import axios from 'axios';
import Loader from 'react-loader';
import RepoConfig from './RepoConfig';
import RepoInfo from './RepoInfo';
require('react-datetime');
export default class RepoTagInfo extends React.Component {
constructor(props){
super(props);
this.state = {
repo: undefined,
tag: undefined,
info: undefined,
loaded: true,
error: undefined
}
}
componentWillReceiveProps(nextProps) {
if(nextProps.getinfo){
this.setState({
repo: nextProps.repo,
loaded: false,
tag: nextProps.tag
})
this.updateList(nextProps.repo, nextProps.tag)
} else {
this.setState({
info: undefined,
tag: undefined,
repo: undefined,
loaded: true
})
}
}
updateList(repo, tag){
this.getTagInfo(repo, tag)
.then(function(data){
this.setState({
info: data,
loaded: true
})
}.bind(this));
}
getTagInfo(repo, tag){
return axios.get(`/container/${repo}/${tag}.json`)
.then(function (response) {
return(response.data);
})
.catch(function (response){
this.setState({
loaded: true,
error: response
})
});
}
render(){
const {info} = this.state;
return(
<div>
<Loader loaded={this.state.loaded} color="red" scale={0.75} >
{this.state.error && "Error Fetching Repos"}
{ this.state.info && <RepoInfo info={this.state.info} />}
<div className="row">
<div className="col-md-12">
{this.state.info && <RepoConfig config={this.state.info.information.config} />}
</div>
</div>
</Loader>
</div>
)
}
}

View File

@ -0,0 +1,77 @@
import React from 'react';
import axios from 'axios';
import Loader from 'react-loader';
import { Button } from 'react-bootstrap';
export default class RepoTags extends React.Component {
constructor(props){
super(props);
this.state = {
tags: [],
repo: undefined,
tag: undefined,
loaded: true
}
}
componentWillReceiveProps(nextProps) {
if(nextProps.repo !== this.state.repo){
this.setState({
repo: nextProps.repo,
loaded: false,
tag: undefined
})
this.updateList(nextProps.repo)
}
}
updateList(repo){
this.getTags(repo)
.then(function(data){
this.setState({
tags: data,
loaded: true
})
}.bind(this));
}
getTags(repo){
return axios.get(`/container/${repo}/tags.json`)
.then(function (response) {
return(response.data);
})
.catch(function (response){
this.setState({
loaded: true,
error: response
})
});
}
handleClick(tag){
this.props.setTag(tag);
this.setState({
tag: tag
})
}
render(){
return(
<div>
<h3>Tags</h3>
<ul className="list-group">
<Loader loaded={this.state.loaded} color="red" scale={0.75}>
{this.state.error && "Error Fetching Repos"}
{this.state.tags.map((tag, index) => (
this.state.tag === tag ?
<Button bsClass="list-group-item active" key={index} onClick={() => this.handleClick(tag)}>{tag}</Button>
:
<Button bsClass="list-group-item" key={index} onClick={() => this.handleClick(tag)}>{tag}</Button>
))}
</Loader>
</ul>
</div>
)
}
}

17
app/components/TagInfo.js Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
import RepoTags from './Repos/RepoTags';
import RepoTagInfo from './Repos/RepoTagInfo';
class TagInfo extends React.Component {
render() {
return (
<div>
<h3>Information</h3>
<RepoTagInfo tag={this.props.tag} repo={this.props.repo} getinfo={this.props.getinfo}/>
</div>
);
}
}
export default TagInfo;

28
app/components/Tags.js Normal file
View File

@ -0,0 +1,28 @@
import React from 'react';
import RepoTags from './Repos/RepoTags';
import List from './Repos/List';
class Tags extends React.Component {
constructor(props){
super(props);
this.state = {
repo: undefined
}
}
componentWillReceiveProps(nextProps) {
this.setState({
repo: nextProps.repo
})
}
render() {
return (
<div>
<RepoTags repo={this.state.repo} setTag={this.props.setTag}/>
</div>
);
}
}
export default Tags;

View File

@ -0,0 +1,47 @@
import React from 'react';
import axios from 'axios';
import { Navbar } from 'react-bootstrap';
export default class Footer extends React.Component {
constructor(props){
super(props);
this.state = {
registry: {}
}
}
getRegistryInfo(){
return axios.get(`/registryinfo`)
.then(function (response) {
return(response.data);
})
.catch(function (response) {
console.log('ERROR IN AXIOS! ' + response);
});
};
componentDidMount(){
this.getRegistryInfo()
.then(function(data){
this.setState({
registry: data
})
}.bind(this));
}
render() {
return (
<div className='footer-pad'>
<Navbar inverse={true} fixedBottom={true}>
<Navbar.Text>
Crane Operator browsing {this.state.registry.host}
</Navbar.Text>
<Navbar.Text pullRight>
<Navbar.Link href="https://github.com/parabuzzle/craneoperator" target="_blank">GitHub Project</Navbar.Link>
</Navbar.Text>
</Navbar>
</div>
);
}
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import { Navbar } from 'react-bootstrap';
export default class Header extends React.Component {
render(){
return (
<div>
<Navbar staticTop={true}>
<Navbar.Header>
<img className='navbar-brand' src="mini-logo.svg"/>
<Navbar.Brand>
Crane Operator
</Navbar.Brand>
</Navbar.Header>
</Navbar>
</div>
);
}
}

View File

@ -7,6 +7,8 @@ dependencies:
- docker info
- gem install httparty
- gem install memoist
- npm install
- npm install webpack
- rake build
test:

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "craneoperator",
"version": "2.0.0",
"description": "The Docker Registry Viewer",
"main": "bundle.js",
"dependencies": {
"axios": "^0.8.0",
"history": "^1.13.1",
"moment": "^2.13.0",
"re-base": "^1.5.1",
"react": "^0.14.3",
"react-bootstrap": "^0.29.3",
"react-datetime": "^2.1.0",
"react-dom": "^0.14.3",
"react-intl": "^2.1.2",
"react-loader": "^2.4.0",
"react-router": "^1.0.1",
"react-time": "^4.0.3",
"babel-core": "^6.3.13",
"babel-loader": "^6.2.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"webpack": "^1.13.0"
},
"devDependencies": {
"webpack-dev-server": "^1.14.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --content-base public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/parabuzzle/craneop.git"
},
"keywords": [
"docker",
"registry",
"browser"
],
"author": "Michael Heijmans",
"license": "MIT",
"bugs": {
"url": "https://github.com/parabuzzle/craneop/issues"
},
"homepage": "https://github.com/parabuzzle/craneop#readme"
}

41
public/404.html Normal file
View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Crane Operator Not Found</title>
<!-- bootstrap css -->
<link rel="stylesheet" href="bootstrap.min.css" crossorigin="anonymous">
<!-- bootstrap theme -->
<link rel="stylesheet" href="bootstrap-theme.css" crossorigin="anonymous">
<!-- application css -->
<link rel="stylesheet" href="app.css" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<img class="navbar-brand" src="mini-logo.svg">
<a href="/" class="navbar-brand">Crane Operator</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
</div><!--/.nav-collapse -->
</div>
</nav>
<br/><br/><br/>
<div class="container">
<h1>Not Found</h1>
<div>
UhOh! the page you're looking for doesn't exist here... Perhaps it fell off the boat?
</div>
<div>
<br/><br/>
<img class="img-responsive" src="404.png"/>
</div>
</div>
</body>
</html>

37
public/500.html Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Crane Operator Not Found</title>
<!-- bootstrap css -->
<link rel="stylesheet" href="bootstrap.min.css" crossorigin="anonymous">
<!-- bootstrap theme -->
<link rel="stylesheet" href="bootstrap-theme.css" crossorigin="anonymous">
<!-- application css -->
<link rel="stylesheet" href="app.css" crossorigin="anonymous">
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<img class="navbar-brand" src="mini-logo.svg">
<a href="/" class="navbar-brand">Crane Operator</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
</div><!--/.nav-collapse -->
</div>
</nav>
<br/><br/><br/>
<div class="container">
<h1>Error!</h1>
<div>
Oh No! Something's gone terribly wrong!
</div>
</div>
</body>
</html>

View File

@ -1,4 +1,32 @@
#main {
margin-top: 35px;
min-height: 500px;
html, body{
height:100%;
}
.footer-pad {
padding-top: 100px;
}
.loader {
top: 0;
right: 0;
bottom: 0;
left: 0;
background: white;
z-index: 9999;
}
.spinner {
position: fixed;
top: 500px;
right: 0;
bottom: 0;
left: 0;
background: white;
z-index: 9999;
}
.col-left-border {
min-height: 400px;
height:100%;
border-left: 1px solid #aaa;
}

File diff suppressed because one or more lines are too long

6
public/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

21
public/index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Crane Operator</title>
<!-- bootstrap css -->
<link rel="stylesheet" href="bootstrap.min.css" crossorigin="anonymous">
<!-- bootstrap theme -->
<link rel="stylesheet" href="bootstrap-theme.css" crossorigin="anonymous">
<!-- application css -->
<link rel="stylesheet" href="app.css" crossorigin="anonymous">
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>

1
public/mini-logo.svg Normal file
View File

@ -0,0 +1 @@
<svg width="60" height="43" viewBox="0 0 60 43" xmlns="http://www.w3.org/2000/svg"><title>logo copy</title><g fill="#FFF" fill-rule="evenodd"><path d="M3.757 15.768h6.04v5.873h-6.04V15.77zM11.17 15.768h6.04v5.873h-6.04V15.77zM11.17 8.24h6.04v5.87h-6.04V8.24zM18.58 15.768h6.043v5.873H18.58V15.77zM18.58 8.24h6.043v5.87H18.58V8.24zM25.993 15.768h6.042v5.873h-6.042V15.77zM25.993 8.24h6.042v5.87h-6.042V8.24zM33.405 15.768h6.042v5.873h-6.042V15.77zM25.993.708h6.042V6.58h-6.042V.708zM12.194 30.12c-.93 0-1.684.733-1.684 1.636 0 .902.755 1.635 1.684 1.635.928 0 1.683-.732 1.683-1.634 0-.903-.755-1.637-1.683-1.637"/><path d="M58.905 18.806c-2.03-1.138-4.73-1.294-7.03-.636-.283-2.377-1.89-4.46-3.8-5.953l-.758-.593-.638.716c-1.28 1.438-1.66 3.83-1.487 5.666.13 1.35.565 2.722 1.42 3.806-.65.38-1.388.682-2.045.9-1.34.44-2.795.685-4.21.685H.613l-.085.89c-.285 2.972.134 5.947 1.398 8.66l.544 1.078.062.1c3.737 6.17 10.3 8.768 17.452 8.768 13.846 0 25.265-6.01 30.51-18.708 3.505.178 7.09-.83 8.805-4.083l.437-.83-.832-.466zm-46.71 16.056c-1.764 0-3.198-1.394-3.198-3.106 0-1.713 1.434-3.107 3.197-3.107 1.763 0 3.197 1.393 3.197 3.106 0 1.712-1.433 3.106-3.196 3.106z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
test.html

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

BIN
screenshots/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

118
server.rb
View File

@ -2,17 +2,29 @@ require 'oj'
require 'httparty'
require 'pry'
require 'erb'
require 'time'
require 'sinatra/base'
require 'sinatra/cross_origin'
class CraneOp < Sinatra::Base
register Sinatra::CrossOrigin
configure do
enable :cross_origin
mime_type :javascript, 'application/javascript'
mime_type :javascript, 'text/javascript'
set :logging, true
set :static, true
set :allow_origin, :any
set :allow_methods, [:get, :post, :options]
set :allow_credentials, true
set :max_age, "1728000"
set :expose_headers, ['Content-Type']
set :json_encoder, :to_json
end
## Setup ##
def registry_host
ENV['REGISTRY_HOST'] || 'localhost'
end
@ -29,37 +41,30 @@ class CraneOp < Sinatra::Base
ENV['REGISTRY_SSL_VERIFY'] || 'true'
end
## Helpers ##
def to_bool(str)
str.downcase == 'true'
end
def html(view)
File.read(File.join('public', "#{view.to_s}.html"))
end
def sort_versions(ary)
valid_version_numbers = ary.select { |i| i if i.match(/(0-9|\.|\-)/)}
non_valid_version_numbers = ary - valid_version_numbers
(valid_version_numbers.sort_by {|v| Gem::Version.new( v ) } + non_valid_version_numbers)
end
## Registry API Methods ##
def containers
response = HTTParty.get( "#{registry_proto}://#{registry_host}:#{registry_port}/v2/_catalog", verify: to_bool(registry_ssl_verify) )
json = Oj.load response.body
json['repositories']
end
def sort_versions(ary)
ary.sort { |x,y|
matcher = /[a-z]/i
if x.match(matcher)
a = nil
else
a = x.split('.').last.to_i
end
if y.match(matcher)
b = nil
else
b = y.split('.').last.to_i
end
a && b ? a <=> b : a ? -1 : 1
}
#ary.sort_by! {|v| v.split('.') }
end
def container_tags(repo)
response = HTTParty.get( "#{registry_proto}://#{registry_host}:#{registry_port}/v2/#{repo}/tags/list", verify: to_bool(registry_ssl_verify) )
json = Oj.load response.body
@ -70,50 +75,65 @@ class CraneOp < Sinatra::Base
def container_info(repo, manifest)
response = HTTParty.get( "#{registry_proto}://#{registry_host}:#{registry_port}/v2/#{repo}/manifests/#{manifest}", verify: to_bool(registry_ssl_verify) )
json = Oj.load response.body
# Add extra fields for easy display
json['information'] = Oj.load(json['history'].first['v1Compatibility'])
created_at = Time.parse(json['information']['created'])
json['information']['created_formatted'] = created_at.to_s
json['information']['created_millis'] = (created_at.to_f * 1000).to_i
return json
end
def container_blob(repo, digest='HEAD')
response = HTTParty.get( "#{registry_proto}://#{registry_host}:#{registry_port}/v2/#{repo}/blobs/#{digest}", verify: to_bool(registry_ssl_verify) )
json = Oj.load response.body
end
get '/test' do
erb :index
end
## Endpoints ##
get '/' do
@containers = containers
erb :index
html :index
end
get '/about' do
erb :about
get '/containers.json' do
content_type :json
containers.to_json
end
get '/container/:name' do |name|
@container_tags = container_tags(name)
@name = name
halt 404 if @container_tags.nil?
erb :container
get '/container/:container/tags.json' do |container|
content_type :json
tags = container_tags(container)
halt 404 if tags.nil?
tags.to_json
end
get '/container/:name/:tag' do |name, tag|
@tag = tag
@name = name
@container_info = container_info(name, tag)
@container_tags = container_tags(name)
halt 404 if @container_info['errors']
halt 404 if @container_info['fsLayers'].nil?
erb :tag
get '/container/:container/:tag.json' do |container, tag|
content_type :json
info = container_info(container, tag)
halt 404 if info['errors']
halt 404 if info['fsLayers'].nil?
info.to_json
end
error 404 do
erb :'404'
get '/registryinfo' do
content_type :json
{
host: registry_host,
port: registry_port,
protocol: registry_proto,
ssl_verify: registry_ssl_verify
}.to_json
end
# Error Handlers
error do
File.read(File.join('public', '500.html'))
end
not_found do
status 404
erb :'404'
File.read(File.join('public', '404.html'))
end
end

View File

@ -1,6 +0,0 @@
<h1>404</h1>
<hr>
oops... the info you're looking for cannot be found. Perhaps it fell off the boat?
<br/><br/>
<img src="/404.png"/>

View File

@ -1,5 +0,0 @@
<h1>About</h1>
Just as a crane operator in a shipyard can see where all the containers are, the Crane Operator does the same for your private registry.
<br/><br/>
<img src="/crane.jpg"/>

View File

@ -1,10 +0,0 @@
<h1><%= @name %></h1>
<hr>
<h3>Tags</h3>
<% if @container_tags.empty? %>
No Tags Found
<% else %>
<% for tag in @container_tags do %>
<a href="/container/<%=@name%>/<%=tag%>"><%=tag%></a> &nbsp; &nbsp;
<%end%>
<%end%>

View File

@ -1,7 +0,0 @@
<h1>Containers</h1>
<hr>
<ul>
<% for container in @containers %>
<li><b><a href="/container/<%=container%>"><%=container%></a></b></li>
<%end%>
</ul>

View File

@ -1,62 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css" />
<link rel="stylesheet" href="/app.css" />
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<title>Crane Operator</title>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Crane Operator</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<br/><br/>
<div id="main">
<div class="container">
<%= yield %>
</div><!-- /.container -->
</div>
<div id="footer">
<hr>
<div class="container">
<div class="row">
<div class="col-md-9"><b>Crane Operator is Open Source</b></div>
<div class="col-md-3"><a href="http://github.com/parabuzzle/craneoperator">GitHub Project</a></div>
</div>
</div>
</div>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.4/angular.min.js"></script>
<script type="application/javascript" src="app.js"></script>
</body>
</html>

View File

@ -1,33 +0,0 @@
<h1><a href="/container/<%=@name%>"><%=@name%></a>:<%=@tag%></h1>
<hr>
<div class="row">
<div class="col-md-2">
<b>Architecture</b>
</div>
<div class="col-md-10">
<%= @container_info['architecture'] %>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-2">
<b>Tags</b>
</div>
<div class="col-md-10">
<% for tag in @container_tags do %>
<a href="/container/<%=@name%>/<%=tag%>"><%=tag%></a><br/>
<%end%>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-2">
<b>Layers</b>
</div>
<div class="col-md-10">
<% for layer in @container_info['fsLayers'] do %>
<%=layer['blobSum'].gsub('sha256:', '')%><br/>
<%end%>
</div>
</div>

24
webpack.config.js Normal file
View File

@ -0,0 +1,24 @@
module.exports = {
entry: "./app/App.js",
output: {
filename: "bundle.js",
path: __dirname + '/public',
publicPath: "/"
},
devServer: {
inline: true,
port: 4000
},
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel',
query: {
presets: ['react', 'es2015']
}
}
]
}
}