def validate(self):
        with open(self.options['config'], 'r') as f:
            self.config = json.load(f)
        self.config['ROOT_DIR'] = os.getcwd()
        missing = []
        for item in self.validation_config:
            key = item['key']
            required = item['required']
            key_found = key in self.config or key in os.environ

            if not self.config.get(key):
                self.config[key] = os.environ.get(key, "")
            if (not key_found and required) or (key_found and required
                                                and not self.config[key]):
                missing.append(item)
            elif not required and not self.config[key]:
                warning = f"Config missing optional field: {key} - {Colors.WARNING}{item['description']}"
                if item.get('default-message'):
                    warning += f" - {Colors.CYAN}{item['default-message']}"
                Prompt.notice(warning)
        if missing:
            missing_formatted = [
                f"{x['key']}: {x['description']}" for x in missing
            ]
            Prompt.error(
                f"The following keys are missing/empty from your env or config file: {missing_formatted}",
                close=True)

        # Prepares build files if provided
        super().docker_tmp_file_handler(self.config, self.TMP_BUILD_FILES)
 def signal_handler(self, sig, frame):
     Prompt.notice("\nCtrl-c captured.  Executing teardown function.")
     if not self.kill_captured:
         self.kill_captured = True
         self.cleanup()
         self.on_sig_kill()
     sys.exit(0)
Beispiel #3
0
    def run(self):
        self.config.setdefault('PERSIST_STACK')
        if not self.config['PERSIST_STACK']:
            self.FAIL_SUFFIX = "--remove-orphans --rmi all"
        del self.config['PERSIST_STACK']

        self.check_if_docker_is_started()
        self.set_default('COMPOSE_PROJECT_NAME',
                         'govready-q')  # Prefix for all docker containers

        self.set_default('GIT_URL',
                         "https://github.com/GovReady/govready-q.git")
        self.set_default(
            'ADMINS',
            [] if not self.config.get('ADMINS') else self.config.get('ADMINS'))
        self.set_default(
            'OKTA',
            {} if not self.config.get('OKTA') else self.config.get('OKTA'))
        self.set_default(
            'OIDC',
            {} if not self.config.get('OIDC') else self.config.get('OIDC'))
        self.set_default('MOUNT_FOLDER', os.path.abspath("../../volumes"))
        self.config['ALLOWED_HOSTS'] = ['app',
                                        self.config['HOST_ADDRESS']] + getattr(
                                            self.config, 'ALLOWED_HOSTS', [])
        self.set_default('DEBUG', "false")
        self.set_default('APP_DOCKER_PORT', "18000")

        if self.check_if_valid_uri(self.config['HOST_ADDRESS']):
            Prompt.error(
                f"HOST_ADDRESS cannot be a valid URI.  It must be the domain only.  "
                f"No protocol or path.  {self.config['HOST_ADDRESS']} is invalid.",
                close=True)

        self.set_default('HEALTH_CHECK_GOVREADY_Q',
                         f"http://*****:*****@postgres:5432/govready_q")
        self.set_default(
            'DB_ENGINE',
            self.config['DATABASE_CONNECTION_STRING'].split(':')[0])
        docker_compose_file = "docker-compose.yaml"
        if using_internal_db:
            self.REQUIRED_PORTS.append(5432)
        else:
            docker_compose_file = 'docker-compose.external-db.yaml'

        self.execute(
            cmd=
            f"docker-compose -f {docker_compose_file} down {self.FAIL_SUFFIX}")

        self.REQUIRED_PORTS += [
            int(self.config['HOST_PORT_HTTPS']),
            int(self.config['APP_DOCKER_PORT'])
        ]
        self.check_ports()
        self.execute(cmd=f"docker-compose -f {docker_compose_file} up -d",
                     show_env=True)
 def __docker_build_tmp_files_cleanup(self, base_path, file_path,
                                      tmp_build):
     if os.path.isdir(file_path):
         dest = os.path.join(base_path,
                             f"{os.path.basename(file_path)}.zip")
     else:
         dest = os.path.join(base_path, os.path.basename(file_path))
     os.remove(dest)
     Prompt.notice(
         f"Removed build artifact for image: {tmp_build['image']} - {dest}")
 def __docker_build_tmp_files_copy(self, base_path, file_path, tmp_build,
                                   config, key):
     if os.path.isdir(file_path):
         dest = make_archive(
             os.path.join(base_path, os.path.basename(file_path)), 'zip',
             file_path)
         config[f"BUILD_FILE_{key}"] = f"{os.path.basename(file_path)}.zip"
     else:
         dest = os.path.join(base_path, os.path.basename(file_path))
         copyfile(file_path, dest)
         config[f"BUILD_FILE_{key}"] = os.path.basename(dest)
     Prompt.notice(
         f"Copied build file for image: {tmp_build['image']} - {dest}")
 def check_ports(self):
     Prompt.notice(
         f"Checking if ports are available for deployment: {self.REQUIRED_PORTS}"
     )
     import socket
     ports_in_use = []
     for port in self.REQUIRED_PORTS:
         with closing(socket.socket(socket.AF_INET,
                                    socket.SOCK_STREAM)) as sock:
             if sock.connect_ex(('127.0.0.1', port)) == 0:
                 ports_in_use.append(port)
     if ports_in_use:
         Prompt.error(
             f"Cannot deploy.  The following ports are in use: {ports_in_use}",
             close=True)
 def docker_tmp_file_handler(self,
                             config,
                             docker_tmp_build_files,
                             copy=True):
     for tmp_build in docker_tmp_build_files:
         for key in tmp_build['keys']:
             file_path = config[key]
             if file_path:
                 if not os.path.exists(file_path):
                     Prompt.error(
                         f"{os.path.abspath(file_path)} does not exist.",
                         close=True)
                 base_path = os.path.join(config['ROOT_DIR'], 'images',
                                          tmp_build['image'])
                 if copy:
                     self.__docker_build_tmp_files_copy(
                         base_path, file_path, tmp_build, config, key)
                 else:
                     self.__docker_build_tmp_files_cleanup(
                         base_path, file_path, tmp_build)
    def generate(self):
        skeleton = {}
        for row in self.validation_config_data:
            skeleton[row['key']] = ""

        self.run(skeleton)
        with open("configuration.json", 'w') as f:
            json.dump(skeleton, f, indent=4, sort_keys=True)
        Prompt.warning(
            f"Configuration created: {Colors.CYAN}configuration.json")
        Prompt.warning(
            f"Please set values in the configuration in line with your needs.")
        Prompt.warning(
            f"Once fully configured.  Deploy by running: {Colors.CYAN}python run.py deploy --type {self.options['type']} --config configuration.json"
        )
Beispiel #9
0
    def on_complete(self):
        logs = self.execute(cmd=f"docker-compose logs", display_stdout=False)
        auto_admin = re.findall(
            'Created administrator account \(username: (admin)\) with password: ([a-zA-Z0-9#?!@$%^&*-]+)',
            logs)
        print()

        if auto_admin:
            Prompt.warning(
                f"Created Administrator Account - {Colors.CYAN}{auto_admin[0][0]} / {auto_admin[0][1]} - {Colors.FAIL} This is the only time you will see this message so make sure to write this down!"
            )
        Prompt.warning(
            f"Logs & Container Artifacts can be found in: {Colors.CYAN}{self.config['MOUNT_FOLDER']}"
        )

        url = f"https://{self.config['HOST_ADDRESS']}"
        if self.config['HOST_PORT_HTTPS']:
            url += f":{self.config['HOST_PORT_HTTPS']}"
        Prompt.warning(f"Access application via Browser: {Colors.CYAN}{url}")
        if not self.FAIL_SUFFIX:
            Prompt.warning(
                f"WARNING: PERSIST_STACK is enabled.  This may prevent configurations made since "
                f"the last creation of the stack.  Persisting a stack will cache the container and won't "
                f"rebuild with your new configurations.")
 def execute(self,
             cmd,
             env_dict,
             display_stdout=True,
             on_error_fn=None,
             show_env=False,
             display_stderr=True):
     env = os.environ.copy()
     normalized_dict = {}
     for key, value in env_dict.items():
         if isinstance(value, (list, dict)):
             value = json.dumps(value)
         if value is None:
             value = ""
         normalized_dict[key] = value
     env.update(normalized_dict)
     output = ""
     Prompt.notice(f"Executing command: {Colors.WARNING}{cmd}")
     if show_env:
         Prompt.notice(
             f"Environment Variables: {json.dumps(env_dict, indent=4, sort_keys=True)}"
         )
     args = dict(stdout=subprocess.PIPE, bufsize=0, env=env, shell=True)
     if not display_stderr:
         args.update(dict(stderr=subprocess.DEVNULL))
     with subprocess.Popen(cmd, **args) as proc:
         for line in proc.stdout:
             formatted = line.rstrip().decode('utf-8', 'ignore')
             output += formatted
             if display_stdout:
                 print(formatted)
     if proc.returncode != 0:
         if on_error_fn:
             on_error_fn()
         Prompt.error(
             f"[{cmd}] Failed [code:{proc.returncode}]- {proc.stderr}",
             close=True)
     return output
Beispiel #11
0
def get_deployment_type(deployment_type_list):
    Prompt.question_banner("Deployment Choices:", True)
    Prompt.banner(deployment_type_list, prevent_all_option=True)
    return deployment_type_list[Prompt.get_response("Choice: ", deployment_type_list) - 1]
Beispiel #12
0
def run(options):
    path = f"deployments.{options['type']}.{options['action']}"
    validator_config = os.path.join(f"{os.path.sep}".join(path.split('.')[:-1]), 'config-validator.json')
    importlib.import_module(path)

    if options['action'] == 'init':
        init_classes = Initialize.__subclasses__()
        if not init_classes:
            Prompt.error(f"Unable to find class inheriting `Initialize` in {path}", close=True)
        if not os.path.exists(validator_config):
            Prompt.error(
                f"File does not exist: {validator_config}.  Each deployment type must have this file to validate required values for deployment.",
                close=True)
        init = init_classes[0](validator_config, options)
        init.generate()
        return

    Prompt.warning(f"Attempting to [{options['action']}] using the [{options['type']}] method")
    if options['action'] == 'deploy':
        if not options['config']:
            Prompt.error(f"Deployments require a configuration json that satisfies: [{validator_config}].  Please run: {Colors.CYAN}python run.py init", close=True)
        deployment_classes = Deployment.__subclasses__()
        if not deployment_classes:
            Prompt.error(f"Unable to find class inheriting `Deployment` in {path}", close=True)
        if not os.path.exists(validator_config):
            Prompt.error(
                f"File does not exist: {validator_config}.  Each deployment type must have this file to validate required values for deployment.",
                close=True)
        deployment = deployment_classes[0](options, validator_config)
        deployment.validate()
        os.chdir(f"deployments/{options['type']}")
        try:
            deployment.run()
        finally:
            deployment.cleanup()
        deployment.on_complete()
        Prompt.success(f"Deployment complete - using the [{options['type']}] method")
    else:
        deployment_classes = UnDeploy.__subclasses__()
        if not deployment_classes:
            Prompt.error(f"Unable to find class inheriting `UnDeploy` in {path}", close=True)
        remove_deployment = deployment_classes[0](options)
        os.chdir(f"deployments/{options['type']}")
        try:
            remove_deployment.run()
        finally:
            remove_deployment.cleanup()

        remove_deployment.on_complete()
        Prompt.success(f"Deployment removed - using the [{options['type']}] method")
Beispiel #13
0
        finally:
            remove_deployment.cleanup()

        remove_deployment.on_complete()
        Prompt.success(f"Deployment removed - using the [{options['type']}] method")


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Deploy/Undeploy GovReadyQ')
    parser.add_argument('action', help='The action to take (init, deploy, undeploy)')
    parser.add_argument('--config', help='Config file - used to deploy process', required=False)
    parser.add_argument('--type', help="(Optional) Deployment type; It will prompt you if you don't include.",
                        required=False)

    args, unknown = parser.parse_known_args()
    args = vars(args)
    args['extra'] = unknown

    valid_actions = ['deploy', 'undeploy', 'init']
    if args['action'] not in valid_actions:
        Prompt.error(f"{args['action']} is not a valid choice.  Choices: {valid_actions}", close=True)

    deployment_types = sorted([y for y in [x[0].split(os.path.sep)[-1] for x in os.walk('deployments')][1:] if
                               not y.startswith('__')])
    if not args['type']:
        args['type'] = get_deployment_type(deployment_types)
    if args['type'] not in deployment_types:
        Prompt.error(f"{args['type']} is not a valid choice.  Choices: {deployment_types}", close=True)

    run(args)
 def on_complete(self):
     print()
     Prompt.warning(f"If you're not using an external database and would like to wipe your DB, run: {Colors.CYAN}docker volume rm govready-q_postgres-data")
Beispiel #15
0
 def on_complete(self):
     Prompt.notice(
         f"Next step: {Colors.CYAN}python run.py deploy --type docker-compose --config configuration.json"
     )
 def offline():
     Prompt.error(
         "Docker Engine is offline.  Please start before continuing.",
         close=True)