diff --git a/stack_orchestrator/constants.py b/stack_orchestrator/constants.py index bb809404..aee36ad8 100644 --- a/stack_orchestrator/constants.py +++ b/stack_orchestrator/constants.py @@ -34,5 +34,6 @@ volumes_key = "volumes" security_key = "security" annotations_key = "annotations" labels_key = "labels" +replicas_key = "replicas" kind_config_filename = "kind-config.yml" kube_config_filename = "kubeconfig.yml" diff --git a/stack_orchestrator/deploy/k8s/cluster_info.py b/stack_orchestrator/deploy/k8s/cluster_info.py index 7c696691..1e17d5cc 100644 --- a/stack_orchestrator/deploy/k8s/cluster_info.py +++ b/stack_orchestrator/deploy/k8s/cluster_info.py @@ -78,28 +78,38 @@ class ClusterInfo: if (opts.o.debug): print(f"Env vars: {self.environment_variables.map}") - def get_nodeport(self): + def get_nodeports(self): + nodeports = [] for pod_name in self.parsed_pod_yaml_map: pod = self.parsed_pod_yaml_map[pod_name] services = pod["services"] for service_name in services: service_info = services[service_name] if "ports" in service_info: - port = int(service_info["ports"][0]) - if opts.o.debug: - print(f"service port: {port}") - service = client.V1Service( - metadata=client.V1ObjectMeta(name=f"{self.app_name}-nodeport"), - spec=client.V1ServiceSpec( - type="NodePort", - ports=[client.V1ServicePort( - port=port, - target_port=port - )], - selector={"app": self.app_name} - ) - ) - return service + for raw_port in service_info["ports"]: + if opts.o.debug: + print(f"service port: {raw_port}") + if ":" in raw_port: + parts = raw_port.split(":", 2) + node_port = int(parts[0]) + pod_port = int(parts[1]) + else: + node_port = None + pod_port = int(raw_port) + service = client.V1Service( + metadata=client.V1ObjectMeta(name=f"{self.app_name}-nodeport-{pod_port}"), + spec=client.V1ServiceSpec( + type="NodePort", + ports=[client.V1ServicePort( + port=pod_port, + target_port=pod_port, + node_port=node_port + )], + selector={"app": self.app_name} + ) + ) + nodeports.append(service) + return nodeports def get_ingress(self, use_tls=False, certificate=None, cluster_issuer="letsencrypt-prod"): # No ingress for a deployment that has no http-proxy defined, for now @@ -373,9 +383,12 @@ class ClusterInfo: spec=client.V1PodSpec(containers=containers, image_pull_secrets=image_pull_secrets, volumes=volumes), ) spec = client.V1DeploymentSpec( - replicas=1, template=template, selector={ + replicas=self.spec.get_replicas(), + template=template, selector={ "matchLabels": - {"app": self.app_name}}) + {"app": self.app_name} + } + ) deployment = client.V1Deployment( api_version="apps/v1", diff --git a/stack_orchestrator/deploy/k8s/deploy_k8s.py b/stack_orchestrator/deploy/k8s/deploy_k8s.py index 5781cd26..7e6ec152 100644 --- a/stack_orchestrator/deploy/k8s/deploy_k8s.py +++ b/stack_orchestrator/deploy/k8s/deploy_k8s.py @@ -246,8 +246,8 @@ class K8sDeployer(Deployer): if opts.o.debug: print("No ingress configured") - nodeport: client.V1Service = self.cluster_info.get_nodeport() - if nodeport: + nodeports: List[client.V1Service] = self.cluster_info.get_nodeports() + for nodeport in nodeports: if opts.o.debug: print(f"Sending this nodeport: {nodeport}") if not opts.o.dry_run: @@ -342,10 +342,10 @@ class K8sDeployer(Deployer): if opts.o.debug: print("No ingress to delete") - nodeport: client.V1Service = self.cluster_info.get_nodeport() - if nodeport: + nodeports: List[client.V1Service] = self.cluster_info.get_nodeports() + for nodeport in nodeports: if opts.o.debug: - print(f"Deleting this nodeport: {ingress}") + print(f"Deleting this nodeport: {nodeport}") try: self.core_api.delete_namespaced_service( namespace=self.k8s_namespace, diff --git a/stack_orchestrator/deploy/spec.py b/stack_orchestrator/deploy/spec.py index cbec8ae5..e8d293e3 100644 --- a/stack_orchestrator/deploy/spec.py +++ b/stack_orchestrator/deploy/spec.py @@ -117,6 +117,9 @@ class Spec: def get_annotations(self): return self.obj.get(constants.annotations_key, {}) + def get_replicas(self): + return self.obj.get(constants.replicas_key, 1) + def get_labels(self): return self.obj.get(constants.labels_key, {}) diff --git a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py index 9031e798..e29eb6d5 100644 --- a/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py +++ b/stack_orchestrator/deploy/webapp/deploy_webapp_from_registry.py @@ -45,6 +45,7 @@ def process_app_deployment_request( image_registry, force_rebuild, fqdn_policy, + recreate_on_deploy, logger ): logger.log("BEGIN - process_app_deployment_request") @@ -66,8 +67,8 @@ def process_app_deployment_request( fqdn = f"{requested_name}.{default_dns_suffix}" # 3. check ownership of existing dnsrecord vs this request - dns_lrn = f"{dns_record_namespace}/{fqdn}" - dns_record = laconic.get_record(dns_lrn) + dns_crn = f"{dns_record_namespace}/{fqdn}" + dns_record = laconic.get_record(dns_crn) if dns_record: matched_owner = match_owner(app_deployment_request, dns_record) if not matched_owner and dns_record.attributes.request: @@ -77,9 +78,9 @@ def process_app_deployment_request( logger.log(f"Matched DnsRecord ownership: {matched_owner}") else: raise Exception("Unable to confirm ownership of DnsRecord %s for request %s" % - (dns_lrn, app_deployment_request.id)) + (dns_crn, app_deployment_request.id)) elif "preexisting" == fqdn_policy: - raise Exception(f"No pre-existing DnsRecord {dns_lrn} could be found for request {app_deployment_request.id}.") + raise Exception(f"No pre-existing DnsRecord {dns_crn} could be found for request {app_deployment_request.id}.") # 4. get build and runtime config from request env_filename = None @@ -90,14 +91,14 @@ def process_app_deployment_request( file.write("%s=%s\n" % (k, shlex.quote(str(v)))) # 5. determine new or existing deployment - # a. check for deployment lrn - app_deployment_lrn = f"{deployment_record_namespace}/{fqdn}" + # a. check for deployment crn + app_deployment_crn = f"{deployment_record_namespace}/{fqdn}" if app_deployment_request.attributes.deployment: - app_deployment_lrn = app_deployment_request.attributes.deployment - if not app_deployment_lrn.startswith(deployment_record_namespace): + app_deployment_crn = app_deployment_request.attributes.deployment + if not app_deployment_crn.startswith(deployment_record_namespace): raise Exception("Deployment CRN %s is not in a supported namespace" % app_deployment_request.attributes.deployment) - deployment_record = laconic.get_record(app_deployment_lrn) + deployment_record = laconic.get_record(app_deployment_crn) deployment_dir = os.path.join(deployment_parent_dir, fqdn) # At present we use this to generate a unique but stable ID for the app's host container # TODO: implement support to derive this transparently from the already-unique deployment id @@ -109,7 +110,7 @@ def process_app_deployment_request( if not os.path.exists(deployment_dir): if deployment_record: raise Exception("Deployment record %s exists, but not deployment dir %s. Please remove name." % - (app_deployment_lrn, deployment_dir)) + (app_deployment_crn, deployment_dir)) logger.log(f"Creating webapp deployment in: {deployment_dir} with container id: {deployment_container_tag}") deploy_webapp.create_deployment(ctx, deployment_dir, deployment_container_tag, f"https://{fqdn}", kube_config, image_registry, env_filename) @@ -165,6 +166,7 @@ def process_app_deployment_request( deploy_to_k8s( deployment_record, deployment_dir, + recreate_on_deploy, logger ) @@ -173,9 +175,9 @@ def process_app_deployment_request( laconic, app, deployment_record, - app_deployment_lrn, + app_deployment_crn, dns_record, - dns_lrn, + dns_crn, deployment_dir, app_deployment_request, logger @@ -214,18 +216,19 @@ def dump_known_requests(filename, requests, status="SEEN"): @click.option("--only-update-state", help="Only update the state file, don't process any requests anything.", is_flag=True) @click.option("--dns-suffix", help="DNS domain to use eg, laconic.servesthe.world") @click.option("--fqdn-policy", help="How to handle requests with an FQDN: prohibit, allow, preexisting", default="prohibit") -@click.option("--record-namespace-dns", help="eg, lrn://laconic/dns") -@click.option("--record-namespace-deployments", help="eg, lrn://laconic/deployments") +@click.option("--record-namespace-dns", help="eg, crn://laconic/dns") +@click.option("--record-namespace-deployments", help="eg, crn://laconic/deployments") @click.option("--dry-run", help="Don't do anything, just report what would be done.", is_flag=True) @click.option("--include-tags", help="Only include requests with matching tags (comma-separated).", default="") @click.option("--exclude-tags", help="Exclude requests with matching tags (comma-separated).", default="") @click.option("--force-rebuild", help="Rebuild even if the image already exists.", is_flag=True) +@click.option("--recreate-on-deploy", help="Remove and recreate deployments instead of updating them.", is_flag=True) @click.option("--log-dir", help="Output build/deployment logs to directory.", default=None) @click.pass_context def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_dir, # noqa: C901 request_id, discover, state_file, only_update_state, dns_suffix, fqdn_policy, record_namespace_dns, record_namespace_deployments, dry_run, - include_tags, exclude_tags, force_rebuild, log_dir): + include_tags, exclude_tags, force_rebuild, recreate_on_deploy, log_dir): if request_id and discover: print("Cannot specify both --request-id and --discover", file=sys.stderr) sys.exit(2) @@ -365,6 +368,7 @@ def command(ctx, kube_config, laconic_config, image_registry, deployment_parent_ image_registry, force_rebuild, fqdn_policy, + recreate_on_deploy, logger ) status = "DEPLOYED" diff --git a/stack_orchestrator/deploy/webapp/util.py b/stack_orchestrator/deploy/webapp/util.py index cc476656..5c5d014d 100644 --- a/stack_orchestrator/deploy/webapp/util.py +++ b/stack_orchestrator/deploy/webapp/util.py @@ -94,7 +94,7 @@ class LaconicRegistryClient: ) def list_records(self, criteria={}, all=False): - args = ["laconic", "-c", self.config_file, "registry", "record", "list"] + args = ["laconic", "-c", self.config_file, "cns", "record", "list"] if all: args.append("--all") @@ -112,13 +112,13 @@ class LaconicRegistryClient: return results - def is_lrn(self, name_or_id: str): + def is_crn(self, name_or_id: str): if name_or_id: - return str(name_or_id).startswith("lrn://") + return str(name_or_id).startswith("crn://") return False def is_id(self, name_or_id: str): - return not self.is_lrn(name_or_id) + return not self.is_crn(name_or_id) def _add_to_cache(self, records): if not records: @@ -127,8 +127,8 @@ class LaconicRegistryClient: for p in records: self.cache["name_or_id"][p.id] = p if p.names: - for lrn in p.names: - self.cache["name_or_id"][lrn] = p + for crn in p.names: + self.cache["name_or_id"][crn] = p if p.attributes.type not in self.cache: self.cache[p.attributes.type] = [] self.cache[p.attributes.type].append(p) @@ -140,7 +140,7 @@ class LaconicRegistryClient: if name in self.cache.name_or_id: return self.cache.name_or_id[name] - args = ["laconic", "-c", self.config_file, "registry", "name", "resolve", name] + args = ["laconic", "-c", self.config_file, "cns", "name", "resolve", name] parsed = [AttrDict(r) for r in json.loads(logged_cmd(self.log_file, *args))] if parsed: @@ -158,14 +158,14 @@ class LaconicRegistryClient: if name_or_id in self.cache.name_or_id: return self.cache.name_or_id[name_or_id] - if self.is_lrn(name_or_id): + if self.is_crn(name_or_id): return self.resolve(name_or_id) args = [ "laconic", "-c", self.config_file, - "registry", + "cns", "record", "get", "--id", @@ -203,16 +203,7 @@ class LaconicRegistryClient: print(open(record_fname, 'r').read(), file=self.log_file) new_record_id = json.loads( - logged_cmd( - self.log_file, - "laconic", "-c", - self.config_file, - "registry", - "record", - "publish", - "--filename", - record_fname - ) + logged_cmd(self.log_file, "laconic", "-c", self.config_file, "cns", "record", "publish", "--filename", record_fname) )["id"] for name in names: self.set_name(name, new_record_id) @@ -221,10 +212,10 @@ class LaconicRegistryClient: logged_cmd(self.log_file, "rm", "-rf", tmpdir) def set_name(self, name, record_id): - logged_cmd(self.log_file, "laconic", "-c", self.config_file, "registry", "name", "set", name, record_id) + logged_cmd(self.log_file, "laconic", "-c", self.config_file, "cns", "name", "set", name, record_id) def delete_name(self, name): - logged_cmd(self.log_file, "laconic", "-c", self.config_file, "registry", "name", "delete", name) + logged_cmd(self.log_file, "laconic", "-c", self.config_file, "cns", "name", "delete", name) def file_hash(filename): @@ -318,26 +309,33 @@ def push_container_image(deployment_dir, logger): logger.log("Finished pushing images.") -def deploy_to_k8s(deploy_record, deployment_dir, logger): - if not deploy_record: - command = "start" - else: - command = "update" - +def deploy_to_k8s(deploy_record, deployment_dir, recreate, logger): logger.log("Deploying to k8s ...") - logger.log(f"Running {command} command on deployment dir: {deployment_dir}") - result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, command], - stdout=logger.file, stderr=logger.file) - result.check_returncode() + + if recreate: + commands_to_run = ["stop", "start"] + else: + if not deploy_record: + commands_to_run = ["start"] + else: + commands_to_run = ["update"] + + for command in commands_to_run: + logger.log(f"Running {command} command on deployment dir: {deployment_dir}") + result = subprocess.run([sys.argv[0], "deployment", "--dir", deployment_dir, command], + stdout=logger.file, stderr=logger.file) + result.check_returncode() + logger.log(f"Finished {command} command on deployment dir: {deployment_dir}") + logger.log("Finished deploying to k8s.") def publish_deployment(laconic: LaconicRegistryClient, app_record, deploy_record, - deployment_lrn, + deployment_crn, dns_record, - dns_lrn, + dns_crn, deployment_dir, app_deployment_request=None, logger=None): @@ -372,7 +370,7 @@ def publish_deployment(laconic: LaconicRegistryClient, if logger: logger.log("Publishing DnsRecord.") - dns_id = laconic.publish(new_dns_record, [dns_lrn]) + dns_id = laconic.publish(new_dns_record, [dns_crn]) new_deployment_record = { "record": { @@ -393,7 +391,7 @@ def publish_deployment(laconic: LaconicRegistryClient, if logger: logger.log("Publishing ApplicationDeploymentRecord.") - deployment_id = laconic.publish(new_deployment_record, [deployment_lrn]) + deployment_id = laconic.publish(new_deployment_record, [deployment_crn]) return {"dns": dns_id, "deployment": deployment_id} @@ -402,7 +400,7 @@ def hostname_for_deployment_request(app_deployment_request, laconic): if not dns_name: app = laconic.get_record(app_deployment_request.attributes.application, require=True) dns_name = generate_hostname_for_app(app) - elif dns_name.startswith("lrn://"): + elif dns_name.startswith("crn://"): record = laconic.get_record(dns_name, require=True) dns_name = record.attributes.name return dns_name