diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 9248782416..db8fd04257 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -19,7 +19,6 @@ on: paths: - 'deployments/sequencer/*' - jobs: deployment: runs-on: ubuntu-latest @@ -33,4 +32,4 @@ jobs: # Synthesize the CDK8s Sequencer app. cd deployments/sequencer cdk8s synth - diff -au references/sequencer-system.k8s.yaml dist/sequencer-system.k8s.yaml + diff -aur references/* dist/* diff --git a/config/sequencer/presets/config-batcher.json b/config/sequencer/presets/config-batcher.json new file mode 100644 index 0000000000..f2bda32063 --- /dev/null +++ b/config/sequencer/presets/config-batcher.json @@ -0,0 +1,8 @@ +{ + "chain_id": "0x5", + "eth_fee_token_address": "0x6", + "strk_fee_token_address": "0x7", + "batcher_config.storage.db_config.path_prefix": "/data", + "batcher_config.storage.db_config.enforce_file_exists": false, + "sequencer_address": "0x1" +} diff --git a/config/sequencer/presets/config.json b/config/sequencer/presets/config.json new file mode 100644 index 0000000000..310df1b209 --- /dev/null +++ b/config/sequencer/presets/config.json @@ -0,0 +1,20 @@ +{ + "chain_id": "0x5", + "eth_fee_token_address": "0x6", + "strk_fee_token_address": "0x7", + "components.batcher.execution_mode": "Disabled", + "components.batcher.local_server_config.#is_none": true, + "components.consensus_manager.execution_mode": "Disabled", + "components.gateway.execution_mode": "Disabled", + "components.http_server.execution_mode": "Disabled", + "components.mempool.execution_mode": "Disabled", + "components.mempool_p2p.execution_mode": "Disabled", + "components.consensus_manager.local_server_config.#is_none": true, + "components.gateway.local_server_config.#is_none": true, + "components.http_server.local_server_config.#is_none": true, + "components.mempool.local_server_config.#is_none": true, + "components.mempool_p2p.local_server_config.#is_none": true, + "components.http_server.remote_server_config.#is_none": true, + "batcher_config.storage.db_config.enforce_file_exists": false, + "batcher_config.storage.db_config.path_prefix": "/data" +} diff --git a/deployments/sequencer/config/sequencer.py b/deployments/sequencer/config/sequencer.py index 2b2efa5992..ec941c8b1d 100644 --- a/deployments/sequencer/config/sequencer.py +++ b/deployments/sequencer/config/sequencer.py @@ -1,30 +1,28 @@ -from typing import Dict, Any +import typing import os import json import jsonschema -ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../../') -CONFIG_DIR = os.path.join(ROOT_DIR, 'config/papyrus/') +from services.objects import Config -class Config(): - def __init__(self, schema: Dict[Any, Any], config: Dict[Any, Any]): - self.schema = schema - self.config = config - - def get(self): - return self.config - - def validate(self): - pass +def load_config(config_path): + root_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../../') + config_dir = os.path.join(root_dir, 'config/sequencer/') + + return json.loads( + open(os.path.join(config_dir, config_path), 'r').read() + ) class SequencerDevConfig(Config): - def __init__(self): + def __init__(self, mount_path: str, custom_config_path: typing.Optional[str]): super().__init__( - schema=json.loads(open(os.path.join(CONFIG_DIR, 'default_config.json'), 'r').read()), - config=json.loads(open(os.path.join(CONFIG_DIR, 'presets', 'sepolia_testnet.json'), 'r').read()) - ) + schema=load_config(config_path='default_config.json'), + config=load_config(config_path='presets/config.json') if not custom_config_path else json.loads(open(os.path.abspath(custom_config_path)).read()), + mount_path=mount_path + ) + def validate(self): jsonschema.validate(self.config, schema=self.schema) diff --git a/deployments/sequencer/main.py b/deployments/sequencer/main.py index 1a16ae9124..76e9f3b6aa 100644 --- a/deployments/sequencer/main.py +++ b/deployments/sequencer/main.py @@ -1,12 +1,25 @@ #!/usr/bin/env python3 -from constructs import Construct -from cdk8s import App, Chart -from typing import Dict, Any -from services.service import Service import dataclasses +import os + +from constructs import Construct # type: ignore +from cdk8s import App, Chart, YamlOutputType # type: ignore +from typing import Dict, Any, Optional + +from services.service import Service +from config.sequencer import Config +from services.objects import * +from services import defaults + -from config.sequencer import Config, SequencerDevConfig +env = os.getenv("ENV", "dev") + + +if env == "dev": + system_preset = defaults.sequencer_dev +elif env == "prod": + system_preset = defaults.sequencer_prod @dataclasses.dataclass @@ -14,48 +27,47 @@ class SystemStructure: topology: str = "mesh" replicas: str = "2" size: str = "small" - config: Config = SequencerDevConfig() + config: Optional[Config] = None def __post_init__(self): self.config.validate() -class SequencerSystem(Chart): +class SequencerNode(Chart): def __init__( self, scope: Construct, name: str, - namespace: str, - system_structure: Dict[str, Dict[str, Any]], + namespace: str ): super().__init__( scope, name, disable_resource_name_hashes=True, namespace=namespace ) - self.mempool = Service( + self.service = Service( self, - "mempool", - image="paulbouwer/hello-kubernetes:1.7", - replicas=2, - config=system_structure.config.get(), - ) - self.batcher = Service(self, "batcher", image="ghost", container_port=2368) - self.sequencer_node = Service( - self, - "sequencer", - image="", - container_port=8082, - startup_probe_path="/monitoring/nodeVersion", - readiness_probe_path="/monitoring/ready", - liveness_probe_path="/monitoring/alive" + name, + namespace=namespace, + deployment=system_preset.deployment, + config=system_preset.config, + image=system_preset.image, + args=system_preset.args, + port_mappings=system_preset.port_mappings, + service_type=system_preset.service_type, + replicas=system_preset.replicas, + health_check=system_preset.health_check, + pvc=system_preset.pvc, + ingress=system_preset.ingress ) -app = App() -a = SequencerSystem( +app = App( + yaml_output_type=YamlOutputType.FOLDER_PER_CHART_FILE_PER_RESOURCE +) + +sequencer_node = SequencerNode( scope=app, - name="sequencer-system", - namespace="test-namespace", - system_structure=SystemStructure(), + name=system_preset.name, + namespace=system_preset.namespace ) app.synth() diff --git a/deployments/sequencer/references/sequencer-node/ConfigMap.sequencer-node-config.k8s.yaml b/deployments/sequencer/references/sequencer-node/ConfigMap.sequencer-node-config.k8s.yaml new file mode 100644 index 0000000000..b17d6f354d --- /dev/null +++ b/deployments/sequencer/references/sequencer-node/ConfigMap.sequencer-node-config.k8s.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: sequencer-node-config + namespace: default +data: + config: '{"chain_id": "0x5", "eth_fee_token_address": "0x6", "strk_fee_token_address": "0x7", "components.batcher.execution_mode": "Disabled", "components.batcher.local_server_config.#is_none": true, "components.consensus_manager.execution_mode": "Disabled", "components.gateway.execution_mode": "Disabled", "components.http_server.execution_mode": "Disabled", "components.mempool.execution_mode": "Disabled", "components.mempool_p2p.execution_mode": "Disabled", "components.consensus_manager.local_server_config.#is_none": true, "components.gateway.local_server_config.#is_none": true, "components.http_server.local_server_config.#is_none": true, "components.mempool.local_server_config.#is_none": true, "components.mempool_p2p.local_server_config.#is_none": true, "components.http_server.remote_server_config.#is_none": true, "batcher_config.storage.db_config.enforce_file_exists": false, "batcher_config.storage.db_config.path_prefix": "/data"}' diff --git a/deployments/sequencer/references/sequencer-node/Deployment.sequencer-node-deployment.k8s.yaml b/deployments/sequencer/references/sequencer-node/Deployment.sequencer-node-deployment.k8s.yaml new file mode 100644 index 0000000000..d6e132e8d6 --- /dev/null +++ b/deployments/sequencer/references/sequencer-node/Deployment.sequencer-node-deployment.k8s.yaml @@ -0,0 +1,63 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: sequencer-node-deployment + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: sequencer-node + template: + metadata: + labels: + app: sequencer-node + spec: + containers: + - args: + - --config_file + - /config/sequencer/presets/config + image: us.gcr.io/starkware-dev/sequencer-node-test:0.0.1-dev.3 + livenessProbe: + failureThreshold: 5 + httpGet: + path: /monitoring/alive + port: 8082 + periodSeconds: 10 + timeoutSeconds: 5 + name: sequencer-node-container + ports: + - containerPort: 8080 + - containerPort: 8081 + - containerPort: 8082 + readinessProbe: + failureThreshold: 5 + httpGet: + path: /monitoring/ready + port: 8082 + periodSeconds: 10 + timeoutSeconds: 5 + startupProbe: + failureThreshold: 10 + httpGet: + path: /monitoring/nodeVersion + port: 8082 + periodSeconds: 10 + timeoutSeconds: 5 + volumeMounts: + - mountPath: /config/sequencer/presets/ + name: sequencer-node-config + readOnly: true + - mountPath: /data + name: sequencer-node-data + readOnly: false + securityContext: + fsGroup: 1000 + volumes: + - configMap: + name: sequencer-node-config + name: sequencer-node-config + - name: sequencer-node-data + persistentVolumeClaim: + claimName: sequencer-node-data + readOnly: false diff --git a/deployments/sequencer/references/sequencer-node/Ingress.sequencer-node-ingress.k8s.yaml b/deployments/sequencer/references/sequencer-node/Ingress.sequencer-node-ingress.k8s.yaml new file mode 100644 index 0000000000..29b5f9413e --- /dev/null +++ b/deployments/sequencer/references/sequencer-node/Ingress.sequencer-node-ingress.k8s.yaml @@ -0,0 +1,29 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + acme.cert-manager.io/http01-edit-in-place: "true" + cert-manager.io/common-name: default.gcp-integration.sw-dev.io + cert-manager.io/issue-temporary-certificate: "true" + cert-manager.io/issuer: letsencrypt-prod + kubernetes.io/tls-acme: "true" + labels: + app: sequencer-node + name: sequencer-node-ingress + namespace: default +spec: + rules: + - host: default.gcp-integration.sw-dev.io + http: + paths: + - backend: + service: + name: sequencer-node-service + port: + number: 8082 + path: /monitoring/ + pathType: Prefix + tls: + - hosts: + - default.gcp-integration.sw-dev.io + secretName: sequencer-tls diff --git a/deployments/sequencer/references/sequencer-node/Namespace.default.k8s.yaml b/deployments/sequencer/references/sequencer-node/Namespace.default.k8s.yaml new file mode 100644 index 0000000000..51e92d9140 --- /dev/null +++ b/deployments/sequencer/references/sequencer-node/Namespace.default.k8s.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: default + namespace: default diff --git a/deployments/sequencer/references/sequencer-node/PersistentVolumeClaim.sequencer-node-data.k8s.yaml b/deployments/sequencer/references/sequencer-node/PersistentVolumeClaim.sequencer-node-data.k8s.yaml new file mode 100644 index 0000000000..f0cd80835e --- /dev/null +++ b/deployments/sequencer/references/sequencer-node/PersistentVolumeClaim.sequencer-node-data.k8s.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: sequencer-node + name: sequencer-node-data + namespace: default +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 64Gi + storageClassName: premium-rwo + volumeMode: Filesystem diff --git a/deployments/sequencer/references/sequencer-node/Service.sequencer-node-service.k8s.yaml b/deployments/sequencer/references/sequencer-node/Service.sequencer-node-service.k8s.yaml new file mode 100644 index 0000000000..0a7d1b4d88 --- /dev/null +++ b/deployments/sequencer/references/sequencer-node/Service.sequencer-node-service.k8s.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: sequencer-node-service + namespace: default +spec: + ports: + - name: http + port: 80 + targetPort: 8080 + - name: rpc + port: 8081 + targetPort: 8081 + - name: monitoring + port: 8082 + targetPort: 8082 + selector: + app: sequencer-node + type: ClusterIP diff --git a/deployments/sequencer/references/sequencer-system.k8s.yaml b/deployments/sequencer/references/sequencer-system.k8s.yaml deleted file mode 100644 index 4f2aad70bc..0000000000 --- a/deployments/sequencer/references/sequencer-system.k8s.yaml +++ /dev/null @@ -1,176 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: sequencer-system-mempool-service - namespace: test-namespace -spec: - ports: - - port: 80 - targetPort: 8082 - selector: - app: sequencer-system-mempool - type: LoadBalancer ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sequencer-system-mempool-config - namespace: test-namespace -data: - config: '{"chain_id": "SN_SEPOLIA", "starknet_url": "https://alpha-sepolia.starknet.io/", "base_layer.starknet_contract_address": "0xe2bb56ee936fd6433dc0f6e7e3b8365c906aa057"}' ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sequencer-system-mempool-deployment - namespace: test-namespace -spec: - replicas: 2 - selector: - matchLabels: - app: sequencer-system-mempool - template: - metadata: - labels: - app: sequencer-system-mempool - spec: - containers: - - image: paulbouwer/hello-kubernetes:1.7 - livenessProbe: - failureThreshold: 5 - httpGet: - path: / - port: http - periodSeconds: 5 - timeoutSeconds: 5 - name: web - ports: - - containerPort: 8082 - readinessProbe: - failureThreshold: 3 - httpGet: - path: / - port: http - periodSeconds: 10 - timeoutSeconds: 5 - startupProbe: - failureThreshold: 12 - httpGet: - path: / - port: http - periodSeconds: 10 - timeoutSeconds: 5 - volumes: - - configMap: - name: sequencer-system-mempool-config - name: sequencer-system-mempool-config ---- -apiVersion: v1 -kind: Service -metadata: - name: sequencer-system-batcher-service - namespace: test-namespace -spec: - ports: - - port: 80 - targetPort: 2368 - selector: - app: sequencer-system-batcher - type: LoadBalancer ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sequencer-system-batcher-deployment - namespace: test-namespace -spec: - replicas: 1 - selector: - matchLabels: - app: sequencer-system-batcher - template: - metadata: - labels: - app: sequencer-system-batcher - spec: - containers: - - image: ghost - livenessProbe: - failureThreshold: 5 - httpGet: - path: / - port: http - periodSeconds: 5 - timeoutSeconds: 5 - name: web - ports: - - containerPort: 2368 - readinessProbe: - failureThreshold: 3 - httpGet: - path: / - port: http - periodSeconds: 10 - timeoutSeconds: 5 - startupProbe: - failureThreshold: 12 - httpGet: - path: / - port: http - periodSeconds: 10 - timeoutSeconds: 5 ---- -apiVersion: v1 -kind: Service -metadata: - name: sequencer-system-sequencer-service - namespace: test-namespace -spec: - ports: - - port: 80 - targetPort: 8082 - selector: - app: sequencer-system-sequencer - type: LoadBalancer ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: sequencer-system-sequencer-deployment - namespace: test-namespace -spec: - replicas: 1 - selector: - matchLabels: - app: sequencer-system-sequencer - template: - metadata: - labels: - app: sequencer-system-sequencer - spec: - containers: - - image: "" - livenessProbe: - failureThreshold: 5 - httpGet: - path: /monitoring/alive - port: http - periodSeconds: 5 - timeoutSeconds: 5 - name: web - ports: - - containerPort: 8082 - readinessProbe: - failureThreshold: 3 - httpGet: - path: /monitoring/ready - port: http - periodSeconds: 10 - timeoutSeconds: 5 - startupProbe: - failureThreshold: 12 - httpGet: - path: /monitoring/nodeVersion - port: http - periodSeconds: 10 - timeoutSeconds: 5 diff --git a/deployments/sequencer/services/defaults.py b/deployments/sequencer/services/defaults.py new file mode 100644 index 0000000000..21d3ad2dfa --- /dev/null +++ b/deployments/sequencer/services/defaults.py @@ -0,0 +1,148 @@ +import os +import dataclasses + +from typing import Sequence, Optional, List +from services.objects import * +from config.sequencer import * + + +NAME = os.getenv("NAME", "sequencer-node") +NAMESPACE = os.getenv("NAMESPACE", "default") +CONFIG = os.getenv("CONFIG", "") + +@dataclasses.dataclass +class ServiceDefaults: + name: Optional[str] | None = None + namespace: Optional[str] | None = None + deployment: Optional[bool] | None = None + config: Optional[Config] | None = None + image: Optional[str] | None = None + replicas: Optional[int] = 1 + service_type: Optional[ServiceType] | None = None + port_mappings: Optional[Sequence[PortMapping]] | None = None + health_check: Optional[HealthCheck] | None = None + pvc: Optional[PersistentVolumeClaim] | None = None + ingress: Optional[Ingress] | None = None + args: Optional[List[str]] | None = None + + +sequencer_dev = ServiceDefaults( + name=NAME, + namespace=NAMESPACE, + deployment=True, + image="us.gcr.io/starkware-dev/sequencer-node-test:0.0.1-dev.3", + replicas=1, + config=SequencerDevConfig(mount_path="/config/sequencer/presets/", custom_config_path=CONFIG), + service_type=ServiceType.CLUSTER_IP, + args=["--config_file", "/config/sequencer/presets/config"], + port_mappings=[ + PortMapping(name="http", port=80, container_port=8080), + PortMapping(name="rpc", port=8081, container_port=8081), + PortMapping(name="monitoring", port=8082, container_port=8082) + ], + health_check=HealthCheck( + startup_probe=Probe(port=8082, path="/monitoring/nodeVersion", period_seconds=10, failure_threshold=10, timeout_seconds=5), + readiness_probe=Probe(port=8082, path="/monitoring/ready", period_seconds=10, failure_threshold=5, timeout_seconds=5), + liveness_probe=Probe(port=8082, path="/monitoring/alive", period_seconds=10, failure_threshold=5, timeout_seconds=5) + ), + pvc=PersistentVolumeClaim( + access_modes=["ReadWriteOnce"], + storage_class_name="premium-rwo", + volume_mode="Filesystem", + storage="64Gi", + mount_path="/data", + read_only=False + ), + ingress=Ingress( + annotations={ + "kubernetes.io/tls-acme": "true", + "cert-manager.io/common-name": f"{NAMESPACE}.gcp-integration.sw-dev.io", + "cert-manager.io/issue-temporary-certificate": "true", + "cert-manager.io/issuer": "letsencrypt-prod", + "acme.cert-manager.io/http01-edit-in-place": "true" + }, + class_name=None, + rules=[ + IngressRule( + host=f"{NAMESPACE}.gcp-integration.sw-dev.io", + paths=[ + IngressRuleHttpPath( + path="/monitoring/", + path_type="Prefix", + backend_service_name="sequencer-node-service", + backend_service_port_number=8082 + ) + ] + ) + ], + tls=[ + IngressTls( + hosts=[ + f"{NAMESPACE}.gcp-integration.sw-dev.io" + ], + secret_name="sequencer-tls" + ) + ] + ) +) + + +sequencer_prod = ServiceDefaults( + name=NAME, + namespace=NAMESPACE, + deployment=True, + image="us.gcr.io/starkware-dev/sequencer-node-test:0.0.1-dev.1", + replicas=1, + config=SequencerDevConfig(mount_path="/config/sequencer/presets/", custom_config_path=CONFIG), + service_type=ServiceType.CLUSTER_IP, + args=["--config_file", "/config/sequencer/presets/config"], + port_mappings=[ + PortMapping(name="http", port=80, container_port=8080), + PortMapping(name="rpc", port=8081, container_port=8081), + PortMapping(name="monitoring", port=8082, container_port=8082) + ], + health_check=HealthCheck( + startup_probe=Probe(port=8082, path="/monitoring/nodeVersion", period_seconds=10, failure_threshold=10, timeout_seconds=5), + readiness_probe=Probe(port=8082, path="/monitoring/ready", period_seconds=10, failure_threshold=5, timeout_seconds=5), + liveness_probe=Probe(port=8082, path="/monitoring/alive", period_seconds=10, failure_threshold=5, timeout_seconds=5) + ), + pvc=PersistentVolumeClaim( + access_modes=["ReadWriteOnce"], + storage_class_name="premium-rwo", + volume_mode="Filesystem", + storage="64Gi", + mount_path="/data", + read_only=False + ), + ingress=Ingress( + annotations={ + "kubernetes.io/tls-acme": "true", + "cert-manager.io/common-name": f"{NAMESPACE}.gcp-integration.sw-dev.io", + "cert-manager.io/issue-temporary-certificate": "true", + "cert-manager.io/issuer": "letsencrypt-prod", + "acme.cert-manager.io/http01-edit-in-place": "true" + }, + class_name=None, + rules=[ + IngressRule( + host=f"{NAMESPACE}.gcp-integration.sw-dev.io", + paths=[ + IngressRuleHttpPath( + path="/monitoring/", + path_type="Prefix", + backend_service_name="sequencer-node-service", + backend_service_port_number=8082 + ) + ] + ) + ], + tls=[ + IngressTls( + hosts=[ + f"{NAMESPACE}.gcp-integration.sw-dev.io" + ], + secret_name="sequencer-tls" + ) + ] + ) +) \ No newline at end of file diff --git a/deployments/sequencer/services/objects.py b/deployments/sequencer/services/objects.py new file mode 100644 index 0000000000..dc92088622 --- /dev/null +++ b/deployments/sequencer/services/objects.py @@ -0,0 +1,95 @@ +import dataclasses + +from typing import Optional, Dict, Any, Mapping, Sequence +from enum import Enum + + +@dataclasses.dataclass +class Namespace: + pass + + +@dataclasses.dataclass +class Probe: + port: int | str + path: str + period_seconds: int + failure_threshold: int + timeout_seconds: int + + def __post_init__(self): + assert not isinstance(self.port, (bool)), \ + "Port must be of type int or str, not bool." + + +@dataclasses.dataclass +class HealthCheck: + startup_probe: Optional[Probe] | None = None + readiness_probe: Optional[Probe] | None = None + liveness_probe: Optional[Probe] | None = None + + +@dataclasses.dataclass +class ServiceType(Enum): + CLUSTER_IP = "ClusterIP" + LOAD_BALANCER = "LoadBalancer" + NODE_PORT = "NodePort" + + +@dataclasses.dataclass +class PersistentVolumeClaim: + storage_class_name: str | None + access_modes: list[str] | None + volume_mode: str | None + storage: str | None + read_only: bool | None + mount_path: str | None + + +@dataclasses.dataclass +class Config: + schema: Dict[Any, Any] + config: Dict[Any, Any] + mount_path: str + + def get(self): + return self.config + + def validate(self): + pass + + +@dataclasses.dataclass +class PortMapping: + name: str + port: int + container_port: int + + +@dataclasses.dataclass +class IngressRuleHttpPath: + path: Optional[str] + path_type: str + backend_service_name: Optional[str] | None = None + backend_service_port_number: Optional[int] | None = None + backend_service_port_name: Optional[str] | None = None + + +@dataclasses.dataclass +class IngressRule: + host: str + paths: Sequence[IngressRuleHttpPath] + + +@dataclasses.dataclass +class IngressTls: + hosts: Sequence[str] | None = None + secret_name: str | None = None + + +@dataclasses.dataclass +class Ingress: + annotations: Mapping[str, str] | None = None + class_name: str | None = None + rules: Sequence[IngressRule] | None = None + tls: Sequence[IngressTls] | None = None diff --git a/deployments/sequencer/services/service.py b/deployments/sequencer/services/service.py index 04e4f29303..2d9fc34924 100644 --- a/deployments/sequencer/services/service.py +++ b/deployments/sequencer/services/service.py @@ -1,13 +1,16 @@ import json -from typing import Optional, Dict, Union +from typing import Optional, List from constructs import Construct -from cdk8s import Names - +from cdk8s import Names, ApiObjectMetadata from imports import k8s +from imports.com.google import cloud as google + +from services.objects import * -class Service(Construct): + +class Service(Construct): def __init__( self, scope: Construct, @@ -15,97 +18,266 @@ def __init__( *, image: str, replicas: int = 1, - port: Optional[int] = 80, - container_port: int = 8082, - config: Optional[Dict[str, str]] = None, - startup_probe_path: Optional[str] = "/", - readiness_probe_path: Optional[str] = "/", - liveness_probe_path: Optional[str] = "/" + namespace: Optional[str] = None, + service_type: Optional[ServiceType] = None, + port_mappings: Optional[Sequence[PortMapping]] = None, + deployment: Optional[bool] = False, + statefulset: Optional[bool] = False, + config: Optional[Config] = None, + health_check: Optional[HealthCheck] = None, + pvc: Optional[PersistentVolumeClaim] = None, + ingress: Optional[Ingress] = None, + args: Optional[List[str]] = None ): super().__init__(scope, id) - label = {"app": Names.to_label_value(self, include_hash=False)} - if port is not None: - k8s.KubeService( - self, - "service", - spec=k8s.ServiceSpec( - type="LoadBalancer", - ports=[ - k8s.ServicePort( - port=port, - target_port=k8s.IntOrString.from_number(container_port), - ) - ], - selector=label, - ), - ) + self.namespace = namespace + self.image = image + self.label = {"app": Names.to_label_value(self, include_hash=False)} + self.deployment = deployment + self.statefulset = statefulset + self.replicas = replicas + self.service_type = service_type + self.port_mappings = port_mappings + self.config = config + self.health_check = health_check + self.pvc = pvc + self.ingress = ingress + self.args = args + if namespace is not None: + self.get_namespace() + + if port_mappings is not None: + self.get_port_mappings() + if config is not None: - service_config = k8s.KubeConfigMap( - self, - "config", - data=dict(config=json.dumps(config)), + self.get_config_map() + + if self.deployment: + self.get_deployment() + + if ingress is not None: + self.get_ingress() + + if pvc is not None: + self.get_pvc() + + + def get_namespace(self): + return k8s.KubeNamespace( + self, + "namespace", + metadata=k8s.ObjectMeta( + name=self.namespace ) + ) + + + def get_config_map(self): + return k8s.KubeConfigMap( + self, + "configmap", + metadata=k8s.ObjectMeta( + name=f"{self.node.id}-config" + ), + data=dict(config=json.dumps(self.config.get())), + ) + - k8s.KubeDeployment( + def get_deployment(self): + return k8s.KubeDeployment( self, "deployment", spec=k8s.DeploymentSpec( - replicas=replicas, - selector=k8s.LabelSelector(match_labels=label), + replicas=self.replicas, + selector=k8s.LabelSelector(match_labels=self.label), template=k8s.PodTemplateSpec( - metadata=k8s.ObjectMeta(labels=label), + metadata=k8s.ObjectMeta(labels=self.label), spec=k8s.PodSpec( + security_context=k8s.PodSecurityContext( + fs_group=1000 + ), containers=[ k8s.Container( - name="web", - image=image, - ports=[ - k8s.ContainerPort(container_port=container_port) - ], + name=f"{self.node.id}-container", + image=self.image, + # command=["sleep", "infinity"], + args=self.args or [], + ports=[k8s.ContainerPort(container_port=port_map.container_port) for port_map in self.port_mappings or []], startup_probe=k8s.Probe( http_get=k8s.HttpGetAction( - port=k8s.IntOrString.from_string("http"), - path=startup_probe_path, + path=self.health_check.startup_probe.path, + port=k8s.IntOrString.from_string(self.health_check.startup_probe.port) + if isinstance(self.health_check.startup_probe.port, str) + else k8s.IntOrString.from_number(self.health_check.startup_probe.port) ), - period_seconds=10, - failure_threshold=12, - timeout_seconds=5, - ), + period_seconds=self.health_check.startup_probe.period_seconds, + failure_threshold=self.health_check.startup_probe.failure_threshold, + timeout_seconds=self.health_check.startup_probe.timeout_seconds + ) if self.health_check.startup_probe is not None else None, + readiness_probe=k8s.Probe( http_get=k8s.HttpGetAction( - port=k8s.IntOrString.from_string("http"), - path=readiness_probe_path, + path=self.health_check.readiness_probe.path, + port=k8s.IntOrString.from_string(self.health_check.readiness_probe.port) + if isinstance(self.health_check.readiness_probe.port, str) + else k8s.IntOrString.from_number(self.health_check.readiness_probe.port) ), - period_seconds=10, - failure_threshold=3, - timeout_seconds=5, - ), + period_seconds=self.health_check.readiness_probe.period_seconds, + failure_threshold=self.health_check.readiness_probe.failure_threshold, + timeout_seconds=self.health_check.readiness_probe.timeout_seconds + ) if self.health_check.readiness_probe is not None else None, + liveness_probe=k8s.Probe( http_get=k8s.HttpGetAction( - port=k8s.IntOrString.from_string("http"), - path=liveness_probe_path, + path=self.health_check.liveness_probe.path, + port=k8s.IntOrString.from_string(self.health_check.liveness_probe.port) + if isinstance(self.health_check.liveness_probe.port, str) + else k8s.IntOrString.from_number(self.health_check.liveness_probe.port) ), - period_seconds=5, - failure_threshold=5, - timeout_seconds=5, - ), + period_seconds=self.health_check.liveness_probe.period_seconds, + failure_threshold=self.health_check.liveness_probe.failure_threshold, + timeout_seconds=self.health_check.liveness_probe.timeout_seconds + ) if self.health_check.liveness_probe is not None else None, + + volume_mounts=[ + mount for mount in [ + k8s.VolumeMount( + name=f"{self.node.id}-config", + mount_path=self.config.mount_path, + read_only=True + ) if self.config is not None else None, + + k8s.VolumeMount( + name=f"{self.node.id}-data", + mount_path=self.pvc.mount_path, + read_only=self.pvc.read_only, + ) if self.pvc is not None else None + ] if mount is not None + ] ) ], - volumes=( - [ + volumes=[ + vol for vol in [ k8s.Volume( - name=service_config.name, - config_map=k8s.ConfigMapVolumeSource( - name=service_config.name - ), - ) - ] - if config is not None - else None - ), + name=f"{self.node.id}-config", + config_map=k8s.ConfigMapVolumeSource(name=f"{self.node.id}-config") + ) if self.config is not None else None, + + k8s.Volume( + name=f"{self.node.id}-data", + persistent_volume_claim=k8s.PersistentVolumeClaimVolumeSource(claim_name=f"{self.node.id}-data", read_only=self.pvc.read_only) + ) if self.pvc is not None else None + ] if vol is not None + ] ), ), ), ) + + def get_backend_config(self): + return google.BackendConfig( + self, + "backendconfig", + metadata=ApiObjectMetadata( + name=f"{self.node.id}-backendconfig", + labels=self.label + ), + spec=google.BackendConfigSpec( + health_check=google.BackendConfigSpecHealthCheck( + check_interval_sec=5, + healthy_threshold=10, + unhealthy_threshold=5, + timeout_sec=5, + request_path="/", + type="http" + ), + iap=google.BackendConfigSpecIap( + enabled=True, + oauthclient_credentials=google.BackendConfigSpecIapOauthclientCredentials( + secret_name="" + ) + ) + ) + ) + + def get_ingress(self): + return k8s.KubeIngress( + self, + "ingress", + metadata=k8s.ObjectMeta( + name=f"{self.node.id}-ingress", + labels=self.label, + annotations=self.ingress.annotations + ), + spec=k8s.IngressSpec( + ingress_class_name=self.ingress.class_name, + tls=[ + k8s.IngressTls( + hosts=tls.hosts, + secret_name=tls.secret_name + ) + for tls in self.ingress.tls or [] + ], + rules=[ + k8s.IngressRule( + host=rule.host, + http=k8s.HttpIngressRuleValue( + paths=[ + k8s.HttpIngressPath( + path=path.path, + path_type=path.path_type, + backend=k8s.IngressBackend( + service=k8s.IngressServiceBackend( + name=path.backend_service_name, + port=k8s.ServiceBackendPort( + number=path.backend_service_port_number + ) + ) + ) + ) + for path in rule.paths or [] + ] + ) + ) + for rule in self.ingress.rules or [] + ] + ) + ) + + def get_pvc(self): + k8s.KubePersistentVolumeClaim( + self, + "pvc", + metadata=k8s.ObjectMeta( + name=f"{self.node.id}-data", + labels=self.label + ), + spec=k8s.PersistentVolumeClaimSpec( + storage_class_name=self.pvc.storage_class_name, + access_modes=self.pvc.access_modes, + volume_mode=self.pvc.volume_mode, + resources=k8s.ResourceRequirements( + requests={"storage": k8s.Quantity.from_string(self.pvc.storage)} + ) + ) + ) + + def get_port_mappings(self): + return k8s.KubeService( + self, + "service", + spec=k8s.ServiceSpec( + type=self.service_type.value if self.service_type is not None else None, + ports=[ + k8s.ServicePort( + name=port_map.name, + port=port_map.port, + target_port=k8s.IntOrString.from_number(port_map.container_port), + ) for port_map in self.port_mappings + ], + selector=self.label + ) + ) +