#!/usr/bin/env python

# docker-build is a script to build docker images from source.
# It will be deprecated once the 'build' feature is incorporated into docker itself.
# (See https://github.com/dotcloud/docker/issues/278)
#
# Author: Solomon Hykes <solomon@dotcloud.com>



# First create a valid Changefile, which defines a sequence of changes to apply to a base image.
# 
#     $ cat Changefile
#     # Start build from a know base image
#     from	base:ubuntu-12.10
#     # Update ubuntu sources
#     run	echo 'deb http://archive.ubuntu.com/ubuntu quantal main universe multiverse' > /etc/apt/sources.list
#     run	apt-get update
#     # Install system packages
#     run	DEBIAN_FRONTEND=noninteractive apt-get install -y -q git
#     run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
#     run DEBIAN_FRONTEND=noninteractive apt-get install -y -q golang
#     # Insert files from the host (./myscript must be present in the current directory)
#     copy	myscript /usr/local/bin/myscript
# 
# 
# Run docker-build, and pass the contents of your Changefile as standard input.
# 
#     $ IMG=$(./docker-build < Changefile)
# 
# This will take a while: for each line of the changefile, docker-build will:
# 
# 1. Create a new container to execute the given command or insert the given file
# 2. Wait for the container to complete execution
# 3. Commit the resulting changes as a new image
# 4. Use the resulting image as the input of the next step


import sys
import subprocess
import json
import hashlib

def docker(args, stdin=None):
	print "# docker " + " ".join(args)
	p = subprocess.Popen(["docker"] + list(args), stdin=stdin, stdout=subprocess.PIPE)
	return p.stdout

def image_exists(img):
	return docker(["inspect", img]).read().strip() != ""

def image_config(img):
	return json.loads(docker(["inspect", img]).read()).get("config", {})

def run_and_commit(img_in, cmd, stdin=None, author=None, run=None):
	run_id = docker(["run"] + (["-i", "-a", "stdin"] if stdin else ["-d"]) + [img_in, "/bin/sh", "-c", cmd], stdin=stdin).read().rstrip()
	print "---> Waiting for " + run_id
	result=int(docker(["wait", run_id]).read().rstrip())
	if result != 0:
		print "!!! '{}' return non-zero exit code '{}'. Aborting.".format(cmd, result)
		sys.exit(1)
	return docker(["commit"] + (["-author", author] if author else []) + (["-run", json.dumps(run)] if run is not None else []) + [run_id]).read().rstrip()

def insert(base, src, dst, author=None):
	print "COPY {} to {} in {}".format(src, dst, base)
	if dst == "":
		raise Exception("Missing destination path")
	stdin = file(src)
	stdin.seek(0)
	return run_and_commit(base, "cat > {0}; chmod +x {0}".format(dst), stdin=stdin, author=author)

def add(base, src, dst, author=None):
	print "PUSH to {} in {}".format(dst, base)
	if src == ".":
		tar = subprocess.Popen(["tar", "-c", "."], stdout=subprocess.PIPE).stdout
	else:
		tar = subprocess.Popen(["curl", src], stdout=subprocess.PIPE).stdout
	if dst == "":
		raise Exception("Missing argument to push")
	return run_and_commit(base, "mkdir -p '{0}' && tar -C '{0}' -x".format(dst), stdin=tar, author=author)

def main():
	base=""
	maintainer=""
	steps = []
	try:
		for line in sys.stdin.readlines():
			line = line.strip()
			# Skip comments and empty lines
			if line == "" or line[0] == "#":
				continue
			op, param = line.split("	", 1)
			print op.upper() + " " + param
			if op == "from":
				base = param
				steps.append(base)
			elif op == "maintainer":
				maintainer = param
			elif op == "run":
				result = run_and_commit(base, param, author=maintainer)
				steps.append(result)
				base = result
				print "===> " + base
			elif op == "copy":
				src, dst = param.split("	", 1)
				result = insert(base, src, dst, author=maintainer)
				steps.append(result)
				base = result
				print "===> " + base
			elif op == "add":
				src, dst = param.split("	", 1)
				result = add(base, src, dst, author=maintainer)
				steps.append(result)
				base=result
				print "===> " + base
			elif op == "expose":
				config = image_config(base)
				if config.get("PortSpecs") is None:
					config["PortSpecs"] = []
				portspec = param.strip()
				config["PortSpecs"].append(portspec)
				result = run_and_commit(base, "# (nop) expose port {}".format(portspec), author=maintainer, run=config)
				steps.append(result)
				base=result
				print "===> " + base
			elif op == "cmd":
				config  = image_config(base)
				cmd = list(json.loads(param))
				config["Cmd"] = cmd
				result = run_and_commit(base, "# (nop) set default command to '{}'".format(" ".join(cmd)), author=maintainer, run=config)
				steps.append(result)
				base=result
				print "===> " + base
			else:
				print "Skipping uknown op " + op
	except:
		docker(["rmi"] + steps[1:])
		raise
	print base

if __name__ == "__main__":
	main()
