refactored the api (fixes #6)
parent
b7cc3f0083
commit
d12cdec3b8
5
Gemfile
5
Gemfile
|
@ -17,7 +17,8 @@ gem 'rake'
|
||||||
# Component requirements
|
# Component requirements
|
||||||
gem 'slim'
|
gem 'slim'
|
||||||
|
|
||||||
gem 'sinatra'
|
gem 'sinatra', '~> 1.4.5'
|
||||||
|
gem 'rack-protection', '~> 1.5.3'
|
||||||
|
|
||||||
gem 'rack', '~> 1.6.0'
|
gem 'rack', '~> 1.6.0'
|
||||||
|
|
||||||
|
@ -28,3 +29,5 @@ gem 'pry'
|
||||||
gem 'memoist'
|
gem 'memoist'
|
||||||
|
|
||||||
gem "sinatra-cross_origin", "~> 0.3.1"
|
gem "sinatra-cross_origin", "~> 0.3.1"
|
||||||
|
|
||||||
|
gem 'sinatra-session'
|
||||||
|
|
|
@ -29,6 +29,8 @@ GEM
|
||||||
rack-protection (~> 1.4)
|
rack-protection (~> 1.4)
|
||||||
tilt (>= 1.3, < 3)
|
tilt (>= 1.3, < 3)
|
||||||
sinatra-cross_origin (0.3.2)
|
sinatra-cross_origin (0.3.2)
|
||||||
|
sinatra-session (1.0.0)
|
||||||
|
sinatra (>= 1.0)
|
||||||
slim (3.0.6)
|
slim (3.0.6)
|
||||||
temple (~> 0.7.3)
|
temple (~> 0.7.3)
|
||||||
tilt (>= 1.3.3, < 2.1)
|
tilt (>= 1.3.3, < 2.1)
|
||||||
|
@ -55,9 +57,11 @@ DEPENDENCIES
|
||||||
oj
|
oj
|
||||||
pry
|
pry
|
||||||
rack (~> 1.6.0)
|
rack (~> 1.6.0)
|
||||||
|
rack-protection (~> 1.5.3)
|
||||||
rake
|
rake
|
||||||
sinatra
|
sinatra (~> 1.4.5)
|
||||||
sinatra-cross_origin (~> 0.3.1)
|
sinatra-cross_origin (~> 0.3.1)
|
||||||
|
sinatra-session
|
||||||
slim
|
slim
|
||||||
thin
|
thin
|
||||||
unicorn
|
unicorn
|
||||||
|
@ -66,4 +70,4 @@ RUBY VERSION
|
||||||
ruby 2.3.1p112
|
ruby 2.3.1p112
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.13.7
|
1.14.6
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
apiserver.rb
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import RepoBrowser from './components/RepoBrowser'
|
import AppContainer from './views/AppContainer.jsx';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<RepoBrowser />,
|
<AppContainer/>,
|
||||||
document.getElementById('app')
|
document.getElementById('app')
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { NavDropdown, NavItem, MenuItem } from 'react-bootstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default class Login extends Component {
|
||||||
|
|
||||||
|
displayLogin(){
|
||||||
|
if (this.props.registry.username){
|
||||||
|
return(
|
||||||
|
<NavDropdown eventKey={this.props.eventKey} title={this.props.registry.username} id="login-dropdown">
|
||||||
|
<MenuItem eventKey={this.props.eventKey + 0.1} href="/logout">Logout</MenuItem>
|
||||||
|
</NavDropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return(
|
||||||
|
<NavItem eventKey={this.props.eventKey}>Login</NavItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
this.displayLogin()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,8 +78,6 @@ class RepoBrowser extends React.Component {
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
return(
|
return(
|
||||||
<div className="main-container">
|
|
||||||
<Header />
|
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<div className="col-sm-3">
|
<div className="col-sm-3">
|
||||||
<Repos repo={this.state.repo} setRepo={(name) => this.handleSetRepo(name)}/>
|
<Repos repo={this.state.repo} setRepo={(name) => this.handleSetRepo(name)}/>
|
||||||
|
@ -91,8 +89,6 @@ class RepoBrowser extends React.Component {
|
||||||
<TagInfo tag={this.state.tag} repo={this.state.repo} handleTagDelete={(repo, tag) => this.handleTagDelete(repo, tag)} getinfo={this.state.getinfo} registry={this.state.registry}/>
|
<TagInfo tag={this.state.tag} repo={this.state.repo} handleTagDelete={(repo, tag) => this.handleTagDelete(repo, tag)} getinfo={this.state.getinfo} registry={this.state.registry}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer registry={this.state.registry} />
|
|
||||||
</div>
|
|
||||||
)};
|
)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import {
|
||||||
|
BrowserRouter as Router,
|
||||||
|
Route,
|
||||||
|
Link
|
||||||
|
} from 'react-router-dom'
|
||||||
|
import HomeView from './HomeView.jsx';
|
||||||
|
import LoginView from './LoginView.jsx';
|
||||||
|
import Header from './sections/Header.jsx';
|
||||||
|
import Footer from './sections/Footer.jsx';
|
||||||
|
|
||||||
|
export default class AppContainer extends React.Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
registry: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchRegistryInfo(){
|
||||||
|
return axios.get(`/api/registryinfo`)
|
||||||
|
.then(function (response) {
|
||||||
|
this.setState({
|
||||||
|
registry: response.data
|
||||||
|
})
|
||||||
|
}.bind(this))
|
||||||
|
.catch(function (response) {
|
||||||
|
console.log('ERROR IN AXIOS! ' + response);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchRegistryInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<div>
|
||||||
|
<Header registry={this.state.registry}/>
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<Route exact path="/" component={HomeView}/>
|
||||||
|
<Route exact path="/login" component={LoginView}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer registry={this.state.registry}/>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export default class HomeView extends Component {
|
||||||
|
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loaded: false,
|
||||||
|
term: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Home</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { FormControl, FormGroup, ControlLabel, Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default class LoginView extends Component {
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
username: undefined,
|
||||||
|
password: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUsername(event){
|
||||||
|
this.setState({
|
||||||
|
username: event.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePassword(event){
|
||||||
|
this.setState({
|
||||||
|
password: event.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Login to Docker Registry</h1>
|
||||||
|
<Row>
|
||||||
|
<form>
|
||||||
|
<Col md={4}>
|
||||||
|
<Row style={{paddingBottom: "1em", paddingTop: "2em"}}>
|
||||||
|
<Col sm={12}>
|
||||||
|
<FormControl type="text"
|
||||||
|
placeholder="Username"
|
||||||
|
onChange={(event) => this.handleUsername(event)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col sm={12}>
|
||||||
|
<FormControl type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
onChange={(event) => this.handlePassword(event)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
</Col>
|
||||||
|
</form>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,3 @@ export default class Footer extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Footer.propTypes = {
|
|
||||||
registry: React.PropTypes.object.isRequired
|
|
||||||
}
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Navbar, Nav, NavItem } from 'react-bootstrap';
|
||||||
|
import Login from '../../components/Login.jsx';
|
||||||
|
|
||||||
|
export default class Header extends React.Component {
|
||||||
|
title(){
|
||||||
|
if(this.props.registry.title){
|
||||||
|
return(this.props.registry.title)
|
||||||
|
}
|
||||||
|
return("Crane Operator")
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Navbar staticTop={true}>
|
||||||
|
<Navbar.Header>
|
||||||
|
<img className='navbar-brand' src="mini-logo.svg"/>
|
||||||
|
<Navbar.Brand>
|
||||||
|
<Link to="/">{this.title()}</Link>
|
||||||
|
</Navbar.Brand>
|
||||||
|
</Navbar.Header>
|
||||||
|
<Nav pullRight>
|
||||||
|
<Login registry={this.props.registry} eventKey={1}/>
|
||||||
|
</Nav>
|
||||||
|
</Navbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
class Configuration
|
||||||
|
|
||||||
|
attr_accessor :registry_password,
|
||||||
|
:registry_username,
|
||||||
|
:registry_host,
|
||||||
|
:registry_port,
|
||||||
|
:registry_protocol,
|
||||||
|
:ssl_verify,
|
||||||
|
:registry_public_url,
|
||||||
|
:delete_allowed,
|
||||||
|
:username,
|
||||||
|
:password,
|
||||||
|
:version
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@registry_username = ENV['REGISTRY_USERNAME']
|
||||||
|
@registry_password = ENV['REGISTRY_PASSWORD']
|
||||||
|
@registry_host = ENV['REGISTRY_HOST'] || 'localhost'
|
||||||
|
@registry_port = ENV['REGISTRY_PORT'] || '5000'
|
||||||
|
@registry_protocol = ENV['REGISTRY_PROTOCOL'] || 'https'
|
||||||
|
@registry_public_url = ENV['REGISTRY_PUBLIC_URL'] || "#{@registry_host}:#{@registry_port}"
|
||||||
|
@ssl_verify = to_bool(ENV['SSL_VERIFY'] || 'true')
|
||||||
|
@delete_allowed = to_bool(ENV['REGISTRY_ALLOW_DELETE'] || 'false')
|
||||||
|
@username = ENV['USERNAME']
|
||||||
|
@password = ENV['PASSWORD']
|
||||||
|
@version = "2.2"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_bool(str)
|
||||||
|
str.to_s.downcase == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
def registry_url
|
||||||
|
"#{registry_protocol}://#{registry_host}:#{registry_port}"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,55 @@
|
||||||
|
require 'base64'
|
||||||
|
require 'oj'
|
||||||
|
require 'httparty'
|
||||||
|
|
||||||
|
module Helpers
|
||||||
|
|
||||||
|
def sort_versions(ary)
|
||||||
|
valid_version_numbers = ary.select { |i| i if i.match(/^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+(-[[:alnum:]]+)?$/) }
|
||||||
|
non_valid_version_numbers = ary - valid_version_numbers
|
||||||
|
versions = valid_version_numbers.sort_by {|v| Gem::Version.new( v.gsub(/^[a-z|A-Z|.]*/, '') ) } + non_valid_version_numbers.sort
|
||||||
|
if versions.include?('latest')
|
||||||
|
# Make sure 'latest' appears at the top of the list
|
||||||
|
versions.delete('latest')
|
||||||
|
versions.push('latest')
|
||||||
|
end
|
||||||
|
versions
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_bool(str)
|
||||||
|
str.to_s.downcase == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
def html(view)
|
||||||
|
File.read(File.join('public', "#{view.to_s}.html"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def generateHeaders(config, session, headers={})
|
||||||
|
username = session[:username] || config.registry_username
|
||||||
|
password = session[:password] || config.registry_password
|
||||||
|
if username
|
||||||
|
headers['Authorization'] = "Basic #{base64_docker_auth(username, password)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def base64_docker_auth(username, password)
|
||||||
|
Base64.encode64("#{username}:#{password}").chomp
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_header(headers, addl_header)
|
||||||
|
headers.merge addl_header
|
||||||
|
end
|
||||||
|
|
||||||
|
def get(url, config, session, headers={})
|
||||||
|
response = HTTParty.get( "#{config.registry_url}#{url}", verify: config.ssl_verify, headers: generateHeaders(config, session, headers) )
|
||||||
|
Oj.load response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_head(url, config, session, headers={})
|
||||||
|
HTTParty.head( "#{registry_url}#{url}", verify: config.ssl_verify, headers: generateHeaders(config, session, headers) )
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_delete(url, config, session, headers={})
|
||||||
|
HTTParty.delete( "#{registry_url}#{url}", verify: config.ssl_verify, headers: generateHeaders(config, session, headers) )
|
||||||
|
end
|
||||||
|
end
|
26
package.json
26
package.json
|
@ -5,24 +5,24 @@
|
||||||
"main": "bundle.js",
|
"main": "bundle.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.8.0",
|
"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-core": "^6.3.13",
|
||||||
"babel-loader": "^6.2.0",
|
"babel-loader": "^6.2.0",
|
||||||
"babel-preset-es2015": "^6.3.13",
|
"babel-preset-es2015": "^6.3.13",
|
||||||
"babel-preset-react": "^6.3.13",
|
"babel-preset-react": "^6.3.13",
|
||||||
"webpack": "^1.13.0",
|
"esprima-fb": "15001.1001.0-dev-harmony-fb",
|
||||||
|
"history": "^1.13.1",
|
||||||
|
"moment": "^2.13.0",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
"esprima-fb": "15001.1001.0-dev-harmony-fb"
|
"re-base": "^1.5.1",
|
||||||
|
"react": "^15.6.1",
|
||||||
|
"react-datetime": "^2.1.0",
|
||||||
|
"react-dom": "^15.6.1",
|
||||||
|
"react-intl": "^2.1.2",
|
||||||
|
"react-loader": "^2.4.0",
|
||||||
|
"react-router": "^1.0.3",
|
||||||
|
"react-router-dom": "^4.1.2",
|
||||||
|
"react-time": "^4.0.3",
|
||||||
|
"webpack": "^1.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"webpack-dev-server": "^1.14.1"
|
"webpack-dev-server": "^1.14.1"
|
||||||
|
|
|
@ -11,23 +11,23 @@
|
||||||
<link rel="stylesheet" href="/bootstrap-theme.css" crossorigin="anonymous">
|
<link rel="stylesheet" href="/bootstrap-theme.css" crossorigin="anonymous">
|
||||||
|
|
||||||
<!-- font awesome -->
|
<!-- font awesome -->
|
||||||
<link rel="stylesheet" href="css/font-awesome.min.css" crossorigin="anonymous">
|
<link rel="stylesheet" href="/css/font-awesome.min.css" crossorigin="anonymous">
|
||||||
|
|
||||||
<!-- application css -->
|
<!-- application css -->
|
||||||
<link rel="stylesheet" href="/app.css" crossorigin="anonymous">
|
<link rel="stylesheet" href="/app.css" crossorigin="anonymous">
|
||||||
|
|
||||||
<!-- jquery -->
|
<!-- jquery -->
|
||||||
<script src="jquery-3.1.0.min.js"></script>
|
<script src="/jquery-3.1.0.min.js"></script>
|
||||||
|
|
||||||
<!-- bootstrap js -->
|
<!-- bootstrap js -->
|
||||||
<script src="bootstrap.min.js"></script>
|
<script src="/bootstrap.min.js"></script>
|
||||||
|
|
||||||
<!-- bootbox -->
|
<!-- bootbox -->
|
||||||
<script src="bootbox.min.js"></script>
|
<script src="/bootbox.min.js"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="bundle.js"></script>
|
<script src="/bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
193
server.rb
193
server.rb
|
@ -1,17 +1,19 @@
|
||||||
require 'oj'
|
require 'oj'
|
||||||
require 'httparty'
|
|
||||||
require 'pry'
|
require 'pry'
|
||||||
require 'erb'
|
require 'erb'
|
||||||
require 'time'
|
require 'time'
|
||||||
require 'sinatra/base'
|
require 'sinatra/base'
|
||||||
require 'sinatra/cross_origin'
|
require 'sinatra/cross_origin'
|
||||||
require 'base64'
|
require './lib/config.rb'
|
||||||
|
require './lib/helpers.rb'
|
||||||
|
|
||||||
class CraneOp < Sinatra::Base
|
class CraneOp < Sinatra::Base
|
||||||
register Sinatra::CrossOrigin
|
register Sinatra::CrossOrigin
|
||||||
|
include Helpers
|
||||||
|
|
||||||
configure do
|
configure do
|
||||||
enable :cross_origin
|
enable :cross_origin
|
||||||
|
enable :sessions
|
||||||
mime_type :javascript, 'application/javascript'
|
mime_type :javascript, 'application/javascript'
|
||||||
mime_type :javascript, 'text/javascript'
|
mime_type :javascript, 'text/javascript'
|
||||||
set :logging, true
|
set :logging, true
|
||||||
|
@ -22,118 +24,43 @@ class CraneOp < Sinatra::Base
|
||||||
set :max_age, "1728000"
|
set :max_age, "1728000"
|
||||||
set :expose_headers, ['Content-Type']
|
set :expose_headers, ['Content-Type']
|
||||||
set :json_encoder, :to_json
|
set :json_encoder, :to_json
|
||||||
|
set :session_secret, (ENV["SESSION_SECRET"] || "insecure-session-secret!")
|
||||||
end
|
end
|
||||||
|
|
||||||
## Setup ##
|
def conf
|
||||||
|
return Configuration.new
|
||||||
def registry_host
|
|
||||||
ENV['REGISTRY_HOST'] || 'localhost'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def registry_port
|
## Basic Auth ##
|
||||||
ENV['REGISTRY_PORT'] || '5000'
|
|
||||||
end
|
|
||||||
|
|
||||||
def registry_proto
|
if Configuration.new.username
|
||||||
ENV['REGISTRY_PROTO'] || 'https'
|
config = Configuration.new
|
||||||
end
|
|
||||||
|
|
||||||
def registry_ssl_verify
|
|
||||||
ENV['REGISTRY_SSL_VERIFY'] || 'true'
|
|
||||||
end
|
|
||||||
|
|
||||||
def registry_public_url
|
|
||||||
ENV['REGISTRY_PUBLIC_URL'] || "#{registry_host}:#{registry_port}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def registry_username
|
|
||||||
ENV['REGISTRY_USERNAME']
|
|
||||||
end
|
|
||||||
|
|
||||||
def registry_password
|
|
||||||
ENV['REGISTRY_PASSWORD']
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete_allowed
|
|
||||||
ENV['REGISTRY_ALLOW_DELETE'] || 'false'
|
|
||||||
end
|
|
||||||
|
|
||||||
## Authentication ##
|
|
||||||
|
|
||||||
if ENV['USERNAME']
|
|
||||||
use Rack::Auth::Basic, "Please Authenticate to View" do |username, password|
|
use Rack::Auth::Basic, "Please Authenticate to View" do |username, password|
|
||||||
username == ENV['USERNAME'] and password == ( ENV['PASSWORD'] || '' )
|
username == config.username and password == ( config.password || '' )
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def base64_docker_auth
|
|
||||||
Base64.encode64("#{registry_username}:#{registry_password}").chomp
|
|
||||||
end
|
|
||||||
|
|
||||||
def hdrs
|
|
||||||
h = {}
|
|
||||||
if registry_username
|
|
||||||
h['Authorization'] = "Basic #{base64_docker_auth}"
|
|
||||||
end
|
|
||||||
return h
|
|
||||||
end
|
|
||||||
|
|
||||||
def append_header(h, addl_header)
|
|
||||||
h.merge addl_header
|
|
||||||
end
|
|
||||||
|
|
||||||
## Helpers ##
|
|
||||||
|
|
||||||
def to_bool(str)
|
|
||||||
str.to_s.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(/^[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+(-[[:alnum:]]+)?$/) }
|
|
||||||
non_valid_version_numbers = ary - valid_version_numbers
|
|
||||||
versions = valid_version_numbers.sort_by {|v| Gem::Version.new( v.gsub(/^[a-z|A-Z|.]*/, '') ) } + non_valid_version_numbers.sort
|
|
||||||
if versions.include?('latest')
|
|
||||||
# Make sure 'latest' appears at the top of the list
|
|
||||||
versions.delete('latest')
|
|
||||||
versions.push('latest')
|
|
||||||
end
|
|
||||||
versions
|
|
||||||
end
|
|
||||||
|
|
||||||
def registry_url
|
|
||||||
url_parts = []
|
|
||||||
|
|
||||||
url_parts << registry_proto
|
|
||||||
url_parts << "://"
|
|
||||||
url_parts << registry_host
|
|
||||||
url_parts << ":"
|
|
||||||
url_parts << registry_port
|
|
||||||
|
|
||||||
url_parts.join
|
|
||||||
end
|
|
||||||
|
|
||||||
## Registry API Methods ##
|
## Registry API Methods ##
|
||||||
|
|
||||||
def containers
|
def containers(filter=nil)
|
||||||
response = HTTParty.get( "#{registry_url}/v2/_catalog", verify: to_bool(registry_ssl_verify), headers: hdrs )
|
json = get("/v2/_catalog", conf, session)
|
||||||
json = Oj.load response.body
|
if filter
|
||||||
|
return json['repositories'].select{ |i| i.match(/#{filter}.*/)}
|
||||||
|
end
|
||||||
json['repositories']
|
json['repositories']
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_tags(repo)
|
def container_tags(repo, filter=nil)
|
||||||
response = HTTParty.get( "#{registry_url}/v2/#{repo}/tags/list", verify: to_bool(registry_ssl_verify), headers: hdrs )
|
json = get("/v2/#{repo}/tags/list", conf, session)
|
||||||
json = Oj.load response.body
|
|
||||||
tags = json['tags'] || []
|
tags = json['tags'] || []
|
||||||
tags = sort_versions(tags).reverse
|
if filter
|
||||||
|
return sort_versions(tags.select{ |i| i.match(/#{filter}.*/)}).reverse
|
||||||
|
end
|
||||||
|
sort_versions(tags).reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
def container_info(repo, manifest)
|
def container_info(repo, manifest)
|
||||||
response = HTTParty.get( "#{registry_url}/v2/#{repo}/manifests/#{manifest}", verify: to_bool(registry_ssl_verify), headers: hdrs )
|
json = get("/v2/#{repo}/manifests/#{manifest}", conf, session)
|
||||||
json = Oj.load response.body
|
|
||||||
|
|
||||||
# Add extra fields for easy display
|
# Add extra fields for easy display
|
||||||
json['information'] = Oj.load(json['history'].first['v1Compatibility'])
|
json['information'] = Oj.load(json['history'].first['v1Compatibility'])
|
||||||
|
@ -145,43 +72,50 @@ class CraneOp < Sinatra::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_digest(repo, manifest)
|
def fetch_digest(repo, manifest)
|
||||||
h = append_header(hdrs, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
|
response = get_head("/v2/#{repo}/manifests/#{manifest}", conf, session, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
|
||||||
response = HTTParty.head( "#{registry_url}/v2/#{repo}/manifests/#{manifest}", verify: to_bool(registry_ssl_verify), headers: h )
|
|
||||||
return response.headers["docker-content-digest"]
|
return response.headers["docker-content-digest"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def image_delete(repo, manifest)
|
def image_delete(repo, manifest)
|
||||||
h = append_header(hdrs, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
|
|
||||||
digest = fetch_digest(repo, manifest)
|
digest = fetch_digest(repo, manifest)
|
||||||
response = HTTParty.delete( "#{registry_url}/v2/#{repo}/manifests/#{digest}", verify: to_bool(registry_ssl_verify), headers: h )
|
return send_delete("/v2/#{repo}/manifests/#{digest}", conf, session, { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'})
|
||||||
return response
|
|
||||||
end
|
end
|
||||||
|
|
||||||
## Endpoints ##
|
## Endpoints ##
|
||||||
|
|
||||||
get '/' do
|
get '/api' do
|
||||||
html :index
|
return "API Version #{conf.version}"
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/containers.json' do
|
get '/api/containers' do
|
||||||
content_type :json
|
content_type :json
|
||||||
|
containers(params[:filter]).to_json
|
||||||
containers.to_json
|
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/container/*/tags.json' do |container|
|
get '/api/tags/*' do |container|
|
||||||
content_type :json
|
content_type :json
|
||||||
|
tags = container_tags(container, params[:filter])
|
||||||
tags = container_tags(container)
|
|
||||||
halt 404 if tags.nil?
|
halt 404 if tags.nil?
|
||||||
tags.to_json
|
tags.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
get /container\/(.*\/)(.*.json)/ do |container, tag|
|
post '/api/login' do
|
||||||
|
content_type :json
|
||||||
|
params = Oj.load(request.body.read)
|
||||||
|
session[:username] = params['username']
|
||||||
|
session[:password] = params['password']
|
||||||
|
{status: "success"}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
get '/logout' do
|
||||||
|
session.destroy
|
||||||
|
redirect '/'
|
||||||
|
end
|
||||||
|
|
||||||
|
get /api\/containers\/(.*\/)(.*)/ do |container, tag|
|
||||||
|
|
||||||
# This is here because we need to handle slashes in container names
|
# This is here because we need to handle slashes in container names
|
||||||
container.chop!
|
container.chop!
|
||||||
tag.gsub!('.json', '')
|
|
||||||
|
|
||||||
content_type :json
|
content_type :json
|
||||||
|
|
||||||
|
@ -193,28 +127,43 @@ class CraneOp < Sinatra::Base
|
||||||
info.to_json
|
info.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
get '/registryinfo' do
|
get '/api/registryinfo' do
|
||||||
content_type :json
|
content_type :json
|
||||||
{
|
info = {
|
||||||
host: registry_host,
|
host: conf.registry_host,
|
||||||
public_url: registry_public_url,
|
public_url: conf.registry_public_url,
|
||||||
port: registry_port,
|
port: conf.registry_port,
|
||||||
protocol: registry_proto,
|
protocol: conf.registry_protocol,
|
||||||
ssl_verify: to_bool(registry_ssl_verify),
|
ssl_verify: conf.ssl_verify,
|
||||||
delete_allowed: to_bool(delete_allowed),
|
delete_allowed: conf.delete_allowed,
|
||||||
}.to_json
|
}
|
||||||
|
if session[:username]
|
||||||
|
info[:username] = session[:username]
|
||||||
|
end
|
||||||
|
info.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
delete /container\/(.*\/)(.*.json)/ do |container, tag|
|
delete /api\/containers\/(.*\/)(.*)/ do |container, tag|
|
||||||
halt 404 unless to_bool(delete_allowed)
|
halt 404 unless to_bool(delete_allowed)
|
||||||
|
|
||||||
container.chop!
|
container.chop!
|
||||||
tag.gsub!('.json', '')
|
|
||||||
response = image_delete( container, tag )
|
response = image_delete( container, tag )
|
||||||
headers = response.headers
|
headers = response.headers
|
||||||
response.body
|
response.body
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# send endpoints that the react app handles
|
||||||
|
[
|
||||||
|
'/',
|
||||||
|
'/container',
|
||||||
|
'/container/*',
|
||||||
|
'/login',
|
||||||
|
].each do |route|
|
||||||
|
get route do
|
||||||
|
html :index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Error Handlers
|
# Error Handlers
|
||||||
error do
|
error do
|
||||||
File.read(File.join('public', '500.html'))
|
File.read(File.join('public', '500.html'))
|
||||||
|
|
Loading…
Reference in New Issue