Commit 8bd86365 authored by Pietro Albini's avatar Pietro Albini

Use a new directory structure for the root directory

This new structure allows parallel builds, is versioned, and also is
cleaner, with every branch in their own directory.
parent df271e89
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import pathlib
import os import os
import sys import sys
import json import json
...@@ -28,6 +29,9 @@ from . import app ...@@ -28,6 +29,9 @@ from . import app
from . import utils from . import utils
DEFAULT_GIT_URL = "http://code.ubuntu-it.org/ubuntu-it-web/www.git"
def error(message, *format): def error(message, *format):
"""Show an error message""" """Show an error message"""
click.echo("managetests: error: {}".format(message.format(*format))) click.echo("managetests: error: {}".format(message.format(*format)))
...@@ -56,25 +60,28 @@ def cli(ctx, root): ...@@ -56,25 +60,28 @@ def cli(ctx, root):
if root is None or not (skip or utils.is_root_valid(root)): if root is None or not (skip or utils.is_root_valid(root)):
error("please provide a valid root path") error("please provide a valid root path")
else: else:
ctx.obj["root"] = os.path.abspath(root) ctx.obj["root"] = pathlib.Path(root)
@cli.command("init") @cli.command("init")
@click.argument("git-url") @click.argument("git-url", default=DEFAULT_GIT_URL)
@click.pass_context @click.pass_context
def init_command(ctx, git_url): def init_command(ctx, git_url):
"""Init a new root directory""" """Init a new root directory"""
root = ctx.obj["root"] root = ctx.obj["root"]
# Check if the directory is empty # Check if the directory is empty
if os.path.exists(root) and os.listdir(root): if root.exists() and len(list(root.iterdir())):
error("root directory not empty") error("root directory not empty")
os.makedirs(root, exist_ok=True) root.mkdir(parents=True)
for dir in ("git", "envs", "caches", "public", "socks", "socks/branches"): for dir in ("git", "branches"):
os.mkdir(os.path.join(root, dir)) (root / dir).mkdir()
with (root / "version").open("w") as f:
f.write("%s\n" % utils.ROOT_DIR_VERSION)
with open(os.path.join(root, "config.json"), "w") as f: with (root / "config.json").open("w") as f:
content = { content = {
"gitlab-url": "http://code.ubuntu-it.org", "gitlab-url": "http://code.ubuntu-it.org",
"gitlab-project": "ubuntu-it-web/www", "gitlab-project": "ubuntu-it-web/www",
...@@ -87,14 +94,14 @@ def init_command(ctx, git_url): ...@@ -87,14 +94,14 @@ def init_command(ctx, git_url):
json.dump(content, f, indent=4) json.dump(content, f, indent=4)
f.write("\n") f.write("\n")
with open(os.path.join(root, "details.json"), "w") as f: with (root / "details.json").open("w") as f:
content = { content = {
"branches": {}, "branches": {},
} }
json.dump(content, f, indent=4) json.dump(content, f, indent=4)
f.write("\n") f.write("\n")
subprocess.call(["git", "clone", git_url, os.path.join(root, "git")]) subprocess.call(["git", "clone", git_url, str(root / "git"), "--bare"])
@cli.command("run") @cli.command("run")
...@@ -103,9 +110,9 @@ def init_command(ctx, git_url): ...@@ -103,9 +110,9 @@ def init_command(ctx, git_url):
@click.pass_obj @click.pass_obj
def run_command(obj, port): def run_command(obj, port):
"""Run managetests""" """Run managetests"""
root = obj["root"] root = obj["root"].resolve()
with open(os.path.join(root, "config.json")) as f: with (root / "config.json").open() as f:
config = json.load(f) config = json.load(f)
inst = app.TestsManager(root, config, port) inst = app.TestsManager(root, config, port)
......
...@@ -14,8 +14,8 @@ ...@@ -14,8 +14,8 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import json import json
import pathlib
import pkg_resources import pkg_resources
...@@ -31,7 +31,7 @@ class TestsManager: ...@@ -31,7 +31,7 @@ class TestsManager:
def __init__(self, root, config, port): def __init__(self, root, config, port):
self.config = config self.config = config
self.root = os.path.abspath(root) self.root = root
self.gitlab = gitlab.GitLabAPI(self) self.gitlab = gitlab.GitLabAPI(self)
...@@ -43,12 +43,7 @@ class TestsManager: ...@@ -43,12 +43,7 @@ class TestsManager:
if not utils.is_root_valid(root): if not utils.is_root_valid(root):
raise RuntimeError("Invalid root directory: %s" % root) raise RuntimeError("Invalid root directory: %s" % root)
# Shortcut for creating multiple attributes self.config_file = root / "config.json"
dirs = ["git", "envs", "caches", "public", "socks", "build"]
for dir in dirs:
setattr(self, "%s_dir" % dir, os.path.join(root, dir))
self.config_file = os.path.join(root, "config.json")
self.gunicorn_config_file = pkg_resources.resource_filename( self.gunicorn_config_file = pkg_resources.resource_filename(
"managetests", "gunicorn_config" "managetests", "gunicorn_config"
) )
...@@ -58,12 +53,14 @@ class TestsManager: ...@@ -58,12 +53,14 @@ class TestsManager:
def _load_details(self): def _load_details(self):
"""Load details from the root directory""" """Load details from the root directory"""
with open(os.path.join(self.root, "details.json")) as f: with (self.root / "details.json").open() as f:
self.details = json.load(f) self.details = json.load(f)
def _load_branches(self): def _load_branches(self):
"""Load all the branches""" """Load all the branches"""
self.branches = {} self.branches = {}
# Load already present branches from merge requests
for branch_name, mr in self.details["branches"].copy().items(): for branch_name, mr in self.details["branches"].copy().items():
self.load_branch(branch_name, mr) self.load_branch(branch_name, mr)
...@@ -117,7 +114,7 @@ class TestsManager: ...@@ -117,7 +114,7 @@ class TestsManager:
def save_details(self): def save_details(self):
"""Save details on disk""" """Save details on disk"""
with open(os.path.join(self.root, "details.json"), "w") as f: with (self.root / "details.json").open("w") as f:
json.dump(self.details, f) json.dump(self.details, f)
def run(self): def run(self):
......
...@@ -20,6 +20,9 @@ import os ...@@ -20,6 +20,9 @@ import os
import shutil import shutil
BRANCH_DIR_VERSION = "1"
class Branch: class Branch:
"""Representation of a branch""" """Representation of a branch"""
...@@ -66,49 +69,103 @@ class Branch: ...@@ -66,49 +69,103 @@ class Branch:
def check_local_status(self): def check_local_status(self):
"""Check if the branch is present locally""" """Check if the branch is present locally"""
present_in = ["envs", "caches", "public"] if (self.manager.root / "branches" / self.name).exists():
for one in present_in: self.present = self.valid()
path = os.path.join(self.manager.root, one, self.name) else:
if not os.path.exists(path): self.present = False
self.present = False
return def valid(self):
self.present = True """Check if a branch is valid"""
root = self.manager.root / "branches" / self.name
# The branch must be a valid directory
if not (root.exists() and root.is_dir()):
return False
# All the subdirectories must be present
dirs = ["env", "public", "data"]
for dir in dirs:
dir = root / dir
if not (dir.exists() and dir.is_dir()):
return False
# All the files must be be present
files = ["version"]
for file in files:
file = root / file
if not (file.exists() and file.is_file()):
return False
# The directory version must be correct
with (root / "version").open() as f:
if f.read().strip() != BRANCH_DIR_VERSION:
return False
return True
def deploy(self): def deploy(self):
"""Deploy the branch""" """Deploy the branch"""
self.check_local_status() self.check_local_status()
# Ok, branch already deployed # Ok, branch already deployed
if self.present: if self.present:
return return
make_dirs_in = ["envs", "caches", "public"] # Start from a fresh branch dir
for dir in make_dirs_in: branch = self.manager.root / "branches" / self.name
os.mkdir(os.path.join(self.manager.root, dir, self.name)) if branch.exists():
shutil.rmtree(str(branch))
commands = [ branch.mkdir()
"rm -rf %s" % self.manager.build_dir,
"mkdir -p %s" % self.manager.build_dir, git_dir = str(self.manager.root / "git")
"cd %s && git fetch" % self.manager.git_dir, build_dir = branch / "build"
"git --git-dir=%s/.git --work-tree=%s checkout -f origin/%s" %
(self.manager.git_dir, self.manager.build_dir, self.name), # Create subdirectories
"cd %s && invoke build" % self.manager.build_dir, subdirs = ["env", "public", "data", "build"]
"virtualenv -p python3 %s/%s" % (self.manager.envs_dir, self.name), for dir in subdirs:
"%s/%s/bin/pip install %s/build/packages/*.whl" % (branch / dir).mkdir()
(self.manager.envs_dir, self.name, self.manager.build_dir),
("ln -s %s/%s/lib/python3.%s/site-packages/uitwww/static "
"%s/%s/static" % (self.manager.envs_dir, self.name,
sys.version_info[1], self.manager.public_dir, self.name)),
"rm -rf %s" % self.manager.build_dir,
"rm -rf %s/%s/*" % (self.manager.caches_dir, self.name),
]
# Provide to the command the current environment plus $HOME
# gulp fails otherwise
env = dict(os.environ)
env["HOME"] = self.manager.build_dir
for command in commands: # Create the version file
subprocess.call(command, shell=True, env=env) with (branch / "version").open("w") as f:
f.write("%s\n" % BRANCH_DIR_VERSION)
# Update the bare git repository and checkout the branch
subprocess.call(["git", "--git-dir", git_dir, "fetch", "origin"])
subprocess.call([
"git", "--git-dir", git_dir, "--work-tree", str(build_dir),
"checkout", "-f", self.name,
])
# Build the website
env = dict(os.environ)
env["HOME"] = str(build_dir)
subprocess.call(["invoke", "build"], cwd=str(build_dir), env=env)
# Check if the build results exists
results = list(build_dir.glob("build/packages/*.whl"))
if not results:
return False
# Create a new environment and install all the results in it
subprocess.call(["virtualenv", "-p", "python3", str(branch / "env")])
for result in results:
subprocess.call([
"%s/bin/pip" % str(branch / "env"), "install", str(result)
])
# Calculate the static files directory
py_version = ".".join(str(i) for i in tuple(sys.version_info)[:2])
static_dir = (
branch / "env" / "lib" / ("python%s" % py_version) /
"site-packages" / "uitwww" / "static"
)
# Link the assets directory to the static files one
assets = branch / "public" / "+assets"
assets.symlink_to(static_dir)
# Delete the build directory
shutil.rmtree(str(build_dir))
self.manager.details["branches"][self.name] = self.mr self.manager.details["branches"][self.name] = self.mr
self.manager.save_details() self.manager.save_details()
...@@ -116,13 +173,12 @@ class Branch: ...@@ -116,13 +173,12 @@ class Branch:
def destroy(self): def destroy(self):
"""Destroy the local copy""" """Destroy the local copy"""
self.check_local_status() self.check_local_status()
# Ok, branch not present # Ok, branch not present
if not self.present: if not self.present:
return return
remove_from = ["envs", "caches", "public"] shutil.rmtree(str(self.manager.root_dir / "branches" / name))
for dir in remove_from:
shutil.rmtree(os.path.join(self.manager.root, dir, self.name))
try: try:
del self.manager.details["branches"][self.name] del self.manager.details["branches"][self.name]
......
# A program which manages Ubuntu-it's web test server # A program which manages Ubuntu-it's web test server
# Copyright (C) 2015 Pietro Albini <pietroalbini@ubuntu.com> # Copyright (C) 2015-2016 Pietro Albini <pietroalbini@ubuntu.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published # it under the terms of the GNU Affero General Public License as published
...@@ -21,13 +21,11 @@ import os ...@@ -21,13 +21,11 @@ import os
if not hasattr(sys, "real_prefix"): if not hasattr(sys, "real_prefix"):
raise RuntimeError("You must use this inside a virtualenv") raise RuntimeError("You must use this inside a virtualenv")
_socks_path = os.path.realpath(os.path.join(sys.prefix, _branch_dir = os.path.realpath(os.path.join(sys.prefix, ".."))
"../../socks/branches"))
_branch_name = sys.prefix.rsplit("/", 1)[-1]
bind = "unix:%s/%s.sock" % (_socks_path, _branch_name) bind = "unix:%s/branch.sock" % (_branch_dir)
workers = 2 workers = 2
del sys, os, _socks_path, _branch_name del sys, os, _branch_dir
...@@ -79,10 +79,13 @@ class InstancesManager: ...@@ -79,10 +79,13 @@ class InstancesManager:
def _run_branch(self, branch): def _run_branch(self, branch):
"""Run a single branch""" """Run a single branch"""
branch_dir = self.manager.root / "branches" / branch.name
command_path = str(branch_dir / "env" / "bin" / "uitwww")
command = [ command = [
"%s/%s/bin/uitwww" % (self.manager.envs_dir, branch.name), command_path, "run", "-g",
"run", "-g", "%s.py" % self.manager.gunicorn_config_file, "%s.py" % self.manager.gunicorn_config_file,
"%s/%s" % (self.manager.caches_dir, branch.name), str(branch_dir / "data")
] ]
try: try:
process = subprocess.Popen(command) process = subprocess.Popen(command)
......
...@@ -14,27 +14,34 @@ ...@@ -14,27 +14,34 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import pathlib
ROOT_DIR_VERSION = "1"
def is_root_valid(path): def is_root_valid(path):
"""Check if a root directory is valid""" """Check if a root directory is valid"""
# Eh... path = pathlib.Path(path)
if not os.path.exists(path):
if not (path.exists() and path.is_dir()):
return False return False
required_dirs = ["git", "envs", "caches", "public", "socks", required_dirs = ["git", "branches"]
"socks/branches"] required_files = ["config.json", "details.json", "version"]
required_files = ["config.json", "details.json"]
base = os.path.abspath(path)
for dir in required_dirs: for dir in required_dirs:
dir_path = os.path.join(base, dir) dir = path / dir
if not (os.path.exists(dir_path) and os.path.isdir(dir_path)): if not (dir.exists() and dir.is_dir()):
return False return False
for file in required_files: for file in required_files:
file_path = os.path.join(base, file) file = path / file
if not (os.path.exists(file_path) and os.path.isfile(file_path)): if not (file.exists() and file.is_file()):
return False
with (path / "version").open("r") as f:
if f.read().strip() != ROOT_DIR_VERSION:
return False return False
return True return True
...@@ -17,13 +17,13 @@ server { ...@@ -17,13 +17,13 @@ server {
location ~ ^/([a-zA-Z0-9\-\._]+)/ { location ~ ^/([a-zA-Z0-9\-\._]+)/ {
set $branch $1; set $branch $1;
root $managetests_root/caches; alias $managetests_root/branches/$branch/data/cache;
try_files $request_uri.html $request_uri/index.html $request_uri try_files $request_uri.html $request_uri/index.html $request_uri
@branch; @branch;
} }
location @branch { location @branch {
proxy_pass http://unix:$managetests_root/socks/branches/$branch.sock; proxy_pass http://unix:$managetests_root/branches/$branch/branch.sock;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme; proxy_set_header X-Scheme $scheme;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment