Skip to content

Commit

Permalink
Merge pull request #8 from sodre/asyncio
Browse files Browse the repository at this point in the history
Use asynchronous IO for Terraform Calls
  • Loading branch information
sodre committed Apr 14, 2019
2 parents 6b93d49 + a04ad49 commit f20cafd
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ install:
- conda config --set always_yes yes
- conda config --append channels conda-forge
- conda update -q conda
- conda install conda-build anaconda-client pytest pytest-cov
- conda install conda-build anaconda-client pytest pytest-cov pytest-asyncio
- conda config --set auto_update_conda no
- conda build conda.recipe --no-test
- conda install --use-local terraformspawner
Expand Down
1 change: 1 addition & 0 deletions conda.recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ test:
requires:
- pytest
- pytest-cov
- pytest-asyncio
commands:
- pytest -v --color=yes tests

Expand Down
39 changes: 20 additions & 19 deletions terraformspawner/terraformspawner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from subprocess import check_call, check_output
from asyncio.subprocess import create_subprocess_exec
from subprocess import check_output, CalledProcessError

from jinja2 import Environment, PackageLoader
from jupyterhub.spawner import Spawner
Expand Down Expand Up @@ -50,15 +51,15 @@ def start(self):
self._write_tf_module()

# (Re)Initialize Terraform
self.tf_init()
yield self.tf_check_call('init')

# Terraform Apply (locally)
self.tf_apply()
yield self.tf_apply()

(ip, port) = (None, None)
while ip is None or port is None:
# Terraform Refresh (globally)
self.tf_refresh()
yield self.tf_check_call('refresh')
try:
ip = self.tf_output('ip')
port = int(self.tf_output('port'))
Expand All @@ -75,8 +76,7 @@ def stop(self, now=False):
module_file = self.get_module_file()

if os.path.exists(module_file):
check_call([self.tf_bin, 'destroy', '-auto-approve',
'-target', 'module.%s' % module_id], cwd=self.tf_dir)
self.tf_destroy()
os.remove(module_file)

@gen.coroutine
Expand All @@ -89,7 +89,7 @@ def poll(self):
if not os.path.exists(module_file):
return 0

self.tf_refresh()
yield self.tf_check_call('refresh')

# Get state from terraform
state = self.tf_output('state')
Expand Down Expand Up @@ -120,21 +120,22 @@ def _build_tf_module(self):
tf_template = self.tf_jinja_env.get_or_select_template('single_user.tf')
return tf_template.render(spawner=self)

@gen.coroutine
def tf_check_call(self, *args, **kwargs):
proc = yield create_subprocess_exec(self.tf_bin, *args, **kwargs, cwd=self.tf_dir)
yield proc.wait()
if proc.returncode != 0:
raise CalledProcessError(proc.returncode, self.tf_bin)

@gen.coroutine
def tf_apply(self):
module_id = self.get_module_id()
check_call([self.tf_bin, 'apply', '-auto-approve',
'-target', 'module.%s'%module_id], cwd=self.tf_dir)

def tf_refresh(self):
check_call([self.tf_bin, 'refresh' ], cwd=self.tf_dir)

def tf_init(self):
"""
(Re)Initialize Terraform
yield self.tf_check_call('apply', '-auto-approve', '-target', 'module.%s' % module_id)

:return:
"""
check_call([self.tf_bin, 'init'], cwd=self.tf_dir)
@gen.coroutine
def tf_destroy(self):
module_id = self.get_module_id()
yield self.tf_check_call('destroy', '-auto-approve', '-target', 'module.%s' % module_id)

def tf_output(self, variable):
"""
Expand Down
32 changes: 18 additions & 14 deletions tests/test_spawner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
from subprocess import CalledProcessError
from unittest.mock import Mock

import pytest
Expand Down Expand Up @@ -50,36 +51,39 @@ def test__write_tf_module(spawner):
with open(spawner.get_module_file()) as f:
assert tf_module == f.read()

@pytest.mark.asyncio
def test_tf_check_call(spawner):
yield from spawner.tf_check_call('-help')

def test_start(spawner):
f = spawner.start()
with pytest.raises(CalledProcessError):
yield from spawner.tf_check_call('does-not-exist')

assert f.done()
assert os.path.exists(spawner.get_module_file())

ip, port = f.result()
@pytest.mark.asyncio
def test_start(spawner):
# noinspection PyTupleAssignmentBalance
(ip, port) = yield from spawner.start()
assert os.path.exists(spawner.get_module_file())
assert port == 8888
assert ip == '127.0.0.1'


@pytest.mark.asyncio
def test_stop(spawner):
spawner.start().result()

assert os.path.exists(spawner.get_module_file())

spawner.stop()
yield from spawner.start()

yield from spawner.stop()
assert not os.path.exists(spawner.get_module_file())


@pytest.mark.asyncio
def test_poll(spawner):
# If poll is called before start, the state is unknown
state = spawner.poll().result()
state = yield from spawner.poll()
assert state == 0

# Now we actually start it
spawner.start().result()

state = spawner.poll().result()
yield from spawner.start()

state = yield from spawner.poll()
assert state is None

0 comments on commit f20cafd

Please sign in to comment.