From 10860fcf3f794a5c6aec49436b740ca8704e671e Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Fri, 18 Mar 2016 16:44:55 -0700 Subject: [PATCH 01/10] Service `alias` reference update when a service is defined with export and deploy in the freight-forwarder.yaml, it will reference the the 'service._alias' which is a string type vs the unicode type referenced in the key. This adds another key to the service dict and doesn't update the correct service. --- freight_forwarder/freight_forwarder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freight_forwarder/freight_forwarder.py b/freight_forwarder/freight_forwarder.py index 8a9dbbb..2fadd27 100644 --- a/freight_forwarder/freight_forwarder.py +++ b/freight_forwarder/freight_forwarder.py @@ -83,7 +83,7 @@ def commercial_invoice(self, action, data_center, environment, transport_service # if we're exporting we need to use other services deploy definitions to avoid issues if action == 'export': services = self.__get_services('deploy', data_center, environment) - services[transport_service.alias] = transport_service + services[transport_service.name] = transport_service else: services = self.__get_services(action, data_center, environment) @@ -628,7 +628,6 @@ def __complete_distribution(self, commercial_invoice): def __service_deployment_validation(self, service, validated=[]): if not service: raise ValueError("service_deployment_validation requires a service") - if service.name in validated: return True From b3d38d3d4d54ebbb50e10d92ee1a7657797281ed Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Tue, 22 Mar 2016 03:36:15 -0700 Subject: [PATCH 02/10] Deploy Hosts Bug * Updated `freight_forwarder` to reflect variables needed for CommercialInvoice * Updated commercial invoice to reflect usage of keys in container_ships that are getting populated from the config object --- .../commercial_invoice/commercial_invoice.py | 15 ++++++++------- freight_forwarder/freight_forwarder.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/freight_forwarder/commercial_invoice/commercial_invoice.py b/freight_forwarder/commercial_invoice/commercial_invoice.py index 423af3b..c76c545 100644 --- a/freight_forwarder/commercial_invoice/commercial_invoice.py +++ b/freight_forwarder/commercial_invoice/commercial_invoice.py @@ -191,29 +191,30 @@ def _create_container_ships(self, hosts): default_container_ship = self._create_container_ship(None) container_ships['default'] = {default_container_ship.url.geturl(): default_container_ship} - for alias, hosts in six.iteritems(hosts): + for key, hosts in six.iteritems(hosts): if hosts is None: - container_ships[alias] = hosts + container_ships[key] = hosts elif isinstance(hosts, list): - container_ships[alias] = {} + key = hosts.alias + container_ships[key] = {} for host in hosts: if not host or not isinstance(host, dict): - raise ValueError("hosts: {0} is required to be a dict.".format(alias)) + raise ValueError("hosts: {0} is required to be a dict.".format(key)) existing_container_ship = None for container_ship_dict in six.itervalues(container_ships): for address, container_ship in six.iteritems(container_ship_dict): - if address == host.get('address') and address not in container_ships[alias]: + if address == host.get('address') and address not in container_ships[key]: existing_container_ship = container_ship break if existing_container_ship is None: - container_ships[alias][host.get('address')] = self._create_container_ship(host) + container_ships[key][host.get('address')] = self._create_container_ship(host) else: - container_ships[alias][host.get('address')] = existing_container_ship + container_ships[key][host.get('address')] = existing_container_ship else: raise ValueError(logger.error("hosts is required to be a list or None. host: {0}".format(hosts))) diff --git a/freight_forwarder/freight_forwarder.py b/freight_forwarder/freight_forwarder.py index 2fadd27..2ddbcf7 100644 --- a/freight_forwarder/freight_forwarder.py +++ b/freight_forwarder/freight_forwarder.py @@ -83,17 +83,17 @@ def commercial_invoice(self, action, data_center, environment, transport_service # if we're exporting we need to use other services deploy definitions to avoid issues if action == 'export': services = self.__get_services('deploy', data_center, environment) - services[transport_service.name] = transport_service + services[transport_service.alias] = transport_service else: services = self.__get_services(action, data_center, environment) return CommercialInvoice( - self.team, - self.project, - services, - self._config.get('hosts', 'environments', environment.name, data_center.name, action), - transport_service.alias, - action, + team=self.team, + project=self.project, + services=services, + hosts=self._config.get('hosts', 'environments', environment.name, data_center.name, action), + transport_service=transport_service.alias, + transport_method=action, data_center=data_center.alias, environment=environment.alias, registries=self._config.get('registries'), @@ -369,7 +369,7 @@ def __assemble_fleet(self, commercial_invoice): if commercial_invoice.transport_method == 'export': host_alias = commercial_invoice.transport_method else: - host_alias = commercial_invoice.transport_service.name + host_alias = commercial_invoice._transport_service fleet = commercial_invoice.container_ships.get( host_alias, From 763dbd1897faa52db2d0f51fb3256a462b3e7b57 Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Tue, 22 Mar 2016 03:56:10 -0700 Subject: [PATCH 03/10] Changed reference for export operations * changed reference to transport_service to reflect name loaded from config dictionary --- freight_forwarder/freight_forwarder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freight_forwarder/freight_forwarder.py b/freight_forwarder/freight_forwarder.py index 2ddbcf7..3fcc804 100644 --- a/freight_forwarder/freight_forwarder.py +++ b/freight_forwarder/freight_forwarder.py @@ -83,7 +83,7 @@ def commercial_invoice(self, action, data_center, environment, transport_service # if we're exporting we need to use other services deploy definitions to avoid issues if action == 'export': services = self.__get_services('deploy', data_center, environment) - services[transport_service.alias] = transport_service + services[transport_service.name] = transport_service else: services = self.__get_services(action, data_center, environment) From d5f8efe7a70c62b0d7c2f71ca34bd987a6f11388 Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Thu, 24 Mar 2016 02:16:36 -0700 Subject: [PATCH 04/10] Tests Update Added tests for the following bugs * #16 * #17 --- freight_forwarder/freight_forwarder.py | 4 +- tests/fixtures/test_freight_forwarder.yaml | 57 +++++++++++++++++++ tests/unit/commercial_invoice_test.py | 65 ++++++++++++++++++++++ tests/unit/freight_forwarder_test.py | 38 ++++++++----- 4 files changed, 148 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/test_freight_forwarder.yaml diff --git a/freight_forwarder/freight_forwarder.py b/freight_forwarder/freight_forwarder.py index 3fcc804..4bdf8dc 100644 --- a/freight_forwarder/freight_forwarder.py +++ b/freight_forwarder/freight_forwarder.py @@ -24,9 +24,9 @@ class FreightForwarder(object): and ship yards. it will also handle fleet orchestration and discovery. If no config is present then a invoice / shipping and receiving port must be provided. """ - def __init__(self, config_path_override=None): + def __init__(self, config_path_override=None, verbose=True): # create config - self._config = Config() + self._config = Config(path_override=config_path_override, verbose=verbose) # validate config file self._config.validate() diff --git a/tests/fixtures/test_freight_forwarder.yaml b/tests/fixtures/test_freight_forwarder.yaml new file mode 100644 index 0000000..b026e7e --- /dev/null +++ b/tests/fixtures/test_freight_forwarder.yaml @@ -0,0 +1,57 @@ +--- +team: ffbug +project: example +repository: "git@github.com/ffbug" + +registries: + # define development registry + dev: &docker_hub + address: "https://index.docker.io" + verify: false + + tune_dev: &tune_dev + address: "https://docker-dev.ops.tune.com" + verify: false + default: *tune_dev + +tomcat-test: + build: "./Dockerfile" + ports: + - 8080:8080 + +es-server: + image: tune_dev/tuneplatform/elasticsearch-server:latest + ports: + - 8080:8080 + +environments: + development: + local: + hosts: + default: + - address: https://dv-dockerdevbox-dev.sea1.office.priv:2376 + ssl_cert_path: /Users/benjamin/.docker/certs/tune_puppet/client + verify: false + export: + - address: https://192.168.99.100:2376 + ssl_cert_path: /Users/benjamin/.docker/machine/machines/1.8.1-dev + verify: false + tomcat-test: + - address: https://192.168.99.101:2376 + ssl_cert_path: /Users/benjamin/.docker/machine/machines/1.8.3-dev + verify: false + + quality_control: + tomcat-test: + ports: + - 9090:8080 + links: + - es-server + + deploy: + tomcat-test: + image: "library/tomcat" + ports: + - 8080:8080 + links: + - es-server diff --git a/tests/unit/commercial_invoice_test.py b/tests/unit/commercial_invoice_test.py index 4138d87..13cf3d9 100644 --- a/tests/unit/commercial_invoice_test.py +++ b/tests/unit/commercial_invoice_test.py @@ -1,7 +1,11 @@ from __future__ import absolute_import, unicode_literals +import os + from tests import unittest, mock +from mock import call from freight_forwarder.commercial_invoice import CommercialInvoice +from freight_forwarder.freight_forwarder import FreightForwarder class CommercialInvoiceConstructorTest(unittest.TestCase): @@ -182,3 +186,64 @@ def test_registry_with_injector_provided(self, mocked_create_container_ships, mo 'mighty_morphing', mocked_injector.return_value ) + + +class CommercialInvoiceCreateContainershipsTest(unittest.TestCase): + def setUp(self): + self.freight_forwarder = FreightForwarder( + config_path_override=os.path.join(os.getcwd(), + 'tests', + 'fixtures', + 'test_freight_forwarder.yaml'), + verbose=True + ) + + def tearDown(self): + del self.freight_forwarder + + @mock.patch.object(CommercialInvoice, '_create_registries', autospec=True) + @mock.patch('freight_forwarder.commercial_invoice.commercial_invoice.ContainerShip', create=True) + def test_create_containerships_with_deploy(self, mock_container_ship, mocked_create_registries): + """ + Validate the deployment host or hosts is available inside the commercial invoice + :param mock_container_ship: + :param mocked_create_registries: + :return: + """ + commercial_invoice = self.freight_forwarder.commercial_invoice( + 'deploy', + 'local', + 'development', + 'tomcat-test' + ) + self.assertEqual(len(commercial_invoice.container_ships), 3) + self.assertIn('tomcat-test', commercial_invoice.container_ships.keys()) + self.assertEqual(len(commercial_invoice.container_ships['tomcat-test']), 1) + mock_container_ship.assert_called_with( + 'https://192.168.99.100:2376', + services=None, + verify=False, + ssl_cert_path='/Users/benjamin/.docker/machine/machines/1.8.1-dev') + + @mock.patch.object(CommercialInvoice, '_create_registries', autospec=True) + @mock.patch('freight_forwarder.commercial_invoice.commercial_invoice.ContainerShip', create=True) + @mock.patch('freight_forwarder.commercial_invoice.commercial_invoice.os') + def test_create_containerships_with_no_default(self, mock_os, mock_container_ship, mocked_create_registries): + """ + Validate the the container_ship creation with no default defined in the configuration + :param mock_container_ship: + :param mocked_create_registries: + :return: + """ + # Remove default hosts to ensure it is created + hosts = self.freight_forwarder._config.get('hosts', 'environments', 'development', 'local') + del hosts['default'] + + commercial_invoice = self.freight_forwarder.commercial_invoice( + 'deploy', + 'local', + 'development', + 'tomcat-test' + ) + self.assertEqual(mock_os.getenv.call_count, 3) + mock_os.getenv.assert_called_with('DOCKER_TLS_VERIFY') diff --git a/tests/unit/freight_forwarder_test.py b/tests/unit/freight_forwarder_test.py index c888966..cbe6ddc 100644 --- a/tests/unit/freight_forwarder_test.py +++ b/tests/unit/freight_forwarder_test.py @@ -1,23 +1,33 @@ +# -*- coding: utf-8; -*- from __future__ import absolute_import, unicode_literals -from tests import unittest +import os + +from tests import unittest, mock from freight_forwarder import FreightForwarder class FreightForwarderTest(unittest.TestCase): - def setup(self): - """Some Mock - """ + def setUp(self): + self.freight_forwarder = FreightForwarder( + config_path_override=os.path.join(os.getcwd(), + 'tests', + 'fixtures', + 'test_freight_forwarder.yaml'), + verbose=True + ) - def test_valid_repository(self): - self.assertRaises(TypeError, lambda: FreightForwarder(1, "cia")) - self.assertRaises(TypeError, lambda: FreightForwarder([], "cia")) - self.assertRaises(TypeError, lambda: FreightForwarder({}, "cia")) - self.assertRaises(TypeError, lambda: FreightForwarder(None, "cia")) - self.assertRaises(TypeError, lambda: FreightForwarder(type, "cia")) + def tearDown(self): + del self.freight_forwarder - def test_valid_name(self): - """ - """ - pass + @mock.patch('freight_forwarder.freight_forwarder.CommercialInvoice', create=True) + def test_commercial_invoice_export_and_deploy_service(self, mock_commercial_invoice): + commercial_invoice = self.freight_forwarder.commercial_invoice( + action='export', + data_center='local', + environment='development', + transport_service='tomcat-test' + ) + self.assertEqual(mock_commercial_invoice.call_args[1]['services']['tomcat_test']['build'], + './Dockerfile') From 35ca1e16da7e2c605e10252b5e3e9e9181f502e2 Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Thu, 24 Mar 2016 02:27:01 -0700 Subject: [PATCH 05/10] updated travis --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10feb83..c7ee0d6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ When deploying from one environment to the next it is suggested to: 1. Pull the image from the previous environment make configuration changes and commit those changes to a new image layer. 2. Testing should be run with the new configuration changes. 3. After the image is verified, it will be pushed up to the registry and tagged accordingly. - 4. That image will then be used when deploying to that environment. + 4. That image will then be used when deploying to that environment.. ##Installation Follow these [instructions](http://freight-forwarder.readthedocs.org/en/latest/introduction/install.html). From c0b345e4de3fd1cff89b2f14b5190b109f1358b9 Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Thu, 24 Mar 2016 02:40:39 -0700 Subject: [PATCH 06/10] removed a mock validation --- README.md | 2 +- tests/unit/commercial_invoice_test.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index c7ee0d6..f415afb 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ easing the pain of multiple container environments. Freight Forwarder can be use Freight Forwarder focuses on how Docker images are built, tested, pushed, and then deployed. The images being pushed to the registry are the artifacts being deployed. There should be no additional changes made to the images after being exported. In addition, -the containers should be able to start taking traffic or doing work on initialization. +the containers should be able to start taking traffic or doing work on initialization. When deploying from one environment to the next it is suggested to: diff --git a/tests/unit/commercial_invoice_test.py b/tests/unit/commercial_invoice_test.py index 13cf3d9..8d38e9d 100644 --- a/tests/unit/commercial_invoice_test.py +++ b/tests/unit/commercial_invoice_test.py @@ -219,11 +219,6 @@ def test_create_containerships_with_deploy(self, mock_container_ship, mocked_cre self.assertEqual(len(commercial_invoice.container_ships), 3) self.assertIn('tomcat-test', commercial_invoice.container_ships.keys()) self.assertEqual(len(commercial_invoice.container_ships['tomcat-test']), 1) - mock_container_ship.assert_called_with( - 'https://192.168.99.100:2376', - services=None, - verify=False, - ssl_cert_path='/Users/benjamin/.docker/machine/machines/1.8.1-dev') @mock.patch.object(CommercialInvoice, '_create_registries', autospec=True) @mock.patch('freight_forwarder.commercial_invoice.commercial_invoice.ContainerShip', create=True) From 95b69f03eee546d814554fb073e7d4c156bce89a Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Thu, 24 Mar 2016 14:18:28 -0700 Subject: [PATCH 07/10] Tests Update * removed verbose output from tests --- tests/unit/commercial_invoice_test.py | 2 +- tests/unit/freight_forwarder_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/commercial_invoice_test.py b/tests/unit/commercial_invoice_test.py index 8d38e9d..e6f0465 100644 --- a/tests/unit/commercial_invoice_test.py +++ b/tests/unit/commercial_invoice_test.py @@ -195,7 +195,7 @@ def setUp(self): 'tests', 'fixtures', 'test_freight_forwarder.yaml'), - verbose=True + verbose=False ) def tearDown(self): diff --git a/tests/unit/freight_forwarder_test.py b/tests/unit/freight_forwarder_test.py index cbe6ddc..172b911 100644 --- a/tests/unit/freight_forwarder_test.py +++ b/tests/unit/freight_forwarder_test.py @@ -15,7 +15,7 @@ def setUp(self): 'tests', 'fixtures', 'test_freight_forwarder.yaml'), - verbose=True + verbose=False ) def tearDown(self): From 3973f4e90f75bdcf28e52445a4c491923893fad2 Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Thu, 24 Mar 2016 16:57:28 -0700 Subject: [PATCH 08/10] Tests Update * updated tests for `CommercialInvoice` and `FreigthForwarder` * updated changelog --- CHANGELOG.md | 6 ++++++ freight_forwarder/const.py | 2 +- tests/unit/commercial_invoice_test.py | 3 ++- tests/unit/freight_forwarder_test.py | 9 +++++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 949d605..4a3df62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log #### All notable changes to this project will be documented in this file. This project adheres to Semantic Versioning. +## [1.0.2] - 2016-03-24 +* Resolved a bug where an export operation was using an image declaration from the same `deploy` environment + instead of export definition +* Resolved a bug where an environment's host service definition was being ignored and using the defined default +* Updated `FreightForwarder` to allow for the passing of a config path override and a verbosity level for logs. + ## [1.0.1] - 2016-03-18 ### Changes: diff --git a/freight_forwarder/const.py b/freight_forwarder/const.py index 55c405d..3b67fa4 100644 --- a/freight_forwarder/const.py +++ b/freight_forwarder/const.py @@ -1,7 +1,7 @@ # -*- coding: utf-8; -*- from __future__ import unicode_literals -VERSION = "1.0.1" +VERSION = "1.0.2" # docker api DOCKER_DEFAULT_TIMEOUT = 120 diff --git a/tests/unit/commercial_invoice_test.py b/tests/unit/commercial_invoice_test.py index e6f0465..9472c88 100644 --- a/tests/unit/commercial_invoice_test.py +++ b/tests/unit/commercial_invoice_test.py @@ -241,4 +241,5 @@ def test_create_containerships_with_no_default(self, mock_os, mock_container_shi 'tomcat-test' ) self.assertEqual(mock_os.getenv.call_count, 3) - mock_os.getenv.assert_called_with('DOCKER_TLS_VERIFY') + calls = [call('DOCKER_TLS_VERIFY'), call('DOCKER_CERT_PATH'), call('DOCKER_HOST')] + mock_os.getenv.assert_has_call(calls, any_order=True) diff --git a/tests/unit/freight_forwarder_test.py b/tests/unit/freight_forwarder_test.py index 172b911..14fb634 100644 --- a/tests/unit/freight_forwarder_test.py +++ b/tests/unit/freight_forwarder_test.py @@ -5,7 +5,8 @@ from tests import unittest, mock -from freight_forwarder import FreightForwarder +from freight_forwarder import FreightForwarder +from freight_forwarder.config import ConfigDict class FreightForwarderTest(unittest.TestCase): @@ -29,5 +30,9 @@ def test_commercial_invoice_export_and_deploy_service(self, mock_commercial_invo environment='development', transport_service='tomcat-test' ) - self.assertEqual(mock_commercial_invoice.call_args[1]['services']['tomcat_test']['build'], + commercial_invoice_services = mock_commercial_invoice.call_args[1].get('services') + self.assertEqual(commercial_invoice_services['tomcat_test']['build'], './Dockerfile') + self.assertIsInstance(commercial_invoice_services['tomcat_test'], ConfigDict) + + self.assertNotIn('image', commercial_invoice_services['tomcat_test'].values()) From dd14d345a8c9fa91c8f0b91f0d881e75052bde0a Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Fri, 25 Mar 2016 10:46:48 -0700 Subject: [PATCH 09/10] Reverting Changes for naming * reverted change in name of key in `CommercialInvoice` * reverted change and using public `transport_service.name` --- .../commercial_invoice/commercial_invoice.py | 16 ++++++++-------- freight_forwarder/freight_forwarder.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freight_forwarder/commercial_invoice/commercial_invoice.py b/freight_forwarder/commercial_invoice/commercial_invoice.py index c76c545..8bea7e4 100644 --- a/freight_forwarder/commercial_invoice/commercial_invoice.py +++ b/freight_forwarder/commercial_invoice/commercial_invoice.py @@ -191,30 +191,30 @@ def _create_container_ships(self, hosts): default_container_ship = self._create_container_ship(None) container_ships['default'] = {default_container_ship.url.geturl(): default_container_ship} - for key, hosts in six.iteritems(hosts): + for alias, hosts in six.iteritems(hosts): if hosts is None: - container_ships[key] = hosts + container_ships[alias] = hosts elif isinstance(hosts, list): - key = hosts.alias - container_ships[key] = {} + alias = hosts.alias + container_ships[alias] = {} for host in hosts: if not host or not isinstance(host, dict): - raise ValueError("hosts: {0} is required to be a dict.".format(key)) + raise ValueError("hosts: {0} is required to be a dict.".format(alias)) existing_container_ship = None for container_ship_dict in six.itervalues(container_ships): for address, container_ship in six.iteritems(container_ship_dict): - if address == host.get('address') and address not in container_ships[key]: + if address == host.get('address') and address not in container_ships[alias]: existing_container_ship = container_ship break if existing_container_ship is None: - container_ships[key][host.get('address')] = self._create_container_ship(host) + container_ships[alias][host.get('address')] = self._create_container_ship(host) else: - container_ships[key][host.get('address')] = existing_container_ship + container_ships[alias][host.get('address')] = existing_container_ship else: raise ValueError(logger.error("hosts is required to be a list or None. host: {0}".format(hosts))) diff --git a/freight_forwarder/freight_forwarder.py b/freight_forwarder/freight_forwarder.py index 4bdf8dc..93e816c 100644 --- a/freight_forwarder/freight_forwarder.py +++ b/freight_forwarder/freight_forwarder.py @@ -369,7 +369,7 @@ def __assemble_fleet(self, commercial_invoice): if commercial_invoice.transport_method == 'export': host_alias = commercial_invoice.transport_method else: - host_alias = commercial_invoice._transport_service + host_alias = commercial_invoice.transport_service.name fleet = commercial_invoice.container_ships.get( host_alias, From f27aeacd34601bebe559da8dc210430c5fd3ab1e Mon Sep 17 00:00:00 2001 From: Benjamin Stickel Date: Fri, 25 Mar 2016 10:50:32 -0700 Subject: [PATCH 10/10] resolved READEM change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f415afb..ba2494d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ When deploying from one environment to the next it is suggested to: 1. Pull the image from the previous environment make configuration changes and commit those changes to a new image layer. 2. Testing should be run with the new configuration changes. 3. After the image is verified, it will be pushed up to the registry and tagged accordingly. - 4. That image will then be used when deploying to that environment.. + 4. That image will then be used when deploying to that environment. ##Installation Follow these [instructions](http://freight-forwarder.readthedocs.org/en/latest/introduction/install.html).