Skip to content

Commit

Permalink
Merge 39cbe5f into 017c9bf
Browse files Browse the repository at this point in the history
  • Loading branch information
cameronmaske committed May 30, 2014
2 parents 017c9bf + 39cbe5f commit 703c7be
Show file tree
Hide file tree
Showing 23 changed files with 612 additions and 59 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ __pycache__/

*.skippercfg
/dist
NOTES.md
NOTES.md
.coverage
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Let's run through an example deployment of a simple flask app.
skipper.yml
```
name: demo
instances:
groups:
web:
size: m1.small
loadbalance:
Expand All @@ -29,7 +29,7 @@ services:
- "80:5000"
scale: 2
test: "python manage.py tests"
registry:
repo:
name: cameronmaske/flask-web
```

Expand Down
5 changes: 3 additions & 2 deletions example/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from flask import Flask
app = Flask(__name__)


@app.route("/")
def hello():
return "Hello World! v2"
return "Hello World! v10"

if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)
app.run(host='0.0.0.0', port=5000)
24 changes: 12 additions & 12 deletions example/skipper.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
name: demo
services:
web:
build: .
repo:
name: cameronmaske/flask-web
groups:
demo:
size: medium
web:
size: t1.m
loadbalance:
- "80:80"
instances: 1
scale: 2
regions:
- us-east-1
- us-west-1
services:
web:
build: .
loadbalance:
- "80:8000"
- redis
redis:
image: dockerfiles/redis
loadbalance:
- "1234:1234"
- web
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pytest
pytest-cov
python-coveralls
mock==1.0.1
mock==1.0.1
HTTPretty
17 changes: 15 additions & 2 deletions skipper/aws/host.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
class Host(object):
from skipper.hosts import BaseHost


class Host(BaseHost):
requirements = {
'field': 'AWS',
'message': """As this is your first time running skipper, we need to store your some AWS Security Credentials.
Please visit https://console.aws.amazon.com/iam/home?#security_credential
Under Access Keys, click Create New Access Key.""",
'keys': {
'ACCESS_KEY': "Enter your Access Key ID",
'SECRET_KEY': "Enter your Secret Access Key",
}
}

def __init__(self, creds=None):
self.creds = creds


host = Host()
43 changes: 43 additions & 0 deletions skipper/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import requests
import docker
import os

from logger import capture_events, EventError


class RepoNoPermission(Exception):
pass


class RepoNotFound(Exception):
pass


class Repo(object):
def __init__(self, name, registry="index.docker.io", tag=None):
self.name = name
self.registry = registry
self.tag = tag

def get_tags(self):
"""
Attempts to retrieve all the tags associated with a repo.
"""
r = requests.get("https://%s/v1/repositories/%s/tags" % (self.registry, self.name))
if r.status_code == 404:
raise RepoNotFound("No such repo %s (on %s)" % (self.name, self.registry))
return r.json()

def upload(self, image_id, tag):
"""
Uploads a tagged version to the repo.
"""
client = docker.Client(os.environ.get('DOCKER_HOST'))
client.tag(image_id, repository=self.name, tag=tag)
output = client.push(self.name, stream=True)
try:
capture_events(output)
except EventError as e:
if '401' in e.message:
raise RepoNoPermission('You currently do not have access to %s.\nPlease try logging in with `docker login`.' % self.name)

62 changes: 48 additions & 14 deletions skipper/cli.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,63 @@
import click
from click.exceptions import FileError, ClickException

from project import Project
from project import Project, NoSuchService
from hosts import get_host
from creds import get_creds
from creds import creds
from conf import get_conf
from logger import log
from builder import RepoNoPermission


class CLIProject(Project):
exception = ClickException

def __init__(self):
try:
self.conf = get_conf()
except IOError as e:
raise FileError(e.filename, hint='Are you in the right directory?')

try:
self.name = self.conf['name']
except KeyError:
raise ClickException('No name in skipper.yml')

self.services = []
for name, details in self.conf['services'].items():
try:
self.services.append(
self.make_service(name=name, **details))
except TypeError as e:
raise ClickException("%s: %s" % (name, e.message))

self.host = get_host(self.conf.get('host', 'aws'))
self.host.creds = creds


pass_config = click.make_pass_decorator(CLIProject, ensure=True)


@click.group()
def cli():
@click.option('--silent', is_flag=True)
@pass_config
def cli(project, silent):
"""
Doc string describing the CLI at a glance.
"""
log.propagate = silent


@cli.command()
@click.argument('groups', nargs=-1, required=False)
def deploy(groups):
@click.argument('services', nargs=-1, required=False)
@pass_config
def build(project, services):
"""
Doc string describing the deploy.
Build and upload service(s).
"""
creds = get_creds()
conf = get_conf()
host = get_host(conf.get('host', 'aws'))
host.creds = creds
project = Project(name=conf['name'], host=host)
for group in conf['groups']:
project.make_group(group)
raise NotImplementedError("Deploy is still a work in progress.")
try:
services = project.filter_services(services)
for service in services:
service.push()
except (RepoNoPermission, NoSuchService) as e:
raise ClickException(e.message)
12 changes: 12 additions & 0 deletions skipper/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import os
import docker


def _docker():
"""
Cached Docker client.
Approach taken from https://github.com/andreasjansson/head-in-the-clouds/blob/master/headintheclouds/ec2.py#L327
"""
if not hasattr(_docker, 'client'):
_docker.client = docker.Client(os.environ.get('DOCKER_HOST'))
return _docker.client
32 changes: 15 additions & 17 deletions skipper/creds.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import json
import click

from utils import contains_keys

import os

class NestedDict(dict):
"""
Expand Down Expand Up @@ -41,32 +38,33 @@ def __delitem__(self, *args, **kwargs):
super(Creds, self).__delitem__(*args, **kwargs)
self.storage.save(self)

def save(self):
self.storage.save(self)


class JSONStorage(object):
"""
Stores a config in a json format in a .skippercfg file.
Stores a creds in a json format in a .skippercfg file.
"""
def __init__(self, path=None):
if path:
self.path = path
else:
self.path = os.getcwd()

def retrieve(self):
try:
with open('.skippercfg', 'r') as f:
with open('%s/.skippercfg' % self.path, 'r') as f:
try:
return json.load(f)
except ValueError:
return {}
except IOError:
return {}

def save(self, config):
with open('.skippercfg', 'w+') as f:
f.write(json.dumps(config, indent=2))
def save(self, creds):
with open('%s/.skippercfg' % self.path, 'w+') as f:
f.write(json.dumps(creds, indent=2))


creds = Creds(storage=JSONStorage())


def get_creds(requirements):
if not contains_keys(requirements['keys'], creds):
click.utils.echo(requirements['message'])
for key, message in requirements['keys'].items():
creds[key] = click.prompt(message)
return creds
14 changes: 14 additions & 0 deletions skipper/hosts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import click
from utils import contains_keys


def get_host(host):
"""
Based on the host agrumement passed in, determine which host to pass back.
"""
# WIP: Only aws for now!
from skipper.aws.host import host
return host


class BaseHost(object):
def check_requirements(self):
field = self.requirements['field']
if not contains_keys(self.requirements['keys'], self.creds[field]):
click.utils.echo(self.requirements['message'])
for key, message in self.requirements['keys'].items():
self.creds[field][key] = click.prompt(message)
self.creds.save()
8 changes: 8 additions & 0 deletions skipper/loadbalancer/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def parse_port(ports_string):
"""
Turns "80:80" -> {80: 80}
"""
guest, host = ports_string.split(':')
ports = {}
ports[int(guest)] = int(host)
return ports
34 changes: 34 additions & 0 deletions skipper/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import logging
import json

console = logging.StreamHandler()

log = logging.getLogger('skipper')
log.setLevel(logging.INFO)
log.addHandler(console)
log.propagate = False


class EventError(Exception):
pass


def capture_events(stream):
"""
Prints out the output from various Docker client commands.
A stripped down version of Fig's `stream_output`
"""
events = []
for line in stream:
try:
line = json.loads(line)
if line.get('stream'):
log.info(line['stream'].replace('\n', ''))
events.append(line['stream'])
elif line.get('status'):
log.info(line['status'])
elif line.get('errorDetail'):
raise EventError(line.get('errorDetail').get('message'))
except ValueError as e:
log.debug(e)
return events
Loading

0 comments on commit 703c7be

Please sign in to comment.