Add generated kind config #623

Merged
telackey merged 7 commits from dboreham/kind-config into main 2023-11-06 06:21:54 +00:00
7 changed files with 172 additions and 17 deletions

View File

@ -13,8 +13,9 @@
# 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 pathlib import Path
from python_on_whales import DockerClient, DockerException from python_on_whales import DockerClient, DockerException
from app.deploy.deployer import Deployer, DeployerException from app.deploy.deployer import Deployer, DeployerException, DeployerConfigGenerator
class DockerDeployer(Deployer): class DockerDeployer(Deployer):
@ -65,3 +66,14 @@ class DockerDeployer(Deployer):
return self.docker.run(image=image, command=command, user=user, volumes=volumes, entrypoint=entrypoint) return self.docker.run(image=image, command=command, user=user, volumes=volumes, entrypoint=entrypoint)
except DockerException as e: except DockerException as e:
raise DeployerException(e) raise DeployerException(e)
class DockerDeployerConfigGenerator(DeployerConfigGenerator):
config_file_name: str = "kind-config.yml"
def __init__(self) -> None:
super().__init__()
# Nothing needed at present for the docker deployer
def generate(self, deployment_dir: Path):
pass

View File

@ -14,6 +14,7 @@
# 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 abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path
class Deployer(ABC): class Deployer(ABC):
@ -50,3 +51,10 @@ class Deployer(ABC):
class DeployerException(Exception): class DeployerException(Exception):
def __init__(self, *args: object) -> None: def __init__(self, *args: object) -> None:
super().__init__(*args) super().__init__(*args)
class DeployerConfigGenerator(ABC):
@abstractmethod
def generate(self, deployment_dir: Path):
pass

View File

@ -13,11 +13,20 @@
# 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 app.deploy.k8s.deploy_k8s import K8sDeployer from app.deploy.k8s.deploy_k8s import K8sDeployer, K8sDeployerConfigGenerator
from app.deploy.compose.deploy_docker import DockerDeployer from app.deploy.compose.deploy_docker import DockerDeployer, DockerDeployerConfigGenerator
def getDeployer(type, compose_files, compose_project_name, compose_env_file): def getDeployerConfigGenerator(type: str):
if type == "compose" or type is None:
return DockerDeployerConfigGenerator()
elif type == "k8s":
return K8sDeployerConfigGenerator()
else:
print(f"ERROR: deploy-to {type} is not valid")
def getDeployer(type: str, compose_files, compose_project_name, compose_env_file):
if type == "compose" or type is None: if type == "compose" or type is None:
return DockerDeployer(compose_files, compose_project_name, compose_env_file) return DockerDeployer(compose_files, compose_project_name, compose_env_file)
elif type == "k8s": elif type == "k8s":

View File

@ -24,6 +24,7 @@ import sys
from app.util import (get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config, global_options, get_yaml, from app.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, get_pod_script_paths, get_plugin_code_paths) get_pod_list, get_pod_file_path, pod_has_scripts, get_pod_script_paths, get_plugin_code_paths)
from app.deploy.deploy_types import DeploymentContext, DeployCommandContext, LaconicStackSetupCommand from app.deploy.deploy_types import DeploymentContext, DeployCommandContext, LaconicStackSetupCommand
from app.deploy.deployer_factory import getDeployerConfigGenerator
def _make_default_deployment_dir(): def _make_default_deployment_dir():
@ -366,6 +367,10 @@ 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(Path(deployment_dir), deployment_command_context) deployment_context = DeploymentContext(Path(deployment_dir), deployment_command_context)
# Call the deployer to generate any deployer-specific files (e.g. for kind)
deployer_config_generator = getDeployerConfigGenerator(parsed_spec["deploy-to"])
# TODO: make deployment_dir a Path above
deployer_config_generator.generate(Path(deployment_dir))
call_stack_deploy_create(deployment_context, [network_dir, initial_peers]) call_stack_deploy_create(deployment_context, [network_dir, initial_peers])

View File

@ -17,8 +17,8 @@ from kubernetes import client
from typing import Any, List, Set from typing import Any, List, Set
from app.opts import opts from app.opts import opts
from app.util import get_yaml
from app.deploy.k8s.helpers import named_volumes_from_pod_files, volume_mounts_for_service, volumes_for_pod_files from app.deploy.k8s.helpers import named_volumes_from_pod_files, volume_mounts_for_service, volumes_for_pod_files
from app.deploy.k8s.helpers import parsed_pod_files_map_from_file_names
class ClusterInfo: class ClusterInfo:
@ -31,12 +31,7 @@ class ClusterInfo:
pass pass
def int_from_pod_files(self, pod_files: List[str]): def int_from_pod_files(self, pod_files: List[str]):
for pod_file in pod_files: self.parsed_pod_yaml_map = parsed_pod_files_map_from_file_names(pod_files)
with open(pod_file, "r") as pod_file_descriptor:
parsed_pod_file = get_yaml().load(pod_file_descriptor)
self.parsed_pod_yaml_map[pod_file] = parsed_pod_file
if opts.o.debug:
print(f"parsed_pod_yaml_map: {self.parsed_pod_yaml_map}")
# Find the set of images in the pods # Find the set of images in the pods
for pod_name in self.parsed_pod_yaml_map: for pod_name in self.parsed_pod_yaml_map:
pod = self.parsed_pod_yaml_map[pod_name] pod = self.parsed_pod_yaml_map[pod_name]

View File

@ -13,11 +13,12 @@
# 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 pathlib import Path
from kubernetes import client, config from kubernetes import client, config
from app.deploy.deployer import Deployer from app.deploy.deployer import Deployer, DeployerConfigGenerator
from app.deploy.k8s.helpers import create_cluster, destroy_cluster, load_images_into_kind from app.deploy.k8s.helpers import create_cluster, destroy_cluster, load_images_into_kind
from app.deploy.k8s.helpers import pods_in_deployment, log_stream_from_string from app.deploy.k8s.helpers import pods_in_deployment, log_stream_from_string, generate_kind_config
from app.deploy.k8s.cluster_info import ClusterInfo from app.deploy.k8s.cluster_info import ClusterInfo
from app.opts import opts from app.opts import opts
@ -46,7 +47,8 @@ class K8sDeployer(Deployer):
def up(self, detach, services): def up(self, detach, services):
# Create the kind cluster # Create the kind cluster
create_cluster(self.kind_cluster_name) # HACK: pass in the config file path here
create_cluster(self.kind_cluster_name, "./test-deployment-dir/kind-config.yml")
self.connect_api() 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)
@ -108,3 +110,19 @@ class K8sDeployer(Deployer):
def run(self, image, command, user, volumes, entrypoint=None): def run(self, image, command, user, volumes, entrypoint=None):
# 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
class K8sDeployerConfigGenerator(DeployerConfigGenerator):
config_file_name: str = "kind-config.yml"
def __init__(self) -> None:
super().__init__()
def generate(self, deployment_dir: Path):
# Check the file isn't already there
# Get the config file contents
content = generate_kind_config(deployment_dir)
config_file = deployment_dir.joinpath(self.config_file_name)
# Write the file
with open(config_file, "w") as output_file:
output_file.write(content)

View File

@ -14,10 +14,12 @@
# 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 kubernetes import client from kubernetes import client
from pathlib import Path
import subprocess import subprocess
from typing import Set from typing import Any, Set
from app.opts import opts from app.opts import opts
from app.util import get_yaml
def _run_command(command: str): def _run_command(command: str):
@ -28,8 +30,8 @@ def _run_command(command: str):
print(f"Result: {result}") print(f"Result: {result}")
def create_cluster(name: str): def create_cluster(name: str, config_file: str):
_run_command(f"kind create cluster --name {name}") _run_command(f"kind create cluster --name {name} --config {config_file}")
def destroy_cluster(name: str): def destroy_cluster(name: str):
@ -102,3 +104,109 @@ def volumes_for_pod_files(parsed_pod_files):
volume = client.V1Volume(name=volume_name, persistent_volume_claim=claim) volume = client.V1Volume(name=volume_name, persistent_volume_claim=claim)
result.append(volume) result.append(volume)
return result return result
def _get_host_paths_for_volumes(parsed_pod_files):
result = {}
for pod in parsed_pod_files:
parsed_pod_file = parsed_pod_files[pod]
if "volumes" in parsed_pod_file:
volumes = parsed_pod_file["volumes"]
for volume_name in volumes.keys():
volume_definition = volumes[volume_name]
host_path = volume_definition["driver_opts"]["device"]
result[volume_name] = host_path
return result
def parsed_pod_files_map_from_file_names(pod_files):
parsed_pod_yaml_map : Any = {}
for pod_file in pod_files:
with open(pod_file, "r") as pod_file_descriptor:
parsed_pod_file = get_yaml().load(pod_file_descriptor)
parsed_pod_yaml_map[pod_file] = parsed_pod_file
if opts.o.debug:
print(f"parsed_pod_yaml_map: {parsed_pod_yaml_map}")
return parsed_pod_yaml_map
def _generate_kind_mounts(parsed_pod_files):
volume_definitions = []
volume_host_path_map = _get_host_paths_for_volumes(parsed_pod_files)
for pod in parsed_pod_files:
parsed_pod_file = parsed_pod_files[pod]
if "services" in parsed_pod_file:
services = parsed_pod_file["services"]
for service_name in services:
service_obj = services[service_name]
if "volumes" in service_obj:
volumes = service_obj["volumes"]
for mount_string in volumes:
# Looks like: test-data:/data
(volume_name, mount_path) = mount_string.split(":")
volume_definitions.append(
f" - hostPath: {volume_host_path_map[volume_name]}\n containerPath: /var/local-path-provisioner"
)
return (
"" if len(volume_definitions) == 0 else (
" extraMounts:\n"
f"{''.join(volume_definitions)}"
)
)
def _generate_kind_port_mappings(parsed_pod_files):
port_definitions = []
for pod in parsed_pod_files:
parsed_pod_file = parsed_pod_files[pod]
if "services" in parsed_pod_file:
services = parsed_pod_file["services"]
for service_name in services:
service_obj = services[service_name]
if "ports" in service_obj:
ports = service_obj["ports"]
for port_string in ports:
# TODO handle the complex cases
# Looks like: 80 or something more complicated
port_definitions.append(f" - containerPort: {port_string}\n hostPort: {port_string}")
return (
"" if len(port_definitions) == 0 else (
" extraPortMappings:\n"
f"{''.join(port_definitions)}"
)
)
# This needs to know:
# The service ports for the cluster
# The bind mounted volumes for the cluster
#
# Make ports like this:
# extraPortMappings:
# - containerPort: 80
# hostPort: 80
# # optional: set the bind address on the host
# # 0.0.0.0 is the current default
# listenAddress: "127.0.0.1"
# # optional: set the protocol to one of TCP, UDP, SCTP.
# # TCP is the default
# protocol: TCP
# Make bind mounts like this:
# extraMounts:
# - hostPath: /path/to/my/files
# containerPath: /files
def generate_kind_config(deployment_dir: Path):
compose_file_dir = deployment_dir.joinpath("compose")
# TODO: this should come from the stack file, not this way
pod_files = [p for p in compose_file_dir.iterdir() if p.is_file()]
parsed_pod_files_map = parsed_pod_files_map_from_file_names(pod_files)
port_mappings_yml = _generate_kind_port_mappings(parsed_pod_files_map)
mounts_yml = _generate_kind_mounts(parsed_pod_files_map)
return (
"kind: Cluster\n"
"apiVersion: kind.x-k8s.io/v1alpha4\n"
"nodes:\n"
"- role: control-plane\n"
f"{port_mappings_yml}\n"
f"{mounts_yml}\n"
)