Merge "Add env_file support for docker-compose hook"
This commit is contained in:
commit
eb3b82b7d1
@ -1 +1,16 @@
|
|||||||
A hook which uses 'docker-compose' to deploy containers.
|
A hook which uses `docker-compose` to deploy containers.
|
||||||
|
|
||||||
|
A special input 'env_files' can be used with SoftwareConfig and
|
||||||
|
StructuredConfig for docker-compose `env_file` key(s).
|
||||||
|
|
||||||
|
if env_file keys specified in the `docker-compose.yml`, do not
|
||||||
|
exist in input_values supplied, docker-compose will throw an
|
||||||
|
error, as it can't find these files.
|
||||||
|
|
||||||
|
Also, `-Pf` option can be used to pass env files from client.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
$ heat stack-create test_stack -f example-docker-compose-template.yaml \
|
||||||
|
-Pf env_file_0=./common.env -Pf env_file_1=./apps/web.env \
|
||||||
|
-Pf env_file_2=./test.env -Pf env_file_3=./busybox.env
|
@ -12,6 +12,6 @@ elif [ -f /etc/redhat-release ]; then
|
|||||||
systemctl enable docker.service
|
systemctl enable docker.service
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pip install -U docker-compose
|
pip install -U dpath docker-compose
|
||||||
|
|
||||||
install -D -g root -o root -m 0755 ${SCRIPTDIR}/hook-docker-compose.py /var/lib/heat-config/hooks/docker-compose
|
install -D -g root -o root -m 0755 ${SCRIPTDIR}/hook-docker-compose.py /var/lib/heat-config/hooks/docker-compose
|
@ -12,11 +12,14 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import dpath
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
WORKING_DIR = os.environ.get('HEAT_DOCKER_COMPOSE_WORKING',
|
WORKING_DIR = os.environ.get('HEAT_DOCKER_COMPOSE_WORKING',
|
||||||
@ -31,6 +34,21 @@ def prepare_dir(path):
|
|||||||
os.makedirs(path, 0o700)
|
os.makedirs(path, 0o700)
|
||||||
|
|
||||||
|
|
||||||
|
def write_input_file(file_path, content):
|
||||||
|
prepare_dir(os.path.dirname(file_path))
|
||||||
|
with os.fdopen(os.open(
|
||||||
|
file_path, os.O_CREAT | os.O_WRONLY, 0o600), 'w') as f:
|
||||||
|
f.write(content.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def build_response(deploy_stdout, deploy_stderr, deploy_status_code):
|
||||||
|
return {
|
||||||
|
'deploy_stdout': deploy_stdout,
|
||||||
|
'deploy_stderr': deploy_stderr,
|
||||||
|
'deploy_status_code': deploy_status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
log = logging.getLogger('heat-config')
|
log = logging.getLogger('heat-config')
|
||||||
handler = logging.StreamHandler(sys.stderr)
|
handler = logging.StreamHandler(sys.stderr)
|
||||||
@ -50,25 +68,38 @@ def main(argv=sys.argv):
|
|||||||
stdout, stderr = {}, {}
|
stdout, stderr = {}, {}
|
||||||
|
|
||||||
if input_values.get('deploy_action') == 'DELETE':
|
if input_values.get('deploy_action') == 'DELETE':
|
||||||
response = {
|
json.dump(build_response(stdout, stderr, 0), sys.stdout)
|
||||||
'deploy_stdout': stdout,
|
|
||||||
'deploy_stderr': stderr,
|
|
||||||
'deploy_status_code': 0,
|
|
||||||
}
|
|
||||||
json.dump(response, sys.stdout)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
config = c.get('config', '')
|
config = c.get('config', '')
|
||||||
if not config:
|
if not config:
|
||||||
log.debug("No 'config' input found, nothing to do.")
|
log.debug("No 'config' input found, nothing to do.")
|
||||||
response = {
|
json.dump(build_response(stdout, stderr, 0), sys.stdout)
|
||||||
'deploy_stdout': stdout,
|
|
||||||
'deploy_stderr': stderr,
|
|
||||||
'deploy_status_code': 0,
|
|
||||||
}
|
|
||||||
json.dump(response, sys.stdout)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
#convert config to dict
|
||||||
|
if not isinstance(config, dict):
|
||||||
|
config = ast.literal_eval(json.dumps(yaml.load(config)))
|
||||||
|
|
||||||
|
os.chdir(proj)
|
||||||
|
|
||||||
|
compose_env_files = []
|
||||||
|
for value in dpath.util.values(config, '*/env_file'):
|
||||||
|
if isinstance(value, list):
|
||||||
|
compose_env_files.extend(value)
|
||||||
|
elif isinstance(value, basestring):
|
||||||
|
compose_env_files.extend([value])
|
||||||
|
|
||||||
|
input_env_files = {}
|
||||||
|
if input_values.get('env_files'):
|
||||||
|
input_env_files = dict(
|
||||||
|
(i['file_name'], i['content'])
|
||||||
|
for i in ast.literal_eval(input_values.get('env_files')))
|
||||||
|
|
||||||
|
for file in compose_env_files:
|
||||||
|
if file in input_env_files.keys():
|
||||||
|
write_input_file(file, input_env_files.get(file))
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
DOCKER_COMPOSE_CMD,
|
DOCKER_COMPOSE_CMD,
|
||||||
'up',
|
'up',
|
||||||
@ -78,8 +109,6 @@ def main(argv=sys.argv):
|
|||||||
|
|
||||||
log.debug('Running %s' % cmd)
|
log.debug('Running %s' % cmd)
|
||||||
|
|
||||||
os.chdir(proj)
|
|
||||||
|
|
||||||
subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
subproc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE)
|
stderr=subprocess.PIPE)
|
||||||
stdout, stderr = subproc.communicate()
|
stdout, stderr = subproc.communicate()
|
||||||
@ -92,15 +121,7 @@ def main(argv=sys.argv):
|
|||||||
else:
|
else:
|
||||||
log.debug('Completed %s' % cmd)
|
log.debug('Completed %s' % cmd)
|
||||||
|
|
||||||
response = {}
|
json.dump(build_response(stdout, stderr, subproc.returncode), sys.stdout)
|
||||||
|
|
||||||
response.update({
|
|
||||||
'deploy_stdout': stdout,
|
|
||||||
'deploy_stderr': stderr,
|
|
||||||
'deploy_status_code': subproc.returncode,
|
|
||||||
})
|
|
||||||
|
|
||||||
json.dump(response, sys.stdout)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main(sys.argv))
|
sys.exit(main(sys.argv))
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
busybox:
|
||||||
|
env_file: ./busybox.env
|
||||||
|
image: busybox
|
||||||
|
command: ['nc', '-p', '8080', '-l', '-l', '-e', 'echo', 'hello world!']
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
@ -15,6 +15,15 @@ parameters:
|
|||||||
public_net:
|
public_net:
|
||||||
type: string
|
type: string
|
||||||
default: public
|
default: public
|
||||||
|
env_file_0:
|
||||||
|
type: string
|
||||||
|
env_file_1:
|
||||||
|
type: string
|
||||||
|
env_file_2:
|
||||||
|
type: string
|
||||||
|
env_file_3:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
the_sg:
|
the_sg:
|
||||||
@ -35,16 +44,31 @@ resources:
|
|||||||
type: OS::Heat::StructuredConfig
|
type: OS::Heat::StructuredConfig
|
||||||
properties:
|
properties:
|
||||||
group: docker-compose
|
group: docker-compose
|
||||||
|
inputs:
|
||||||
|
- name: env_files
|
||||||
config:
|
config:
|
||||||
db:
|
db:
|
||||||
image: redis
|
image: redis
|
||||||
|
env_file:
|
||||||
|
- ./common.env
|
||||||
|
- ./apps/web.env
|
||||||
web:
|
web:
|
||||||
image: nginx
|
image: nginx
|
||||||
|
env_file: ./test.env
|
||||||
links:
|
links:
|
||||||
- db
|
- db
|
||||||
ports:
|
ports:
|
||||||
- 80:8000
|
- 80:8000
|
||||||
|
|
||||||
|
other_config:
|
||||||
|
type: OS::Heat::SoftwareConfig
|
||||||
|
properties:
|
||||||
|
group: docker-compose
|
||||||
|
inputs:
|
||||||
|
- name: env_files
|
||||||
|
config:
|
||||||
|
get_file: config-scripts/example-docker-compose.yml
|
||||||
|
|
||||||
deployment:
|
deployment:
|
||||||
type: OS::Heat::StructuredDeployment
|
type: OS::Heat::StructuredDeployment
|
||||||
properties:
|
properties:
|
||||||
@ -53,6 +77,27 @@ resources:
|
|||||||
get_resource: config
|
get_resource: config
|
||||||
server:
|
server:
|
||||||
get_resource: server
|
get_resource: server
|
||||||
|
input_values:
|
||||||
|
env_files:
|
||||||
|
- file_name: ./common.env
|
||||||
|
content: {get_param: env_file_0}
|
||||||
|
- file_name: ./apps/web.env
|
||||||
|
content: {get_param: env_file_1}
|
||||||
|
- file_name: ./test.env
|
||||||
|
content: {get_param: env_file_2}
|
||||||
|
|
||||||
|
other_deployment:
|
||||||
|
type: OS::Heat::SoftwareDeployment
|
||||||
|
properties:
|
||||||
|
name: other_deployment
|
||||||
|
config:
|
||||||
|
get_resource: other_config
|
||||||
|
server:
|
||||||
|
get_resource: server
|
||||||
|
input_values:
|
||||||
|
env_files:
|
||||||
|
- file_name: ./busybox.env
|
||||||
|
content: {get_param: env_file_3}
|
||||||
|
|
||||||
server:
|
server:
|
||||||
type: OS::Nova::Server
|
type: OS::Nova::Server
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
coverage>=3.6
|
coverage>=3.6
|
||||||
discover
|
discover
|
||||||
|
dpath>=1.3.2
|
||||||
fixtures>=0.3.14
|
fixtures>=0.3.14
|
||||||
# Hacking already pins down pep8, pyflakes and flake8
|
# Hacking already pins down pep8, pyflakes and flake8
|
||||||
hacking>=0.8.0,<0.9
|
hacking>=0.8.0,<0.9
|
||||||
|
@ -23,19 +23,47 @@ class HookDockerComposeTest(common.RunScriptTest):
|
|||||||
data = {
|
data = {
|
||||||
"id": "abcdef001",
|
"id": "abcdef001",
|
||||||
"group": "docker-compose",
|
"group": "docker-compose",
|
||||||
"inputs": {},
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "env_files",
|
||||||
|
"value": u'[ { "file_name": "./common.env", '
|
||||||
|
u'"content": "xxxxx" }, '
|
||||||
|
u'{ "file_name": "./test.env", '
|
||||||
|
u'"content": "yyyy" }, '
|
||||||
|
u'{ "file_name": "./test1.env", '
|
||||||
|
u'"content": "zzz" } ]'
|
||||||
|
}
|
||||||
|
],
|
||||||
"config": {
|
"config": {
|
||||||
"web": {
|
"web": {
|
||||||
"image": "nginx",
|
"name": "x",
|
||||||
"links": [
|
"env_file": [
|
||||||
"db"
|
"./common.env",
|
||||||
],
|
"./test.env"
|
||||||
"ports": [
|
|
||||||
"8000:8000"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"db": {
|
"db": {
|
||||||
"image": "redis"
|
"name": "y",
|
||||||
|
"env_file": "./test1.env"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data_without_input = {
|
||||||
|
"id": "abcdef001",
|
||||||
|
"group": "docker-compose",
|
||||||
|
"inputs": [],
|
||||||
|
"config": {
|
||||||
|
"web": {
|
||||||
|
"name": "x",
|
||||||
|
"env_file": [
|
||||||
|
"./common.env",
|
||||||
|
"./test.env"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"db": {
|
||||||
|
"name": "y",
|
||||||
|
"env_file": "./test1.env"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,6 +120,34 @@ class HookDockerComposeTest(common.RunScriptTest):
|
|||||||
],
|
],
|
||||||
state['args'])
|
state['args'])
|
||||||
|
|
||||||
|
def test_hook_without_inputs(self):
|
||||||
|
|
||||||
|
self.env.update({
|
||||||
|
'TEST_RESPONSE': json.dumps({
|
||||||
|
'stdout': '',
|
||||||
|
'stderr': 'env_file_not found...',
|
||||||
|
'returncode': 1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
returncode, stdout, stderr = self.run_cmd(
|
||||||
|
[self.hook_path], self.env, json.dumps(self.data_without_input))
|
||||||
|
|
||||||
|
self.assertEqual({
|
||||||
|
'deploy_stdout': '',
|
||||||
|
'deploy_stderr': 'env_file_not found...',
|
||||||
|
'deploy_status_code': 1
|
||||||
|
}, json.loads(stdout))
|
||||||
|
|
||||||
|
state = self.json_from_file(self.test_state_path)
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
self.fake_tool_path,
|
||||||
|
'up',
|
||||||
|
'-d',
|
||||||
|
'--no-build',
|
||||||
|
],
|
||||||
|
state['args'])
|
||||||
|
|
||||||
def test_hook_failed(self):
|
def test_hook_failed(self):
|
||||||
|
|
||||||
self.env.update({
|
self.env.update({
|
||||||
@ -104,8 +160,6 @@ class HookDockerComposeTest(common.RunScriptTest):
|
|||||||
returncode, stdout, stderr = self.run_cmd(
|
returncode, stdout, stderr = self.run_cmd(
|
||||||
[self.hook_path], self.env, json.dumps(self.data))
|
[self.hook_path], self.env, json.dumps(self.data))
|
||||||
|
|
||||||
self.assertEqual(0, returncode, stderr)
|
|
||||||
|
|
||||||
self.assertEqual({
|
self.assertEqual({
|
||||||
'deploy_stdout': '',
|
'deploy_stdout': '',
|
||||||
'deploy_stderr': 'Error: image library/xxx:latest not found',
|
'deploy_stderr': 'Error: image library/xxx:latest not found',
|
||||||
|
Loading…
Reference in New Issue
Block a user