Compare commits

..
Author SHA1 Message Date
dboreham e989368793 Add generated kind config (#623) 2023-11-05 23:21:53 -07:00
dboreham 0f93d30d54 Basic volume support (#622) 2023-11-03 17:02:13 -06:00
11 changed files with 272 additions and 103 deletions
-30
View File
@@ -1,30 +0,0 @@
name: Fixturenet-Plugeth Test
on:
push:
branches: '*'
paths:
- '!**'
- '.github/workflows/triggers/fixturenet-plugeth-test'
jobs:
test:
name: "Run fixturenet-plugeth test suite"
runs-on: ubuntu-latest
steps:
- name: "Clone project repository"
uses: actions/checkout@v3
- name: "Install Python"
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: "Print Python version"
run: python3 --version
- name: "Install shiv"
run: pip install shiv
- name: "Generate build version file"
run: ./scripts/create_build_tag_file.sh
- name: "Build local shiv package"
run: ./scripts/build_shiv_package.sh
- name: "Run fixturenet-plugeth tests"
run: ./tests/fixturenet-plugeth/run-test.sh
@@ -1,3 +0,0 @@
Change this file to trigger running the fixturenet-plugeth-test CI job
trigger
+13 -1
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
+1 -1
View File
@@ -151,7 +151,7 @@ def exec_operation(ctx, extra_args):
if global_context.verbose: if global_context.verbose:
print(f"Running compose exec {service_name} {command_to_exec}") print(f"Running compose exec {service_name} {command_to_exec}")
try: try:
ctx.obj.deployer.execute(service_name, command_to_exec, tty=True, envs=container_exec_env) ctx.obj.deployer.execute(service_name, command_to_exec, envs=container_exec_env)
except DeployerException: except DeployerException:
print("container command returned error exit status") print("container command returned error exit status")
+8
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
+12 -3
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":
+5
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])
+29 -8
View File
@@ -17,7 +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 parsed_pod_files_map_from_file_names
class ClusterInfo: class ClusterInfo:
@@ -30,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]
@@ -47,6 +43,28 @@ class ClusterInfo:
if opts.o.debug: if opts.o.debug:
print(f"image_set: {self.image_set}") print(f"image_set: {self.image_set}")
def get_pvcs(self):
result = []
volumes = named_volumes_from_pod_files(self.parsed_pod_yaml_map)
if opts.o.debug:
print(f"Volumes: {volumes}")
for volume_name in volumes:
spec = client.V1PersistentVolumeClaimSpec(
storage_class_name="standard",
access_modes=["ReadWriteOnce"],
resources=client.V1ResourceRequirements(
requests={"storage": "2Gi"}
)
)
pvc = client.V1PersistentVolumeClaim(
metadata=client.V1ObjectMeta(name=volume_name,
labels={"volume-label": volume_name}),
spec=spec,
)
result.append(pvc)
return result
# to suit the deployment, and also annotate the container specs to point at said volumes
def get_deployment(self): def get_deployment(self):
containers = [] containers = []
for pod_name in self.parsed_pod_yaml_map: for pod_name in self.parsed_pod_yaml_map:
@@ -56,19 +74,22 @@ class ClusterInfo:
container_name = service_name container_name = service_name
service_info = services[service_name] service_info = services[service_name]
image = service_info["image"] image = service_info["image"]
volume_mounts = volume_mounts_for_service(self.parsed_pod_yaml_map, service_name)
container = client.V1Container( container = client.V1Container(
name=container_name, name=container_name,
image=image, image=image,
ports=[client.V1ContainerPort(container_port=80)], ports=[client.V1ContainerPort(container_port=80)],
volume_mounts=volume_mounts,
resources=client.V1ResourceRequirements( resources=client.V1ResourceRequirements(
requests={"cpu": "100m", "memory": "200Mi"}, requests={"cpu": "100m", "memory": "200Mi"},
limits={"cpu": "500m", "memory": "500Mi"}, limits={"cpu": "500m", "memory": "500Mi"},
), ),
) )
containers.append(container) containers.append(container)
volumes = volumes_for_pod_files(self.parsed_pod_yaml_map)
template = client.V1PodTemplateSpec( template = client.V1PodTemplateSpec(
metadata=client.V1ObjectMeta(labels={"app": self.app_name}), metadata=client.V1ObjectMeta(labels={"app": self.app_name}),
spec=client.V1PodSpec(containers=containers), spec=client.V1PodSpec(containers=containers, volumes=volumes),
) )
spec = client.V1DeploymentSpec( spec = client.V1DeploymentSpec(
replicas=1, template=template, selector={ replicas=1, template=template, selector={
+39 -10
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
@@ -26,6 +27,7 @@ class K8sDeployer(Deployer):
name: str = "k8s" name: str = "k8s"
core_api: client.CoreV1Api core_api: client.CoreV1Api
apps_api: client.AppsV1Api apps_api: client.AppsV1Api
k8s_namespace: str = "default"
kind_cluster_name: str kind_cluster_name: str
cluster_info : ClusterInfo cluster_info : ClusterInfo
@@ -45,21 +47,32 @@ 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)
# Figure out the PVCs for this deployment
pvcs = self.cluster_info.get_pvcs()
for pvc in pvcs:
if opts.o.debug:
print(f"Sending this: {pvc}")
pvc_resp = self.core_api.create_namespaced_persistent_volume_claim(body=pvc, namespace=self.k8s_namespace)
if opts.o.debug:
print("PVCs created:")
print(f"{pvc_resp}")
# Process compose files into a Deployment # Process compose files into a Deployment
deployment = self.cluster_info.get_deployment() deployment = self.cluster_info.get_deployment()
# Create the k8s objects # Create the k8s objects
resp = self.apps_api.create_namespaced_deployment(
body=deployment, namespace="default"
)
if opts.o.debug: if opts.o.debug:
print("Deployment created.\n") print(f"Sending this: {deployment}")
print(f"{resp.metadata.namespace} {resp.metadata.name} \ deployment_resp = self.apps_api.create_namespaced_deployment(
{resp.metadata.generation} {resp.spec.template.spec.containers[0].image}") body=deployment, namespace=self.k8s_namespace
)
if opts.o.debug:
print("Deployment created:")
print(f"{deployment_resp.metadata.namespace} {deployment_resp.metadata.name} \
{deployment_resp.metadata.generation} {deployment_resp.spec.template.spec.containers[0].image}")
def down(self, timeout, volumes): def down(self, timeout, volumes):
# Delete the k8s objects # Delete the k8s objects
@@ -97,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)
+158 -3
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):
@@ -55,3 +57,156 @@ def pods_in_deployment(core_api: client.CoreV1Api, deployment_name: str):
def log_stream_from_string(s: str): def log_stream_from_string(s: str):
# Note response has to be UTF-8 encoded because the caller expects to decode it # Note response has to be UTF-8 encoded because the caller expects to decode it
yield ("ignore", s.encode()) yield ("ignore", s.encode())
def named_volumes_from_pod_files(parsed_pod_files):
# Parse the compose files looking for named volumes
named_volumes = []
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 in volumes.keys():
# Volume definition looks like:
# 'laconicd-data': None
named_volumes.append(volume)
return named_volumes
def volume_mounts_for_service(parsed_pod_files, service):
result = []
# Find the service
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:
if service_name == service:
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_device = client.V1VolumeMount(mount_path=mount_path, name=volume_name)
result.append(volume_device)
return result
def volumes_for_pod_files(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():
claim = client.V1PersistentVolumeClaimVolumeSource(claim_name=volume_name)
volume = client.V1Volume(name=volume_name, persistent_volume_claim=claim)
result.append(volume)
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"
)
+7 -44
View File
@@ -24,58 +24,21 @@ echo "Deploying the cluster"
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy up $TEST_TARGET_SO --stack $CERC_STACK_NAME deploy up
# Verify that the fixturenet is up and running # Verify that the fixturenet is up and running
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy ps $TEST_TARGET_SO --stack $CERC_STACK_NAME deploy ps
# $TEST_TARGET_SO --stack $CERC_STACK_NAME deploy exec fixturenet-eth-bootnode-lighthouse /scripts/status-internal.sh $TEST_TARGET_SO --stack $CERC_STACK_NAME deploy exec fixturenet-eth-bootnode-lighthouse /scripts/status-internal.sh
initial_block_number=$($TEST_TARGET_SO --stack fixturenet-plugeth-tx deploy exec foundry "cast block-number")
timeout=900 # 15 minutes # Check that the block number increases some time later
echo "$(date +"%Y-%m-%d %T"): Getting initial block number. Timeout set to $timeout seconds" sleep 12
start_time=$(date +%s) subsequent_block_number=$($TEST_TARGET_SO --stack $CERC_STACK_NAME deploy exec foundry "cast block-number")
elapsed_time=0
initial_block_number=0
while [ "$initial_block_number" -eq 0 ] && [ $elapsed_time -lt $timeout ]; do
sleep 10
echo "$(date +"%Y-%m-%d %T"): Waiting for initial block..."
initial_block_number=$($TEST_TARGET_SO --stack $CERC_STACK_NAME deploy exec foundry "cast block-number")
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
done
subsequent_block_number=$initial_block_number
# if initial block was 0 after timeout, assume chain did not start successfully and skip finding subsequent block
if [[ $initial_block_number -gt 0 ]]; then
timeout=300
echo "$(date +"%Y-%m-%d %T"): Getting subsequent block number. Timeout set to $timeout seconds"
start_time=$(date +%s)
elapsed_time=0
# wait for 5 blocks or timeout
while [ "$subsequent_block_number" -le $((initial_block_number + 5)) ] && [ $elapsed_time -lt $timeout ]; do
sleep 10
echo "$(date +"%Y-%m-%d %T"): Waiting for five blocks or $timeout seconds..."
subsequent_block_number=$($TEST_TARGET_SO --stack $CERC_STACK_NAME deploy exec foundry "cast block-number")
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
done
fi
# will return 0 if either of the above loops timed out
block_number_difference=$((subsequent_block_number - initial_block_number)) block_number_difference=$((subsequent_block_number - initial_block_number))
echo "$(date +"%Y-%m-%d %T"): Results of block height queries:"
echo "Initial block height: $initial_block_number"
echo "Subsequent block height: $subsequent_block_number"
# Block height difference should be between 1 and some small number # Block height difference should be between 1 and some small number
if [[ $block_number_difference -gt 1 && $block_number_difference -lt 100 ]]; then if [[ $block_number_difference -gt 1 && $block_number_difference -lt 10 ]]; then
echo "Test passed" echo "Test passed"
test_result=0 test_result=0
else else
echo "Test failed: block numbers were ${initial_block_number} and ${subsequent_block_number}" echo "Test failed: block numbers were ${initial_block_number} and ${subsequent_block_number}"
echo "Logs from stack:"
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy logs
test_result=1 test_result=1
fi fi
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy down
$TEST_TARGET_SO --stack $CERC_STACK_NAME deploy down --delete-volumes
echo "Removing cloned repositories" echo "Removing cloned repositories"
rm -rf $CERC_REPO_BASE_DIR rm -rf $CERC_REPO_BASE_DIR
exit $test_result exit $test_result