Changes for remote k8s (#655)

This commit is contained in:
David Boreham 2023-11-20 09:12:57 -07:00 committed by GitHub
parent 5c80887215
commit c9c6a0eee3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 101 additions and 56 deletions

View File

@ -14,3 +14,9 @@
# along with this program. If not, see <http:#www.gnu.org/licenses/>. # along with this program. If not, see <http:#www.gnu.org/licenses/>.
stack_file_name = "stack.yml" stack_file_name = "stack.yml"
compose_deploy_type = "compose"
k8s_kind_deploy_type = "k8s-kind"
k8s_deploy_type = "k8s"
kube_config_key = "kube-config"
kind_config_filename = "kind-config.yml"
kube_config_filename = "kubeconfig.yml"

View File

@ -20,10 +20,12 @@ from stack_orchestrator.deploy.deployer import Deployer, DeployerException, Depl
class DockerDeployer(Deployer): class DockerDeployer(Deployer):
name: str = "compose" name: str = "compose"
type: str
def __init__(self, deployment_dir, compose_files, compose_project_name, compose_env_file) -> None: def __init__(self, type, deployment_dir, compose_files, compose_project_name, compose_env_file) -> None:
self.docker = DockerClient(compose_files=compose_files, compose_project_name=compose_project_name, self.docker = DockerClient(compose_files=compose_files, compose_project_name=compose_project_name,
compose_env_file=compose_env_file) compose_env_file=compose_env_file)
self.type = type
def up(self, detach, services): def up(self, detach, services):
try: try:
@ -70,9 +72,8 @@ class DockerDeployer(Deployer):
class DockerDeployerConfigGenerator(DeployerConfigGenerator): class DockerDeployerConfigGenerator(DeployerConfigGenerator):
config_file_name: str = "kind-config.yml"
def __init__(self) -> None: def __init__(self, type: str) -> None:
super().__init__() super().__init__()
# Nothing needed at present for the docker deployer # Nothing needed at present for the docker deployer

View File

@ -39,7 +39,7 @@ from stack_orchestrator.deploy.deployment_create import setup as deployment_setu
@click.option("--exclude", help="don\'t start these components") @click.option("--exclude", help="don\'t start these components")
@click.option("--env-file", help="env file to be used") @click.option("--env-file", help="env file to be used")
@click.option("--cluster", help="specify a non-default cluster name") @click.option("--cluster", help="specify a non-default cluster name")
@click.option("--deploy-to", help="cluster system to deploy to (compose or k8s)") @click.option("--deploy-to", help="cluster system to deploy to (compose or k8s or k8s-kind)")
@click.pass_context @click.pass_context
def command(ctx, include, exclude, env_file, cluster, deploy_to): def command(ctx, include, exclude, env_file, cluster, deploy_to):
'''deploy a stack''' '''deploy a stack'''
@ -62,11 +62,11 @@ def command(ctx, include, exclude, env_file, cluster, deploy_to):
def create_deploy_context( def create_deploy_context(
global_context, deployment_context: DeploymentContext, stack, include, exclude, cluster, env_file, deployer): global_context, deployment_context: DeploymentContext, stack, include, exclude, cluster, env_file, deploy_to):
cluster_context = _make_cluster_context(global_context, stack, include, exclude, cluster, env_file) cluster_context = _make_cluster_context(global_context, stack, include, exclude, cluster, env_file)
deployment_dir = deployment_context.deployment_dir if deployment_context else None deployment_dir = deployment_context.deployment_dir if deployment_context else None
# See: https://gabrieldemarmiesse.github.io/python-on-whales/sub-commands/compose/ # See: https://gabrieldemarmiesse.github.io/python-on-whales/sub-commands/compose/
deployer = getDeployer(deployer, deployment_dir, compose_files=cluster_context.compose_files, deployer = getDeployer(deploy_to, deployment_dir, compose_files=cluster_context.compose_files,
compose_project_name=cluster_context.cluster, compose_project_name=cluster_context.cluster,
compose_env_file=cluster_context.env_file) compose_env_file=cluster_context.env_file)
return DeployCommandContext(stack, cluster_context, deployer) return DeployCommandContext(stack, cluster_context, deployer)

View File

@ -13,23 +13,24 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http:#www.gnu.org/licenses/>. # along with this program. If not, see <http:#www.gnu.org/licenses/>.
from stack_orchestrator import constants
from stack_orchestrator.deploy.k8s.deploy_k8s import K8sDeployer, K8sDeployerConfigGenerator from stack_orchestrator.deploy.k8s.deploy_k8s import K8sDeployer, K8sDeployerConfigGenerator
from stack_orchestrator.deploy.compose.deploy_docker import DockerDeployer, DockerDeployerConfigGenerator from stack_orchestrator.deploy.compose.deploy_docker import DockerDeployer, DockerDeployerConfigGenerator
def getDeployerConfigGenerator(type: str): def getDeployerConfigGenerator(type: str):
if type == "compose" or type is None: if type == "compose" or type is None:
return DockerDeployerConfigGenerator() return DockerDeployerConfigGenerator(type)
elif type == "k8s": elif type == constants.k8s_deploy_type or type == constants.k8s_kind_deploy_type:
return K8sDeployerConfigGenerator() return K8sDeployerConfigGenerator(type)
else: else:
print(f"ERROR: deploy-to {type} is not valid") print(f"ERROR: deploy-to {type} is not valid")
def getDeployer(type: str, deployment_dir, compose_files, compose_project_name, compose_env_file): def getDeployer(type: str, deployment_dir, compose_files, compose_project_name, compose_env_file):
if type == "compose" or type is None: if type == "compose" or type is None:
return DockerDeployer(deployment_dir, compose_files, compose_project_name, compose_env_file) return DockerDeployer(type, deployment_dir, compose_files, compose_project_name, compose_env_file)
elif type == "k8s": elif type == type == constants.k8s_deploy_type or type == constants.k8s_kind_deploy_type:
return K8sDeployer(deployment_dir, compose_files, compose_project_name, compose_env_file) return K8sDeployer(type, deployment_dir, compose_files, compose_project_name, compose_env_file)
else: else:
print(f"ERROR: deploy-to {type} is not valid") print(f"ERROR: deploy-to {type} is not valid")

View File

@ -16,6 +16,7 @@
import click import click
from pathlib import Path from pathlib import Path
import sys import sys
from stack_orchestrator import constants
from stack_orchestrator.deploy.deploy import up_operation, down_operation, ps_operation, port_operation from stack_orchestrator.deploy.deploy import up_operation, down_operation, ps_operation, port_operation
from stack_orchestrator.deploy.deploy import exec_operation, logs_operation, create_deploy_context from stack_orchestrator.deploy.deploy import exec_operation, logs_operation, create_deploy_context
from stack_orchestrator.deploy.deployment_context import DeploymentContext from stack_orchestrator.deploy.deployment_context import DeploymentContext
@ -50,8 +51,12 @@ def make_deploy_context(ctx):
stack_file_path = context.get_stack_file() stack_file_path = context.get_stack_file()
env_file = context.get_env_file() env_file = context.get_env_file()
cluster_name = context.get_cluster_name() cluster_name = context.get_cluster_name()
if "deploy-to" in context.spec.obj:
deployment_type = context.spec.obj["deploy-to"]
else:
deployment_type = constants.compose_deploy_type
return create_deploy_context(ctx.parent.parent.obj, context, stack_file_path, None, None, cluster_name, env_file, return create_deploy_context(ctx.parent.parent.obj, context, stack_file_path, None, None, cluster_name, env_file,
context.spec.obj["deploy-to"]) deployment_type)
@command.command() @command.command()

View File

@ -21,16 +21,17 @@ from typing import List
import random import random
from shutil import copy, copyfile, copytree from shutil import copy, copyfile, copytree
import sys import sys
from stack_orchestrator import constants
from stack_orchestrator.util import (get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config, from stack_orchestrator.util import (get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config,
global_options, get_yaml, get_pod_list, get_pod_file_path, pod_has_scripts, global_options, get_yaml, get_pod_list, get_pod_file_path, pod_has_scripts,
get_pod_script_paths, get_plugin_code_paths) get_pod_script_paths, get_plugin_code_paths, error_exit)
from stack_orchestrator.deploy.deploy_types import LaconicStackSetupCommand from stack_orchestrator.deploy.deploy_types import LaconicStackSetupCommand
from stack_orchestrator.deploy.deployer_factory import getDeployerConfigGenerator from stack_orchestrator.deploy.deployer_factory import getDeployerConfigGenerator
from stack_orchestrator.deploy.deployment_context import DeploymentContext from stack_orchestrator.deploy.deployment_context import DeploymentContext
def _make_default_deployment_dir(): def _make_default_deployment_dir():
return "deployment-001" return Path("deployment-001")
def _get_ports(stack): def _get_ports(stack):
@ -248,17 +249,21 @@ def _parse_config_variables(variable_values: str):
@click.command() @click.command()
@click.option("--config", help="Provide config variables for the deployment") @click.option("--config", help="Provide config variables for the deployment")
@click.option("--kube-config", help="Provide a config file for a k8s deployment")
@click.option("--output", required=True, help="Write yaml spec file here") @click.option("--output", required=True, help="Write yaml spec file here")
@click.option("--map-ports-to-host", required=False, @click.option("--map-ports-to-host", required=False,
help="Map ports to the host as one of: any-variable-random (default), " help="Map ports to the host as one of: any-variable-random (default), "
"localhost-same, any-same, localhost-fixed-random, any-fixed-random") "localhost-same, any-same, localhost-fixed-random, any-fixed-random")
@click.pass_context @click.pass_context
def init(ctx, config, output, map_ports_to_host): def init(ctx, config, kube_config, output, map_ports_to_host):
yaml = get_yaml() yaml = get_yaml()
stack = global_options(ctx).stack stack = global_options(ctx).stack
debug = global_options(ctx).debug debug = global_options(ctx).debug
deployer_type = ctx.obj.deployer.type
default_spec_file_content = call_stack_deploy_init(ctx.obj) default_spec_file_content = call_stack_deploy_init(ctx.obj)
spec_file_content = {"stack": stack, "deploy-to": ctx.obj.deployer.name} spec_file_content = {"stack": stack, "deploy-to": deployer_type}
if deployer_type == "k8s":
spec_file_content.update({constants.kube_config_key: kube_config})
if default_spec_file_content: if default_spec_file_content:
spec_file_content.update(default_spec_file_content) spec_file_content.update(default_spec_file_content)
config_variables = _parse_config_variables(config) config_variables = _parse_config_variables(config)
@ -296,6 +301,12 @@ def _write_config_file(spec_file: Path, config_env_file: Path):
output_file.write(f"{variable_name}={variable_value}\n") output_file.write(f"{variable_name}={variable_value}\n")
def _write_kube_config_file(external_path: Path, internal_path: Path):
if not external_path.exists():
error_exit(f"Kube config file {external_path} does not exist")
copyfile(external_path, internal_path)
def _copy_files_to_directory(file_paths: List[Path], directory: Path): def _copy_files_to_directory(file_paths: List[Path], directory: Path):
for path in file_paths: for path in file_paths:
# Using copy to preserve the execute bit # Using copy to preserve the execute bit
@ -310,29 +321,34 @@ def _copy_files_to_directory(file_paths: List[Path], directory: Path):
@click.option("--initial-peers", help="Initial set of persistent peers") @click.option("--initial-peers", help="Initial set of persistent peers")
@click.pass_context @click.pass_context
def create(ctx, spec_file, deployment_dir, network_dir, initial_peers): def create(ctx, spec_file, deployment_dir, network_dir, initial_peers):
# This function fails with a useful error message if the file doens't exist
parsed_spec = get_parsed_deployment_spec(spec_file) parsed_spec = get_parsed_deployment_spec(spec_file)
stack_name = parsed_spec["stack"] stack_name = parsed_spec["stack"]
deployment_type = parsed_spec["deploy-to"]
stack_file = get_stack_file_path(stack_name) stack_file = get_stack_file_path(stack_name)
parsed_stack = get_parsed_stack_config(stack_name) parsed_stack = get_parsed_stack_config(stack_name)
if global_options(ctx).debug: if global_options(ctx).debug:
print(f"parsed spec: {parsed_spec}") print(f"parsed spec: {parsed_spec}")
if deployment_dir is None: if deployment_dir is None:
deployment_dir = _make_default_deployment_dir() deployment_dir_path = _make_default_deployment_dir()
if os.path.exists(deployment_dir): else:
print(f"Error: {deployment_dir} already exists") deployment_dir_path = Path(deployment_dir)
sys.exit(1) if deployment_dir_path.exists():
os.mkdir(deployment_dir) error_exit(f"{deployment_dir_path} already exists")
os.mkdir(deployment_dir_path)
# Copy spec file and the stack file into the deployment dir # Copy spec file and the stack file into the deployment dir
copyfile(spec_file, os.path.join(deployment_dir, "spec.yml")) copyfile(spec_file, deployment_dir_path.joinpath("spec.yml"))
copyfile(stack_file, os.path.join(deployment_dir, os.path.basename(stack_file))) copyfile(stack_file, deployment_dir_path.joinpath(os.path.basename(stack_file)))
# Copy any config varibles from the spec file into an env file suitable for compose # Copy any config varibles from the spec file into an env file suitable for compose
_write_config_file(spec_file, os.path.join(deployment_dir, "config.env")) _write_config_file(spec_file, deployment_dir_path.joinpath("config.env"))
# Copy any k8s config file into the deployment dir
if deployment_type == "k8s":
_write_kube_config_file(Path(parsed_spec[constants.kube_config_key]),
deployment_dir_path.joinpath(constants.kube_config_filename))
# Copy the pod files into the deployment dir, fixing up content # Copy the pod files into the deployment dir, fixing up content
pods = get_pod_list(parsed_stack) pods = get_pod_list(parsed_stack)
destination_compose_dir = os.path.join(deployment_dir, "compose") destination_compose_dir = deployment_dir_path.joinpath("compose")
os.mkdir(destination_compose_dir) os.mkdir(destination_compose_dir)
destination_pods_dir = os.path.join(deployment_dir, "pods") destination_pods_dir = deployment_dir_path.joinpath("pods")
os.mkdir(destination_pods_dir) os.mkdir(destination_pods_dir)
data_dir = Path(__file__).absolute().parent.parent.joinpath("data") data_dir = Path(__file__).absolute().parent.parent.joinpath("data")
yaml = get_yaml() yaml = get_yaml()
@ -340,12 +356,12 @@ def create(ctx, spec_file, deployment_dir, network_dir, initial_peers):
pod_file_path = get_pod_file_path(parsed_stack, pod) pod_file_path = get_pod_file_path(parsed_stack, pod)
parsed_pod_file = yaml.load(open(pod_file_path, "r")) parsed_pod_file = yaml.load(open(pod_file_path, "r"))
extra_config_dirs = _find_extra_config_dirs(parsed_pod_file, pod) extra_config_dirs = _find_extra_config_dirs(parsed_pod_file, pod)
destination_pod_dir = os.path.join(destination_pods_dir, pod) destination_pod_dir = destination_pods_dir.joinpath(pod)
os.mkdir(destination_pod_dir) os.mkdir(destination_pod_dir)
if global_options(ctx).debug: if global_options(ctx).debug:
print(f"extra config dirs: {extra_config_dirs}") print(f"extra config dirs: {extra_config_dirs}")
_fixup_pod_file(parsed_pod_file, parsed_spec, destination_compose_dir) _fixup_pod_file(parsed_pod_file, parsed_spec, destination_compose_dir)
with open(os.path.join(destination_compose_dir, "docker-compose-%s.yml" % pod), "w") as output_file: with open(destination_compose_dir.joinpath("docker-compose-%s.yml" % pod), "w") as output_file:
yaml.dump(parsed_pod_file, output_file) yaml.dump(parsed_pod_file, output_file)
# Copy the config files for the pod, if any # Copy the config files for the pod, if any
config_dirs = {pod} config_dirs = {pod}
@ -353,13 +369,13 @@ def create(ctx, spec_file, deployment_dir, network_dir, initial_peers):
for config_dir in config_dirs: for config_dir in config_dirs:
source_config_dir = data_dir.joinpath("config", config_dir) source_config_dir = data_dir.joinpath("config", config_dir)
if os.path.exists(source_config_dir): if os.path.exists(source_config_dir):
destination_config_dir = os.path.join(deployment_dir, "config", config_dir) destination_config_dir = deployment_dir_path.joinpath("config", config_dir)
# If the same config dir appears in multiple pods, it may already have been copied # If the same config dir appears in multiple pods, it may already have been copied
if not os.path.exists(destination_config_dir): if not os.path.exists(destination_config_dir):
copytree(source_config_dir, destination_config_dir) copytree(source_config_dir, destination_config_dir)
# Copy the script files for the pod, if any # Copy the script files for the pod, if any
if pod_has_scripts(parsed_stack, pod): if pod_has_scripts(parsed_stack, pod):
destination_script_dir = os.path.join(destination_pod_dir, "scripts") destination_script_dir = destination_pod_dir.joinpath("scripts")
os.mkdir(destination_script_dir) os.mkdir(destination_script_dir)
script_paths = get_pod_script_paths(parsed_stack, pod) script_paths = get_pod_script_paths(parsed_stack, pod)
_copy_files_to_directory(script_paths, destination_script_dir) _copy_files_to_directory(script_paths, destination_script_dir)
@ -369,11 +385,11 @@ def create(ctx, spec_file, deployment_dir, network_dir, initial_peers):
deployment_command_context = ctx.obj deployment_command_context = ctx.obj
deployment_command_context.stack = stack_name deployment_command_context.stack = stack_name
deployment_context = DeploymentContext() deployment_context = DeploymentContext()
deployment_context.init(Path(deployment_dir)) deployment_context.init(deployment_dir_path)
# Call the deployer to generate any deployer-specific files (e.g. for kind) # Call the deployer to generate any deployer-specific files (e.g. for kind)
deployer_config_generator = getDeployerConfigGenerator(parsed_spec["deploy-to"]) deployer_config_generator = getDeployerConfigGenerator(deployment_type)
# TODO: make deployment_dir a Path above # TODO: make deployment_dir_path a Path above
deployer_config_generator.generate(Path(deployment_dir)) deployer_config_generator.generate(deployment_dir_path)
call_stack_deploy_create(deployment_context, [network_dir, initial_peers]) call_stack_deploy_create(deployment_context, [network_dir, initial_peers])

View File

@ -16,6 +16,7 @@
from pathlib import Path from pathlib import Path
from kubernetes import client, config from kubernetes import client, config
from stack_orchestrator import constants
from stack_orchestrator.deploy.deployer import Deployer, DeployerConfigGenerator from stack_orchestrator.deploy.deployer import Deployer, DeployerConfigGenerator
from stack_orchestrator.deploy.k8s.helpers import create_cluster, destroy_cluster, load_images_into_kind from stack_orchestrator.deploy.k8s.helpers import create_cluster, destroy_cluster, load_images_into_kind
from stack_orchestrator.deploy.k8s.helpers import pods_in_deployment, log_stream_from_string, generate_kind_config from stack_orchestrator.deploy.k8s.helpers import pods_in_deployment, log_stream_from_string, generate_kind_config
@ -25,6 +26,7 @@ from stack_orchestrator.opts import opts
class K8sDeployer(Deployer): class K8sDeployer(Deployer):
name: str = "k8s" name: str = "k8s"
type: str
core_api: client.CoreV1Api core_api: client.CoreV1Api
apps_api: client.AppsV1Api apps_api: client.AppsV1Api
k8s_namespace: str = "default" k8s_namespace: str = "default"
@ -32,28 +34,35 @@ class K8sDeployer(Deployer):
cluster_info : ClusterInfo cluster_info : ClusterInfo
deployment_dir: Path deployment_dir: Path
def __init__(self, deployment_dir, compose_files, compose_project_name, compose_env_file) -> None: def __init__(self, type, deployment_dir, compose_files, compose_project_name, compose_env_file) -> None:
if (opts.o.debug): if (opts.o.debug):
print(f"Deployment dir: {deployment_dir}") print(f"Deployment dir: {deployment_dir}")
print(f"Compose files: {compose_files}") print(f"Compose files: {compose_files}")
print(f"Project name: {compose_project_name}") print(f"Project name: {compose_project_name}")
print(f"Env file: {compose_env_file}") print(f"Env file: {compose_env_file}")
print(f"Type: {type}")
self.type = type
self.deployment_dir = deployment_dir self.deployment_dir = deployment_dir
self.kind_cluster_name = compose_project_name self.kind_cluster_name = compose_project_name
self.cluster_info = ClusterInfo() self.cluster_info = ClusterInfo()
self.cluster_info.int(compose_files, compose_env_file) self.cluster_info.int(compose_files, compose_env_file)
def connect_api(self): def connect_api(self):
if self.is_kind():
config.load_kube_config(context=f"kind-{self.kind_cluster_name}") config.load_kube_config(context=f"kind-{self.kind_cluster_name}")
else:
# Get the config file and pass to load_kube_config()
config.load_kube_config(config_file=self.deployment_dir.joinpath(constants.kube_config_filename).as_posix())
self.core_api = client.CoreV1Api() self.core_api = client.CoreV1Api()
self.apps_api = client.AppsV1Api() self.apps_api = client.AppsV1Api()
def up(self, detach, services): def up(self, detach, services):
if self.is_kind():
# Create the kind cluster # Create the kind cluster
create_cluster(self.kind_cluster_name, self.deployment_dir.joinpath("kind-config.yml")) create_cluster(self.kind_cluster_name, self.deployment_dir.joinpath(constants.kind_config_filename))
self.connect_api()
# Ensure the referenced containers are copied into kind # Ensure the referenced containers are copied into kind
load_images_into_kind(self.kind_cluster_name, self.cluster_info.image_set) load_images_into_kind(self.kind_cluster_name, self.cluster_info.image_set)
self.connect_api()
# Create the host-path-mounted PVs for this deployment # Create the host-path-mounted PVs for this deployment
pvs = self.cluster_info.get_pvs() pvs = self.cluster_info.get_pvs()
@ -89,6 +98,7 @@ class K8sDeployer(Deployer):
def down(self, timeout, volumes): def down(self, timeout, volumes):
# Delete the k8s objects # Delete the k8s objects
if self.is_kind():
# Destroy the kind cluster # Destroy the kind cluster
destroy_cluster(self.kind_cluster_name) destroy_cluster(self.kind_cluster_name)
@ -124,20 +134,26 @@ class K8sDeployer(Deployer):
# We need to figure out how to do this -- check why we're being called first # We need to figure out how to do this -- check why we're being called first
pass pass
def is_kind(self):
return self.type == "k8s-kind"
class K8sDeployerConfigGenerator(DeployerConfigGenerator): class K8sDeployerConfigGenerator(DeployerConfigGenerator):
config_file_name: str = "kind-config.yml" type: str
def __init__(self) -> None: def __init__(self, type: str) -> None:
self.type = type
super().__init__() super().__init__()
def generate(self, deployment_dir: Path): def generate(self, deployment_dir: Path):
# No need to do this for the remote k8s case
if self.type == "k8s-kind":
# Check the file isn't already there # Check the file isn't already there
# Get the config file contents # Get the config file contents
content = generate_kind_config(deployment_dir) content = generate_kind_config(deployment_dir)
if opts.o.debug: if opts.o.debug:
print(f"kind config is: {content}") print(f"kind config is: {content}")
config_file = deployment_dir.joinpath(self.config_file_name) config_file = deployment_dir.joinpath(constants.kind_config_filename)
# Write the file # Write the file
with open(config_file, "w") as output_file: with open(config_file, "w") as output_file:
output_file.write(content) output_file.write(content)

View File

@ -21,7 +21,7 @@ mkdir -p $CERC_REPO_BASE_DIR
# Test basic stack-orchestrator deploy # Test basic stack-orchestrator deploy
test_deployment_dir=$CERC_REPO_BASE_DIR/test-deployment-dir test_deployment_dir=$CERC_REPO_BASE_DIR/test-deployment-dir
test_deployment_spec=$CERC_REPO_BASE_DIR/test-deployment-spec.yml test_deployment_spec=$CERC_REPO_BASE_DIR/test-deployment-spec.yml
$TEST_TARGET_SO --stack test deploy --deploy-to k8s init --output $test_deployment_spec --config CERC_TEST_PARAM_1=PASSED $TEST_TARGET_SO --stack test deploy --deploy-to k8s-kind init --output $test_deployment_spec --config CERC_TEST_PARAM_1=PASSED
# Check the file now exists # Check the file now exists
if [ ! -f "$test_deployment_spec" ]; then if [ ! -f "$test_deployment_spec" ]; then
echo "deploy init test: spec file not present" echo "deploy init test: spec file not present"