From 1b33b1ee7062370e2490e01121dee73885c887ec Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 19:45:12 +0100 Subject: [PATCH 001/148] feat: add Terraform S3 backend templating system --- .gitignore | 46 ++++++++++++++ backend/README.md | 26 ++++++++ backend/backend.hcl | 23 +++++++ backend/generate.py | 116 +++++++++++++++++++++++++++++++++++ backend/generated/.gitignore | 2 + 5 files changed, 213 insertions(+) create mode 100644 .gitignore create mode 100644 backend/README.md create mode 100644 backend/backend.hcl create mode 100644 backend/generate.py create mode 100644 backend/generated/.gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d00545b --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Include example automated tfvars file +!template.automated.tfvars + +# Exclude pycache +__pycache__/ +**/__pycache__ + +**/.env diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..2030ce3 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,26 @@ +# Backend Terraform Configuration Generator + +This script generates Terraform backend configuration files for different components of CTFp. + +## Usage + +To generate a backend configuration file, run the script with the required arguments: + +```bash +python generate.py +``` + +It will create a backend configuration file in the `generated` directory. + +This can be used when initializing Terraform for the respective component: + +```bash +tofu init -backend-config=../backend/generated/.hcl +``` + +### Arguments + +- ``: The component for which to generate the backend configuration. Valid options are `cluster`, `ops`, `platform`, and `challenges`. +- ``: The S3 bucket name where the Terraform state will be stored. +- ``: The region of the S3 bucket. +- ``: The endpoint URL for the S3-compatible storage. diff --git a/backend/backend.hcl b/backend/backend.hcl new file mode 100644 index 0000000..18899b6 --- /dev/null +++ b/backend/backend.hcl @@ -0,0 +1,23 @@ +# BACKEND CONFIGURATION TEMPLATE FOR TERRAFORM +# This file is a template for the backend configurations located in the `generated` directory. + +key = "%%KEY%%" + +bucket = "%%S3_BUCKET%%" +region = "%%S3_REGION%%" +endpoints = { + s3 = "%%S3_ENDPOINT%%" +} + +workspace_key_prefix = "state/%%COMPONENT%%" + +# The following settings are to skip various +# aws related checks and validation +# which is not possible when using third party s3 compatible storage +skip_region_validation = true +skip_credentials_validation = true +skip_requesting_account_id = true +skip_metadata_api_check = true + +skip_s3_checksum = false +use_path_style = false diff --git a/backend/generate.py b/backend/generate.py new file mode 100644 index 0000000..3baec0d --- /dev/null +++ b/backend/generate.py @@ -0,0 +1,116 @@ +import os +import sys +import argparse + +class Args: + args = None + subcommand = False + + def __init__(self, parent_parser = None): + if parent_parser: + self.subcommand = True + self.parser = parent_parser.add_parser("generate-backend", help="Generate Terraform backend configuration") + else: + self.parser = argparse.ArgumentParser(description="Backend generator for Terraform") + + self.parser.add_argument("component", help="Component to generate backend for", choices=["cluster", "ops", "platform", "challenges"]) + self.parser.add_argument("bucket", help="S3 bucket name for Terraform state storage") + self.parser.add_argument("region", help="Region for S3 bucket") + self.parser.add_argument("endpoint", help="Endpoint URL for S3-compatible storage") + + def parse(self): + if self.subcommand: + self.args = self.parser.parse_args(sys.argv[2:]) + else: + self.args = self.parser.parse_args() + + def __getattr__(self, name): + return getattr(self.args, name) + +class Template: + component = None + bucket = None + region = None + endpoint = None + + def __init__(self, component, bucket, region, endpoint): + self.component = component + self.bucket = bucket + self.region = region + self.endpoint = endpoint + pass + + def replace(self, template_str, replacements): + for key, value in replacements.items(): + template_str = template_str.replace(f"%%{key}%%", value) + return template_str + + def get_template_path(self): + base_dir = os.path.dirname(os.path.abspath(__file__)) + template_path = os.path.join(base_dir, "backend.hcl") + return template_path + + def get_target_path(self): + base_dir = os.path.dirname(os.path.abspath(__file__)) + target_dir = os.path.join(base_dir, "generated") + if not os.path.exists(target_dir): + os.makedirs(target_dir) + target_path = os.path.join(target_dir, f"{self.component}.hcl") + return target_path + + def get_template(self): + template_path = self.get_template_path() + with open(template_path, "r") as f: + template_str = f.read() + return template_str + + def template(self) -> str: + template = self.get_template() + replacements = { + "COMPONENT": self.component, + "KEY": f"{self.component}.tfstate", + "S3_BUCKET": self.bucket, + "S3_REGION": self.region, + "S3_ENDPOINT": self.endpoint + } + output = self.replace(template, replacements) + return output + + def run(self): + backend = self.template() + target_path = self.get_target_path() + with open(target_path, "w") as f: + f.write(backend) + print(f"Generated backend file at: {target_path}") + + +class Generator: + args = None + parent_parser = None + + def __init__(self, parent_parser = None): + self.parent_parser = parent_parser + + def register_subcommand(self): + self.args = Args(self.parent_parser) + + def run(self): + if not self.args: + arguments = Args(self.parent_parser) + arguments.parse() + self.args = arguments + else: + self.args.parse() + + args = self.args + + template = Template( + component=args.component, + bucket=args.bucket, + region=args.region, + endpoint=args.endpoint + ) + template.run() + +if __name__ == "__main__": + Generator().run() \ No newline at end of file diff --git a/backend/generated/.gitignore b/backend/generated/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/backend/generated/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file From 01d269c8474b50072acd350a7193230a328a2e28 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 19:46:10 +0100 Subject: [PATCH 002/148] feat: add CTFp cluster setup --- cluster/.env.example | 2 + cluster/.gitignore | 43 + cluster/.terraform.lock.hcl | 204 ++++ cluster/README.md | 106 ++ cluster/cloudflare.tf | 103 ++ cluster/hcloud-microos-snapshots.pkr.hcl | 164 +++ cluster/keys/.gitignore | 3 + cluster/keys/create.sh | 10 + cluster/kube.tf | 1282 ++++++++++++++++++++++ cluster/providers.tf | 33 + cluster/tfvars/.gitignore | 1 + cluster/tfvars/template.tfvars | 77 ++ cluster/variables.tf | 229 ++++ 13 files changed, 2257 insertions(+) create mode 100644 cluster/.env.example create mode 100644 cluster/.gitignore create mode 100644 cluster/.terraform.lock.hcl create mode 100644 cluster/README.md create mode 100644 cluster/cloudflare.tf create mode 100644 cluster/hcloud-microos-snapshots.pkr.hcl create mode 100644 cluster/keys/.gitignore create mode 100755 cluster/keys/create.sh create mode 100644 cluster/kube.tf create mode 100644 cluster/providers.tf create mode 100644 cluster/tfvars/.gitignore create mode 100644 cluster/tfvars/template.tfvars create mode 100644 cluster/variables.tf diff --git a/cluster/.env.example b/cluster/.env.example new file mode 100644 index 0000000..5fe1f9d --- /dev/null +++ b/cluster/.env.example @@ -0,0 +1,2 @@ +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= \ No newline at end of file diff --git a/cluster/.gitignore b/cluster/.gitignore new file mode 100644 index 0000000..943abaa --- /dev/null +++ b/cluster/.gitignore @@ -0,0 +1,43 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +**/k3s_kubeconfig.yaml +**/k3s_kustomization_backup.yaml + +# Extracted kubeconfig file +kube-config.yml diff --git a/cluster/.terraform.lock.hcl b/cluster/.terraform.lock.hcl new file mode 100644 index 0000000..8c4f5c2 --- /dev/null +++ b/cluster/.terraform.lock.hcl @@ -0,0 +1,204 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/anapsix/semvers" { + version = "0.7.1" + constraints = ">= 0.7.1" + hashes = [ + "h1:muej1ceXoABJVeyCSQa42xSfRCCOYuX+HGNYaa91cdo=", + "zh:049fa2bc555b1264427296c55462c24151aedd251ec32673e7775c451c5b0339", + "zh:18d72a3d0e3e502ea68e477396651922e59d97ddaeda132004ca6bc8e13334ed", + "zh:20cacd13b826250ce29e19691492e958db95fd8e66163bb6402050f791f82c93", + "zh:3d13be2d81197f66e69d544c7b708184a4f0341b3fd413a76ec7ff37dbd67999", + "zh:408012764fab3b5d79751ff1c3413dc17ff02a0b1e64655e131218b5a2c970da", + "zh:416c589984585c19952e75866a08a7299c9a2eeb81b015302962bbe09004484a", + "zh:4227a3dcac531608b6b89a2db5f40e99def6864c24b5e8f3e04c15290e2233dd", + "zh:6c1f4226a2fa7ee74c87974f0b5b1420668541d614a958744e17090ea0e6476e", + "zh:90c381aab648cd7507e93725c2bed847c91d2b186eaff09193adec771e689a5c", + "zh:9aa5755bdadff19265f3a434fcadc4a04cd622a40bac7381b5a6ac74b4d5fe8f", + "zh:ac6b01165bc361ddff7d3392311ea494aa6af2d089ab0b43b48f24ac949612ae", + "zh:ba589c0dfa18929244578f02c2d9f4a4c32c79cb57cd9c3ad7e8cbe123cc98fa", + "zh:bc0f3ff5b24e1d2ccf8e1e85fb0f931fd58e1ddaac4abe6b07f94844a1425cd1", + "zh:ced293727d8d91f7ddf85d07c897d23c0a3188251d1065dea5a65342637c5853", + "zh:d3f92af0ee440a540826a61a7b07f30d0155ddc40d70484d1c68c5abdbdabed2", + "zh:d4736f830f2913ba018868fe888a267441b33d1ecb8275ce941fb60150dd60f3", + "zh:df202a4bf895a3a60fdcd39a55296c95e44d3ce300cf6ce3e3e12744c24a941c", + "zh:ec198fa89c5039e3a41dfe0a704e9428389a2f663a69dc438136945887cc6711", + "zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32", + "zh:fe0d8d1bbe515d52b04b01cb93f3cdd25fb3f74833d5e62b3a7ea40ea8f229bf", + ] +} + +provider "registry.opentofu.org/cloudflare/cloudflare" { + version = "4.52.0" + constraints = "4.52.0" + hashes = [ + "h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=", + "zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0", + "zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8", + "zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238", + "zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f", + "zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f", + "zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8", + "zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9", + "zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6", + "zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb", + "zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b", + "zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380", + "zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7", + "zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672", + ] +} + +provider "registry.opentofu.org/hashicorp/assert" { + version = "0.16.0" + constraints = ">= 0.16.0" + hashes = [ + "h1:2jeV46S9jN2rk0GXOa+HGNlVvyWzaB3wz0T65elbjOc=", + "zh:3c04d08d1bb4ae810b7972a219c8dd42a8ab901a9bc25197b250c38f3fa57033", + "zh:46119bcc47b545809c0ee873a72d44f4f875cca4d7228605f5c7a8956a5e7d55", + "zh:511949ee8a6ac8ff7296b4c9778deb2aec2783f5b85c4f27382a3b623fc50a4a", + "zh:b4ebb8b832bae26443880d2e17493f754495db2d6c3f02c6d0070cbf5ae21598", + "zh:bebed6c1873871eb824103f08e72055c077f01b10a40944760d19ffdd721d9ab", + "zh:e412855fd2fd81e0a847e45308bdbac99995315c503fdddf262ee59e1b7c5263", + "zh:ed47c4fe28c6f148f11fa4098516abea008c49fa670c3cedd2ff94596cac0831", + "zh:edee914b1d12ac6db241a1fecaa5186c47f361f4ceb2deb23ad45d67bf95c7b1", + "zh:eff5b2e1c2128217bdbc600eda4fe011831e5c655bf4acd84b6495fc20d128d3", + "zh:ff64424784171a3361b1ea95d8cef334ec1c4a395812edd0a77a1ed6b4119b0f", + ] +} + +provider "registry.opentofu.org/hashicorp/cloudinit" { + version = "2.3.7" + hashes = [ + "h1:dkGeAxGbAGgglocp0fl1OzvT6O4KKsJTEsCW0ixdQJs=", + "zh:2d48b8452eae9bac2e62273e8f535f73694d8cb05ea38f4b27ee735dcc38eed4", + "zh:4add11b87e48d0e6ecd19243a06ecfc42fc07d0a3748fe568c2971d5f4767486", + "zh:4c9c4e3319cf3328595ea2d68eba7c604325fbcba38cd443e39e982b0b4e29f2", + "zh:503dd83a05b0421ecbcb140d5fdbe3a6b82f163495a82587a1390cf66d7a27be", + "zh:7dd34de7e68036dbbb70c249968a2a10bccba1cb92d3b4dccbc0eb65a3fc58ea", + "zh:a4d7b4480d38446b8da96ce4ecbc2e5a081c4ddc3da2bad97d7b228821b77895", + "zh:bdec6329c3d2d5f034080d9cd6f9a15a2c052faacd716f981e247b48e6845c01", + "zh:e1519544ae3f67196d144e18c21ad681dc29da3133a537ffdd5c2c6271b8db0c", + "zh:e58cd6b05ed51a6fa072e5de2208ba36a58557c3fb414d50c42b3d40a11366b7", + "zh:fafc4a49c297516f2a40490f9a7e6d2b437d77a94330797d4eead178c987ccb5", + ] +} + +provider "registry.opentofu.org/hashicorp/local" { + version = "2.5.3" + constraints = ">= 2.5.2" + hashes = [ + "h1:mC9+u1eaUILTjxey6Ivyf/3djm//RNNze9kBVX/trng=", + "zh:32e1d4b0595cea6cda4ca256195c162772ddff25594ab4008731a2ec7be230bf", + "zh:48c390af0c87df994ec9796f04ec2582bcac581fb81ed6bb58e0671da1c17991", + "zh:4be7289c969218a57b40902e2f359914f8d35a7f97b439140cb711aa21e494bd", + "zh:4cf958e631e99ed6c8b522c9b22e1f1b568c0bdadb01dd002ca7dffb1c927764", + "zh:7a0132c0faca4c4c96aa70808effd6817e28712bf5a39881666ac377b4250acf", + "zh:7d60de08fac427fb045e4590d1b921b6778498eee9eb16f78c64d4c577bde096", + "zh:91003bee5981e99ec3925ce2f452a5f743827f9d0e131a86613549c1464796f0", + "zh:9fe2fe75977c8149e2515fb30c6cc6cfd57b225d4ce592c570d81a3831d7ffa3", + "zh:e210e6be54933ce93e03d0994e520ba289aa01b2c1f70e77afb8f2ee796b0fe3", + "zh:e8793e5f9422f2b31a804e51806595f335b827c9a38db18766960464566f21d5", + ] +} + +provider "registry.opentofu.org/hashicorp/null" { + version = "3.2.4" + hashes = [ + "h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=", + "zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3", + "zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb", + "zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2", + "zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4", + "zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d", + "zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6", + "zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072", + "zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447", + "zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58", + "zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80", + ] +} + +provider "registry.opentofu.org/hashicorp/random" { + version = "3.7.2" + hashes = [ + "h1:yHMBbZOIHlXUuBQ8Mhioe0hwmhermuboq2eNNoCJaf8=", + "zh:2ffeb1058bd7b21a9e15a5301abb863053a2d42dffa3f6cf654a1667e10f4727", + "zh:519319ed8f4312ed76519652ad6cd9f98bc75cf4ec7990a5684c072cf5dd0a5d", + "zh:7371c2cc28c94deb9dba62fbac2685f7dde47f93019273a758dd5a2794f72919", + "zh:9b0ac4c1d8e36a86b59ced94fa517ae9b015b1d044b3455465cc6f0eab70915d", + "zh:c6336d7196f1318e1cbb120b3de8426ce43d4cacd2c75f45dba2dbdba666ce00", + "zh:c71f18b0cb5d55a103ea81e346fb56db15b144459123f1be1b0209cffc1deb4e", + "zh:d2dc49a6cac2d156e91b0506d6d756809e36bf390844a187f305094336d3e8d8", + "zh:d5b5fc881ccc41b268f952dae303501d6ec9f9d24ee11fe2fa56eed7478e15d0", + "zh:db9723eaca26d58c930e13fde221d93501529a5cd036b1f167ef8cff6f1a03cc", + "zh:fe3359f733f3ab518c6f85f3a9cd89322a7143463263f30321de0973a52d4ad8", + ] +} + +provider "registry.opentofu.org/hetznercloud/hcloud" { + version = "1.51.0" + constraints = ">= 1.51.0" + hashes = [ + "h1:yER+O3OKYfxBAO7KVYZzH+4EYrmorCO0J0hlnRUfH00=", + "zh:0e8e78084c12866e8e3873011bcac125780b62afeaa518d4749b9a063ae6e32b", + "zh:145738cee21bcdeea1cf82f0d44f7f239c27c2214249e5e5079668c479522a8a", + "zh:164406be8ee83952f58a449d514837cc6d9763b6d29e72262d5582d5d5b89315", + "zh:1a0e6ffab3196b35ca65eb445622615bb8dddd68d0bf350ed60d25e1e74f67dc", + "zh:3b7729d1bb5cc7a5af60b42a607f7b3fec690192b1efb55e2341cee88405ecb0", + "zh:3bcfc5c40d1b7702f39dac5d2dd9eef58c9c934effb4676e26fbe85fe2057e8f", + "zh:3ce193892dca025b804de6d99316c50a33462eb36336006a9db7ea44be439eba", + "zh:4f92437e1eba8eafe4417f8b61d557ed47f121622305ee2b3c13c31e45c69ca4", + "zh:554c308bf64b603a075a8f13a151a136b68ba382c2d83977a0df26de7dea2d3d", + "zh:8c57aa6032fed5da43a0102a4f26262c0496803b99f2f92e5ceb02c80161e291", + "zh:99cd4d246d0ad3a3529176df22a47f254700f8c4fc33f62c14464259284945b7", + "zh:af38a4d1e93f2392a296970ba4ecea341204e888d579cd74642e9f23a94b3b06", + "zh:f0766d42dd97b3eac6fa614fa5809ff2511c9104f3834d0d4b6e84674f13f092", + "zh:f20f7379876ede225f3b6f0719826706a171ea4c1dd438a8a3103dee8fe43ccc", + ] +} + +provider "registry.opentofu.org/integrations/github" { + version = "6.6.0" + constraints = ">= 6.4.0" + hashes = [ + "h1:Fp0RrNe+w167AQkVUWC1WRAsyjhhHN7aHWUky7VkKW8=", + "zh:0b1b5342db6a17de7c71386704e101be7d6761569e03fb3ff1f3d4c02c32d998", + "zh:2fb663467fff76852126b58315d9a1a457e3b04bec51f04bf1c0ddc9dfbb3517", + "zh:4183e557a1dfd413dae90ca4bac37dbbe499eae5e923567371f768053f977800", + "zh:48b2979f88fb55cdb14b7e4c37c44e0dfbc21b7a19686ce75e339efda773c5c2", + "zh:5d803fb06625e0bcf83abb590d4235c117fa7f4aa2168fa3d5f686c41bc529ec", + "zh:6f1dd094cbab36363583cda837d7ca470bef5f8abf9b19f23e9cd8b927153498", + "zh:772edb5890d72b32868f9fdc0a9a1d4f4701d8e7f8acb37a7ac530d053c776e3", + "zh:798f443dbba6610431dcef832047f6917fb5a4e184a3a776c44e6213fb429cc6", + "zh:cc08dfcc387e2603f6dbaff8c236c1254185450d6cadd6bad92879fe7e7dbce9", + "zh:d5e2c8d7f50f91d6847ddce27b10b721bdfce99c1bbab42a68fa271337d73d63", + "zh:e69a0045440c706f50f84a84ff8b1df520ec9bf757de4b8f9959f2ed20c3f440", + "zh:efc5358573a6403cbea3a08a2fcd2407258ac083d9134c641bdcb578966d8bdf", + "zh:f627a255e5809ec2375f79949c79417847fa56b9e9222ea7c45a463eb663f137", + "zh:f7c02f762e4cf1de7f58bde520798491ccdd54a5bd52278d579c146d1d07d4f0", + "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25", + ] +} + +provider "registry.opentofu.org/loafoe/ssh" { + version = "2.7.0" + constraints = "2.7.0" + hashes = [ + "h1:MYcyNF/9w/O0nEeKmopbji1NqeD9kpd2a55r9E4rFXs=", + "zh:0301be53defa9294c713fb3ce4c9925e83051b7444b6eb7262c692ad514f9c46", + "zh:2670797441d6fefddaaac4498f31b0dc8053fe82a3744fca44da7471e6449f1f", + "zh:2d70166644fba761aec397920e9e843cce2c060875ddd224f7791ea2cd7bd6e6", + "zh:30bda314598fee47cf890adfb6f3e1db606feab99252ccfdd0e5c93108f38fdd", + "zh:3a0c0c9f1aff15818fb5fe97b361b879baf19886d413fa468165c3c6de49d348", + "zh:5183c1a7fb5d1f1394bfcfe716a61c4191198ccbd64311601c68c52a3a1ea7e2", + "zh:5190fd7e18f0e46d2263fafa04a6862578abb1c14d60ea3e6597f1b00b041ec7", + "zh:825e2a7eb6c176dc96b82a1123d63ce6e04ef502a973a7ac44ab156cae4f991a", + "zh:8e0716c9a628801284663cad3a8f70e026780f34d04fa5ffb822f0cd5876c353", + "zh:8f19c94a72fb4cecdc70ac97f04c24fa24c46a4e125bbb7c24f642e95f753c70", + "zh:a965929f10651c7139009aa509a6929f2205f90e85ce91a8354416d17624ed04", + ] +} diff --git a/cluster/README.md b/cluster/README.md new file mode 100644 index 0000000..fc51275 --- /dev/null +++ b/cluster/README.md @@ -0,0 +1,106 @@ +# CTF Pilot's Kubernetes Cluster on Hetzner Cloud + +> [!IMPORTANT] +> You are leaving the automated CTF Pilot setup and entering a more advanced manual setup. +> This requires knowledge of Kubernetes, Terraform/OpenTofu, and cloud infrastructure management. +> If you are not comfortable with these technologies, it is recommended to use the automated setup provided by CTF Pilot. +> Learn more about the automated setup in the [CTF Pilot's CTF Platform main README](../README.md). + +This setup uses [Terraform](https://www.terraform.io/) / [OpenTofu](https://opentofu.org) to create and manage a Kubernetes cluster on [Hetzner Cloud](https://www.hetzner.com/cloud), using the [kube-hetzner](https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner). The cluster is configured to use [Cloudflare](https://www.cloudflare.com/) for DNS management. + +## Pre-requisites + +The following software needs to be installed on your local machine: + +- [Terraform](https://www.terraform.io/downloads.html) / [OpenTofu](https://opentofu.org) +- [Packer](https://developer.hashicorp.com/packer/tutorials/docker-get-started/get-started-install-cli#installing-packer) (For initial setup of snapshots for the servers) +- [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) (For interacting with the Kubernetes cluster) +- [hcloud cli tool](https://github.com/hetznercloud/cli) (For interacting with the Hetzner Cloud API) +- SSH client (For connecting to the servers) + +The following services are required, in order to deploy the Kubernetes cluster: + +- [Hetzner Cloud](https://www.hetzner.com/cloud) account +- [Hetzner Cloud API Token](https://console.hetzner.cloud/projects) (For authenticating with the Hetzner Cloud API) +- [Cloudflare](https://www.cloudflare.com/) account +- [Cloudflare API Token](https://dash.cloudflare.com/profile/api-tokens) (For authenticating with the Cloudflare API) +- [Cloudflare controlled domain](https://dash.cloudflare.com/) (For allowing the system to allocate a domain for the Kubernetes cluster) + +### SSH keys + +In order to connect to the servers, you need to have an SSH key pair. The keys path needs to be set in the tfvars file. + +*The SSH key should be ssh-ed25519 or rsa-sha2-512 (for easy use, passphrase-less)* +`ssh-keygen -t ed25519` + +*Can be generated by running: `./keys/create.sh` and copying the keys into tfvars.* + +## Setup + +### Image creation (Done once) + +> [!TIP] +> +> Image creation is only required once, to create the snapshots for the servers. +> If you want to update the the snapshots, this step can be repeated. + +In order to create the cluster, we need to create snapshots of the servers that will be used in the cluster. This is done by running the following command (say yes, to build snapshots using packer): + +```bash +tmp_script=$(mktemp) && curl -sSL -o "${tmp_script}" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x "${tmp_script}" && "${tmp_script}" && rm "${tmp_script}" +``` + +**Note:** This will create a snapshot of the server, which will be used as the base image for the Kubernetes cluster, as well as ensuring local software is installed. +*The software has been provided by the [kube-hetzner](https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner) project.* + +### Cluster creation + +Copy the `tfvars/template.tfvars` file to `tfvars/data.tfvars` and edit the file with your own values. +The [`tfvars/template.tfvars`](tfvars/template.tfvars) file contains further information on each variable. + +For deeper customization, you can edit the `kube.tf` file to change the cluster configuration. +This file is a configured version of the [kube-hetzner `kube.tf`](https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner). + +> [!IMPORTANT] +> Make sure you generate the backend configuration file before creating the cluster. +> See the [backend generation instructions](../backend/README.md) for more information. +> +> You will also need to set the following environment variables for authentication to the S3 backend: +> - `AWS_ACCESS_KEY_ID` +> - `AWS_SECRET_ACCESS_KEY` +> +> See [OpenTofub backend S3 configuration](https://opentofu.org/docs/language/settings/backends/s3/) for more information. + +Run the following command to create the Kubernetes cluster: + +```bash +tofu init -backend-config=../backend/generated/cluster.hcl +tofu apply --var-file tfvars/data.tfvars +``` + +The creation process may take several minutes to complete. + +If you expereience issues, it may be one or multiple of the following issues: + +- The type of servers in Hetzner may not be available. Check to see if they are available in your selected datacenter. +- Your firewall may also be blocking ssh requests to the servers, which causes a deadlock in the configuration of the servers. + +For more detailed troubleshooting, refer to the [kube-hetzner documentation](https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner). + +During creation, Cloudflare DNS records will be created for the cluster. + +### Cluster deletion + +To delete the Kubernetes cluster, run the following command: + +```bash +tofu destroy --var-file tfvars/template.tfvars +``` + +## Accessing the cluster + +To access the Kubernetes cluster, you need to set up the `kubeconfig` file. This can be done by running the following command: + +```bash +tofu output --raw kubeconfig > ./kube-config.yml +``` diff --git a/cluster/cloudflare.tf b/cluster/cloudflare.tf new file mode 100644 index 0000000..1f70dfb --- /dev/null +++ b/cluster/cloudflare.tf @@ -0,0 +1,103 @@ +# ---------------------- +# DNS +# ---------------------- + +data "cloudflare_zones" "domain_name_zone_management" { + filter { + name = var.cloudflare_dns_management + } +} + +# Create DNS A record +resource "cloudflare_record" "domain_name_management" { + zone_id = data.cloudflare_zones.domain_name_zone_management.zones.0.id + name = var.cluster_dns_management + content = module.kube-hetzner.ingress_public_ipv4 + type = "A" + ttl = 1 + + depends_on = [ + data.cloudflare_zones.domain_name_zone_management, + ] +} + +# Create DNS A wildcard record +resource "cloudflare_record" "wildcard_domain_name_management" { + zone_id = data.cloudflare_zones.domain_name_zone_management.zones.0.id + name = "*.${var.cluster_dns_management}" + content = var.cluster_dns_management + type = "CNAME" + ttl = 1 + + depends_on = [ + data.cloudflare_zones.domain_name_zone_management, + ] +} + +data "cloudflare_zones" "domain_name_zone_ctf" { + filter { + name = var.cloudflare_dns_ctf + } +} + +# Create DNS A record +resource "cloudflare_record" "domain_name_ctf" { + zone_id = data.cloudflare_zones.domain_name_zone_ctf.zones.0.id + name = var.cluster_dns_ctf + content = module.kube-hetzner.ingress_public_ipv4 + type = "A" + ttl = 1 + proxied = true + + depends_on = [ + data.cloudflare_zones.domain_name_zone_ctf, + ] +} + +# Create DNS A wildcard record +resource "cloudflare_record" "wildcard_domain_name_ctf" { + zone_id = data.cloudflare_zones.domain_name_zone_ctf.zones.0.id + name = "*.${var.cluster_dns_ctf}" + content = module.kube-hetzner.ingress_public_ipv4 + type = "A" + ttl = 1 + proxied = false + + depends_on = [ + data.cloudflare_zones.domain_name_zone_ctf, + ] +} + +data "cloudflare_zones" "domain_name_zone_platform" { + filter { + name = var.cloudflare_dns_platform + } +} + +# Create DNS A record +resource "cloudflare_record" "domain_name_platform" { + zone_id = data.cloudflare_zones.domain_name_zone_platform.zones.0.id + name = var.cluster_dns_platform + content = module.kube-hetzner.ingress_public_ipv4 + type = "A" + ttl = 1 + proxied = true + + depends_on = [ + data.cloudflare_zones.domain_name_zone_platform, + ] +} + +# Create DNS A wildcard record +resource "cloudflare_record" "wildcard_domain_name_platform" { + zone_id = data.cloudflare_zones.domain_name_zone_platform.zones.0.id + name = "*.${var.cluster_dns_platform}" + content = module.kube-hetzner.ingress_public_ipv4 + type = "A" + ttl = 1 + proxied = true + + depends_on = [ + data.cloudflare_zones.domain_name_zone_platform, + ] +} diff --git a/cluster/hcloud-microos-snapshots.pkr.hcl b/cluster/hcloud-microos-snapshots.pkr.hcl new file mode 100644 index 0000000..0e3a954 --- /dev/null +++ b/cluster/hcloud-microos-snapshots.pkr.hcl @@ -0,0 +1,164 @@ +/* + * Creates a MicroOS snapshot for Kube-Hetzner + */ +packer { + required_plugins { + hcloud = { + version = ">= 1.0.5" + source = "github.com/hashicorp/hcloud" + } + } +} + +variable "hcloud_token" { + type = string + default = env("HCLOUD_TOKEN") + sensitive = true +} + +# We download the OpenSUSE MicroOS x86 image from an automatically selected mirror. +variable "opensuse_microos_x86_mirror_link" { + type = string + default = "https://download.opensuse.org/tumbleweed/appliances/openSUSE-MicroOS.x86_64-ContainerHost-OpenStack-Cloud.qcow2" +} + +# We download the OpenSUSE MicroOS ARM image from an automatically selected mirror. +variable "opensuse_microos_arm_mirror_link" { + type = string + default = "https://download.opensuse.org/ports/aarch64/tumbleweed/appliances/openSUSE-MicroOS.aarch64-ContainerHost-OpenStack-Cloud.qcow2" +} + +# If you need to add other packages to the OS, do it here in the default value, like ["vim", "curl", "wget"] +# When looking for packages, you need to search for OpenSUSE Tumbleweed packages, as MicroOS is based on Tumbleweed. +variable "packages_to_install" { + type = list(string) + default = [] +} + +locals { + needed_packages = join(" ", concat(["restorecond policycoreutils policycoreutils-python-utils setools-console audit bind-utils wireguard-tools open-iscsi nfs-client xfsprogs cryptsetup lvm2 git cifs-utils bash-completion mtr tcpdump"], var.packages_to_install)) + + # Add local variables for inline shell commands + download_image = "wget --timeout=5 --waitretry=5 --tries=5 --retry-connrefused --inet4-only " + + write_image = <<-EOT + set -ex + echo 'MicroOS image loaded, writing to disk... ' + qemu-img convert -p -f qcow2 -O host_device $(ls -a | grep -ie '^opensuse.*microos.*qcow2$') /dev/sda + echo 'done. Rebooting...' + sleep 1 && udevadm settle && reboot + EOT + + install_packages = <<-EOT + set -ex + echo "First reboot successful, installing needed packages..." + transactional-update --continue pkg install -y ${local.needed_packages} + transactional-update --continue shell <<- EOF + setenforce 0 + rpm --import https://rpm.rancher.io/public.key + zypper install -y https://github.com/k3s-io/k3s-selinux/releases/download/v1.4.stable.1/k3s-selinux-1.4-1.sle.noarch.rpm + zypper addlock k3s-selinux + restorecon -Rv /etc/selinux/targeted/policy + restorecon -Rv /var/lib + setenforce 1 + EOF + sleep 1 && udevadm settle && reboot + EOT + + clean_up = <<-EOT + set -ex + echo "Second reboot successful, cleaning-up..." + rm -rf /etc/ssh/ssh_host_* + echo "Make sure to use NetworkManager" + touch /etc/NetworkManager/NetworkManager.conf + sleep 1 && udevadm settle + EOT +} + +# Source for the MicroOS x86 snapshot +source "hcloud" "microos-x86-snapshot" { + image = "ubuntu-22.04" + rescue = "linux64" + location = "fsn1" + server_type = "cpx11" # disk size of >= 40GiB is needed to install the MicroOS image + snapshot_labels = { + microos-snapshot = "yes" + creator = "kube-hetzner" + } + snapshot_name = "OpenSUSE MicroOS x86 by Kube-Hetzner" + ssh_username = "root" + token = var.hcloud_token +} + +# Source for the MicroOS ARM snapshot +source "hcloud" "microos-arm-snapshot" { + image = "ubuntu-22.04" + rescue = "linux64" + location = "fsn1" + server_type = "cax11" # disk size of >= 40GiB is needed to install the MicroOS image + snapshot_labels = { + microos-snapshot = "yes" + creator = "kube-hetzner" + } + snapshot_name = "OpenSUSE MicroOS ARM by Kube-Hetzner" + ssh_username = "root" + token = var.hcloud_token +} + +# Build the MicroOS x86 snapshot +build { + sources = ["source.hcloud.microos-x86-snapshot"] + + # Download the MicroOS x86 image + provisioner "shell" { + inline = ["${local.download_image}${var.opensuse_microos_x86_mirror_link}"] + } + + # Write the MicroOS x86 image to disk + provisioner "shell" { + inline = [local.write_image] + expect_disconnect = true + } + + # Ensure connection to MicroOS x86 and do house-keeping + provisioner "shell" { + pause_before = "5s" + inline = [local.install_packages] + expect_disconnect = true + } + + # Ensure connection to MicroOS x86 and do house-keeping + provisioner "shell" { + pause_before = "5s" + inline = [local.clean_up] + } +} + +# Build the MicroOS ARM snapshot +build { + sources = ["source.hcloud.microos-arm-snapshot"] + + # Download the MicroOS ARM image + provisioner "shell" { + inline = ["${local.download_image}${var.opensuse_microos_arm_mirror_link}"] + } + + # Write the MicroOS ARM image to disk + provisioner "shell" { + inline = [local.write_image] + expect_disconnect = true + } + + # Ensure connection to MicroOS ARM and do house-keeping + provisioner "shell" { + pause_before = "5s" + inline = [local.install_packages] + expect_disconnect = true + } + + # Ensure connection to MicroOS ARM and do house-keeping + provisioner "shell" { + pause_before = "5s" + inline = [local.clean_up] + } +} diff --git a/cluster/keys/.gitignore b/cluster/keys/.gitignore new file mode 100644 index 0000000..b2e756f --- /dev/null +++ b/cluster/keys/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!create.sh diff --git a/cluster/keys/create.sh b/cluster/keys/create.sh new file mode 100755 index 0000000..40f0fc4 --- /dev/null +++ b/cluster/keys/create.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Get location of this file +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +ssh-keygen -t ed25519 -f $DIR/k8s -q -N "" + +# base64 encode the keys (into single base64 string) +base64 $DIR/k8s | tr -d '\n' >$DIR/k8s.b64 +base64 $DIR/k8s.pub | tr -d '\n' >$DIR/k8s.pub.b64 diff --git a/cluster/kube.tf b/cluster/kube.tf new file mode 100644 index 0000000..f5309bd --- /dev/null +++ b/cluster/kube.tf @@ -0,0 +1,1282 @@ +# ---------------------- +# Cluster setup +# ---------------------- + +module "kube-hetzner" { + providers = { + hcloud = hcloud + } + hcloud_token = var.hcloud_token + + # Then fill or edit the below values. Only the first values starting with a * are obligatory; the rest can remain with their default values, or you + # could adapt them to your needs. + + # * source can be specified in multiple ways: + # 1. For normal use, (the official version published on the Terraform Registry), use + source = "kube-hetzner/kube-hetzner/hcloud" + # When using the terraform registry as source, you can optionally specify a version number. + # See https://registry.terraform.io/modules/kube-hetzner/kube-hetzner/hcloud for the available versions + version = "2.18.2" + # 2. For local dev, path to the git repo + # source = "../../kube-hetzner/" + # 3. If you want to use the latest master branch (see https://developer.hashicorp.com/terraform/language/modules/sources#github), use + # source = "github.com/The0mikkel/terraform-hcloud-kube-hetzner?ref=2.18.0-fix" + + # Note that some values, notably "location" and "public_key" have no effect after initializing the cluster. + # This is to keep Terraform from re-provisioning all nodes at once, which would lose data. If you want to update + # those, you should instead change the value here and manually re-provision each node. Grep for "lifecycle". + + # Customize the SSH port (by default 22) + # ssh_port = 2222 + + # * Your ssh public key + ssh_public_key = base64decode(var.ssh_key_public_base64) + # * Your private key must be "ssh_private_key = null" when you want to use ssh-agent for a Yubikey-like device authentication or an SSH key-pair with a passphrase. + # For more details on SSH see https://github.com/kube-hetzner/kube-hetzner/blob/master/docs/ssh.md + ssh_private_key = base64decode(var.ssh_key_private_base64) + # You can add additional SSH public Keys to grant other team members root access to your cluster nodes. + # ssh_additional_public_keys = var.ssh_extra_keys_path + + # You can also add additional SSH public Keys which are saved in the hetzner cloud by a label. + # See https://docs.hetzner.cloud/#label-selector + # ssh_hcloud_key_label = var.ssh_extra_keys_label + + # These can be customized, or left with the default values + # * For Hetzner locations see https://docs.hetzner.com/general/others/data-centers-and-connection/ + network_region = var.network_zone + + # IMPORTANT: Before you create your cluster, you can do anything you want with the nodepools, but you need at least one of each, control plane and agent. + # Once the cluster is up and running, you can change nodepool count and even set it to 0 (in the case of the first control-plane nodepool, the minimum is 1). + # You can also rename it (if the count is 0), but do not remove a nodepool from the list. + + # You can safely add or remove nodepools at the end of each list. That is due to how subnets and IPs get allocated (FILO). + # The maximum number of nodepools you can create combined for both lists is 50 (see above). + # Also, before decreasing the count of any nodepools to 0, it's essential to drain and cordon the nodes in question. Otherwise, it will leave your cluster in a bad state. + + # Before initializing the cluster, you can change all parameters and add or remove any nodepools. You need at least one nodepool of each kind, control plane, and agent. + # ⚠️ The nodepool names are entirely arbitrary, but all lowercase, no special characters or underscore (dashes are allowed), and they must be unique. + + # If you want to have a single node cluster, have one control plane nodepools with a count of 1, and one agent nodepool with a count of 0. + + # Please note that changing labels and taints after the first run will have no effect. If needed, you can do that through Kubernetes directly. + + # Multi-architecture clusters are OK for most use cases, as container underlying images tend to be multi-architecture too. + + # * Example below: + + control_plane_nodepools = [ + { + name = "control-plane-1", + server_type = var.control_plane_type_1, + location = var.region_1, + labels = [ + "ressource-type=node", + "node-type=control-plane", + "control-plane=fsn1", + "node-pool=control-plane-fsn1", + "cluster.ctfpilot.com/node=control-plane", + ], + taints = [], + count = var.control_plane_count_1 + # swap_size = "2G" # remember to add the suffix, examples: 512M, 1G + # zram_size = "2G" # remember to add the suffix, examples: 512M, 1G + kubelet_args = ["kube-reserved=cpu=250m,memory=1500Mi,ephemeral-storage=1Gi", "system-reserved=cpu=250m,memory=300Mi"] + + # Fine-grained control over placement groups (nodes in the same group are spread over different physical servers, 10 nodes per placement group max): + # placement_group = "default" + + # Enable automatic backups via Hetzner (default: false) + backups = false + + # To disable public ips (default: false) + # WARNING: If both values are set to "true", your server will only be accessible via a private network. Make sure you have followed + # the instructions regarding this type of setup in README.md: "Use only private IPs in your cluster". + # disable_ipv4 = true + # disable_ipv6 = true + }, + { + name = "control-plane-2", + server_type = var.control_plane_type_2, + location = var.region_2, + labels = [ + "ressource-type=node", + "node-type=control-plane", + "control-plane=nbg1", + "node-pool=control-plane-nbg1", + "cluster.ctfpilot.com/node=control-plane", + ], + taints = [], + count = var.control_plane_count_2 + kubelet_args = ["kube-reserved=cpu=250m,memory=1500Mi,ephemeral-storage=1Gi", "system-reserved=cpu=250m,memory=300Mi"] + + # Fine-grained control over placement groups (nodes in the same group are spread over different physical servers, 10 nodes per placement group max): + # placement_group = "default" + + # Enable automatic backups via Hetzner (default: false) + backups = false + }, + { + name = "control-plane-3", + server_type = var.control_plane_type_3, + location = var.region_3, + labels = [ + "ressource-type=node", + "node-type=control-plane", + "control-plane=hel1", + "node-pool=control-plane-hel1", + "cluster.ctfpilot.com/node=control-plane", + ], + taints = [], + count = var.control_plane_count_3 + kubelet_args = ["kube-reserved=cpu=250m,memory=1500Mi,ephemeral-storage=1Gi", "system-reserved=cpu=250m,memory=300Mi"] + + # Fine-grained control over placement groups (nodes in the same group are spread over different physical servers, 10 nodes per placement group max): + # placement_group = "default" + + # Enable automatic backups via Hetzner (default: false) + backups = false + } + ] + + agent_nodepools = [ + { + name = "agents-1", + server_type = var.agent_type_1, + location = var.region_1, + labels = [ + "ressource-type=node", + "node-type=agent", + "region=${var.region_2}", + "node-pool=agents-${var.region_2}", + "cluster.ctfpilot.com/node=agent", + ], + taints = [], + count = var.agent_count_1 + kubelet_args = [ + "kube-reserved=cpu=250m,memory=750Mi,ephemeral-storage=1Gi", + "system-reserved=cpu=400m,memory=750Mi", + "eviction-soft=memory.available<512Mi", # Recommend 1Gi for larger nodes + "eviction-soft-grace-period=memory.available=1m", + "eviction-hard=memory.available<500Mi,nodefs.available<5%,imagefs.available<5%", + ] + # swap_size = "2G" # remember to add the suffix, examples: 512M, 1G + # zram_size = "2G" # remember to add the suffix, examples: 512M, 1G + + # Fine-grained control over placement groups (nodes in the same group are spread over different physical servers, 10 nodes per placement group max): + # placement_group = "default" + + # Enable automatic backups via Hetzner (default: false) + backups = false + }, + { + name = "agents-2", + server_type = var.agent_type_2, + location = var.region_2, + labels = [ + "ressource-type=node", + "node-type=agent", + "region=${var.region_2}", + "node-pool=agents-${var.region_2}", + "cluster.ctfpilot.com/node=agent", + ], + taints = [], + count = var.agent_count_2 + kubelet_args = [ + "kube-reserved=cpu=250m,memory=750Mi,ephemeral-storage=1Gi", + "system-reserved=cpu=400m,memory=750Mi", + "eviction-soft=memory.available<512Mi", # Recommend 1Gi for larger nodes + "eviction-soft-grace-period=memory.available=1m", + "eviction-hard=memory.available<500Mi,nodefs.available<5%,imagefs.available<5%", + ] + # swap_size = "2G" # remember to add the suffix, examples: 512M, 1G + # zram_size = "2G" # remember to add the suffix, examples: 512M, 1G + # kubelet_args = ["kube-reserved=cpu=50m,memory=300Mi,ephemeral-storage=1Gi", "system-reserved=cpu=250m,memory=300Mi"] + + # Fine-grained control over placement groups (nodes in the same group are spread over different physical servers, 10 nodes per placement group max): + # placement_group = "default" + + # Enable automatic backups via Hetzner (default: false) + backups = false + }, + { + name = "agents-3", + server_type = var.agent_type_3, + location = var.region_3, + labels = [ + "ressource-type=node", + "node-type=agent", + "region=${var.region_2}", + "node-pool=agents-${var.region_2}", + "cluster.ctfpilot.com/node=agent", + ], + taints = [], + count = var.agent_count_3 + kubelet_args = [ + "kube-reserved=cpu=250m,memory=750Mi,ephemeral-storage=1Gi", + "system-reserved=cpu=400m,memory=750Mi", + "eviction-soft=memory.available<512Mi", # Recommend 1Gi for larger nodes + "eviction-soft-grace-period=memory.available=1m", + "eviction-hard=memory.available<500Mi,nodefs.available<5%,imagefs.available<5%", + ] + + # Fine-grained control over placement groups (nodes in the same group are spread over different physical servers, 10 nodes per placement group max): + # placement_group = "default" + + # Enable automatic backups via Hetzner (default: false) + backups = false + }, + { + name = "challs-1", + server_type = var.scale_type, + location = var.region_1, + labels = [ + "ressource-type=node", + "node-type=scale", + "region=${var.region_1}", + "node-pool=challs", + "cluster.ctfpilot.com/node=scaler" + ] + taints = [ + "cluster.ctfpilot.com/node=scaler:PreferNoSchedule" + ] + count = var.scale_min + kubelet_args = [ + "kube-reserved=cpu=150m,memory=750Mi,ephemeral-storage=1Gi", + "system-reserved=cpu=300m,memory=750Mi", + "eviction-soft=memory.available<2Gi", # Recommend 3Gi for larger nodes + "eviction-soft-grace-period=memory.available=10m", + "eviction-hard=memory.available<500Mi,nodefs.available<5%,imagefs.available<5%", + ] + + # Fine-grained control over placement groups (nodes in the same group are spread over different physical servers, 10 nodes per placement group max): + # placement_group = "default" + + # Enable automatic backups via Hetzner (default: false) + backups = false + }, + #{ + # name = "storage", + # server_type = "cx21", + # location = "hel1", + # # Fully optional, just a demo. + # labels = [ + # "node.kubernetes.io/server-usage=storage" + # ], + # taints = [], + # count = 1 + + # In the case of using Longhorn, you can use Hetzner volumes instead of using the node's own storage by specifying a value from 10 to 10240 (in GB) + # It will create one volume per node in the nodepool, and configure Longhorn to use them. + # Something worth noting is that Volume storage is slower than node storage, which is achieved by not mentioning longhorn_volume_size or setting it to 0. + # So for something like DBs, you definitely want node storage, for other things like backups, volume storage is fine, and cheaper. + # longhorn_volume_size = 20 + + # Enable automatic backups via Hetzner (default: false) + # backups = true + #}, + # Egress nodepool useful to route egress traffic using Hetzner Floating IPs (https://docs.hetzner.com/cloud/floating-ips) + # used with Cilium's Egress Gateway feature https://docs.cilium.io/en/stable/gettingstarted/egress-gateway/ + # See the https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner#examples for an example use case. + #{ + # name = "egress", + # server_type = "cx21", + # location = "fsn1", + # labels = [ + # "node.kubernetes.io/role=egress" + # ], + # taints = [ + # "node.kubernetes.io/role=egress:NoSchedule" + # ], + # floating_ip = true + # Optionally associate a reverse DNS entry with the floating IP(s). + # This is useful in combination with the Egress Gateway feature for hosting certain services in the cluster, such as email servers. + # floating_ip_rns = "my.domain.com" + # count = 1 + #}, + # Arm based nodes + #{ + # name = "agent-arm-small", + # server_type = "cax11", + # location = "fsn1", + # labels = [], + # taints = [], + # count = 1 + #}, + # For fine-grained control over the nodes in a node pool, replace the count variable with a nodes map. + # In this case, the node-pool variables are defaults which can be overridden on a per-node basis. + # Each key in the nodes map refers to a single node and must be an integer string ("1", "123", ...). + #{ + # name = "agent-arm-small", + # server_type = "cax11", + # location = "fsn1", + # labels = [], + # taints = [], + # nodes = { + # "1" : { + # location = "nbg1" + # labels = [ + # "testing-labels=a1", + # ] + # }, + # "20" : { + # labels = [ + # "testing-labels=b1", + # ] + # } + # } + #}, + ] + # Add additional configuration options for control planes here. + # E.g to enable monitoring for etcd, proxy etc: + # control_planes_custom_config = { + # etcd-expose-metrics = true, + # kube-controller-manager-arg = "bind-address=0.0.0.0", + # kube-proxy-arg ="metrics-bind-address=0.0.0.0", + # kube-scheduler-arg = "bind-address=0.0.0.0", + # } + + # Add additional configuration options for agent nodes and autoscaler nodes here. + # E.g to enable monitoring for proxy: + # agent_nodes_custom_config = { + # kube-proxy-arg ="metrics-bind-address=0.0.0.0", + # } + + # You can enable encrypted wireguard for the CNI by setting this to "true". Default is "false". + # FYI, Hetzner says "Traffic between cloud servers inside a Network is private and isolated, but not automatically encrypted." + # Source: https://docs.hetzner.com/cloud/networks/faq/#is-traffic-inside-hetzner-cloud-networks-encrypted + # It works with all CNIs that we support. + # Just note, that if Cilium with cilium_values, the responsibility of enabling of disabling Wireguard falls on you. + enable_wireguard = true + + # * LB location and type, the latter will depend on how much load you want it to handle, see https://www.hetzner.com/cloud/load-balancer + load_balancer_type = var.load_balancer_type + load_balancer_location = var.region_1 + + # Disable IPv6 for the load balancer, the default is false. + # load_balancer_disable_ipv6 = true + + # Disables the public network of the load balancer. (default: false). + # load_balancer_disable_public_network = true + + # Specifies the algorithm type of the load balancer. (default: round_robin). + # load_balancer_algorithm_type = "least_connections" + + # Specifies the interval at which a health check is performed. Minimum is 3s (default: 15s). + # load_balancer_health_check_interval = "5s" + + # Specifies the timeout of a single health check. Must not be greater than the health check interval. Minimum is 1s (default: 10s). + # load_balancer_health_check_timeout = "3s" + + # Specifies the number of times a health check is retried before a target is marked as unhealthy. (default: 3) + # load_balancer_health_check_retries = 3 + + # Setup a NAT router, and automatically disable public ips on all control plane and agent nodes. + # To use this, you must also set use_control_plane_lb = true, otherwise kubectl can never + # reach the cluster. The NAT router will also function as bastion. This makes securing the cluster + # easier, as all public traffic passes through a single strongly secured node. It does + # however also introduce a single point of failure, so if you need high-availability on your + # egress, you should consider other configurations. + # + # + # nat_router = { + # server_type = "cax21" + # location = "fsn1" + # enable_sudo = false # optional, default to false. Set to true to add nat-router user to the sudo'ers. Note that ssh as root is disabled. + # labels = {} # optionally add labels. + # } + + ### The following values are entirely optional (and can be removed from this if unused) + + # You can refine a base domain name to be use in this form of nodename.base_domain for setting the reverse dns inside Hetzner + # base_domain = "mycluster.example.com" + + # Cluster Autoscaler + # Providing at least one map for the array enables the cluster autoscaler feature, default is disabled. + # ⚠️ Based on how the autoscaler works with this project, you can only choose either x86 instances or ARM server types for ALL autoscaler nodepools. + # If you are curious, it's ok to have a multi-architecture cluster, as most underlying container images are multi-architecture too. + # + # ⚠️ Setting labels and taints will only work on cluster-autoscaler images versions released after > 20 October 2023. Or images built from master after that date. + # + # * Example below: + autoscaler_nodepools = [ + { + name = "autoscaled-challs-nodes" + server_type = var.scale_type + location = var.region_1 + min_nodes = 0 # var.scale_count > 0 ? var.scale_min : 0 + max_nodes = var.scale_count + labels = { + "ressource-type" : "node", + "node-type" : "scale", + "region" : "${var.region_1}", + "node-pool" : "scaled", + "cluster.ctfpilot.com/node" : "scaler" + } + taints = [ + { + key = "cluster.ctfpilot.com/node" + value = "scaler" + effect = "PreferNoSchedule" + } + ] + kubelet_args = [ + "kube-reserved=cpu=150m,memory=750Mi,ephemeral-storage=1Gi", + "system-reserved=cpu=300m,memory=750Mi", + "eviction-soft=memory.available<512Mi", # Recommend 3Gi for larger nodes + "eviction-soft-grace-period=memory.available=10m", + "eviction-hard=memory.available<500Mi,nodefs.available<5%,imagefs.available<5%", + ] + } + ] + # + # To disable public ips on your autoscaled nodes, uncomment the following lines: + # autoscaler_disable_ipv4 = true + # autoscaler_disable_ipv6 = true + + # ⚠️ Deprecated, will be removed after a new Cluster Autoscaler version has been released which support the new way of setting labels and taints. See above. + # Add extra labels on nodes started by the Cluster Autoscaler + # This argument is not used if autoscaler_nodepools is not set, because the Cluster Autoscaler is installed only if autoscaler_nodepools is set + # autoscaler_labels = [ + # "node.kubernetes.io/role=peak-workloads" + # ] + + # Add extra taints on nodes started by the Cluster Autoscaler + # This argument is not used if autoscaler_nodepools is not set, because the Cluster Autoscaler is installed only if autoscaler_nodepools is set + # autoscaler_taints = [ + # "node.kubernetes.io/role=specific-workloads:NoExecute" + # ] + + # Configuration of the Cluster Autoscaler binary + # + # These arguments and variables are not used if autoscaler_nodepools is not set, because the Cluster Autoscaler is installed only if autoscaler_nodepools is set. + # + # Image and version of Kubernetes Cluster Autoscaler for Hetzner Cloud: + # - cluster_autoscaler_image: Image of Kubernetes Cluster Autoscaler for Hetzner Cloud to be used. + # The default is the official image from the Kubernetes project: registry.k8s.io/autoscaling/cluster-autoscaler + # - cluster_autoscaler_version: Version of Kubernetes Cluster Autoscaler for Hetzner Cloud. Should be aligned with Kubernetes version. + # Available versions for the official image can be found at https://explore.ggcr.dev/?repo=registry.k8s.io%2Fautoscaling%2Fcluster-autoscaler + # + # Logging related arguments are managed using separate variables: + # - cluster_autoscaler_log_level: Controls the verbosity of logs (--v), the value is from 0 to 5, default is 4, for max debug info set it to 5. + # - cluster_autoscaler_log_to_stderr: Determines whether to log to stderr (--logtostderr). + # - cluster_autoscaler_stderr_threshold: Sets the threshold for logs that go to stderr (--stderrthreshold). + # + # Server/node creation timeout variable: + # - cluster_autoscaler_server_creation_timeout: Sets the timeout (in minutes) until which a newly created server/node has to become available before giving up and destroying it (defaults to 15, unit is minutes) + # + # Example: + # + # cluster_autoscaler_image = "registry.k8s.io/autoscaling/cluster-autoscaler" + # cluster_autoscaler_version = "v1.30.3" + # cluster_autoscaler_log_level = 4 + # cluster_autoscaler_log_to_stderr = true + # cluster_autoscaler_stderr_threshold = "INFO" + # cluster_autoscaler_server_creation_timeout = 15 + + # Additional Cluster Autoscaler binary configuration + # + # cluster_autoscaler_extra_args can be used for additional arguments. The default is an empty array. + # + # Please note that following arguments are managed by terraform-hcloud-kube-hetzner or the variables above and should not be set manually: + # - --v=${var.cluster_autoscaler_log_level} + # - --logtostderr=${var.cluster_autoscaler_log_to_stderr} + # - --stderrthreshold=${var.cluster_autoscaler_stderr_threshold} + # - --cloud-provider=hetzner + # - --nodes ... + # + # See the Cluster Autoscaler FAQ for the full list of arguments: https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#what-are-the-parameters-to-ca + # + # Example: + # + cluster_autoscaler_extra_args = [ + "--ignore-daemonsets-utilization=true", + "--enforce-node-group-min-size=true", + ] + + # Enable delete protection on compatible resources to prevent accidental deletion from the Hetzner Cloud Console. + # This does not protect deletion from Terraform itself. + # enable_delete_protection = { + # floating_ip = true + # load_balancer = true + # volume = true + # } + + # Enable etcd snapshot backups to S3 storage. + # Just provide a map with the needed settings (according to your S3 storage provider) and backups to S3 will + # be enabled (with the default settings for etcd snapshots). + # Cloudflare's R2 offers 10GB, 10 million reads and 1 million writes per month for free. + # For proper context, have a look at https://docs.k3s.io/datastore/backup-restore. + # You also can use additional parameters from https://docs.k3s.io/cli/etcd-snapshot, such as `etc-s3-folder` + # etcd_s3_backup = { + # etcd-s3-endpoint = "xxxx.r2.cloudflarestorage.com" + # etcd-s3-access-key = "" + # etcd-s3-secret-key = "" + # etcd-s3-bucket = "k3s-etcd-snapshots" + # etcd-s3-region = "" + # } + + # To enable Hetzner Storage Box support, you can enable csi-driver-smb, default is "false". + # enable_csi_driver_smb = true + + # To enable iscid without setting enable_longhorn = true, set enable_iscsid = true. You will need this if + # you install your own version of longhorn outside of this module. + # Default is false. If enable_longhorn=true, this variable is ignored and iscsid is enabled anyway. + # enable_iscsid = true + + # To use local storage on the nodes, you can enable Longhorn, default is "false". + # See a full recap on how to configure agent nodepools for longhorn here https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner/discussions/373#discussioncomment-3983159 + # Also see Longhorn best practices here https://gist.github.com/ifeulner/d311b2868f6c00e649f33a72166c2e5b + enable_longhorn = false + + # By default, longhorn is pulled from https://charts.longhorn.io. + # If you need a version of longhorn which assures compatibility with rancher you can set this variable to https://charts.rancher.io. + # longhorn_repository = "https://charts.rancher.io" + + # The namespace for longhorn deployment, default is "longhorn-system". + # longhorn_namespace = "longhorn-system" + + # The file system type for Longhorn, if enabled (ext4 is the default, otherwise you can choose xfs). + # longhorn_fstype = "xfs" + + # how many replica volumes should longhorn create (default is 3). + # longhorn_replica_count = 1 + + # When you enable Longhorn, you can go with the default settings and just modify the above two variables OR you can add a longhorn_values variable + # with all needed helm values, see towards the end of the file in the advanced section. + # If that file is present, the system will use it during the deploy, if not it will use the default values with the two variable above that can be customized. + # After the cluster is deployed, you can always use HelmChartConfig definition to tweak the configuration. + + # Also, you can choose to use a Hetzner volume with Longhorn. By default, it will use the nodes own storage space, but if you add an attribute of + # longhorn_volume_size (⚠️ not a variable, just a possible agent nodepool attribute) with a value between 10 and 10000 GB to your agent nodepool definition, it will create and use the volume in question. + # See the agent nodepool section for an example of how to do that. + + # To disable Hetzner CSI storage, you can set the following to "true", default is "false". + # disable_hetzner_csi = true + + # If you want to use a specific Hetzner CCM and CSI version, set them below; otherwise, leave them as-is for the latest versions. + # hetzner_ccm_version = "" + # hetzner_csi_version = "" + + # If you want to specify the Kured version, set it below - otherwise it'll use the latest version available. + # See https://github.com/kubereboot/kured/releases for the available versions. + # kured_version = "" + + # Default is "traefik". + # If you want to enable the Nginx (https://kubernetes.github.io/ingress-nginx/) or HAProxy ingress controller instead of Traefik, you can set this to "nginx" or "haproxy". + # By the default we load optimal Traefik, Nginx or HAProxy ingress controller config for Hetzner, however you may need to tweak it to your needs, so to do, + # we allow you to add a traefik_values, nginx_values or haproxy_values, see towards the end of this file in the advanced section. + # After the cluster is deployed, you can always use HelmChartConfig definition to tweak the configuration. + # If you want to disable both controllers set this to "none" + # ingress_controller = "nginx" + # Namespace in which to deploy the ingress controllers. Defaults to the ingress_controller variable, eg (haproxy, nginx, traefik) + # ingress_target_namespace = "" + + # Use the klipperLB (similar to metalLB), instead of the default Hetzner one, that has an advantage of dropping the cost of the setup. + # Automatically "true" in the case of single node cluster (as it does not make sense to use the Hetzner LB in that situation). + # It can work with any ingress controller that you choose to deploy. + # Please note that because the klipperLB points to all nodes, we automatically allow scheduling on the control plane when it is active. + # enable_klipper_metal_lb = "true" + + # If you want to configure additional arguments for traefik, enter them here as a list and in the form of traefik CLI arguments; see https://doc.traefik.io/traefik/reference/static-configuration/cli/ + # They are the options that go into the additionalArguments section of the Traefik helm values file. + # We already add "providers.kubernetesingress.ingressendpoint.publishedservice" by default so that Traefik works automatically with services such as External-DNS and ArgoCD. + # Example: + traefik_additional_options = ["--api", "--api.dashboard=true", "--api.insecure=true"] # ["--log.level=DEBUG", "--tracing=true"] + + # By default traefik image tag is an empty string which uses latest image tag. + # The default is "". + # traefik_image_tag = "v3.0.0-beta5" + + # By default traefik is configured to redirect http traffic to https, you can set this to "false" to disable the redirection. + # The default is true. + # traefik_redirect_to_https = false + + # Enable or disable Horizontal Pod Autoscaler for traefik. + # The default is true. + # traefik_autoscaling = false + + # Enable or disable pod disruption budget for traefik. Values are maxUnavailable: 33% and minAvailable: 1. + # The default is true. + # traefik_pod_disruption_budget = false + + # Enable or disable default resource requests and limits for traefik. Values requested are 100m & 50Mi and limits 300m & 150Mi. + # The default is true. + # traefik_resource_limits = false + + # If you want to configure additional ports for traefik, enter them here as a list of objects with name, port, and exposedPort properties. + # Example: + traefik_additional_ports = [ + # { + # name = "blockchain", + # port = 8545, + # exposedPort = 8545 + # }, + ] + + # If you want to configure additional trusted IPs for traefik, enter them here as a list of IPs (strings). + # Example for Cloudflare: + # traefik_additional_trusted_ips = [ + # "173.245.48.0/20", + # "103.21.244.0/22", + # "103.22.200.0/22", + # "103.31.4.0/22", + # "141.101.64.0/18", + # "108.162.192.0/18", + # "190.93.240.0/20", + # "188.114.96.0/20", + # "197.234.240.0/22", + # "198.41.128.0/17", + # "162.158.0.0/15", + # "104.16.0.0/13", + # "104.24.0.0/14", + # "172.64.0.0/13", + # "131.0.72.0/22", + # "2400:cb00::/32", + # "2606:4700::/32", + # "2803:f800::/32", + # "2405:b500::/32", + # "2405:8100::/32", + # "2a06:98c0::/29", + # "2c0f:f248::/32" + # ] + + # If you want to disable the metric server set this to "false". Default is "true". + # enable_metrics_server = false + + # If you want to enable the k3s built-in local-storage controller set this to "true". Default is "false". + # Warning: When enabled together with the Hetzner CSI, there will be two default storage classes: "local-path" and "hcloud-volumes"! + # Even if patched to remove the "default" label, the local-path storage class will be reset as default on each reboot of + # the node where the controller runs. + # This is not a problem if you explicitly define which storageclass to use in your PVCs. + # Workaround if you don't want two default storage classes: leave this to false and add the local-path-provisioner helm chart + # as an extra (https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner#adding-extras). + # enable_local_storage = false + + # If you want to allow non-control-plane workloads to run on the control-plane nodes, set this to "true". The default is "false". + # True by default for single node clusters, and when enable_klipper_metal_lb is true. In those cases, the value below will be ignored. + # allow_scheduling_on_control_plane = true + + # If you want to disable the automatic upgrade of k3s, you can set below to "false". + # Ideally, keep it on, to always have the latest Kubernetes version, but lock the initial_k3s_channel to a kube major version, + # of your choice, like v1.25 or v1.26. That way you get the best of both worlds without the breaking changes risk. + # For production use, always use an HA setup with at least 3 control-plane nodes and 2 agents, and keep this on for maximum security. + + # The default is "true" (in HA setup i.e. at least 3 control plane nodes & 2 agents, just keep it enabled since it works flawlessly). + automatically_upgrade_k3s = false + + # By default nodes are drained before k3s upgrade, which will delete and transfer all pods to other nodes. + # Set this to false to cordon nodes instead, which just prevents scheduling new pods on the node during upgrade + # and keeps all pods running. This may be useful if you have pods which are known to be slow to start e.g. + # because they have to mount volumes with many files which require to get the right security context applied. + system_upgrade_use_drain = true + + # During k3s via system-upgrade-manager pods are evicted by default. + # On small clusters this can lead to hanging upgrades and indefinitely unschedulable nodes, + # in that case, set this to false to immediately delete pods before upgrading. + # NOTE: Turning this flag off might lead to downtimes of services (which may be acceptable for your use case) + # NOTE: This flag takes effect only when system_upgrade_use_drain is set to true. + # system_upgrade_enable_eviction = false + + # The default is "true" (in HA setup it works wonderfully well, with automatic roll-back to the previous snapshot in case of an issue). + # IMPORTANT! For non-HA clusters i.e. when the number of control-plane nodes is < 3, you have to turn it off. + automatically_upgrade_os = false + + # If you need more control over kured and the reboot behaviour, you can pass additional options to kured. + # For example limiting reboots to certain timeframes. For all options see: https://kured.dev/docs/configuration/ + # By default, the kured lock does not expire and is only released once a node successfully reboots. You can add the option + # "lock-ttl" : "30m", if you have a single node which sometimes gets stuck. Note however, that in that case, kured continuous + # draining the next node because the lock was released. You may end up with all nodes drained and your cluster completely down. + # The default options are: `--reboot-command=/usr/bin/systemctl reboot --pre-reboot-node-labels=kured=rebooting --post-reboot-node-labels=kured=done --period=5m` + # Defaults can be overridden by using the same key. + # kured_options = { + # "reboot-days": "su", + # "start-time": "3am", + # "end-time": "8am", + # "time-zone": "Local", + # "lock-ttl" : "30m", + # } + + # Allows you to specify the k3s version. If defined, supersedes initial_k3s_channel. + # See https://github.com/k3s-io/k3s/releases for the available versions. + # install_k3s_version = "v1.30.2+k3s2" + + # Allows you to specify either stable, latest, testing or supported minor versions. + # see https://rancher.com/docs/k3s/latest/en/upgrades/basic/ and https://update.k3s.io/v1-release/channels + # ⚠️ If you are going to use Rancher addons for instance, it's always a good idea to fix the kube version to one minor version below the latest stable, + # e.g. v1.29 instead of the stable v1.30. + # The default is "v1.30". + # initial_k3s_channel = "stable" + + # Allows to specify the version of the System Upgrade Controller for automated upgrades of k3s + # See https://github.com/rancher/system-upgrade-controller/releases for the available versions. + # sys_upgrade_controller_version = "v0.14.2" + + # The cluster name, by default "k3s" + # cluster_name = "k3s" + + # Whether to use the cluster name in the node name, in the form of {cluster_name}-{nodepool_name}, the default is "true". + # use_cluster_name_in_node_name = false + + # Extra k3s registries. This is useful if you have private registries and you want to pull images without additional secrets. + # Or if you want to proxy registries for various reasons like rate-limiting. + # It will create the registries.yaml file, more info here https://docs.k3s.io/installation/private-registry. + # Note that you do not need to get this right from the first time, you can update it when you want during the life of your cluster. + # The default is blank. + /* k3s_registries = <<-EOT + mirrors: + hub.my_registry.com: + endpoint: + - "hub.my_registry.com" + configs: + hub.my_registry.com: + auth: + username: username + password: password + EOT */ + + # Additional environment variables for the host OS on which k3s runs. See for example https://docs.k3s.io/advanced#configuring-an-http-proxy . + # additional_k3s_environment = { + # "CONTAINERD_HTTP_PROXY" : "http://your.proxy:port", + # "CONTAINERD_HTTPS_PROXY" : "http://your.proxy:port", + # "NO_PROXY" : "127.0.0.0/8,10.0.0.0/8,", + # } + + # Additional commands to execute on the host OS before the k3s install, for example fetching and installing certs. + # preinstall_exec = [ + # "curl https://somewhere.over.the.rainbow/ca.crt > /root/ca.crt", + # "trust anchor --store /root/ca.crt", + # ] + + # Structured authentication configuration. Multiple authentication providers support requires v1.30+ of + # kubernetes. + # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#using-authentication-configuration + # + # authentication_config = <<-EOT + # apiVersion: apiserver.config.k8s.io/v1beta1 + # kind: AuthenticationConfiguration + # jwt: + # - issuer: + # url: "https://token.actions.githubusercontent.com" + # audiences: + # - "https://github.com/octo-org" + # claimMappings: + # username: + # claim: sub + # prefix: "gh:" + # groups: + # claim: repository_owner + # prefix: "gh:" + # claimValidationRules: + # - claim: repository + # requiredValue: "octo-org/octo-repo" + # - claim: "repository_visibility" + # requiredValue: "public" + # - claim: "ref" + # requiredValue: "refs/heads/main" + # - claim: "ref_type" + # requiredValue: "branch" + # - issuer: + # url: "https://your.oidc.issuer" + # audiences: + # - "oidc_client_id" + # claimMappings: + # username: + # claim: oidc_username_claim + # prefix: "oidc:" + # groups: + # claim: oidc_groups_claim + # prefix: "oidc:" + # EOT + + # Set to true if util-linux breaks on the OS (temporary regression fixed in util-linux v2.41.1). + # k3s_prefer_bundled_bin = true + + # Additional flags to pass to the k3s server command (the control plane). + # k3s_exec_server_args = "--kube-apiserver-arg enable-admission-plugins=PodTolerationRestriction,PodNodeSelector" + + # Additional flags to pass to the k3s agent command (every agents nodes, including autoscaler nodepools). + # k3s_exec_agent_args = "--kubelet-arg kube-reserved=cpu=100m,memory=200Mi,ephemeral-storage=1Gi" + + # The vars below here passes it to the k3s config.yaml. This way it persist across reboots + # Make sure you set "feature-gates=NodeSwap=true,CloudDualStackNodeIPs=true" if want to use swap_size + # see https://github.com/k3s-io/k3s/issues/8811#issuecomment-1856974516 + # k3s_global_kubelet_args = ["kube-reserved=cpu=100m,ephemeral-storage=1Gi", "system-reserved=cpu=memory=200Mi", "image-gc-high-threshold=50", "image-gc-low-threshold=40"] + # k3s_control_plane_kubelet_args = [] + # k3s_agent_kubelet_args = [] + # k3s_autoscaler_kubelet_args = [] + + # If you want to allow all outbound traffic you can set this to "false". Default is "true". + # restrict_outbound_traffic = false + + # Allow access to the Kube API from the specified networks. The default is ["0.0.0.0/0", "::/0"]. + # Allowed values: null (disable Kube API rule entirely) or a list of allowed networks with CIDR notation. + # For maximum security, it's best to disable it completely by setting it to null. However, in that case, to get access to the kube api, + # you would have to connect to any control plane node via SSH, as you can run kubectl from within these. + # Please be advised that this setting has no effect on the load balancer when the use_control_plane_lb variable is set to true. This is + # because firewall rules cannot be applied to load balancers yet. + # firewall_kube_api_source = null + + # Allow SSH access from the specified networks. Default: ["0.0.0.0/0", "::/0"] + # Allowed values: null (disable SSH rule entirely) or a list of allowed networks with CIDR notation. + # Ideally you would set your IP there. And if it changes after cluster deploy, you can always update this variable and apply again. + # firewall_ssh_source = ["1.2.3.4/32"] + + # By default, SELinux is enabled in enforcing mode on all nodes. For container-specific SELinux issues, + # consider using the pre-installed 'udica' tool to create custom, targeted SELinux policies instead of + # disabling SELinux globally. See the "Fix SELinux issues with udica" example in the README for details. + # disable_selinux = false + + # Adding extra firewall rules, like opening a port + # More info on the format here https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs/resources/firewall + extra_firewall_rules = [ + { + description = "Allow Outbound SMTP Requests - Brevo" + direction = "out" + protocol = "tcp" + port = "587" + source_ips = [] + destination_ips = ["1.179.112.0/20", "172.246.240.0/20"] + }, + { + description = "CTF - Allow all outbound connection" + direction = "out" + protocol = "tcp" + port = "1024-65535" + source_ips = [] + destination_ips = ["0.0.0.0/0", "::/0"] + }, + # { + # description = "For tcpsecure" + # direction = "in" + # protocol = "tcp" + # port = "32000" + # source_ips = ["0.0.0.0/0", "::/0"] + # destination_ips = [] # Won't be used for this rule + # }, + # { + # description = "For tcp" + # direction = "in" + # protocol = "tcp" + # port = "33000" + # source_ips = ["0.0.0.0/0", "::/0"] + # destination_ips = [] # Won't be used for this rule + # }, + + # { + # description = "For tcp 2" + # direction = "in" + # protocol = "tcp" + # port = "31000" + # source_ips = ["0.0.0.0/0", "::/0"] + # destination_ips = [] # Won't be used for this rule + # }, + ] + + # If you want to configure a different CNI for k3s, use this flag + # possible values: flannel (Default), calico, and cilium + # As for Cilium, we allow infinite configurations via helm values, please check the CNI section of the readme over at https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner/#cni. + # Also, see the cilium_values at towards the end of this file, in the advanced section. + # ⚠️ Depending on your setup, sometimes you need your control-planes to have more than + # 2GB of RAM if you are going to use Cilium, otherwise the pods will not start. + # cni_plugin = "cilium" + + # You can choose the version of Cilium that you want. By default we keep the version up to date and configure Cilium with compatible settings according to the version. + # See https://github.com/cilium/cilium/releases for the available versions. + # cilium_version = "v1.14.0" + + # Set native-routing mode ("native") or tunneling mode ("tunnel"). Default: tunnel + # cilium_routing_mode = "native" + + # Used when Cilium is configured in native routing mode. The CNI assumes that the underlying network stack will forward packets to this destination without the need to apply SNAT. Default: value of "cluster_ipv4_cidr" + # cilium_ipv4_native_routing_cidr = "10.0.0.0/8" + + # Enables egress gateway to redirect and SNAT the traffic that leaves the cluster. Default: false + # cilium_egress_gateway_enabled = true + + # Enables Hubble Observability to collect and visualize network traffic. Default: false + # cilium_hubble_enabled = true + + # You can choose the version of Calico that you want. By default, the latest is used. + # More info on available versions can be found at https://github.com/projectcalico/calico/releases + # Please note that if you are getting 403s from Github, it's also useful to set the version manually. However there is rarely a need for that! + # calico_version = "v3.27.2" + + # If you want to disable the k3s kube-proxy, use this flag. The default is "false". + # Ensure that your CNI is capable of handling all the functionalities typically covered by kube-proxy. + # disable_kube_proxy = true + + # If you want to disable the k3s default network policy controller, use this flag! + # Both Calico and Cilium cni_plugin values override this value to true automatically, the default is "false". + # disable_network_policy = true + + # If you want to disable the automatic use of placement group "spread". See https://docs.hetzner.com/cloud/placement-groups/overview/ + # We advise to not touch that setting, unless you have a specific purpose. + # The default is "false", meaning it's enabled by default. + # placement_group_disable = true + + # By default, we allow ICMP ping in to the nodes, to check for liveness for instance. If you do not want to allow that, you can. Just set this flag to true (false by default). + # block_icmp_ping_in = true + + # You can enable cert-manager (installed by Helm behind the scenes) with the following flag, the default is "true". + enable_cert_manager = false + + # IP Addresses to use for the DNS Servers, the defaults are the ones provided by Hetzner https://docs.hetzner.com/dns-console/dns/general/recursive-name-servers/. + # The number of different DNS servers is limited to 3 by Kubernetes itself. + # It's always a good idea to have at least 1 IPv4 and 1 IPv6 DNS server for robustness. + dns_servers = [ + "1.1.1.1", + "8.8.8.8", + "2606:4700:4700::1111", + ] + + # When this is enabled, rather than the first node, all external traffic will be routed via a control-plane loadbalancer, allowing for high availability. + # The default is false. + use_control_plane_lb = true + + # When the above use_control_plane_lb is enabled, you can change the lb type for it, the default is "lb11". + # control_plane_lb_type = "lb21" + + # When the above use_control_plane_lb is enabled, you can change to disable the public interface for control plane load balancer, the default is true. + # control_plane_lb_enable_public_interface = false + + # Let's say you are not using the control plane LB solution above, and still want to have one hostname point to all your control-plane nodes. + # You could create multiple A records of to let's say cp.cluster.my.org pointing to all of your control-plane nodes ips. + # In which case, you need to define that hostname in the k3s TLS-SANs config to allow connection through it. It can be hostnames or IP addresses. + # additional_tls_sans = ["cp.cluster.my.org"] + + # If you create a hostname with multiple A records pointing to all of your + # control-plane nodes ips, you may want to use that hostname in the generated + # kubeconfig. + # kubeconfig_server_address = "cp.cluster.my.org" + + # lb_hostname Configuration: + # + # Purpose: + # The lb_hostname setting optimizes communication between services within the Kubernetes cluster + # when they use domain names instead of direct service names. By associating a domain name directly + # with the Hetzner Load Balancer, this setting can help reduce potential communication delays. + # + # Scenario: + # If Service B communicates with Service A using a domain (e.g., `a.mycluster.domain.com`) that points + # to an external Load Balancer, there can be a slowdown in communication. + # + # Guidance: + # - If your internal services use domain names pointing to an external LB, set lb_hostname to a domain + # like `mycluster.domain.com`. + # - Create an A record pointing `mycluster.domain.com` to your LB's IP. + # - Create a CNAME record for `a.mycluster.domain.com` (or xyz.com) pointing to `mycluster.domain.com`. + # + # Technical Note: + # This setting sets the `load-balancer.hetzner.cloud/hostname` in the Hetzner LB definition, suitable for + # HAProxy, Nginx and Traefik ingress controllers. + # + # Recommendation: + # This setting is optional. If services communicate using direct service names, you can leave this unset. + # For inter-namespace communication, use `.service_name` as per Kubernetes norms. + # + # Example: + # lb_hostname = var.cluster_dns + + # You can enable Rancher (installed by Helm behind the scenes) with the following flag, the default is "false". + # ⚠️ Rancher often doesn't support the latest Kubernetes version. You will need to set initial_k3s_channel to a supported version. + # When Rancher is enabled, it automatically installs cert-manager too, and it uses rancher's own self-signed certificates. + # See for options https://ranchermanager.docs.rancher.com/getting-started/installation-and-upgrade/install-upgrade-on-a-kubernetes-cluster#3-choose-your-ssl-configuration + # The easiest thing is to leave everything as is (using the default rancher self-signed certificate) and put Cloudflare in front of it. + # As for the number of replicas, by default it is set to the number of control plane nodes. + # You can customized all of the above by adding a rancher_values variable see at the end of this file in the advanced section. + # After the cluster is deployed, you can always use HelmChartConfig definition to tweak the configuration. + # IMPORTANT: Rancher's install is quite memory intensive, you will require at least 4GB if RAM, meaning cx21 server type (for your control plane). + # ALSO, in order for Rancher to successfully deploy, you have to set the "rancher_hostname". + # enable_rancher = true + + # If using Rancher you can set the Rancher hostname, it must be unique hostname even if you do not use it. + # If not pointing the DNS, you can just port-forward locally via kubectl to get access to the dashboard. + # If you already set the lb_hostname above and are using a Hetzner LB, you do not need to set this one, as it will be used by default. + # But if you set this one explicitly, it will have preference over the lb_hostname in rancher settings. + # rancher_hostname = "rancher.xyz.dev" + + # When Rancher is deployed, by default is uses the "latest" channel. But this can be customized. + # The allowed values are "stable" or "latest". + # rancher_install_channel = "stable" + + # Finally, you can specify a bootstrap-password for your rancher instance. Minimum 48 characters long! + # If you leave empty, one will be generated for you. + # (Can be used by another rancher2 provider to continue setup of rancher outside this module.) + # rancher_bootstrap_password = "" + + # Separate from the above Rancher config (only use one or the other). You can import this cluster directly on an + # an already active Rancher install. By clicking "import cluster" choosing "generic", giving it a name and pasting + # the cluster registration url below. However, you can also ignore that and apply the url via kubectl as instructed + # by Rancher in the wizard, and that would register your cluster too. + # More information about the registration can be found here https://rancher.com/docs/rancher/v2.6/en/cluster-provisioning/registered-clusters/ + # rancher_registration_manifest_url = "https://rancher.xyz.dev/v3/import/xxxxxxxxxxxxxxxxxxYYYYYYYYYYYYYYYYYYYzzzzzzzzzzzzzzzzzzzzz.yaml" + + # Extra commands to be executed after the `kubectl apply -k` (useful for post-install actions, e.g. wait for CRD, apply additional manifests, etc.). + # extra_kustomize_deployment_commands="" + + # Extra values that will be passed to the `extra-manifests/kustomization.yaml.tpl` if its present. + # extra_kustomize_parameters={} + + # See an working example for just a manifest.yaml, a HelmChart and a HelmChartConfig examples/kustomization_user_deploy/README.md + + # It is best practice to turn this off, but for backwards compatibility it is set to "true" by default. + # See https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner/issues/349 + # When "false". The kubeconfig file can instead be created by executing: "terraform output --raw kubeconfig > cluster_kubeconfig.yaml" + # Always be careful to not commit this file! + create_kubeconfig = false + + # Don't create the kustomize backup. This can be helpful for automation. + # create_kustomization = false + + # Export the values.yaml files used for the deployment of traefik, longhorn, cert-manager, etc. + # This can be helpful to use them for later deployments like with ArgoCD. + # The default is false. + # export_values = true + + # MicroOS snapshot IDs to be used. Per default empty, the most recent image created using createkh will be used. + # We recommend the default, but if you want to use specific IDs you can. + # You can fetch the ids with the hcloud cli by running the "hcloud image list --selector 'microos-snapshot=yes'" command. + # microos_x86_snapshot_id = "1234567" + # microos_arm_snapshot_id = "1234567" + + ### ADVANCED - Custom helm values for packages above (search _values if you want to located where those are mentioned upper in this file) + # ⚠️ Inside the _values variable below are examples, up to you to find out the best helm values possible, we do not provide support for customized helm values. + # Please understand that the indentation is very important, inside the EOTs, as those are proper yaml helm values. + # We advise you to use the default values, and only change them if you know what you are doing! + + # You can inline the values here in heredoc-style (as the examples below with the < 0 +scale_min = 0 + +load_balancer_type = "lb11" # Load balancer type, see https://www.hetzner.com/cloud/load-balancer diff --git a/cluster/variables.tf b/cluster/variables.tf new file mode 100644 index 0000000..8010dda --- /dev/null +++ b/cluster/variables.tf @@ -0,0 +1,229 @@ +# ---------------------- +# Variables +# ---------------------- + +# Hetzner +variable "hcloud_token" { + sensitive = true + description = "Hetzner cloud project token (obtained from a specific project in Hetzner cloud)" +} + +# SSH +variable "ssh_key_private_base64" { + sensitive = true + description = "The private key to use for SSH access to the servers (base64 encoded)" +} + +variable "ssh_key_public_base64" { + description = "The public key to use for SSH access to the servers (base64 encoded)" +} + +# Cloudflare & DNS variables +variable "cloudflare_api_token" { + sensitive = true # Requires terraform >= 0.14 + type = string + description = "Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains)" +} + +variable "cloudflare_dns_management" { + type = string + description = "The top level domain (TLD) to use for the DNS records for the management part of the cluster" +} + +variable "cluster_dns_management" { + type = string + description = "The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management`" +} + +variable "cloudflare_dns_ctf" { + type = string + description = "The top level domain (TLD) to use for the DNS records for the CTF part of the cluster" +} + +variable "cloudflare_dns_platform" { + type = string + description = "The domain name to use for the DNS records for the CTF part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf`" +} + +variable "cluster_dns_ctf" { + type = string + description = "The top level domain (TLD) to use for the DNS records for the platform part of the cluster" +} + +variable "cluster_dns_platform" { + type = string + description = "The domain name to use for the DNS records for the platform part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_platform`" +} + +# Cluster configuration +variable "region_1" { + type = string + description = "Region to deploy nodes in subgroup 1" + default = "fsn1" + validation { + condition = contains(["fsn1", "hel1", "nbg1"], var.region_1) + error_message = "Region must be one of fsn1, hel1, or nbg1." + } +} + +variable "region_2" { + type = string + description = "Region to deploy nodes in subgroup 2" + default = "fsn1" + validation { + condition = contains(["fsn1", "hel1", "nbg1"], var.region_2) + error_message = "Region must be one of fsn1, hel1, or nbg1." + } +} + +variable "region_3" { + type = string + description = "Region to deploy nodes in subgroup 3" + default = "fsn1" + validation { + condition = contains(["fsn1", "hel1", "nbg1", "ash", "hil", "sin"], var.region_3) + error_message = "Region must be one of fsn1, hel1, or nbg1." + } +} + +variable "network_zone" { + type = string + description = "The Hetzner network zone to deploy the cluster in" + default = "eu-central" + validation { + condition = contains(["eu-central", "us-east", "us-west", "ap-southeast"], var.network_zone) + error_message = "Network zone must be one of eu-central or us-west." + } +} + +variable "control_plane_type_1" { + type = string + description = "Control plane group 1 server type" + default = "cx32" +} + +variable "control_plane_type_2" { + type = string + description = "Control plane group 2 server type" + default = "cx32" +} + +variable "control_plane_type_3" { + type = string + description = "Control plane group 3 server type" + default = "cx32" +} + +variable "agent_type_1" { + type = string + description = "Agent group 1 server type" + default = "cx32" +} + +variable "agent_type_2" { + type = string + description = "Agent group 2 server type" + default = "cx32" +} + +variable "agent_type_3" { + type = string + description = "Agent group 3 server type" + default = "cx32" +} + +variable "scale_type" { + type = string + description = "Scale group server type" + default = "cx32" +} + +variable "load_balancer_type" { + type = string + description = "Load balancer type" + default = "lb11" + validation { + condition = contains(["lb11", "lb21", "lb31"], var.load_balancer_type) + error_message = "Load balancer type must be one of lb11, lb21, or lb31." + } +} + +variable "control_plane_count_1" { + type = number + description = "Number of control plane nodes in group 1" + default = 1 + validation { + condition = var.control_plane_count_1 >= 0 + error_message = "Control plane count must be at least 0." + } +} + +variable "control_plane_count_2" { + type = number + description = "Number of control plane nodes in group 2" + default = 1 + validation { + condition = var.control_plane_count_2 >= 0 + error_message = "Control plane count must be at least 0." + } +} + +variable "control_plane_count_3" { + type = number + description = "Number of control plane nodes in group 3" + default = 1 + validation { + condition = var.control_plane_count_3 >= 0 + error_message = "Control plane count must be at least 0." + } +} + +variable "agent_count_1" { + type = number + description = "Number of agent nodes in group 1" + default = 1 + validation { + condition = var.agent_count_1 >= 0 + error_message = "Agent count must be at least 0." + } +} + +variable "agent_count_2" { + type = number + description = "Number of agent nodes in group 2" + default = 1 + validation { + condition = var.agent_count_2 >= 0 + error_message = "Agent count must be at least 0." + } +} + +variable "agent_count_3" { + type = number + description = "Number of agent nodes in group 3" + default = 1 + validation { + condition = var.agent_count_3 >= 0 + error_message = "Agent count must be at least 0." + } +} + +variable "scale_count" { + type = number + description = "Number of scale nodes" + default = 0 + validation { + condition = var.scale_count >= 0 + error_message = "Scale count must be at least 0." + } +} + +variable "scale_min" { + type = number + description = "Minimum number of scale nodes - Only applicable if scale_count > 0" + default = 0 +# validation { +# condition = var.scale_min >= 0 && var.scale_min <= var.scale_count +# error_message = "Scale min must be between 0 and the scale count." +# } +} From ec2a8dbdf030128263b2974f7e5f7a112265d503 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 20:07:05 +0100 Subject: [PATCH 003/148] Update formatting of variables --- cluster/tfvars/template.tfvars | 10 +++++----- cluster/variables.tf | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index 20dad01..84c06a6 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -20,8 +20,8 @@ ssh_key_public_base64 = "" # The public key to use for SSH access # This is to sepearte the two parts of the cluster, and to allow for different DNS records for the two parts. It may be the same domain. The specific subdomains is set later. cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) cloudflare_dns_management = "" # The top level domain (TLD) to use for the DNS records for the management part of the cluster -cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF part of the cluster cloudflare_dns_platform = "" # The top level domain (TLD) to use for the DNS records for the platform part of the cluster +cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF part of the cluster # ------------------------ # DNS information @@ -29,8 +29,8 @@ cloudflare_dns_platform = "" # The top level domain (TLD) t # The cluster uses two domains for the management and CTF parts of the cluster. # The following is the actually used subdomains for the two parts of the cluster. They may be either TLD or subdomains. cluster_dns_management = "" # The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management` -cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf` cluster_dns_platform = "" # The domain name to use for the DNS records for the platform part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_platform` +cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF challenges part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf` # ------------------------ # Cluster configuration @@ -39,9 +39,9 @@ cluster_dns_platform = "" # The domain name to use for # For uptimal performance, it is recommended to use the same region for all servers. # Region 1 is used for scale nodes and loadbalancer. # Possible values: fsn1, hel1, nbg1 -region_1 = "fsn1" # Region for subgroup 1 -region_2 = "fsn1" # Region for subgroup 2 -region_3 = "fsn1" # Region for subgroup 3 +region_1 = "fsn1" # Region for subgroup 1 +region_2 = "fsn1" # Region for subgroup 2 +region_3 = "fsn1" # Region for subgroup 3 network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central", "us-east", "us-west", "ap-southeast". Regions must be within the network zone. # Servers diff --git a/cluster/variables.tf b/cluster/variables.tf index 8010dda..2b6c7d2 100644 --- a/cluster/variables.tf +++ b/cluster/variables.tf @@ -30,29 +30,29 @@ variable "cloudflare_dns_management" { description = "The top level domain (TLD) to use for the DNS records for the management part of the cluster" } -variable "cluster_dns_management" { +variable "cloudflare_dns_platform" { type = string - description = "The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management`" + description = "The top level domain (TLD) to use for the DNS records for the platform part of the cluster" } variable "cloudflare_dns_ctf" { type = string - description = "The top level domain (TLD) to use for the DNS records for the CTF part of the cluster" + description = "The top level domain (TLD) to use for the DNS records for the CTF challenges part of the cluster" } -variable "cloudflare_dns_platform" { +variable "cluster_dns_management" { type = string - description = "The domain name to use for the DNS records for the CTF part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf`" + description = "The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management`" } -variable "cluster_dns_ctf" { +variable "cluster_dns_platform" { type = string - description = "The top level domain (TLD) to use for the DNS records for the platform part of the cluster" + description = "The domain name to use for the DNS records for the platform part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_platform`" } -variable "cluster_dns_platform" { +variable "cluster_dns_ctf" { type = string - description = "The domain name to use for the DNS records for the platform part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_platform`" + description = "The domain name to use for the DNS records for the CTF challenges part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf`" } # Cluster configuration From 1c72e3f634947020958ae2cac10e5d1fe062e09f Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 20:17:15 +0100 Subject: [PATCH 004/148] Update scaling variables for challenge nodes and autoscaler --- cluster/kube.tf | 6 +++--- cluster/tfvars/template.tfvars | 8 ++++---- cluster/variables.tf | 36 +++++++++++++++++----------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cluster/kube.tf b/cluster/kube.tf index f5309bd..213869e 100644 --- a/cluster/kube.tf +++ b/cluster/kube.tf @@ -239,7 +239,7 @@ module "kube-hetzner" { taints = [ "cluster.ctfpilot.com/node=scaler:PreferNoSchedule" ] - count = var.scale_min + count = var.challs_count kubelet_args = [ "kube-reserved=cpu=150m,memory=750Mi,ephemeral-storage=1Gi", "system-reserved=cpu=300m,memory=750Mi", @@ -403,8 +403,8 @@ module "kube-hetzner" { name = "autoscaled-challs-nodes" server_type = var.scale_type location = var.region_1 - min_nodes = 0 # var.scale_count > 0 ? var.scale_min : 0 - max_nodes = var.scale_count + min_nodes = 0 + max_nodes = var.scale_max labels = { "ressource-type" : "node", "node-type" : "scale", diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index 84c06a6..d165c49 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -69,9 +69,9 @@ control_plane_count_3 = 1 # Number of control plane nodes in group 3 agent_count_1 = 1 # Number of agent nodes in group 1 agent_count_2 = 1 # Number of agent nodes in group 2 agent_count_3 = 1 # Number of agent nodes in group 3 -# Optional - 0 means no scale nodes available to the autoscaler. -scale_count = 0 -# Minimum number of scale nodes - Only applicable if scale_count > 0 -scale_min = 0 +# Challenge nodes - Nodes dedicated to running CTF challenges. These nodes are tainted to only run challenge workloads. +challs_count = 0 # Number of challenge nodes. +# Scale nodes - Nodes that are automatically scaled by the cluster autoscaler. These nodes are used to scale the cluster up or down dynamically. +scale_max = 0 # Maximum number of scale nodes. Set to 0 to disable autoscaling. load_balancer_type = "lb11" # Load balancer type, see https://www.hetzner.com/cloud/load-balancer diff --git a/cluster/variables.tf b/cluster/variables.tf index 2b6c7d2..808dd0c 100644 --- a/cluster/variables.tf +++ b/cluster/variables.tf @@ -20,38 +20,38 @@ variable "ssh_key_public_base64" { # Cloudflare & DNS variables variable "cloudflare_api_token" { - sensitive = true # Requires terraform >= 0.14 - type = string + sensitive = true # Requires terraform >= 0.14 + type = string description = "Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains)" } variable "cloudflare_dns_management" { - type = string + type = string description = "The top level domain (TLD) to use for the DNS records for the management part of the cluster" } variable "cloudflare_dns_platform" { - type = string + type = string description = "The top level domain (TLD) to use for the DNS records for the platform part of the cluster" } variable "cloudflare_dns_ctf" { - type = string + type = string description = "The top level domain (TLD) to use for the DNS records for the CTF challenges part of the cluster" } variable "cluster_dns_management" { - type = string + type = string description = "The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management`" } variable "cluster_dns_platform" { - type = string + type = string description = "The domain name to use for the DNS records for the platform part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_platform`" } variable "cluster_dns_ctf" { - type = string + type = string description = "The domain name to use for the DNS records for the CTF challenges part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf`" } @@ -208,22 +208,22 @@ variable "agent_count_3" { } } -variable "scale_count" { +variable "challs_count" { type = number - description = "Number of scale nodes" + description = "Number of CTF challenge nodes" default = 0 validation { - condition = var.scale_count >= 0 - error_message = "Scale count must be at least 0." + condition = var.challs_count >= 0 + error_message = "CTF challenge count must be at least 0." } } -variable "scale_min" { +variable "scale_max" { type = number - description = "Minimum number of scale nodes - Only applicable if scale_count > 0" + description = "Maximum number of scale nodes. Set to 0 to disable autoscaling (default: 0)" default = 0 -# validation { -# condition = var.scale_min >= 0 && var.scale_min <= var.scale_count -# error_message = "Scale min must be between 0 and the scale count." -# } + validation { + condition = var.scale_max >= 0 + error_message = "Scale max must be at least 0." + } } From d9f4603eb22c05b92e74e40b896de4777d548f29 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 21:24:49 +0100 Subject: [PATCH 005/148] feat: add CTFp ops setup --- ops/.gitignore | 37 + ops/.terraform.lock.hcl | 131 + ops/README.md | 59 + ops/argocd.tf | 116 + ops/cert-manager.tf | 90 + ops/descheduler.tf | 24 + ops/errors.tf | 146 + ops/filebeat-values/values.yaml | 22 + ops/filebeat.tf | 226 + ops/ingress.tf | 44 + ops/mariadb-operator.tf | 52 + ops/prod-default-web.tf | 132 + ops/prometheus.tf | 214 + ops/prometheus/README.md | 7 + ops/prometheus/grafana/alerts/.gitkeep | 0 .../grafana/contact/notificationpolicy.yaml | 9 + ops/prometheus/grafana/contact/notifiers.yaml | 11 + .../dashboards/ctf/container-usage.json | 1096 +++++ .../dashboards/ctf/ctfd-challenges.json | 116 + .../grafana/dashboards/ctf/ctfd.json | 1550 +++++++ .../grafana/dashboards/ctf/kubectf.json | 1577 +++++++ .../grafana/dashboards/ctf/node-usage.json | 472 ++ .../dashboards/ctf/team-intsances.json | 174 + .../dashboards/k8s/k8s-addons-prometheus.json | 3187 +++++++++++++ .../k8s/k8s-addons-trivy-operator.json | 2733 +++++++++++ .../dashboards/k8s/k8s-system-api-server.json | 1399 ++++++ .../dashboards/k8s/k8s-system-coredns.json | 1600 +++++++ .../dashboards/k8s/k8s-views-global.json | 3561 +++++++++++++++ .../dashboards/k8s/k8s-views-namespaces.json | 3035 +++++++++++++ .../dashboards/k8s/k8s-views-nodes.json | 4019 +++++++++++++++++ .../dashboards/k8s/k8s-views-pods.json | 2717 +++++++++++ .../dashboards/redis/redis-dashboard.json | 1543 +++++++ .../dashboards/redis/redis-operator.json | 874 ++++ .../dashboards/traefik/traefik-custom.json | 1482 ++++++ ops/prometheus/grafana/notification/.gitkeep | 0 .../kube_prometheus_custom_values.yaml | 147 + ops/providers.tf | 81 + ops/redis-operator.tf | 25 + ops/tfvars/.gitignore | 1 + ops/tfvars/template.tfvars | 76 + ops/traefik.tf | 246 + ops/variables.tf | 169 + 42 files changed, 33200 insertions(+) create mode 100644 ops/.gitignore create mode 100644 ops/.terraform.lock.hcl create mode 100644 ops/README.md create mode 100644 ops/argocd.tf create mode 100644 ops/cert-manager.tf create mode 100644 ops/descheduler.tf create mode 100644 ops/errors.tf create mode 100644 ops/filebeat-values/values.yaml create mode 100644 ops/filebeat.tf create mode 100644 ops/ingress.tf create mode 100644 ops/mariadb-operator.tf create mode 100644 ops/prod-default-web.tf create mode 100644 ops/prometheus.tf create mode 100644 ops/prometheus/README.md create mode 100644 ops/prometheus/grafana/alerts/.gitkeep create mode 100644 ops/prometheus/grafana/contact/notificationpolicy.yaml create mode 100644 ops/prometheus/grafana/contact/notifiers.yaml create mode 100644 ops/prometheus/grafana/dashboards/ctf/container-usage.json create mode 100644 ops/prometheus/grafana/dashboards/ctf/ctfd-challenges.json create mode 100644 ops/prometheus/grafana/dashboards/ctf/ctfd.json create mode 100644 ops/prometheus/grafana/dashboards/ctf/kubectf.json create mode 100644 ops/prometheus/grafana/dashboards/ctf/node-usage.json create mode 100644 ops/prometheus/grafana/dashboards/ctf/team-intsances.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-addons-prometheus.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-addons-trivy-operator.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-system-api-server.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-system-coredns.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-views-global.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-views-namespaces.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-views-nodes.json create mode 100644 ops/prometheus/grafana/dashboards/k8s/k8s-views-pods.json create mode 100644 ops/prometheus/grafana/dashboards/redis/redis-dashboard.json create mode 100644 ops/prometheus/grafana/dashboards/redis/redis-operator.json create mode 100644 ops/prometheus/grafana/dashboards/traefik/traefik-custom.json create mode 100644 ops/prometheus/grafana/notification/.gitkeep create mode 100644 ops/prometheus/kube_prometheus_custom_values.yaml create mode 100644 ops/providers.tf create mode 100644 ops/redis-operator.tf create mode 100644 ops/tfvars/.gitignore create mode 100644 ops/tfvars/template.tfvars create mode 100644 ops/traefik.tf create mode 100644 ops/variables.tf diff --git a/ops/.gitignore b/ops/.gitignore new file mode 100644 index 0000000..2faf43d --- /dev/null +++ b/ops/.gitignore @@ -0,0 +1,37 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/ops/.terraform.lock.hcl b/ops/.terraform.lock.hcl new file mode 100644 index 0000000..f710863 --- /dev/null +++ b/ops/.terraform.lock.hcl @@ -0,0 +1,131 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/alekc/kubectl" { + version = "2.1.3" + constraints = "~> 2.0, >= 2.0.2" + hashes = [ + "h1:AymCb0DCWzmyLqn1qEhVs2pcFUZGT/kxPK+I/BObFH8=", + "zh:0e601ae36ebc32eb8c10aff4c48c1125e471fa09f5668465af7581c9057fa22c", + "zh:1773f08a412d1a5f89bac174fe1efdfd255ecdda92d31a2e31937e4abf843a2f", + "zh:1da2db1f940c5d34e31c2384c7bd7acba68725cc1d3ba6db0fec42efe80dbfb7", + "zh:20dc810fb09031bcfea4f276e1311e8286d8d55705f55433598418b7bcc76357", + "zh:326a01c86ba90f6c6eb121bacaabb85cfa9059d6587aea935a9bbb6d3d8e3f3f", + "zh:5a3737ea1e08421fe3e700dc833c6fd2c7b8c3f32f5444e844b3fe0c2352757b", + "zh:5f490acbd0348faefea273cb358db24e684cbdcac07c71002ee26b6cfd2c54a0", + "zh:777688cda955213ba637e2ac6b1994e438a5af4d127a34ecb9bb010a8254f8a8", + "zh:7acc32371053592f55ee0bcbbc2f696a8466415dea7f4bc5a6573f03953fc926", + "zh:81f0108e2efe5ae71e651a8826b61d0ce6918811ccfdc0e5b81b2cfb0f7f57fe", + "zh:88b785ea7185720cf40679cb8fa17e57b8b07fd6322cf2d4000b835282033d81", + "zh:89d833336b5cd027e671b46f9c5bc7d10c5109e95297639bbec8001da89aa2f7", + "zh:df108339a89d4372e5b13f77bd9d53c02a04362fb5d85e1d9b6b47292e30821c", + "zh:e8a2e3a5c50ca124e6014c361d72a9940d8e815f37ae2d1e9487ac77c3043013", + ] +} + +provider "registry.opentofu.org/hashicorp/helm" { + version = "3.0.2" + constraints = ">= 2.16.1, ~> 3.0" + hashes = [ + "h1:17Ro1Gs9aCN5QGQ6RDvuianmNV3AxgegYqTJODlYdHI=", + "zh:100f75a700074568cfaee7884e4477c50b5468e086db5bb95d7d519581b65621", + "zh:578d09c7319d0dd0fee03a7fcb48bf68ac978c1fefaa0752cfcb9ecfb0a56a4e", + "zh:64e7cce303362b4bf132d1c61858ef0ada221af4a2ea0fdfd16ec43e562d459c", + "zh:7a64933e70733aeec44bf9b9b6ea3617fd075acb346b082197ded993cfa7d2be", + "zh:7caf4655a5bf72e6d212209ad5ea5c619269eca6e0d9930c85b59bbbdf57ce28", + "zh:a1e0208423445e2443516e52a4d72c556b1303705c90aaeb139fbb64a10d7c1c", + "zh:ac9e4417e9e0486bc60f6796da06356b59161c9923c56a7a5c9b4900a46ee52d", + "zh:b9588da386c17456b242bd18122836baeccdce3227aac4752e189ec9ad218da7", + "zh:d5b6ac3b0b6beb3d94886f45a5a96eb6d78ca2b657efd62b8e0650d8097ee60f", + "zh:db6761e7cf86825f13628e8f4e32818683efff61b0d909211e1096cc6ad84f83", + ] +} + +provider "registry.opentofu.org/hashicorp/http" { + version = "3.5.0" + hashes = [ + "h1:yvwvVZ0vdbsTUMru+7Cr0On1FVgDJHAaC6TNvy/OWzM=", + "zh:0a2b33494eec6a91a183629cf217e073be063624c5d3f70870456ddb478308e9", + "zh:180f40124fa01b98b3d2f79128646b151818e09d6a1a9ca08e0b032a0b1e9cb1", + "zh:3e29e1de149dc10bf78620526c7cb8c62cd76087f5630dfaba0e93cda1f3aa7b", + "zh:4420950200cf86042ec940d0e2c9b7c89966bf556bf8038ba36217eae663bca5", + "zh:5d1f7d02109b2e2dca7ec626e5563ee765583792d0fd64081286f16f9433bd0d", + "zh:8500b138d338b1994c4206aa577b5c44e1d7260825babcf43245a7075bfa52a5", + "zh:b42165a6c4cfb22825938272d12b676e4a6946ac4e750f85df870c947685df2d", + "zh:b919bf3ee8e3b01051a0da3433b443a925e272893d3724ee8fc0f666ec7012c9", + "zh:d13b81ea6755cae785b3e11634936cdff2dc1ec009dc9610d8e3c7eb32f42e69", + "zh:f1c9d2eb1a6b618ae77ad86649679241bd8d6aacec06d0a68d86f748687f4eb3", + ] +} + +provider "registry.opentofu.org/hashicorp/kubernetes" { + version = "2.38.0" + constraints = "~> 2.0, >= 2.32.0" + hashes = [ + "h1:nY7J9jFXcsRINog0KYagiWZw1GVYF9D2JmtIB7Wnrao=", + "zh:1096b41c4e5b2ee6c1980916fb9a8579bc1892071396f7a9432be058aabf3cbc", + "zh:2959fde9ae3d1deb5e317df0d7b02ea4977951ee6b9c4beb083c148ca8f3681c", + "zh:5082f98fcb3389c73339365f7df39fc6912bf2bd1a46d5f97778f441a67fd337", + "zh:620fd5d0fbc2d7a24ac6b420a4922e6093020358162a62fa8cbd37b2bac1d22e", + "zh:7f47c2de179bba35d759147c53082cad6c3449d19b0ec0c5a4ca8db5b06393e1", + "zh:89c3aa2a87e29febf100fd21cead34f9a4c0e6e7ae5f383b5cef815c677eb52a", + "zh:96eecc9f94938a0bc35b8a63d2c4a5f972395e44206620db06760b730d0471fc", + "zh:e15567c1095f898af173c281b66bffdc4f3068afdd9f84bb5b5b5521d9f29584", + "zh:ecc6b912629734a9a41a7cf1c4c73fb13b4b510afc9e7b2e0011d290bcd6d77f", + ] +} + +provider "registry.opentofu.org/hashicorp/random" { + version = "3.7.2" + hashes = [ + "h1:yHMBbZOIHlXUuBQ8Mhioe0hwmhermuboq2eNNoCJaf8=", + "zh:2ffeb1058bd7b21a9e15a5301abb863053a2d42dffa3f6cf654a1667e10f4727", + "zh:519319ed8f4312ed76519652ad6cd9f98bc75cf4ec7990a5684c072cf5dd0a5d", + "zh:7371c2cc28c94deb9dba62fbac2685f7dde47f93019273a758dd5a2794f72919", + "zh:9b0ac4c1d8e36a86b59ced94fa517ae9b015b1d044b3455465cc6f0eab70915d", + "zh:c6336d7196f1318e1cbb120b3de8426ce43d4cacd2c75f45dba2dbdba666ce00", + "zh:c71f18b0cb5d55a103ea81e346fb56db15b144459123f1be1b0209cffc1deb4e", + "zh:d2dc49a6cac2d156e91b0506d6d756809e36bf390844a187f305094336d3e8d8", + "zh:d5b5fc881ccc41b268f952dae303501d6ec9f9d24ee11fe2fa56eed7478e15d0", + "zh:db9723eaca26d58c930e13fde221d93501529a5cd036b1f167ef8cff6f1a03cc", + "zh:fe3359f733f3ab518c6f85f3a9cd89322a7143463263f30321de0973a52d4ad8", + ] +} + +provider "registry.opentofu.org/hashicorp/time" { + version = "0.13.1" + hashes = [ + "h1:ueilLAoXlZPufdJYuPFeqznwP39ZwLsRcQtqow+NUiI=", + "zh:10f32af8b544a039f19abd546e345d056a55cb7bdd69d5bbd7322cbc86883848", + "zh:35dd5beb34a9f73de8d0fed332814c69acae69397c9c065ce63ccd8315442bef", + "zh:56545d1dd5f2e7262e0c0c124264974229ec9cc234d0d7a0e36e14b869590f4a", + "zh:8d7259c3f819fd3470ff933c904b6a549502a8351feb1b5c040a4560decaf7e0", + "zh:a40f26878826b142e26fe193f7e3e14fc97f615cd6af140e88ce5bc25f3fcf50", + "zh:b2e82f25fecff172a9a9e24ea37d37e4fc630ee9245617cb40b10e66a6b979c8", + "zh:d4b699850a40ed07ef83c6b827605d24050b2732646ee017bda278e4ddf01c91", + "zh:e4e6a5e5614b6a54557400aabb748ebd57e947cdbd21ad1c7602c51368a80559", + "zh:eb78fb97bca22931e730487a20a90f5a6221ddfb3138aaf070737ea2b7c9c885", + "zh:faba366a1352ee679bba2a5b09c073c6854721db94b191d49b620b60946a065f", + ] +} + +provider "registry.opentofu.org/loafoe/htpasswd" { + version = "1.2.1" + hashes = [ + "h1:W1euQGM6t+QlB6Rq4fDbRKRHmeCIyYdIYdHrxL97BeE=", + "zh:14460c85ddc40a9ecadf583c22a7de91b83798a8ca4843949d50c3288c6f5bdd", + "zh:1af9416e28dd0a77c5d2c685561c4f60e19e2d606df0477ebc18eaa110c77807", + "zh:2245325864faaf027701ab12a04d641359a0dc439dd23c6e8f768407b78a5c18", + "zh:3813ff98198405d7c467565b52c7f0ad4533f43957da6390477dc898f8ed02c2", + "zh:3c0658e132232a181223f7ff65678d99cd2e8431c317f72281b67464e5e16892", + "zh:43505c0f42bc7635ec7c1fe5043c502f9b00ae4b5e74b81464bc494936643fc1", + "zh:52efdabb0abba99a33fd3ed981610f13c99bb383f94e997f90d95441d8558177", + "zh:75b5d9b4a610dfd0ff4dfb4039f61e79a0e56338e0a4cd45e0bc0edec34dfa62", + "zh:7aee5df091672d29f29dda57382a41d771fa21740cef6bb9a1b15afc6d84ffa4", + "zh:7ff618706e2953a21a22c7555e11f5cbe8e95c171704fcfdc6beedb0c25e49c0", + "zh:94e8a15c83a1a5a60ff1b58938dd9692d800fe05c5d8269e0916b5de03d89d3a", + "zh:c1ace4f322f9ec4956e4f30086da5b6a73f4d05e1266047d629b14a485c5a76d", + "zh:d4570075de49e3ee98494f7c44eab12e964c9776029ed536fd9352c3203cc635", + "zh:d99403b843de5939ea2e54b3ca46fd901d5c5b7fe34f44b8aeb8b38f4f792df6", + ] +} diff --git a/ops/README.md b/ops/README.md new file mode 100644 index 0000000..44dfd4a --- /dev/null +++ b/ops/README.md @@ -0,0 +1,59 @@ +# CTF Pilot's Kubernetes Operations (Ops) + +> [!IMPORTANT] +> You are leaving the automated CTF Pilot setup and entering a more advanced manual setup. +> This requires knowledge of Kubernetes, Terraform/OpenTofu, and cloud infrastructure management. +> If you are not comfortable with these technologies, it is recommended to use the automated setup provided by CTF Pilot. +> Learn more about the automated setup in the [CTF Pilot's CTF Platform main README](../README.md). + +This directory contains various operational applications, services and configurations, deployed as a base on top of the Kubernetes cluster. + +Ops contians elements, that needs to be properly configured and deployed, before the CTF Platform can be correctly deployed within the cluster. + +The following applications/services are included in the Ops: + +- [ArgoCD](https://argo-cd.readthedocs.io/) - GitOps continuous delivery tool, used to deploy and manage applications within the Kubernetes cluster. + +## Pre-requisites + +The following software needs to be installed on your local machine: + +- [Terraform](https://www.terraform.io/downloads.html) / [OpenTofu](https://opentofu.org) +- [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) (For interacting with the Kubernetes cluster) + +The following services are required, in order to deploy the Kubernetes cluster: + +- A Kubernetes cluster (Deployed using the [CTF Pilot's Kubernetes Cluster on Hetzner Cloud](../cluster/README.md) guide or other means) +- [Cloudflare](https://www.cloudflare.com/) account +- [Cloudflare API Token](https://dash.cloudflare.com/profile/api-tokens) (For authenticating with the Cloudflare API) +- [Cloudflare controlled domain](https://dash.cloudflare.com/) (For allowing the system to do DNS challenges for TLS certificates) + +## Setup + +Copy the `tfvars/template.tfvars` file to `tfvars/data.tfvars` and edit the file with your own values. +The [`tfvars/template.tfvars`](tfvars/template.tfvars) file contains further information on each variable. + +> [!IMPORTANT] +> Make sure you generate the backend configuration file before creating the cluster. +> See the [backend generation instructions](../backend/README.md) for more information. +> +> You will also need to set the following environment variables for authentication to the S3 backend: +> - `AWS_ACCESS_KEY_ID` +> - `AWS_SECRET_ACCESS_KEY` +> +> See [OpenTofub backend S3 configuration](https://opentofu.org/docs/language/settings/backends/s3/) for more information. + +Run the following command to apply the ressources to the Kubernetes cluster: + +```bash +tofu init -backend-config=../backend/generated/cluster.hcl +tofu apply --var-file tfvars/data.tfvars +``` + +### Destroying the Ops + +To destroy the deployed ops, run the following command: + +```bash +tofu destroy --var-file tfvars/data.tfvars +``` diff --git a/ops/argocd.tf b/ops/argocd.tf new file mode 100644 index 0000000..101ec46 --- /dev/null +++ b/ops/argocd.tf @@ -0,0 +1,116 @@ +resource "kubernetes_namespace_v1" "argocd" { + metadata { + name = "argocd" + } +} + +resource "helm_release" "argocd" { + namespace = kubernetes_namespace_v1.argocd.metadata.0.name + create_namespace = false + name = "argocd" + repository = "https://argoproj.github.io/argo-helm" + chart = "argo-cd" + version = var.argocd_version + + # Helm chart deployment can sometimes take longer than the default 5 minutes + timeout = 800 + + # If values file specified by the var.values_file input variable exists then apply the values from this file + # else apply the default values from the chart + values = [ + yamlencode({ + # "redis-ha" = { + # enabled = true + # }, + controller = { + replicas : 1 + }, + server = { + replicas : 2 + }, + repoServer = { + replicas : 2 + }, + applicationSet = { + replicas : 2 + } + }), + ] + + set_sensitive = [{ + name = "configs.secret.argocdServerAdminPassword" + value = var.argocd_admin_password == "" ? "" : bcrypt(var.argocd_admin_password) + }, + { + name = "configs.secret.githubSecret" + value = var.argocd_github_secret + }] + + set = [ + { + name = "dex.enabled" + value = true + }, + { + name = "configs.params.server\\.insecure" + value = true + } + ] + + depends_on = [ + kubernetes_namespace_v1.argocd + ] +} + +resource "kubernetes_ingress_v1" "argocd-ingress" { + metadata { + name = "argocd-ingress" + namespace = kubernetes_namespace_v1.argocd.metadata.0.name + + annotations = { + "cert-manager.io/cluster-issuer" = module.cert_manager.cluster_issuer_name + "traefik.ingress.kubernetes.io/router.middlewares" = "errors-errors@kubernetescrd" + } + } + + spec { + default_backend { + service { + name = "argocd-server" + port { + number = 80 + } + } + } + + rule { + host = "argocd.${var.cluster_dns_management}" + http { + path { + backend { + service { + name = "argocd-server" + port { + number = 80 + } + } + } + } + } + } + + tls { + hosts = [ + "argocd.${var.cluster_dns_management}" + ] + + secret_name = "argocd-cert" + } + } + + depends_on = [ + kubernetes_namespace_v1.argocd, + helm_release.argocd, + module.cert_manager, + ] +} diff --git a/ops/cert-manager.tf b/ops/cert-manager.tf new file mode 100644 index 0000000..81594d9 --- /dev/null +++ b/ops/cert-manager.tf @@ -0,0 +1,90 @@ +resource "kubernetes_namespace_v1" "cert_manager" { + metadata { + name = "cert-manager" + } +} + +module "cert_manager" { + source = "terraform-iaac/cert-manager/kubernetes" + + cluster_issuer_email = var.email + cluster_issuer_name = "cert-manager-global" + cluster_issuer_private_key_secret_name = "cert-manager-private-key" + chart_version = var.cert_manager_version + + namespace_name = kubernetes_namespace_v1.cert_manager.metadata.0.name + create_namespace = false + + + solvers = [ + { + dns01 = { + cloudflare = { + email = var.email + apiTokenSecretRef = { + name = kubernetes_secret.cloudflare_api_key_secret.metadata.0.name + key = "API" + } + }, + }, + selector = { + dnsZones = [ + var.cloudflare_dns_management + ] + } + }, + { + dns01 = { + cloudflare = { + email = var.email + apiTokenSecretRef = { + name = kubernetes_secret.cloudflare_api_key_secret.metadata.0.name + key = "API" + } + }, + }, + selector = { + dnsZones = [ + var.cloudflare_dns_ctf + ] + } + }, + { + dns01 = { + cloudflare = { + email = var.email + apiTokenSecretRef = { + name = kubernetes_secret.cloudflare_api_key_secret.metadata.0.name + key = "API" + } + }, + }, + selector = { + dnsZones = [ + var.cloudflare_dns_platform + ] + } + } + ] + + depends_on = [ + kubernetes_namespace_v1.cert_manager, + kubernetes_secret.cloudflare_api_key_secret + ] +} + +# Cloudflare api token secret +resource "kubernetes_secret" "cloudflare_api_key_secret" { + metadata { + name = "cloudflare-api-key-secret" + namespace = kubernetes_namespace_v1.cert_manager.metadata.0.name + } + + data = { + API = var.cloudflare_api_token + } + + depends_on = [ + kubernetes_namespace_v1.cert_manager + ] +} diff --git a/ops/descheduler.tf b/ops/descheduler.tf new file mode 100644 index 0000000..84b34e0 --- /dev/null +++ b/ops/descheduler.tf @@ -0,0 +1,24 @@ +resource "kubernetes_namespace_v1" "descheduler" { + metadata { + name = "descheduler" + } +} + +resource "helm_release" "descheduler" { + name = "descheduler" + repository = "https://kubernetes-sigs.github.io/descheduler/" + chart = "descheduler/descheduler" + version = var.descheduler_version + + namespace = kubernetes_namespace_v1.descheduler.metadata.0.name + create_namespace = false + + values = [ + yamlencode({ + serviceMonitor = { + enabled = true + namespace = "prometheus" + } + }) + ] +} diff --git a/ops/errors.tf b/ops/errors.tf new file mode 100644 index 0000000..fc115e9 --- /dev/null +++ b/ops/errors.tf @@ -0,0 +1,146 @@ +resource "kubernetes_namespace" "errors" { + metadata { + name = "errors" + labels = { + role = "errors" + } + } +} + +module "errors-pull-secret" { + source = "../tf-modules/pull-secret" + + namespace = "errors" + ghcr_token = var.ghcr_token + ghcr_username = var.ghcr_username +} + +resource "kubernetes_deployment_v1" "errors" { + metadata { + name = "errors" + namespace = "errors" + + labels = { + role = "errors" + } + } + + spec { + replicas = 2 + + selector { + match_labels = { + role = "errors" + } + } + + template { + metadata { + labels = { + role = "errors" + } + } + + spec { + enable_service_links = false + automount_service_account_token = false + + image_pull_secrets { + name = var.ghcr_token != "" ? module.errors-pull-secret.pull-secret : "" + } + + container { + name = "errors" + image = var.image_error_fallback + image_pull_policy = "Always" + + port { + container_port = 80 + } + + resources { + limits = { + cpu = "100m" + memory = "256Mi" + } + requests = { + cpu = "10m" + memory = "50Mi" + } + } + + liveness_probe { + http_get { + path = "/" + port = 80 + } + + initial_delay_seconds = 5 + period_seconds = 10 + } + } + } + } + } + + depends_on = [ + kubernetes_namespace.errors, + module.errors-pull-secret + ] +} + +resource "kubernetes_service_v1" "errors" { + metadata { + name = "errors" + namespace = "errors" + + labels = { + role = "errors" + } + } + + spec { + selector = { + role = "errors" + } + + port { + port = 80 + target_port = 80 + } + } + + depends_on = [ + kubernetes_deployment_v1.errors + ] +} + +resource "kubernetes_manifest" "traefik-errors-middleware" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "errors" + namespace = "errors" + } + spec = { + errors = { + status = [ + "502", + "503", + "504" + ] + query = "/{status}.html" + service = { + name = "errors" + port = 80 + } + } + } + } + + depends_on = [ + kubernetes_namespace.errors, + kubernetes_service_v1.errors + ] +} diff --git a/ops/filebeat-values/values.yaml b/ops/filebeat-values/values.yaml new file mode 100644 index 0000000..c07a667 --- /dev/null +++ b/ops/filebeat-values/values.yaml @@ -0,0 +1,22 @@ +daemonset: + # Include the daemonset + enabled: true + extraEnvs: + - name: "ELASTICSEARCH_HOSTS" + valueFrom: + secretKeyRef: + name: es-credentials + key: hosts + - name: "ELASTICSEARCH_USERNAME" + valueFrom: + secretKeyRef: + name: es-credentials + key: username + - name: "ELASTICSEARCH_PASSWORD" + valueFrom: + secretKeyRef: + name: es-credentials + key: password + secretMounts: NULL +deployments: + secretMounts: NULL diff --git a/ops/filebeat.tf b/ops/filebeat.tf new file mode 100644 index 0000000..60a24bb --- /dev/null +++ b/ops/filebeat.tf @@ -0,0 +1,226 @@ +resource "kubernetes_namespace" "logging-namespace" { + metadata { + name = "logging" + } + lifecycle { + ignore_changes = [metadata] + } +} + +resource "kubernetes_secret" "es_credentials" { + metadata { + name = "es-credentials" + namespace = kubernetes_namespace.logging-namespace.metadata.0.name + } + data = { + "username" = var.filebeat_elasticsearch_username + "password" = var.filebeat_elasticsearch_password + } + type = "Opaque" +} + +resource "kubernetes_service_account" "filebeat_service_account" { + metadata { + name = "filebeat" + namespace = kubernetes_namespace.logging-namespace.metadata.0.name + } +} + +resource "kubernetes_cluster_role_v1" "filebeat_cluster_role" { + metadata { + name = "filebeat" + } + rule { + api_groups = [""] + resources = ["namespaces", "pods", "serviceaccounts", "nodes", "endpoints"] + verbs = ["get", "list", "watch"] + } +} + +resource "kubernetes_cluster_role_binding_v1" "filebeat_cluster_role_binding" { + metadata { + name = "filebeat" + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role_v1.filebeat_cluster_role.metadata.0.name + } + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.filebeat_service_account.metadata.0.name + namespace = kubernetes_namespace.logging-namespace.metadata.0.name + } +} + +resource "kubernetes_config_map" "filebeat_config" { + metadata { + name = "filebeat-config" + namespace = kubernetes_namespace.logging-namespace.metadata.0.name + } + data = { + "filebeat.yml" = <<-EOF + filebeat.inputs: + - type: container + paths: + - /var/log/containers/*.log + processors: + - add_kubernetes_metadata: + host: $${NODE_NAME} + matchers: + - logs_path: + logs_path: "/var/log/containers/" + - add_fields: + target: '' + fields: + cluster_dns: "${var.cluster_dns_management}" + + output.elasticsearch: + hosts: ["https://${var.filebeat_elasticsearch_host}:443"] + username: "${var.filebeat_elasticsearch_username}" + password: "${var.filebeat_elasticsearch_password}" + protocol: https + ssl.verification_mode: "full" + index: filebeat-${var.environment}-logs + + setup: + template: + name: "filebeat-${var.environment}-logs" + pattern: "filebeat-${var.environment}-logs*" + overwrite: false + ilm: + enabled: true + policy_name: "filebeat" + EOF + } +} + +resource "kubernetes_daemonset" "filebeat_daemonset" { + metadata { + name = "filebeat" + namespace = kubernetes_namespace.logging-namespace.metadata.0.name + labels = { + k8s-app = "filebeat-logging" + version = "v1" + app = "filebeat" + } + } + + spec { + selector { + match_labels = { + k8s-app = "filebeat-logging" + version = "v1" + app = "filebeat" + } + } + + template { + metadata { + labels = { + k8s-app = "filebeat-logging" + version = "v1" + app = "filebeat" + } + } + + spec { + service_account_name = kubernetes_service_account.filebeat_service_account.metadata.0.name + + toleration { + key = "node-role.kubernetes.io/control-plane" + effect = "NoSchedule" + } + toleration { + key = "node-role.kubernetes.io/master" + effect = "NoSchedule" + } + toleration { + key = "cluster.ctfpilot.com/node" + value = "scaler" + effect = "PreferNoSchedule" + } + + container { + name = "filebeat" + image = var.image_filebeat + security_context { + privileged = true + } + env { + name = "ELASTICSEARCH_HOST" + value = "https://${var.filebeat_elasticsearch_host}:443" + } + env { + name = "ELASTICSEARCH_USERNAME" + value_from { + secret_key_ref { + name = "es-credentials" + key = "username" + } + } + } + env { + name = "ELASTICSEARCH_PASSWORD" + value_from { + secret_key_ref { + name = "es-credentials" + key = "password" + } + } + } + env { + name = "NODE_NAME" + value_from { + field_ref { + field_path = "spec.nodeName" + } + } + } + + resources { + requests = { + cpu = "10m" + memory = "100Mi" + } + limits = { + cpu = "200m" + memory = "200Mi" + } + } + + volume_mount { + name = "varlog" + mount_path = "/var/log" + } + volume_mount { + name = "filebeat-config" + mount_path = "/usr/share/filebeat/filebeat.yml" + sub_path = "filebeat.yml" + } + } + + termination_grace_period_seconds = 30 + + volume { + name = "varlog" + host_path { + path = "/var/log" + } + } + volume { + name = "filebeat-config" + config_map { + name = kubernetes_config_map.filebeat_config.metadata.0.name + } + } + } + } + } + + depends_on = [ + kubernetes_namespace.logging-namespace, + kubernetes_secret.es_credentials, + kubernetes_config_map.filebeat_config + ] +} diff --git a/ops/ingress.tf b/ops/ingress.tf new file mode 100644 index 0000000..a9754d7 --- /dev/null +++ b/ops/ingress.tf @@ -0,0 +1,44 @@ +resource "htpasswd_password" "traefik_basic_auth" { + password = var.traefik_basic_auth.password + salt = random_password.salt.result + + depends_on = [ + random_password.salt + ] +} + +resource "kubernetes_secret" "traefik_basic_auth" { + metadata { + name = "admin-ui-basic-auth" + namespace = var.traefik_namespace + } + + data = { + "auth" = "${var.traefik_basic_auth.user}:${htpasswd_password.traefik_basic_auth.apr1}" + } + + depends_on = [ + htpasswd_password.traefik_basic_auth + ] +} + +# Traefic basic auth middleware +resource "kubernetes_manifest" "traefik_basic_auth" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = kubernetes_secret.traefik_basic_auth.metadata.0.name + namespace = var.traefik_namespace + } + spec = { + basicAuth = { + secret = kubernetes_secret.traefik_basic_auth.metadata.0.name + } + } + } + + depends_on = [ + kubernetes_secret.traefik_basic_auth + ] +} diff --git a/ops/mariadb-operator.tf b/ops/mariadb-operator.tf new file mode 100644 index 0000000..3801b3c --- /dev/null +++ b/ops/mariadb-operator.tf @@ -0,0 +1,52 @@ +resource "kubernetes_namespace_v1" "mariadb" { + metadata { + name = "mariadb-operator" + } +} + +resource "helm_release" "mariadb-operator-crds" { + name = "mariadb-operator-crds" + repository = "https://helm.mariadb.com/mariadb-operator" + namespace = kubernetes_namespace_v1.mariadb.metadata.0.name + create_namespace = false + + chart = "mariadb-operator-crds" + version = var.mariadb_operator_version + + // timeot 10min + timeout = 600 + + // Force use of longhorn storage class + # set = [{ + # name = "mariadb-operator.storageClass" + # value = "longhorn" + # }] + + depends_on = [ + kubernetes_namespace_v1.mariadb + ] +} + +resource "helm_release" "mariadb-operator" { + name = "mariadb-operator" + repository = "https://helm.mariadb.com/mariadb-operator" + namespace = kubernetes_namespace_v1.mariadb.metadata.0.name + create_namespace = false + + chart = "mariadb-operator" + version = var.mariadb_operator_version + + # timeot 10min + timeout = 600 + + // Force use of longhorn storage class + # set = [{ + # name = "mariadb-operator.storageClass" + # value = "longhorn" + # }] + + depends_on = [ + helm_release.mariadb-operator-crds, + kubernetes_namespace_v1.mariadb + ] +} diff --git a/ops/prod-default-web.tf b/ops/prod-default-web.tf new file mode 100644 index 0000000..743b310 --- /dev/null +++ b/ops/prod-default-web.tf @@ -0,0 +1,132 @@ +# ---------------------- +# Default web entrypoint +# ---------------------- + +# Namespace +resource "kubernetes_namespace" "prod-default-web" { + metadata { + name = "prod-default-web" + } +} + +# Ingress +resource "kubernetes_ingress_v1" "prod-default-web" { + metadata { + name = "prod-default-web-ingress" + namespace = kubernetes_namespace.prod-default-web.metadata.0.name + + annotations = { + "cert-manager.io/cluster-issuer" = module.cert_manager.cluster_issuer_name + "traefik.ingress.kubernetes.io/router.middlewares" = "errors-errors@kubernetescrd" + } + } + + spec { + default_backend { + service { + name = kubernetes_service_v1.prod-default-web.metadata.0.name + port { + number = 80 + } + } + } + + rule { + host = var.cluster_dns_management + http { + path { + path = "/" + backend { + service { + name = kubernetes_service_v1.prod-default-web.metadata.0.name + port { + number = 80 + } + } + } + } + } + } + + tls { + hosts = [ + "${var.cluster_dns_management}" + ] + + secret_name = "prod-default-web-cert" + } + } + + depends_on = [ + kubernetes_namespace.prod-default-web, + kubernetes_service_v1.prod-default-web, + module.cert_manager, + ] +} + +# Service +resource "kubernetes_service_v1" "prod-default-web" { + metadata { + name = "prod-default-web" + namespace = kubernetes_namespace.prod-default-web.metadata.0.name + } + + spec { + selector = { + app = "prod-default-web" + } + + port { + port = 80 + target_port = 5678 + } + } + + depends_on = [ + kubernetes_deployment_v1.prod-default-web + ] +} + +# Deployment +resource "kubernetes_deployment_v1" "prod-default-web" { + metadata { + name = "prod-default-web" + namespace = kubernetes_namespace.prod-default-web.metadata.0.name + } + + spec { + replicas = 3 + + selector { + match_labels = { + app = "prod-default-web" + } + } + + template { + metadata { + labels = { + app = "prod-default-web" + } + } + + spec { + container { + name = "prod-default-web" + image = "hashicorp/http-echo" + args = [ + "-text=Welcome to CTF Pilot!" + ] + + port { + container_port = 5678 + } + } + } + } + } + + depends_on = [ + kubernetes_namespace.prod-default-web + ] +} diff --git a/ops/prometheus.tf b/ops/prometheus.tf new file mode 100644 index 0000000..2ff5fc3 --- /dev/null +++ b/ops/prometheus.tf @@ -0,0 +1,214 @@ +resource "kubernetes_namespace_v1" "prometheus" { + metadata { + name = "prometheus" + } +} + +# --- Grafana Dashboards ConfigMaps --- +resource "kubernetes_config_map" "grafana-dashboards-k8s" { + metadata { + name = "grafana-dashboards-k8s" + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + + labels = { + grafana_dashboard = 1 + } + + annotations = { + k8s-sidecar-target-directory = "/tmp/dashboards/k8s" + } + } + + data = { + for file in fileset("${path.module}/prometheus/grafana/dashboards/k8s", "*.json") : file => file("${path.module}/prometheus/grafana/dashboards/k8s/${file}") + } +} + +resource "kubernetes_config_map" "grafana-dashboards-redis" { + metadata { + name = "grafana-dashboards-redis" + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + + labels = { + grafana_dashboard = 1 + } + + annotations = { + k8s-sidecar-target-directory = "/tmp/dashboards/redis" + } + } + + data = { + for file in fileset("${path.module}/prometheus/grafana/dashboards/redis", "*.json") : file => file("${path.module}/prometheus/grafana/dashboards/redis/${file}") + } +} + +resource "kubernetes_config_map" "grafana-dashboards-traefik" { + metadata { + name = "grafana-dashboards-traefik" + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + + labels = { + grafana_dashboard = 1 + } + + annotations = { + k8s-sidecar-target-directory = "/tmp/dashboards/traefik" + } + } + + data = { + for file in fileset("${path.module}/prometheus/grafana/dashboards/traefik", "*.json") : file => file("${path.module}/prometheus/grafana/dashboards/traefik/${file}") + } +} + +resource "kubernetes_config_map" "grafana-dashboards-ctf" { + metadata { + name = "grafana-dashboards-ctf" + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + + labels = { + grafana_dashboard = 1 + } + + annotations = { + k8s-sidecar-target-directory = "/tmp/dashboards/ctf" + } + } + + data = { + for file in fileset("${path.module}/prometheus/grafana/dashboards/ctf", "*.json") : file => file("${path.module}/prometheus/grafana/dashboards/ctf/${file}") + } +} + +# --- Grafana Alerting Rules and Contacts --- +resource "kubernetes_secret" "grafana-alerts-contact-rules" { + metadata { + name = "grafana-alerts-contact-rules" + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + + labels = { + grafana_alert = "1" + } + } + + data = { + for file in fileset("${path.module}/prometheus/grafana/contact", "*.yaml") : file => templatefile("${path.module}/prometheus/grafana/contact/${file}", { + cluster_dns_management = var.cluster_dns_management, + discord_webhook_url = var.discord_webhook_url, + }) + } + type = "Opaque" +} + +resource "kubernetes_config_map" "grafana-alerts-notification-rules" { + metadata { + name = "grafana-alerts-notification-rules" + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + + labels = { + grafana_alert = "1" + } + } + + data = { + for file in fileset("${path.module}/prometheus/grafana/notification", "*.yaml") : file => templatefile("${path.module}/prometheus/grafana/notification/${file}", { + cluster_dns_management = var.cluster_dns_management, + discord_webhook_url = var.discord_webhook_url, + }) + } +} + +# --- Prometheus Helm Release --- +resource "helm_release" "prometheus" { + name = "prometheus" + + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + create_namespace = false + + repository = "https://prometheus-community.github.io/helm-charts" + chart = "kube-prometheus-stack" + version = var.kube_prometheus_stack_version + + # Set password for grafana dashboard + set_sensitive = [{ + name = "grafana.adminPassword" + value = var.grafana_admin_password + }] + + # Use PVC for prometheus data + set = [ + # { + # name = "prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.storageClassName" + # value = "longhorn" + # }, + { + name = "prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage" + value = var.prometheus_storage_size + } + ] + + values = [ + templatefile("${path.module}/prometheus/kube_prometheus_custom_values.yaml", { + cluster_dns_management = var.cluster_dns_management, + discord_webhook_url = var.discord_webhook_url, + }) + ] + + depends_on = [ + kubernetes_namespace_v1.prometheus, + kubernetes_config_map.grafana-dashboards-k8s, + kubernetes_config_map.grafana-dashboards-redis + ] +} + +# --- Grafana Ingress --- +resource "kubernetes_ingress_v1" "grafana-ingress" { + metadata { + name = "grafana-ingress" + namespace = kubernetes_namespace_v1.prometheus.metadata.0.name + + annotations = { + "cert-manager.io/cluster-issuer" = module.cert_manager.cluster_issuer_name + "traefik.ingress.kubernetes.io/router.middlewares" = "errors-errors@kubernetescrd" + } + } + + spec { + default_backend { + service { + name = "prometheus-grafana" + port { + number = 80 + } + } + } + + rule { + host = "grafana.${var.cluster_dns_management}" + http { + path { + backend { + service { + name = "prometheus-grafana" + port { + number = 80 + } + } + } + } + } + } + + tls { + hosts = [ + "grafana.${var.cluster_dns_management}" + ] + secret_name = "grafana-ingress-tls-cert" + } + } + + depends_on = [ + helm_release.prometheus + ] +} diff --git a/ops/prometheus/README.md b/ops/prometheus/README.md new file mode 100644 index 0000000..198ea89 --- /dev/null +++ b/ops/prometheus/README.md @@ -0,0 +1,7 @@ +# Dashboards + +Custom dashboards can be added in [`/dashboards`](./dashboards/). + +There are a number of external dashboards: + +- diff --git a/ops/prometheus/grafana/alerts/.gitkeep b/ops/prometheus/grafana/alerts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ops/prometheus/grafana/contact/notificationpolicy.yaml b/ops/prometheus/grafana/contact/notificationpolicy.yaml new file mode 100644 index 0000000..f6e81aa --- /dev/null +++ b/ops/prometheus/grafana/contact/notificationpolicy.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 +policies: + - orgId: 1 + receiver: discord + matchers: + - severity = critical + group_wait: 30s + group_interval: 5m + repeat_interval: 4h diff --git a/ops/prometheus/grafana/contact/notifiers.yaml b/ops/prometheus/grafana/contact/notifiers.yaml new file mode 100644 index 0000000..cb78b3f --- /dev/null +++ b/ops/prometheus/grafana/contact/notifiers.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 +contactPoints: + - name: "discord" + org_id: 1 + receivers: + - uid: "discord-contact" + name: "discord" + type: "discord" + is_default: true + settings: + url: "${discord_webhook_url}" diff --git a/ops/prometheus/grafana/dashboards/ctf/container-usage.json b/ops/prometheus/grafana/dashboards/ctf/container-usage.json new file mode 100644 index 0000000..15f9c02 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/ctf/container-usage.json @@ -0,0 +1,1096 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 13, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "count(kube_deployment_labels{namespace=~\"$namespaces\", label_ctfpilot_com_name!=\"\"})", + "instant": false, + "legendFormat": "Deployments", + "range": true, + "refId": "A" + } + ], + "title": "Total CTF Pilot deployments", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "vCPU" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Total vCPU usage", + "Total vCPU available", + "Node available vCPU" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 0, + "y": 12 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=~\"$namespaces\", container!=\"\", image!=\"\", node=~\"$nodes\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "Total vCPU usage", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(node:node_num_cpu:sum{node=~\"$nodes\"})", + "hide": false, + "instant": false, + "legendFormat": "Total vCPU available", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(node:node_num_cpu:sum{node=~\"$nodes\"})\r\n/\r\ncount(count(node:node_num_cpu:sum{node=~\"$nodes\"}) by (node))\r\n", + "hide": false, + "instant": false, + "legendFormat": "Node available vCPU", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(node:node_num_cpu:sum)", + "hide": false, + "instant": false, + "legendFormat": "Cluster vCPU available", + "range": true, + "refId": "D" + } + ], + "title": "CPU usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": -3, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Total memory usage", + "Total available memory", + "Node available memory" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 12, + "x": 12, + "y": 12 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(container_memory_working_set_bytes{namespace=~\"$namespaces\", container!=\"\", image!=\"\", instance=~\"$internalIp:10250\"})", + "hide": false, + "instant": false, + "legendFormat": "Total memory usage", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(node_memory_MemTotal_bytes{instance=~\"$internalIp:9100\"})", + "hide": false, + "instant": false, + "legendFormat": "Total available memory", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(node_memory_MemTotal_bytes{instance=~\"$internalIp:9100\"}) /\r\ncount(count(node_memory_MemTotal_bytes{instance=~\"$internalIp:9100\"}) by (instance))", + "hide": false, + "instant": false, + "legendFormat": "Node available memory", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(node_memory_MemTotal_bytes)", + "hide": false, + "instant": false, + "legendFormat": "Cluster memory available", + "range": true, + "refId": "D" + } + ], + "title": "RAM usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 0, + "y": 24 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(kube_node_info{node=~\"$nodes\"})", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Node", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "vCPU" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 3, + "y": 24 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(node:node_num_cpu:sum{node=~\"$nodes\"}) / count(count(node:node_num_cpu:sum{node=~\"$nodes\"}) by (node))", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Node vCPU", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 6, + "y": 24 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(node_memory_MemTotal_bytes{instance=~\"$internalIp:9100\"}) /\r\ncount(count(node_memory_MemTotal_bytes{instance=~\"$internalIp:9100\"}) by (instance))", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Node memory", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "vCPU" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 9, + "y": 24 + }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(node:node_num_cpu:sum{node=~\"$nodes\"}) / count(count(node:node_num_cpu:sum{node=~\"$nodes\"}) by (node))", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Total vCPU", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 12, + "y": 24 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(node_memory_MemTotal_bytes{instance=~\"$internalIp:9100\"})", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Total memory", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 15, + "y": 24 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(kube_node_info)", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Cluster node count", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "vCPU" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 18, + "y": 24 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(node:node_num_cpu:sum)", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Cluster vCPU", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 3, + "x": 21, + "y": 24 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(node_memory_MemTotal_bytes)", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Node memory", + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": [ + "ctfpilot-challenges", + "ctfpilot-challenges-instanced" + ], + "value": [ + "ctfpilot-challenges", + "ctfpilot-challenges-instanced" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespaces", + "multi": true, + "name": "namespaces", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(container_cpu_usage_seconds_total{namespace=~\"$namespaces\"},node)", + "hide": 0, + "includeAll": true, + "label": "Nodes", + "multi": true, + "name": "nodes", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(container_cpu_usage_seconds_total{namespace=~\"$namespaces\"},node)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(kube_node_info{node=~\"$nodes\"},internal_ip)", + "hide": 2, + "includeAll": true, + "label": "Internal IP", + "multi": true, + "name": "internalIp", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info{node=~\"$nodes\"},internal_ip)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "2025-08-22T11:30:00.000Z", + "to": "2025-08-24T12:15:00.000Z" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Container usage", + "uid": "bew212y4i2sqoe", + "version": 3, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/ctf/ctfd-challenges.json b/ops/prometheus/grafana/dashboards/ctf/ctfd-challenges.json new file mode 100644 index 0000000..f516d0d --- /dev/null +++ b/ops/prometheus/grafana/dashboards/ctf/ctfd-challenges.json @@ -0,0 +1,116 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 55, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 21, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "interval": "1", + "maxDataPoints": 999999999, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "titleSize": 16, + "valueSize": 22 + }, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ctfd_challenge_solves{}", + "instant": false, + "legendFormat": "{{name}}", + "range": true, + "refId": "A" + } + ], + "title": "Challenge Solves", + "type": "stat" + } + ], + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "CTFd Challenges", + "uid": "devryawzpgav4b", + "version": 4, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/ctf/ctfd.json b/ops/prometheus/grafana/dashboards/ctf/ctfd.json new file mode 100644 index 0000000..545f386 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/ctf/ctfd.json @@ -0,0 +1,1550 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 23095, + "graphTooltip": 0, + "id": 12, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 11, + "panels": [], + "title": "Core information", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Top 10 teams in brackets", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 3, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 17, + "w": 11, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "code", + "exemplar": false, + "expr": "topk(10, sum by(name) (ctfd_team_score{bracket=~\"$brackets\"}))", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Scoreboard", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "name" + }, + "properties": [ + { + "id": "displayName", + "value": "User" + }, + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "Score" + } + ] + } + ] + }, + "gridPos": { + "h": 17, + "w": 5, + "x": 11, + "y": 1 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(15, sum by(name) (last_over_time(ctfd_team_score[$__interval])))", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Top 15 teams", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 17, + "w": 4, + "x": 16, + "y": 1 + }, + "id": 17, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(15, sum(ctfd_submission_solves) by(name))", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Most solves", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "name", + "Value" + ] + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Solves", + "name": "Name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "category" + }, + "properties": [ + { + "id": "custom.width", + "value": 122 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "category" + }, + "properties": [ + { + "id": "custom.width" + }, + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + }, + { + "id": "displayName", + "value": "Category" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "name" + }, + "properties": [ + { + "id": "displayName", + "value": "Name" + }, + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "Solves" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Name" + }, + "properties": [ + { + "id": "custom.width", + "value": 237 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Solves" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Category" + }, + "properties": [ + { + "id": "custom.width", + "value": 111 + } + ] + } + ] + }, + "gridPos": { + "h": 17, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "bottomk(\n 15,\n sum(ctfd_challenge_solves) by(name) > 0\n)", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Least solved (> 0)", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 5, + "x": 0, + "y": 18 + }, + "id": 3, + "options": { + "displayLabels": [ + "name", + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "values": [] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(ctfd_challenge_solves) by (category)", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Solves per category", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Time" + }, + "properties": [ + { + "id": "custom.hidden", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "category" + }, + "properties": [ + { + "id": "custom.width", + "value": 122 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "category" + }, + "properties": [ + { + "id": "custom.width" + }, + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + }, + { + "id": "displayName", + "value": "Category" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "name" + }, + "properties": [ + { + "id": "displayName", + "value": "Name" + }, + { + "id": "color", + "value": { + "fixedColor": "text", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "displayName", + "value": "Solves" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Name" + }, + "properties": [ + { + "id": "custom.width", + "value": 237 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Solves" + }, + "properties": [ + { + "id": "custom.width", + "value": 89 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Category" + }, + "properties": [ + { + "id": "custom.width", + "value": 111 + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 6, + "x": 5, + "y": 18 + }, + "id": 19, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "bottomk(\n 10,\n sum(ctfd_challenge_solves) by(name, category) == 0\n)", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Unsolved challenges", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "color-text" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 5, + "x": 11, + "y": 18 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(10, sum(ctfd_submission_fails) by(name))", + "format": "table", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Most fails", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Fails", + "name": "Name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 16, + "y": 18 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ctfd_users_total", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Total users", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 19, + "y": 18 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ctfd_teams_total", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Total teams", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 2, + "x": 22, + "y": 18 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ctfd_challenges_total", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Total challenges", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "displayName": "Alive containers", + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 16, + "y": 23 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.5.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Active instances", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 2, + "x": 22, + "y": 23 + }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Total instances", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 12, + "panels": [], + "title": "Scoreboard", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "left", + "cellOptions": { + "type": "auto", + "wrapText": true + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 13, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.5", + "repeat": "brackets", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "topk(20, ctfd_team_score{bracket=\"$brackets\"})", + "format": "table", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A", + "useBackend": false + } + ], + "title": "Top 20 teams for $brackets", + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "names": [ + "name", + "Value" + ] + } + } + }, + { + "id": "sortBy", + "options": { + "fields": {}, + "sort": [ + { + "desc": true, + "field": "Value" + } + ] + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Score", + "name": "Team name" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 49 + }, + "id": 15, + "panels": [], + "title": "Additional data", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 50 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ctfd_users_total", + "instant": false, + "legendFormat": "Users", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ctfd_teams_total", + "hide": false, + "instant": false, + "legendFormat": "Teams", + "range": true, + "refId": "B" + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "refresh": "1m", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": "", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(ctfd_team_score,bracket)", + "hide": 0, + "includeAll": true, + "label": "Brackets", + "multi": true, + "name": "brackets", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(ctfd_team_score,bracket)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "CTFd", + "uid": "ctfdcet4q2i3du29", + "version": 7, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/ctf/kubectf.json b/ops/prometheus/grafana/dashboards/ctf/kubectf.json new file mode 100644 index 0000000..7867aa0 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/ctf/kubectf.json @@ -0,0 +1,1577 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 13, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "title": "Running challenges", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count(kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "instant": true, + "legendFormat": "Total Instances", + "refId": "A" + } + ], + "title": "Total Instances", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 4, + "y": 1 + }, + "id": 6, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "count by (label_ctfpilot_com_name) (kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "A" + } + ], + "title": "Instances by Challenge Name", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Count", + "label_ctfpilot_com_name": "Challenge Name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 9, + "y": 1 + }, + "id": 15, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "count by (label_ctfpilot_com_type) (kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "A" + } + ], + "title": "Instances by Challenge Type", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Count", + "label_ctfpilot_com_name": "Challenge Name", + "label_ctfpilot_com_type": "Challenge Type" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 14, + "y": 1 + }, + "id": 16, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "count by (label_instanced_challenges_ctfpilot_com_owner) (kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "A" + } + ], + "title": "Instances by Challenge Owner", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Count", + "label_instanced_challenges_ctfpilot_com_owner": "Challenge Owner", + "label_ctfpilot_com_name": "Challenge Name" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "deployment" + }, + "properties": [ + { + "id": "custom.width", + "value": 363 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 19, + "y": 1 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count by (deployment) (kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_instanced_challenges_ctfpilot_com_deployment!=\"\"})", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Instances by Deployment ID", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value": "Count", + "deployment": "Challenge Deployment", + "label_instanced_challenges_ctfpilot_com_deployment": "Deployment ID" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 4, + "x": 0, + "y": 6 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count(kube_configmap_labels{namespace=\"challenge-config\", label_challenges_ctfpilot_com_name!=\"\", label_challenges_ctfpilot_com_enabled!=\"false\"})", + "instant": true, + "legendFormat": "Enabled Challenges", + "refId": "A" + } + ], + "title": "Total Enabled Challenges", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "count(kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "legendFormat": "Total Instances", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "count(kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"}) by (label_ctfpilot_com_name)", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" + } + ], + "title": "Total Instances Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 21 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count by (label_ctfpilot_com_name) (kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"})", + "legendFormat": "{{label_ctfpilot_com_name}}", + "refId": "A" + } + ], + "title": "Instances by Challenge Name Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 21 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count by (label_instanced_challenges_ctfpilot_com_owner) (kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_instanced_challenges_ctfpilot_com_owner!=\"\"})", + "legendFormat": "{{label_instanced_challenges_ctfpilot_com_owner}}", + "refId": "A" + } + ], + "title": "Instances by Owner Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "expr": "count by (label_ctfpilot_com_type) (kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_type!=\"\"})", + "legendFormat": "{{label_ctfpilot_com_type}}", + "refId": "A" + } + ], + "title": "Instances by Type Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count by (label_challenges_ctfpilot_com_type) (kube_configmap_labels{namespace=\"challenge-config\", label_challenges_ctfpilot_com_name!=\"\", label_challenges_ctfpilot_com_enabled!=\"false\"})", + "instant": false, + "legendFormat": "{{label_challenges_ctfpilot_com_type}}", + "range": true, + "refId": "A" + } + ], + "title": "Enabled Challenges by Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum (rate(container_cpu_usage_seconds_total{namespace=~\"ctfpilot-challenges|ctfpilot-challenges-instanced\"}[$__rate_interval]))", + "hide": false, + "instant": false, + "legendFormat": "Total CPU", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(namespace) (rate(container_cpu_usage_seconds_total{namespace=~\"ctfpilot-challenges|ctfpilot-challenges-instanced\"}[$__rate_interval]))", + "instant": false, + "legendFormat": "{{label_challenges_ctfpilot_com_type}}", + "range": true, + "refId": "A" + } + ], + "title": "CPU usage of instances", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decgbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "sum(rate(container_memory_usage_bytes{namespace=~\"ctfpilot-challenges|ctfpilot-challenges-instanced\"}[$__rate_interval])) / 1000000000", + "fullMetaSearch": false, + "hide": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "Total CPU", + "range": true, + "refId": "B", + "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "disableTextWrap": false, + "editorMode": "builder", + "exemplar": false, + "expr": "sum by(namespace) (rate(container_memory_usage_bytes{namespace=~\"ctfpilot-challenges|ctfpilot-challenges-instanced\"}[$__rate_interval])) / 1000000000", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "{{label_challenges_ctfpilot_com_type}}", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Memory usage of instances", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 0, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 0, + "y": 52 + }, + "id": 18, + "options": { + "displayLabels": [ + "name", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count by (label_challenges_ctfpilot_com_category) (kube_configmap_labels{namespace=\"challenge-config\", label_challenges_ctfpilot_com_name!=\"\", label_challenges_ctfpilot_com_enabled!=\"false\"})", + "instant": true, + "legendFormat": "{{label_challenges_ctfpilot_com_type}}", + "range": false, + "refId": "A" + } + ], + "title": "Enabled Challenges by Category", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "decimals": 0, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 5, + "y": 52 + }, + "id": 17, + "options": { + "displayLabels": [ + "name", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count by (label_challenges_ctfpilot_com_enabled) (kube_configmap_labels{namespace=\"challenge-config\", label_challenges_ctfpilot_com_name!=\"\", label_challenges_ctfpilot_com_enabled!=\"false\"})", + "instant": true, + "legendFormat": "{{label_challenges_ctfpilot_com_type}}", + "range": false, + "refId": "A" + } + ], + "title": "Challenges by Enabled", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 10, + "y": 52 + }, + "id": 19, + "options": { + "displayLabels": [ + "name", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"}) by (label_ctfpilot_com_name)", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "B" + } + ], + "title": "Instances deployed by name", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 5, + "x": 15, + "y": 52 + }, + "id": 22, + "options": { + "displayLabels": [ + "name", + "value" + ], + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(kube_deployment_labels{namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\", label_ctfpilot_com_name!=\"\"}) by (label_ctfpilot_com_type)", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "B" + } + ], + "title": "Instances deployed by type", + "type": "piechart" + } + ], + "refresh": "1m", + "schemaVersion": 39, + "tags": [ + "ctfpilot", + "kubernetes" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "1h", + "value": "1h" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "Granularity", + "options": [ + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + }, + { + "selected": false, + "text": "15m", + "value": "15m" + }, + { + "selected": true, + "text": "1h", + "value": "1h" + } + ], + "query": "1m,5m,15m,1h", + "queryValue": "", + "refresh": 1, + "skipUrlSync": false, + "type": "interval" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Challenges Dashboard", + "uid": "ctfpilot-overview", + "version": 10, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/ctf/node-usage.json b/ops/prometheus/grafana/dashboards/ctf/node-usage.json new file mode 100644 index 0000000..751e8fc --- /dev/null +++ b/ops/prometheus/grafana/dashboards/ctf/node-usage.json @@ -0,0 +1,472 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": 53, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 5, + "panels": [], + "title": "Cluster Totals", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "GB" + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 8, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(node_memory_MemTotal_bytes{instance=~\"$node\"} - node_memory_MemAvailable_bytes{instance=~\"$node\"}) / (1024*1024*1024)", + "legendFormat": "Memory Usage", + "range": true, + "refId": "A" + } + ], + "title": "Total Memory Usage (GB)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "cores" + }, + "overrides": [] + }, + "gridPos": { + "h": 13, + "w": 9, + "x": 8, + "y": 1 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(rate(node_cpu_seconds_total{mode!=\"idle\", instance=~\"$node\"}[5m]))", + "legendFormat": "CPU Usage", + "range": true, + "refId": "A" + } + ], + "title": "Total CPU Usage (Cores)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 6, + "panels": [], + "repeat": "node", + "repeatDirection": "h", + "title": "Per-Node Breakdown", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "YOUR_DS_UID" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "cores" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 15 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "expr": "sum by (instance)(rate(node_cpu_seconds_total{mode!=\"idle\", instance=~\"$node\"}[5m]))", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "title": "CPU Usage per Node (Cores)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "YOUR_DS_UID" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "GB" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 18 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "expr": "(node_memory_MemTotal_bytes{instance=~\"$node\"} - node_memory_MemAvailable_bytes{instance=~\"$node\"}) / (1024*1024*1024)", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "title": "Memory Usage per Node (GB)", + "type": "timeseries" + } + ], + "schemaVersion": 39, + "tags": [ + "kubernetes", + "nodes", + "resources" + ], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": [ + "10.0.0.101:9100" + ], + "value": [ + "10.0.0.101:9100" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(node_cpu_seconds_total,instance)", + "hide": 0, + "includeAll": false, + "multi": true, + "name": "node", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(node_cpu_seconds_total,instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes Nodes - CPU & Memory (by Label)", + "uid": "k8s-nodes-extended2", + "version": 4, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/ctf/team-intsances.json b/ops/prometheus/grafana/dashboards/ctf/team-intsances.json new file mode 100644 index 0000000..096ebe9 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/ctf/team-intsances.json @@ -0,0 +1,174 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 56, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count by (deployment) (\r\n kube_deployment_labels{\r\n namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\",\r\n label_instanced_challenges_ctfpilot_com_deployment!=\"\",\r\n label_instanced_challenges_ctfpilot_com_owner=\"276\"\r\n }\r\n)", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Panel Title", + "transformations": [ + { + "id": "rowsToFields", + "options": { + "mappings": [] + } + } + ], + "type": "timeseries" + } + ], + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "100", + "value": "100" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(label_instanced_challenges_ctfpilot_com_owner)", + "hide": 0, + "includeAll": false, + "label": "Team id", + "multi": false, + "name": "teamid", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(label_instanced_challenges_ctfpilot_com_owner)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-2d", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Team instances", + "uid": "aevzf9eqwc2kga", + "version": 3, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-addons-prometheus.json b/ops/prometheus/grafana/dashboards/k8s/k8s-addons-prometheus.json new file mode 100644 index 0000000..b2dbb65 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-addons-prometheus.json @@ -0,0 +1,3187 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "8.5.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "terraform" + ], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "oncall" + ], + "type": "tags" + } + } + ] + }, + "description": "This is a modern 'Prometheus' dashboard for your Kubernetes cluster(s). Made for kube-prometheus-stack and take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 89, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "refId": "A" + } + ], + "title": "Information", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "?", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 78, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "name" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "prometheus_build_info{pod=~\"$pod\", cluster=~\"$cluster\"}", + "instant": true, + "interval": "", + "legendFormat": "{{ version }}", + "range": false, + "refId": "A" + } + ], + "title": "Prometheus version", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 92, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "up{pod=~\"$pod\", cluster=~\"$cluster\"} < 1", + "instant": true, + "interval": "", + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Instance Down", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 72, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(prometheus_tsdb_head_series{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod)", + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "TSDB Head Series", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 94, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(prometheus_sd_discovered_targets{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod)", + "instant": true, + "interval": "", + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Discovered Targets", + "type": "stat" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 64, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "refId": "A" + } + ], + "title": "Prometheus", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 93, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "up{pod=~\"$pod\", cluster=~\"$cluster\"}", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Liveness by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 96, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(prometheus_config_last_reload_successful{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod)", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Config - Last Successful Reload by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 74, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(prometheus_target_scrapes_exceeded_body_size_limit_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "", + "legendFormat": "{{ pod }} - Exceeded body size limit", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_target_scrapes_exceeded_sample_limit_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Exceeded sample limit", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_target_scrapes_sample_duplicate_timestamp_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Duplicate timestamp", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_target_scrapes_sample_out_of_bounds_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Sample out of bounds", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_target_scrapes_sample_out_of_order_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Sample out of order", + "range": true, + "refId": "E" + } + ], + "title": "Target Scrapes Errors by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 84, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(prometheus_sd_discovered_targets{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod)", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Number of Targets by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 75, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(prometheus_target_sync_length_seconds_sum{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod, scrape_job) * 1000", + "interval": "", + "legendFormat": "{{ pod }} - {{ scrape_job }}", + "range": true, + "refId": "A" + } + ], + "title": "Target Sync by pod, scrape_job", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 85, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "round(sum(rate(prometheus_target_interval_length_seconds_sum{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval]) / rate(prometheus_target_interval_length_seconds_count{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod))", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Average Scrape Interval by pod", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 98, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "refId": "A" + } + ], + "title": "Prometheus TSDB / Query Engine", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 59, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(prometheus_tsdb_head_series{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod)", + "interval": "", + "legendFormat": "{{ pod }} - Head Series", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(prometheus_tsdb_head_chunks{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Head Chunks", + "range": true, + "refId": "B" + } + ], + "title": "TSDB Head Series & Chunks by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 60, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(prometheus_tsdb_head_samples_appended_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "TSDB Head samples appended - rate by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 101, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(prometheus_tsdb_blocks_loaded{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod)", + "interval": "", + "legendFormat": "{{ pod }} - Head Series", + "range": true, + "refId": "A" + } + ], + "title": "TSDB Blocks Loaded by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 102, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(prometheus_tsdb_compactions_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "", + "legendFormat": "{{ pod }} - Total Compactions", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_tsdb_compactions_triggered_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Triggered Compactions", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_tsdb_compactions_skipped_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Skipped Compactions", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_tsdb_compactions_failed_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Failed Compactions", + "range": true, + "refId": "D" + } + ], + "title": "TSDB Rate of Compactions by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 47 + }, + "id": 90, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(prometheus_tsdb_reloads_failures_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "TSDB Reload Failures by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 95, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(prometheus_tsdb_head_series_created_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "", + "legendFormat": "{{ pod }} - Created series", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(prometheus_tsdb_head_series_removed_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "hide": false, + "legendFormat": "{{ pod }} - Deleted series", + "range": true, + "refId": "B" + } + ], + "title": "TSDB Created & Deleted series by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 55 + }, + "id": 73, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(prometheus_engine_query_duration_seconds_count{pod=~\"$pod\", slice=\"inner_eval\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Engine Query Count by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 55 + }, + "id": 86, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "max(prometheus_engine_query_duration_seconds{pod=~\"$pod\", cluster=~\"$cluster\"}) by (pod, slice) * 1000", + "interval": "", + "legendFormat": "{{ pod }} - {{ slice }}", + "range": true, + "refId": "A" + } + ], + "title": "Engine Query Duration by pod, slice", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 63 + }, + "id": 47, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Resources", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU Cores", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 4, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 64 + }, + "id": 29, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_usage_seconds_total{pod=~\"$pod\", image!=\"\", container!=\"\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod, container)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - {{ container }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Usage by pod, container", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 64 + }, + "id": 51, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{pod=~\"$pod\", image!=\"\", container!=\"\", cluster=~\"$cluster\"}) by (pod, container)", + "interval": "", + "legendFormat": "{{ pod }} - {{ container }}", + "range": true, + "refId": "A" + } + ], + "title": "Memory Usage by container", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 72 + }, + "id": 66, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "refId": "A" + } + ], + "title": "Storage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 62, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim=~\".*prom.*\", cluster=~\"$cluster\"}) by (persistentvolumeclaim) / sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim=~\".*prom.*\", cluster=~\"$cluster\"}) by (persistentvolumeclaim)", + "interval": "", + "legendFormat": "{{ persistentvolumeclaim }}", + "range": true, + "refId": "A" + } + ], + "title": "Persistent Volumes - Capacity and usage in %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 87, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kubelet_volume_stats_used_bytes{persistentvolumeclaim=~\".*prom.*\", cluster=~\"$cluster\"}) by (persistentvolumeclaim)", + "interval": "", + "legendFormat": "{{ persistentvolumeclaim }} - Used", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kubelet_volume_stats_capacity_bytes{persistentvolumeclaim=~\".*prom.*\", cluster=~\"$cluster\"}) by (persistentvolumeclaim)", + "hide": false, + "legendFormat": "{{ persistentvolumeclaim }} - Capacity", + "range": true, + "refId": "B" + } + ], + "title": "Persistent Volumes - Capacity and usage in bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 68, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "1 - sum(kubelet_volume_stats_inodes_used{persistentvolumeclaim=~\".*prom.*\", cluster=~\"$cluster\"}) by (persistentvolumeclaim) / sum(kubelet_volume_stats_inodes{persistentvolumeclaim=~\".*prom.*\", cluster=~\"$cluster\"}) by (persistentvolumeclaim)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ persistentvolumeclaim }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Persistent Volumes - Inodes", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 89 + }, + "id": 45, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Network", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 90 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_bytes_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Received", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_bytes_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Transmitted", + "range": true, + "refId": "B" + } + ], + "title": "Network - Bandwidth by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 90 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_packets_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Received", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_packets_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Transmitted", + "range": true, + "refId": "B" + } + ], + "title": "Network - Packets rate by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 98 + }, + "id": 36, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_packets_dropped_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Received", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_packets_dropped_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Transmitted", + "range": true, + "refId": "B" + } + ], + "title": "Network - Packets Dropped by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 98 + }, + "id": 37, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_errors_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Received", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_errors_total{pod=~\"$pod\", cluster=~\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }} - Transmitted", + "range": true, + "refId": "B" + } + ], + "title": "Network - Errors by pod", + "type": "timeseries" + } + ], + "refresh": "30s", + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [ + "Kubernetes", + "Prometheus" + ], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(prometheus_build_info{cluster=\"$cluster\"}, pod)", + "hide": 0, + "includeAll": true, + "multi": false, + "name": "pod", + "options": [], + "query": { + "query": "label_values(prometheus_build_info{cluster=\"$cluster\"}, pod)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Prometheus", + "uid": "k8s_addons_prometheus", + "version": 7, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-addons-trivy-operator.json b/ops/prometheus/grafana/dashboards/k8s/k8s-addons-trivy-operator.json new file mode 100644 index 0000000..803bc0f --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-addons-trivy-operator.json @@ -0,0 +1,2733 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "8.5.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "terraform" + ], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "oncall" + ], + "type": "tags" + } + } + ] + }, + "description": "This is a modern dashboard for the Trivy Operator from Aqua Security. Made to take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 43, + "panels": [], + "title": "Vulnerabilities", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 51, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{severity=\"Critical\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "CRITICAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 1 + }, + "id": 50, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{severity=\"High\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "HIGH", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 1 + }, + "id": 49, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{severity=\"Medium\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "MEDIUM", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 1 + }, + "id": 60, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{severity=\"Low\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "LOW", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "purple", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 1 + }, + "id": 52, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{severity=\"Unknown\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "UNKNOWN", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "text", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 20, + "y": 1 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "TOTAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 5 + }, + "id": 58, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{cluster=~\"$cluster\", namespace=~\"$namespace\"}) by (namespace)", + "instant": false, + "interval": "$resolution", + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + } + ], + "title": "Total vulnerabilities by namespaces", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Critical" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "High" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Medium" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Low" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unknown" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 5 + }, + "id": 61, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{cluster=~\"$cluster\"}) by (severity)", + "instant": false, + "interval": "$resolution", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total vulnerabilities by severity in selected namespace(s)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 85, + "panels": [], + "title": "Vulnerability Details", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "displayMode": "auto", + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "severity" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "Critical": { + "color": "red", + "index": 0 + }, + "High": { + "color": "orange", + "index": 1 + }, + "Low": { + "color": "blue", + "index": 3 + }, + "Medium": { + "color": "yellow", + "index": 2 + }, + "Unknown": { + "color": "purple", + "index": 4 + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.displayMode", + "value": "color-text" + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 83, + "options": { + "footer": { + "enablePagination": true, + "fields": [ + "Value" + ], + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_vulnerabilities{namespace=~\"$namespace\", cluster=~\"$cluster\"}) by (namespace, image_registry, image_repository, image_tag, severity) > 0", + "format": "table", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Vulnerability count per image and severity in $namespace namespace(s)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": false + }, + "indexByName": { + "Time": 0, + "Value": 6, + "image_registry": 2, + "image_repository": 3, + "image_tag": 4, + "namespace": 1, + "severity": 5 + }, + "renameByName": { + "Value": "Nb of vulnerabilities", + "image_registry": "Image Registry", + "image_repository": "Image Repository", + "image_tag": "Image Tag", + "namespace": "Namespace", + "severity": "Severity" + } + } + }, + { + "id": "groupBy", + "options": { + "fields": { + "All values": { + "aggregations": [], + "operation": "groupby" + }, + "Count": { + "aggregations": [], + "operation": "groupby" + }, + "Image Registry": { + "aggregations": [], + "operation": "groupby" + }, + "Image Repository": { + "aggregations": [], + "operation": "groupby" + }, + "Image Tag": { + "aggregations": [], + "operation": "groupby" + }, + "Namespace": { + "aggregations": [], + "operation": "groupby" + }, + "Nb of vulnerabilities": { + "aggregations": [], + "operation": "groupby" + }, + "Severity": { + "aggregations": [], + "operation": "groupby" + }, + "Value": { + "aggregations": [], + "operation": "groupby" + }, + "image_registry": { + "aggregations": [], + "operation": "groupby" + }, + "image_repository": { + "aggregations": [], + "operation": "groupby" + }, + "image_tag": { + "aggregations": [], + "operation": "groupby" + }, + "namespace": { + "aggregations": [], + "operation": "groupby" + }, + "severity": { + "aggregations": [], + "operation": "groupby" + } + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Require operator.metricsVulnIdEnabled: true", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "filterable": true, + "inspect": false + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "severity" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "options": { + "Critical": { + "color": "red", + "index": 0 + }, + "High": { + "color": "orange", + "index": 1 + }, + "Low": { + "color": "blue", + "index": 3 + }, + "Medium": { + "color": "yellow", + "index": 2 + }, + "Unknown": { + "color": "purple", + "index": 4 + } + }, + "type": "value" + } + ] + }, + { + "id": "custom.displayMode", + "value": "color-text" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "vuln_id" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "https://nvd.nist.gov/vuln/detail/${__value.text}", + "url": "https://nvd.nist.gov/vuln/detail/${__value.text}" + } + ] + } + ] + } + ] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 78, + "options": { + "footer": { + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_vulnerability_id{vuln_id=~\"CVE.*\", namespace=~\"$namespace\", cluster=~\"$cluster\"}) by (namespace, image_registry, image_repository, image_tag, vuln_id, severity)", + "format": "table", + "instant": false, + "interval": "$resolution", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Detailed CVE vulnerabilities in $namespace namespace(s)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true, + "__name__": true, + "container": true, + "endpoint": true, + "instance": true, + "job": true, + "namespace": false, + "service": true + }, + "indexByName": { + "Time": 0, + "Value": 7, + "image_registry": 2, + "image_repository": 3, + "image_tag": 4, + "namespace": 1, + "severity": 6, + "vuln_id": 5 + }, + "renameByName": { + "image_namespace": "namespace", + "image_registry": "Image Registry", + "image_repository": "Image Repository", + "image_tag": "Image Tag", + "namespace": "Namespace", + "severity": "Severity", + "vuln_id": "Vulnerability", + "vulnerability_id": "" + } + } + }, + { + "id": "groupBy", + "options": { + "fields": { + "Image Registry": { + "aggregations": [], + "operation": "groupby" + }, + "Image Repository": { + "aggregations": [], + "operation": "groupby" + }, + "Image Tag": { + "aggregations": [], + "operation": "groupby" + }, + "Namespace": { + "aggregations": [], + "operation": "groupby" + }, + "Severity": { + "aggregations": [], + "operation": "groupby" + }, + "Value": { + "aggregations": [ + "lastNotNull" + ] + }, + "Vulnerability": { + "aggregations": [], + "operation": "groupby" + }, + "image_namespace": { + "aggregations": [], + "operation": "groupby" + }, + "namespace": { + "aggregations": [], + "operation": "groupby" + }, + "severity": { + "aggregations": [], + "operation": "groupby" + }, + "vuln_id": { + "aggregations": [], + "operation": "groupby" + }, + "vulnerability_id": { + "aggregations": [], + "operation": "groupby" + } + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 47, + "panels": [], + "title": "Config Audit Reports", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 39 + }, + "id": 56, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"Critical\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "CRITICAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 39 + }, + "id": 55, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"High\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "HIGH", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 39 + }, + "id": 54, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"Medium\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "MEDIUM", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 39 + }, + "id": 53, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{severity=\"Low\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "LOW", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "text", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 39 + }, + "id": 65, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "TOTAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 43 + }, + "id": 62, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{cluster=~\"$cluster\", namespace=~\"$namespace\"}) by (namespace)", + "instant": false, + "interval": "$resolution", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total config audit report by namespaces", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Critical" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "High" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Medium" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Low" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 43 + }, + "id": 63, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_resource_configaudits{cluster=~\"$cluster\"}) by (severity)", + "instant": false, + "interval": "$resolution", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total config audit report by severity", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 68, + "panels": [], + "title": "RBAC Assessments", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 52 + }, + "id": 72, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_role_rbacassessments{severity=\"Critical\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "CRITICAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 52 + }, + "id": 71, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_role_rbacassessments{severity=\"High\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "HIGH", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 52 + }, + "id": 70, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_role_rbacassessments{severity=\"Medium\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "MEDIUM", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 12, + "y": 52 + }, + "id": 69, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_role_rbacassessments{severity=\"Low\", namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "LOW", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "text", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 16, + "y": 52 + }, + "id": 73, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.8", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_role_rbacassessments{namespace=~\"$namespace\", cluster=~\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "__auto", + "refId": "A" + } + ], + "title": "TOTAL", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 74, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_role_rbacassessments{cluster=~\"$cluster\"}) by (namespace)", + "instant": false, + "interval": "$resolution", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total RBAC Assessments by namespaces", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Critical" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "High" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Medium" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "yellow", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Low" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 75, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_role_rbacassessments{cluster=~\"$cluster\"}) by (severity)", + "instant": false, + "interval": "$resolution", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total RBAC Assessments by severity", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 64 + }, + "id": 81, + "panels": [], + "title": "Exposed Secrets", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "blue", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 65 + }, + "id": 76, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.5.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(trivy_image_exposedsecrets{cluster=~\"$cluster\"}) by (namespace)", + "instant": false, + "interval": "$resolution", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total Exposed Secrets by namespaces", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 37, + "style": "dark", + "tags": [ + "Prometheus", + "Addons", + "Trivy", + "Trivy-operator" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_pod_info{cluster=\"$cluster\"}, namespace)", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(kube_pod_info{cluster=\"$cluster\"}, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Trivy Operator - Vulnerabilities", + "uid": "security_trivy_operator", + "version": 15, + "weekStart": "" +} diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-system-api-server.json b/ops/prometheus/grafana/dashboards/k8s/k8s-system-api-server.json new file mode 100644 index 0000000..4c520f3 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-system-api-server.json @@ -0,0 +1,1399 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "8.4.4" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "terraform" + ], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "oncall" + ], + "type": "tags" + } + } + ] + }, + "description": "This is a modern API Server dashboard for your Kubernetes cluster(s). Made for kube-prometheus-stack and take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "text": "DOWN" + }, + "1": { + "text": "UP" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 42, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "up{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}", + "interval": "", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "API Server - Health Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "__name__" + }, + "properties": [ + { + "id": "custom.width", + "value": 188 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 60, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "removed_release" + } + ] + }, + "pluginVersion": "10.0.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "apiserver_requested_deprecated_apis{cluster=~\"$cluster\"}", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Deprecated Kubernetes Resources", + "transformations": [ + { + "id": "labelsToFields", + "options": { + "keepLabels": [ + "group", + "job", + "removed_release", + "resource", + "version", + "name" + ], + "mode": "columns" + } + }, + { + "id": "merge", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true, + "job": true + }, + "indexByName": { + "Time": 6, + "Value": 7, + "group": 1, + "job": 5, + "namespace": 0, + "removed_release": 4, + "resource": 3, + "version": 2 + }, + "renameByName": {} + } + }, + { + "id": "groupBy", + "options": { + "fields": { + "group": { + "aggregations": [ + "lastNotNull" + ], + "operation": "groupby" + }, + "job": { + "aggregations": [], + "operation": "groupby" + }, + "namespace": { + "aggregations": [ + "lastNotNull" + ], + "operation": "groupby" + }, + "removed_release": { + "aggregations": [], + "operation": "groupby" + }, + "resource": { + "aggregations": [ + "lastNotNull" + ], + "operation": "groupby" + }, + "version": { + "aggregations": [], + "operation": "groupby" + } + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 38, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum by (code) (rate(apiserver_request_total{cluster=~\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "{{ code }}", + "refId": "A" + } + ], + "title": "API Server - HTTP Requests by code", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 39, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum by (verb) (rate(apiserver_request_total{cluster=~\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "{{ verb}}", + "refId": "A" + } + ], + "title": "API Server - HTTP Requests by verb", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 53, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(apiserver_request_duration_seconds_sum{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval])) by (instance)\n/\nsum(rate(apiserver_request_duration_seconds_count{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval])) by (instance)", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "API Server - HTTP Requests Latency by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 54, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(apiserver_request_duration_seconds_sum{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval])) by (verb)\n/\nsum(rate(apiserver_request_duration_seconds_count{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval])) by (verb)", + "interval": "$resolution", + "legendFormat": "{{ verb }}", + "refId": "A" + } + ], + "title": "API Server - HTTP Requests Latency by verb", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 50, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum by(instance) (rate(apiserver_request_total{code=~\"5..\", job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval]))\n / sum by(instance) (rate(apiserver_request_total{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "API Server - Errors by Instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 51, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum by(verb) (rate(apiserver_request_total{code=~\"5..\",job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval]))\n / sum by(verb) (rate(apiserver_request_total{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "{{ verb }}", + "refId": "A" + } + ], + "title": "API Server - Errors by verb", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 40, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(apiserver_request_total{cluster=~\"$cluster\"}[$__rate_interval])) by (instance)", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "API Server - Stacked HTTP Requests by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 56, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(workqueue_depth{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval])) by (instance)", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "API Server - Work Queue by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 40 + }, + "id": 47, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "rate(process_cpu_seconds_total{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}[$__rate_interval])", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "API Server - CPU Usage by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 40 + }, + "id": 48, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "process_resident_memory_bytes{job=~\"kubernetes-apiservers|apiserver\", cluster=~\"$cluster\"}", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "API Server - Memory Usage by instance", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 38, + "style": "dark", + "tags": [ + "Kubernetes", + "Prometheus" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes / System / API Server", + "uid": "k8s_system_apisrv", + "version": 19, + "weekStart": "" +} diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-system-coredns.json b/ops/prometheus/grafana/dashboards/k8s/k8s-system-coredns.json new file mode 100644 index 0000000..266ec29 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-system-coredns.json @@ -0,0 +1,1600 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "8.4.4" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "terraform" + ], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "oncall" + ], + "type": "tags" + } + } + ] + }, + "description": "This is a modern CoreDNS dashboard for your Kubernetes cluster(s). Made for kube-prometheus-stack and take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [ + { + "options": { + "0": { + "text": "DOWN" + }, + "1": { + "text": "UP" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 25, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "vertical", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "10.4.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "up{job=~\"$job\", instance=~\"$instance\", cluster=~\"$cluster\"}", + "interval": "", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "CoreDNS - Health Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 3 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "rate(process_cpu_seconds_total{job=~\"$job\", instance=~\"$instance\", cluster=~\"$cluster\"}[$__rate_interval])", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "CoreDNS - CPU Usage by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 3 + }, + "id": 21, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "process_resident_memory_bytes{job=~\"$job\", instance=~\"$instance\", cluster=~\"$cluster\"}", + "interval": "", + "legendFormat": "{{ instance }}", + "refId": "A" + } + ], + "title": "CoreDNS - Memory Usage by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 11 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_dns_requests_total{instance=~\"$instance\",proto=\"$protocol\", cluster=~\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "total $protocol requests", + "refId": "A" + } + ], + "title": "CoreDNS - Total DNS Requests ($protocol)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 11 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_dns_request_size_bytes_sum{instance=~\"$instance\",proto=\"$protocol\", cluster=~\"$cluster\"}[$__rate_interval])) by (proto) / sum(rate(coredns_dns_request_size_bytes_count{instance=~\"$instance\",proto=\"$protocol\", cluster=~\"$cluster\"}[$__rate_interval])) by (proto)", + "interval": "$resolution", + "legendFormat": "average $protocol packet size", + "refId": "A" + } + ], + "title": "CoreDNS - Average Packet Size ($protocol)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 19 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_dns_requests_total{instance=~\"$instance\", cluster=~\"$cluster\"}[$__rate_interval])) by (type)", + "interval": "$resolution", + "legendFormat": "{{ type }}", + "refId": "A" + } + ], + "title": "CoreDNS - Requests by type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 19 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_dns_responses_total{instance=~\"$instance\", cluster=~\"$cluster\"}[$__rate_interval])) by (rcode)", + "interval": "$resolution", + "legendFormat": "{{ rcode }}", + "refId": "A" + } + ], + "title": "CoreDNS - Requests by return code", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 23, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_forward_requests_total{cluster=~\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "total forward requests", + "refId": "A" + } + ], + "title": "CoreDNS - Total Forward Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_forward_responses_total{rcode=~\"SERVFAIL|REFUSED\", cluster=~\"$cluster\"}[$__rate_interval])) by (rcode)", + "interval": "$resolution", + "legendFormat": "{{ rcode }}", + "refId": "A" + } + ], + "title": "CoreDNS - DNS Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_cache_hits_total{instance=~\"$instance\", cluster=~\"$cluster\"}[$__rate_interval])) by (type)", + "interval": "$resolution", + "legendFormat": "{{ type }}", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(coredns_cache_misses_total{instance=~\"$instance\", cluster=~\"$cluster\"}[$__rate_interval])) by (type)", + "interval": "$resolution", + "legendFormat": "misses", + "refId": "B" + } + ], + "title": "CoreDNS - Cache Hits / Misses", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 35 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(coredns_cache_entries{cluster=~\"$cluster\"}) by (type)", + "interval": "", + "legendFormat": "{{ type }}", + "refId": "A" + } + ], + "title": "CoreDNS - Cache Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 43 + }, + "id": 27, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "RdYlBu", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "s" + } + }, + "pluginVersion": "10.4.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(increase(coredns_dns_request_duration_seconds_bucket{instance=~\"$instance\", cluster=~\"$cluster\"}[$__rate_interval])) by (le)", + "format": "heatmap", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "CoreDNS - DNS request duration", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 43 + }, + "id": 28, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "RdYlBu", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "decbytes" + } + }, + "pluginVersion": "10.4.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(increase(coredns_dns_request_size_bytes_bucket{instance=~\"$instance\", le!=\"0\", cluster=~\"$cluster\"}[$__rate_interval])) by (le)", + "format": "heatmap", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "CoreDNS - DNS request size", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 29, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "exponent": 0.5, + "fill": "dark-orange", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "RdYlBu", + "steps": 64 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": true + }, + "rowsFrame": { + "layout": "auto" + }, + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "decbytes" + } + }, + "pluginVersion": "10.4.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(increase(coredns_dns_response_size_bytes_bucket{instance=~\"$instance\", le!=\"0\", cluster=~\"$cluster\"}[$__rate_interval])) by (le)", + "format": "heatmap", + "legendFormat": "{{le}}", + "range": true, + "refId": "A" + } + ], + "title": "CoreDNS - DNS response size", + "type": "heatmap" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "Kubernetes", + "Prometheus" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(up{job=\"$job\", cluster=\"$cluster\"},instance)", + "hide": 0, + "includeAll": true, + "label": "", + "multi": false, + "name": "instance", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(up{job=\"$job\", cluster=\"$cluster\"},instance)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": "udp,tcp", + "current": { + "selected": false, + "text": "udp", + "value": "udp" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(coredns_dns_requests_total{cluster=\"$cluster\"}, proto)", + "hide": 0, + "includeAll": false, + "label": "", + "multi": false, + "name": "protocol", + "options": [], + "query": { + "query": "label_values(coredns_dns_requests_total{cluster=\"$cluster\"}, proto)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": true, + "text": [ + "coredns" + ], + "value": [ + "coredns" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(coredns_build_info{cluster=\"$cluster\"},job)", + "hide": 0, + "includeAll": false, + "multi": true, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(coredns_build_info{cluster=\"$cluster\"},job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes / System / CoreDNS", + "uid": "k8s_system_coredns", + "version": 20, + "weekStart": "" +} diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-views-global.json b/ops/prometheus/grafana/dashboards/k8s/k8s-views-global.json new file mode 100644 index 0000000..eb7e016 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-views-global.json @@ -0,0 +1,3561 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "bargauge", + "name": "Bar gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.3.1" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "terraform" + ], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "oncall" + ], + "type": "tags" + } + } + ] + }, + "description": "This is a modern 'Global View' dashboard for your Kubernetes cluster(s). Made for kube-prometheus-stack and take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 67, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 77, + "options": { + "displayMode": "lcd", + "maxVizHeight": 300, + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(sum by (instance, cpu) (rate(node_cpu_seconds_total{mode!~\"idle|iowait|steal\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])))", + "interval": "", + "legendFormat": "Real Linux", + "range": true, + "refId": "Real Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(sum by (core) (rate(windows_cpu_time_total{mode!=\"idle\", cluster=\"$cluster\"}[$__rate_interval])))", + "hide": false, + "interval": "", + "legendFormat": "Real Windows", + "range": true, + "refId": "Real Windows" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_requests{resource=\"cpu\", cluster=\"$cluster\"}) / sum(machine_cpu_cores{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Requests", + "range": true, + "refId": "Requests" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_limits{resource=\"cpu\", cluster=\"$cluster\"}) / sum(machine_cpu_cores{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Limits", + "range": true, + "refId": "Limits" + } + ], + "title": "Global CPU Usage", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "Real", + "mode": "reduceRow", + "reduce": { + "include": [ + "Real Linux", + "Real Windows" + ], + "reducer": "mean" + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Real Linux": true, + "Real Windows": true, + "Time": true + }, + "indexByName": { + "Limits": 5, + "Real": 1, + "Real Linux": 2, + "Real Windows": 3, + "Requests": 4, + "Time": 0 + }, + "renameByName": {} + } + } + ], + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 78, + "options": { + "displayMode": "lcd", + "maxVizHeight": 300, + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "sizing": "auto", + "text": {}, + "valueMode": "color" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(node_memory_MemTotal_bytes{cluster=\"$cluster\", job=\"$job\"} - node_memory_MemAvailable_bytes{cluster=\"$cluster\", job=\"$job\"}) / sum(node_memory_MemTotal_bytes{cluster=\"$cluster\", job=\"$job\"})", + "hide": false, + "interval": "", + "legendFormat": "Real Linux", + "range": true, + "refId": "Real Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(windows_memory_available_bytes{cluster=\"$cluster\"} + windows_memory_cache_bytes{cluster=\"$cluster\"}) / sum(windows_os_visible_memory_bytes{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Real Windows", + "range": true, + "refId": "Real Windows" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_requests{resource=\"memory\", cluster=\"$cluster\"}) / sum(machine_memory_bytes{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Requests", + "range": true, + "refId": "Requests" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_limits{resource=\"memory\", cluster=\"$cluster\"}) / sum(machine_memory_bytes{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Limits", + "range": true, + "refId": "Limits" + } + ], + "title": "Global RAM Usage", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "Real", + "mode": "reduceRow", + "reduce": { + "include": [ + "Real Linux", + "Real Windows" + ], + "reducer": "mean" + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Real Linux": true, + "Real Windows": true, + "Time": true + }, + "includeByName": {}, + "indexByName": { + "Limits": 5, + "Real": 3, + "Real Linux": 1, + "Real Windows": 2, + "Requests": 4, + "Time": 0 + }, + "renameByName": {} + } + } + ], + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 12, + "y": 1 + }, + "id": 63, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "count(count by (node) (kube_node_info{cluster=\"$cluster\"}))", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + } + ], + "title": "Nodes", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 10, + "x": 14, + "y": 1 + }, + "id": 52, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(kube_namespace_labels{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Namespaces", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_status_running{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Running Containers", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_status_phase{phase=\"Running\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Running Pods", + "refId": "O" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_service_info{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Services", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_endpoint_info{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Endpoints", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_ingress_info{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Ingresses", + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_deployment_labels{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Deployments", + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_statefulset_labels{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Statefulsets", + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_daemonset_labels{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Daemonsets", + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_persistentvolumeclaim_info{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Persistent Volume Claims", + "refId": "I" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_hpa_labels{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Horizontal Pod Autoscalers", + "refId": "J" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_configmap_info{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Configmaps", + "refId": "K" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_secret_info{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Secrets", + "refId": "L" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_networkpolicy_labels{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Network Policies", + "refId": "M" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "count(count by (node) (kube_node_info{cluster=\"$cluster\"}))", + "hide": false, + "interval": "", + "legendFormat": "Nodes", + "refId": "N" + } + ], + "title": "Kubernetes Resource Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 12, + "y": 5 + }, + "id": 59, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "count(kube_namespace_created{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Namespaces", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 37, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_cpu_seconds_total{mode!~\"idle|iowait|steal\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval]))", + "interval": "", + "legendFormat": "Real Linux", + "range": true, + "refId": "Real Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(windows_cpu_time_total{mode!=\"idle\", cluster=\"$cluster\"}[$__rate_interval]))", + "hide": false, + "interval": "", + "legendFormat": "Real Windows", + "range": true, + "refId": "Real Windows" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_requests{resource=\"cpu\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Requests", + "range": true, + "refId": "Requests" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_limits{resource=\"cpu\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Limits", + "range": true, + "refId": "Limits" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(machine_cpu_cores{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Total", + "range": true, + "refId": "Total" + } + ], + "title": "CPU Usage", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "Real", + "mode": "reduceRow", + "reduce": { + "include": [ + "Real Linux", + "Real Windows" + ], + "reducer": "sum" + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Real Linux": true, + "Real Windows": true, + "Time": true, + "Total Linux": true, + "Total Windows": true + }, + "indexByName": { + "Limits": 5, + "Real": 3, + "Real Linux": 1, + "Real Windows": 2, + "Requests": 4, + "Time": 0, + "Total": 8, + "Total Linux": 6, + "Total Windows": 7 + }, + "renameByName": {} + } + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 39, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(node_memory_MemTotal_bytes{cluster=\"$cluster\", job=\"$job\"} - node_memory_MemAvailable_bytes{cluster=\"$cluster\", job=\"$job\"})", + "interval": "", + "legendFormat": "Real Linux", + "range": true, + "refId": "Real Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(windows_os_visible_memory_bytes{cluster=\"$cluster\"} - windows_memory_available_bytes{cluster=\"$cluster\"} - windows_memory_cache_bytes{cluster=\"$cluster\"})", + "hide": false, + "interval": "", + "legendFormat": "Real Windows", + "range": true, + "refId": "Real Windows" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_requests{resource=\"memory\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Requests", + "range": true, + "refId": "Requests" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_limits{resource=\"memory\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Limits", + "range": true, + "refId": "Limits" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(machine_memory_bytes{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Total", + "range": true, + "refId": "Total" + } + ], + "title": "RAM Usage", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "Real", + "mode": "reduceRow", + "reduce": { + "include": [ + "Real Linux", + "Real Windows" + ], + "reducer": "mean" + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Real Linux": true, + "Real Windows": true, + "Time": true + }, + "includeByName": {}, + "indexByName": { + "Limits": 5, + "Real": 3, + "Real Linux": 1, + "Real Windows": 2, + "Requests": 4, + "Time": 0, + "Total": 6 + }, + "renameByName": {} + } + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 12, + "y": 9 + }, + "id": 62, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_status_phase{phase=\"Running\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Running Pods", + "type": "stat" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 71, + "panels": [], + "title": "Resources", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd", + "seriesBy": "last" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU %", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 0.5 + }, + { + "color": "red", + "value": 0.7 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 72, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(sum by (instance, cpu) (rate(node_cpu_seconds_total{mode!~\"idle|iowait|steal\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])))", + "interval": "$resolution", + "legendFormat": "Linux", + "range": true, + "refId": "Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "1 - avg(rate(windows_cpu_time_total{cluster=\"$cluster\",mode=\"idle\"}[$__rate_interval]))", + "hide": false, + "interval": "$resolution", + "legendFormat": "Windows", + "range": true, + "refId": "Windows" + } + ], + "title": "Cluster CPU Utilization", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "CPU usage in %", + "mode": "reduceRow", + "reduce": { + "reducer": "mean" + }, + "replaceFields": true + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "MEMORY", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 0.5 + }, + { + "color": "red", + "value": 0.7 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 55, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "hidden", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(node_memory_MemTotal_bytes{cluster=\"$cluster\", job=\"$job\"} - node_memory_MemAvailable_bytes{cluster=\"$cluster\", job=\"$job\"}) / sum(node_memory_MemTotal_bytes{cluster=\"$cluster\", job=\"$job\"})", + "interval": "$resolution", + "legendFormat": "Linux", + "range": true, + "refId": "Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(windows_os_visible_memory_bytes{cluster=\"$cluster\"} - windows_memory_available_bytes{cluster=\"$cluster\"}) / sum(windows_os_visible_memory_bytes{cluster=\"$cluster\"})", + "hide": false, + "interval": "$resolution", + "legendFormat": "Windows", + "range": true, + "refId": "Windows" + } + ], + "title": "Cluster Memory Utilization", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "Memory usage in %", + "mode": "reduceRow", + "reduce": { + "reducer": "mean" + }, + "replaceFields": true + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU CORES", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 46, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_usage_seconds_total{image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (namespace)\n+ on (namespace)\n(sum(rate(windows_container_cpu_usage_seconds_total{container_id!=\"\", cluster=\"$cluster\"}[$__rate_interval]) * on (container_id) group_left (container, pod, namespace) max by ( container, container_id, pod, namespace) (kube_pod_container_info{container_id!=\"\", cluster=\"$cluster\"}) OR kube_namespace_created{cluster=\"$cluster\"} * 0) by (namespace))", + "format": "time_series", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{ namespace }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Utilization by namespace", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 50, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{image!=\"\", cluster=\"$cluster\"}) by (namespace)\n+ on (namespace)\n(sum(windows_container_memory_usage_commit_bytes{container_id!=\"\", cluster=\"$cluster\"} * on (container_id) group_left (container, pod, namespace) max by ( container, container_id, pod, namespace) (kube_pod_container_info{container_id!=\"\", cluster=\"$cluster\"}) OR kube_namespace_created{cluster=\"$cluster\"} * 0) by (namespace))", + "interval": "$resolution", + "legendFormat": "{{ namespace }}", + "range": true, + "refId": "A" + } + ], + "title": "Memory Utilization by namespace", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU %", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 54, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(sum by (instance, cpu) (rate(node_cpu_seconds_total{mode!~\"idle|iowait|steal\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval]))) by (instance)", + "interval": "$resolution", + "legendFormat": "{{ node }}", + "range": true, + "refId": "Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(sum by (instance,core) (rate(windows_cpu_time_total{mode!=\"idle\", cluster=\"$cluster\"}[$__rate_interval]))) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{ node }}", + "range": true, + "refId": "Windows" + } + ], + "title": "CPU Utilization by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "MEMORY", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 73, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(node_memory_MemTotal_bytes{cluster=\"$cluster\", job=\"$job\"} - node_memory_MemAvailable_bytes{cluster=\"$cluster\", job=\"$job\"}) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "range": true, + "refId": "Linux" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(windows_os_visible_memory_bytes{cluster=\"$cluster\"} - windows_memory_available_bytes{cluster=\"$cluster\"}) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "range": true, + "refId": "Windows" + } + ], + "title": "Memory Utilization by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "No data is generally a good thing here.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "SECONDS", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 38 + }, + "id": 82, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_cfs_throttled_seconds_total{image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (namespace) > 0", + "interval": "$resolution", + "legendFormat": "{{ namespace }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Throttled seconds by namespace", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "No data is generally a good thing here.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "NB", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 38 + }, + "id": 83, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_cpu_core_throttles_total{cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (instance)", + "interval": "$resolution", + "legendFormat": "{{ instance }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Core Throttled by instance", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 46 + }, + "id": 86, + "panels": [], + "title": "Kubernetes", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 47 + }, + "id": 84, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kube_pod_status_qos_class{cluster=\"$cluster\"}) by (qos_class)", + "interval": "", + "legendFormat": "{{ qos_class }} pods", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_info{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Total pods", + "range": true, + "refId": "B" + } + ], + "title": "Kubernetes Pods QoS classes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 85, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kube_pod_status_reason{cluster=\"$cluster\"}) by (reason)", + "interval": "", + "legendFormat": "{{ reason }}", + "range": true, + "refId": "A" + } + ], + "title": "Kubernetes Pods Status Reason", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "No data is generally a good thing here.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 87, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(increase(container_oom_events_total{cluster=\"$cluster\"}[$__rate_interval])) by (namespace) > 0", + "interval": "", + "legendFormat": "{{ namespace }}", + "range": true, + "refId": "A" + } + ], + "title": "OOM Events by namespace", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "No data is generally a good thing here.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 88, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(increase(kube_pod_container_status_restarts_total{cluster=\"$cluster\"}[$__rate_interval])) by (namespace) > 0", + "interval": "", + "legendFormat": "{{ namespace }}", + "range": true, + "refId": "A" + } + ], + "title": "Container Restarts by namespace", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 65 + }, + "id": 69, + "panels": [], + "title": "Network", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Dropped noisy virtual devices for readability.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "BANDWIDTH", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 66 + }, + "id": 44, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_network_receive_bytes_total{device!~\"(veth|azv|lxc).*\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (device)", + "interval": "$resolution", + "legendFormat": "Received : {{ device }}", + "range": true, + "refId": "Linux Received" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(node_network_transmit_bytes_total{device!~\"(veth|azv|lxc).*\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (device)", + "interval": "$resolution", + "legendFormat": "Transmitted : {{ device }}", + "range": true, + "refId": "Linux Transmitted" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(windows_net_bytes_received_total{cluster=\"$cluster\"}[$__rate_interval])) by (nic)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Received : {{ nic }}", + "range": true, + "refId": "Windows Received" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(windows_net_bytes_sent_total{cluster=\"$cluster\"}[$__rate_interval])) by (nic)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Transmitted : {{ device }}", + "range": true, + "refId": "Windows Transmitted" + } + ], + "title": "Global Network Utilization by device", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "DROPPED PACKETS", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 66 + }, + "id": 53, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_network_receive_drop_total{cluster=\"$cluster\", job=\"$job\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Linux Packets dropped (receive)", + "range": true, + "refId": "Linux Packets dropped (receive)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(node_network_transmit_drop_total{cluster=\"$cluster\", job=\"$job\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Linux Packets dropped (transmit)", + "range": true, + "refId": "Linux Packets dropped (transmit)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(windows_net_packets_received_discarded_total{cluster=\"$cluster\"}[$__rate_interval]))", + "hide": false, + "interval": "$resolution", + "legendFormat": "Windows Packets dropped (receive)", + "range": true, + "refId": "Windows Packets dropped (receive)" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(windows_net_packets_outbound_discarded_total{cluster=\"$cluster\"}[$__rate_interval]))", + "hide": false, + "interval": "$resolution", + "legendFormat": "Windows Packets dropped (transmit)", + "range": true, + "refId": "Windows Packets dropped (transmit)" + } + ], + "title": "Network Saturation - Packets dropped", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "Packets dropped (receive)", + "mode": "reduceRow", + "reduce": { + "include": [ + "Linux Packets dropped (receive)", + "Windows Packets dropped (receive)" + ], + "reducer": "mean" + } + } + }, + { + "id": "calculateField", + "options": { + "alias": "Packets dropped (transmit)", + "mode": "reduceRow", + "reduce": { + "include": [ + "Linux Packets dropped (transmit)", + "Windows Packets dropped (transmit)" + ], + "reducer": "mean" + } + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Linux Packets dropped (receive)": true, + "Linux Packets dropped (transmit)": true, + "Time": false, + "Windows Packets dropped (receive)": true, + "Windows Packets dropped (transmit)": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "BANDWIDTH", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 74 + }, + "id": 79, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_bytes_total{cluster=\"$cluster\"}[$__rate_interval])) by (namespace)\n+ on (namespace)\n(sum(rate(windows_container_network_receive_bytes_total{container_id!=\"\", cluster=\"$cluster\"}[$__rate_interval]) * on (container_id) group_left (container, pod, namespace) max by ( container, container_id, pod, namespace) (kube_pod_container_info{container_id!=\"\", cluster=\"$cluster\"}) OR kube_namespace_created{cluster=\"$cluster\"} * 0) by (namespace))", + "interval": "$resolution", + "legendFormat": "Received : {{ namespace }}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "- (sum(rate(container_network_transmit_bytes_total{cluster=\"$cluster\"}[$__rate_interval])) by (namespace)\n+ on (namespace)\n(sum(rate(windows_container_network_transmit_bytes_total{container_id!=\"\", cluster=\"$cluster\"}[$__rate_interval]) * on (container_id) group_left (container, pod, namespace) max by ( container, container_id, pod, namespace) (kube_pod_container_info{container_id!=\"\", cluster=\"$cluster\"}) OR kube_namespace_created{cluster=\"$cluster\"} * 0) by (namespace)))", + "hide": false, + "interval": "$resolution", + "legendFormat": "Transmitted : {{ namespace }}", + "range": true, + "refId": "B" + } + ], + "title": "Network Received by namespace", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "BANDWIDTH", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 74 + }, + "id": 80, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_network_receive_bytes_total{cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (instance)", + "interval": "$resolution", + "legendFormat": "Received bytes in {{ instance }}", + "range": true, + "refId": "Linux Received bytes" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "- sum(rate(node_network_transmit_bytes_total{cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Transmitted bytes in {{ instance }}", + "range": true, + "refId": "Linux Transmitted bytes" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(windows_net_bytes_received_total{cluster=\"$cluster\"}[$__rate_interval])) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Received bytes in {{ instance }}", + "range": true, + "refId": "Windows Received bytes" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "- sum(rate(windows_net_bytes_sent_total{cluster=\"$cluster\"}[$__rate_interval])) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Transmitted bytes in {{ instance }}", + "range": true, + "refId": "Windows Transmitted bytes" + } + ], + "title": "Total Network Received (with all virtual devices) by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Dropped noisy virtual devices for readability.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "BANDWIDTH", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 82 + }, + "id": 56, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_network_receive_bytes_total{device!~\"(veth|azv|lxc|lo).*\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (instance)", + "interval": "$resolution", + "legendFormat": "Received bytes in {{ instance }}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "- sum(rate(node_network_transmit_bytes_total{device!~\"(veth|azv|lxc|lo).*\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Transmitted bytes in {{ instance }}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(windows_net_bytes_received_total{nic!~\".*Virtual.*\",cluster=\"$cluster\"}[$__rate_interval])) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Received bytes in {{ instance }}", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(windows_net_bytes_sent_total{nic!~\".*Virtual.*\",cluster=\"$cluster\"}[$__rate_interval])) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Transmitted bytes in {{ instance }}", + "range": true, + "refId": "D" + } + ], + "title": "Network Received (without loopback) by instance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Dropped noisy virtual devices for readability.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "BANDWIDTH", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 82 + }, + "id": 81, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_network_receive_bytes_total{device=\"lo\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (instance)", + "interval": "$resolution", + "legendFormat": "Received bytes in {{ instance }}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "- sum(rate(node_network_transmit_bytes_total{device=\"lo\", cluster=\"$cluster\", job=\"$job\"}[$__rate_interval])) by (instance)", + "hide": false, + "interval": "$resolution", + "legendFormat": "Transmitted bytes in {{ instance }}", + "range": true, + "refId": "B" + } + ], + "title": "Network Received (loopback only) by instance", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "Kubernetes", + "Prometheus" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(node_cpu_seconds_total{cluster=\"$cluster\"},job)", + "hide": 0, + "includeAll": false, + "multi": true, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(node_cpu_seconds_total{cluster=\"$cluster\"},job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes / Views / Global", + "uid": "k8s_views_global", + "version": 43, + "weekStart": "" +} diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-views-namespaces.json b/ops/prometheus/grafana/dashboards/k8s/k8s-views-namespaces.json new file mode 100644 index 0000000..8b8efca --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-views-namespaces.json @@ -0,0 +1,3035 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.3.1" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "terraform" + ], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "oncall" + ], + "type": "tags" + } + } + ] + }, + "description": "This is a modern 'Namespaces View' dashboard for your Kubernetes cluster(s). Made for kube-prometheus-stack and take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 38, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 46, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto", + "text": {} + }, + "pluginVersion": "11.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=~\"$namespace\", image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) / sum(machine_cpu_cores{cluster=\"$cluster\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "range": false, + "refId": "A" + } + ], + "title": "Namespace(s) usage on total cluster CPU in %", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 48, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto", + "text": {} + }, + "pluginVersion": "11.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{namespace=~\"$namespace\", image!=\"\", cluster=\"$cluster\"}) / sum(machine_memory_bytes{cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Namespace(s) usage on total cluster RAM in %", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 32, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_info{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Running Pods", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_service_info{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Services", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_ingress_info{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Ingresses", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_deployment_labels{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Deployments", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_statefulset_labels{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Statefulsets", + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_daemonset_labels{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Daemonsets", + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_persistentvolumeclaim_info{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Persistent Volume Claims", + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_hpa_labels{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Horizontal Pod Autoscalers", + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_configmap_info{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Configmaps", + "refId": "I" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_secret_info{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Secrets", + "refId": "J" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_networkpolicy_labels{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Network Policies", + "refId": "K" + } + ], + "title": "Kubernetes Resource Count", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 62, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=~\"$namespace\", image!=\"\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "", + "legendFormat": "Real", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_requests{namespace=~\"$namespace\", resource=\"cpu\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Requests", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_limits{namespace=~\"$namespace\", resource=\"cpu\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Limits", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(machine_cpu_cores{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Cluster Total", + "range": true, + "refId": "D" + } + ], + "title": "Namespace(s) CPU Usage in cores", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 8 + }, + "id": 64, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.1", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{namespace=~\"$namespace\", image!=\"\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Real", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_requests{namespace=~\"$namespace\", resource=\"memory\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Requests", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_resource_limits{namespace=~\"$namespace\", resource=\"memory\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Limits", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(machine_memory_bytes{cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Cluster Total", + "range": true, + "refId": "D" + } + ], + "title": "Namespace(s) RAM Usage in bytes", + "type": "stat" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 40, + "panels": [], + "title": "Resources", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU CORES", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 29, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=~\"$namespace\", image!=\"\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU usage by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{namespace=~\"$namespace\", image!=\"\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Memory usage by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "SECONDS", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 21 + }, + "id": 68, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_cfs_throttled_seconds_total{namespace=~\"$namespace\", image!=\"\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod) > 0", + "interval": "$resolution", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Throttled seconds by pod", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 73, + "panels": [], + "title": "Kubernetes", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 70, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kube_pod_status_qos_class{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (qos_class)", + "interval": "", + "legendFormat": "{{ qos_class }} pods", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_info{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "hide": false, + "legendFormat": "Total pods", + "range": true, + "refId": "B" + } + ], + "title": "Kubernetes Pods QoS classes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 30 + }, + "id": 72, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kube_pod_status_reason{cluster=\"$cluster\"}) by (reason)", + "interval": "", + "legendFormat": "{{ reason }}", + "range": true, + "refId": "A" + } + ], + "title": "Kubernetes Pods Status Reason", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "No data is generally a good thing here.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 74, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(increase(container_oom_events_total{namespace=~\"${namespace}\", cluster=\"$cluster\"}[$__rate_interval])) by (namespace, pod) > 0", + "interval": "", + "legendFormat": "namespace: {{ namespace }} - pod: {{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "OOM Events by namespace, pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "No data is generally a good thing here.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "points", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 75, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(increase(kube_pod_container_status_restarts_total{namespace=~\"${namespace}\", cluster=\"$cluster\"}[$__rate_interval])) by (namespace, pod) > 0", + "interval": "", + "legendFormat": "namespace: {{ namespace }} - pod: {{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Container Restarts by namespace, pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_status_ready{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Ready", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_status_running{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Running", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_status_waiting{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Waiting", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_status_restarts_total{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Restarts Total", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(kube_pod_container_status_terminated{namespace=~\"$namespace\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "Terminated", + "refId": "E" + } + ], + "title": "Nb of pods by state", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "hidden", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_container_info{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}) by (pod)", + "interval": "", + "legendFormat": "{{ pod }}", + "range": true, + "refId": "A" + } + ], + "title": "Nb of containers by pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kube_deployment_status_replicas_available{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (deployment)", + "interval": "", + "legendFormat": "{{ deployment }}", + "range": true, + "refId": "A" + } + ], + "title": "Replicas available by deployment", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_deployment_status_replicas_unavailable{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}) by (deployment)", + "interval": "", + "legendFormat": "{{ deployment }}", + "range": true, + "refId": "A" + } + ], + "title": "Replicas unavailable by deployment", + "type": "timeseries" + }, + { + "datasource": { + "default": false, + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "List of pods that are not in Running or Succeeded status.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 64 + }, + "id": 83, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(kube_pod_status_phase{phase!~\"Running|Succeeded\", namespace=~\"$namespace\", cluster=\"$cluster\"}) by (pod) > 0", + "interval": "", + "legendFormat": "{{ deployment }}", + "range": true, + "refId": "A" + } + ], + "title": "Pods with unexpected status", + "type": "timeseries" + }, + { + "datasource": { + "default": false, + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 25, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 64 + }, + "id": 82, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "last" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "count(rate(container_cpu_usage_seconds_total{namespace=~\"$namespace\", image!=\"\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (image)", + "interval": "", + "legendFormat": "{{ image }}", + "range": true, + "refId": "A" + } + ], + "title": "Container Image Used", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 72 + }, + "id": 42, + "panels": [], + "title": "Kubernetes Storage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 65, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(kubelet_volume_stats_used_bytes{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (persistentvolumeclaim) / sum(kubelet_volume_stats_capacity_bytes{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (persistentvolumeclaim)", + "interval": "", + "legendFormat": "{{ persistentvolumeclaim }}", + "refId": "A" + } + ], + "title": "Persistent Volumes - Capacity and usage in %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 66, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(kubelet_volume_stats_used_bytes{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (persistentvolumeclaim)", + "interval": "", + "legendFormat": "{{ persistentvolumeclaim }} - Used", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(kubelet_volume_stats_capacity_bytes{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (persistentvolumeclaim)", + "hide": false, + "interval": "", + "legendFormat": "{{ persistentvolumeclaim }} - Capacity", + "refId": "B" + } + ], + "title": "Persistent Volumes - Capacity and usage in bytes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 27, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "1 - sum(kubelet_volume_stats_inodes_used{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (persistentvolumeclaim) / sum(kubelet_volume_stats_inodes{namespace=~\"$namespace\", cluster=\"$cluster\"}) by (persistentvolumeclaim)", + "interval": "", + "legendFormat": "{{ persistentvolumeclaim }}", + "refId": "A" + } + ], + "title": "Persistent Volumes - Inodes", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 89 + }, + "id": 76, + "panels": [], + "title": "Network", + "type": "row" + }, + { + "datasource": { + "default": false, + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 90 + }, + "id": 78, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_bytes_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Received - {{ pod }}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_bytes_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Transmitted - {{ pod }}", + "range": true, + "refId": "B" + } + ], + "title": "Network - Bandwidth by pod", + "type": "timeseries" + }, + { + "datasource": { + "default": false, + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 90 + }, + "id": 79, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_packets_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Received - {{ pod }}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_packets_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Transmitted - {{ pod }}", + "range": true, + "refId": "B" + } + ], + "title": "Network - Packets Rate by pod", + "type": "timeseries" + }, + { + "datasource": { + "default": false, + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 98 + }, + "id": 80, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_packets_dropped_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Received - {{ pod }}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_packets_dropped_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Transmitted - {{ pod }}", + "range": true, + "refId": "B" + } + ], + "title": "Network - Packets Dropped by pod", + "type": "timeseries" + }, + { + "datasource": { + "default": false, + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 98 + }, + "id": 81, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_network_receive_errors_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Received - {{ pod }}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_errors_total{namespace=~\"$namespace\", pod=~\"${created_by}.*\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "Transmitted - {{ pod }}", + "range": true, + "refId": "B" + } + ], + "title": "Network - Errors by pod", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "Kubernetes", + "Prometheus" + ], + "templating": { + "list": [ + { + "current": {}, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_pod_info{cluster=\"$cluster\"}, namespace)", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(kube_pod_info{cluster=\"$cluster\"}, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "allValue": ".*", + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_pod_info{namespace=~\"$namespace\", cluster=\"$cluster\"},created_by_name)", + "description": "Can be used to filter on a specific deployment, statefulset or deamonset (only relevant panels).", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "created_by", + "options": [], + "query": { + "query": "label_values(kube_pod_info{namespace=~\"$namespace\", cluster=\"$cluster\"},created_by_name)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes / Views / Namespaces", + "uid": "k8s_views_ns", + "version": 43, + "weekStart": "" +} diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-views-nodes.json b/ops/prometheus/grafana/dashboards/k8s/k8s-views-nodes.json new file mode 100644 index 0000000..d4e3040 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-views-nodes.json @@ -0,0 +1,4019 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.3.1" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "terraform" + ], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": [ + "oncall" + ], + "type": "tags" + } + } + ] + }, + "description": "This is a modern 'Nodes View' dashboard for your Kubernetes cluster(s). Made for kube-prometheus-stack and take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 40, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 7, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto", + "text": {} + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "avg(sum by (cpu) (rate(node_cpu_seconds_total{mode!~\"idle|iowait|steal\", instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval])))", + "instant": true, + "interval": "$resolution", + "legendFormat": "", + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 50 + }, + { + "color": "red", + "value": 70 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 4, + "y": 1 + }, + "id": 13, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto", + "text": {} + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_memory_MemTotal_bytes{instance=\"$instance\", cluster=\"$cluster\"} - node_memory_MemAvailable_bytes{instance=\"$instance\", cluster=\"$cluster\"}) / sum(node_memory_MemTotal_bytes{instance=\"$instance\", cluster=\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "", + "refId": "A" + } + ], + "title": "RAM Usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 8, + "y": 1 + }, + "id": 24, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(kube_pod_info{node=\"$node\", cluster=\"$cluster\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Pods on node", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "links": [ + { + "targetBlank": true, + "title": "Pod details", + "url": "/d/k8s_views_pods/kubernetes-views-pods?${datasource:queryparam}&var-namespace=${__data.fields.namespace}&${cluster:queryparam}&var-pod=${__data.fields.pod}&${resolution:queryparam}&${__url_time_range}" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "pod" + }, + "properties": [ + { + "id": "custom.width", + "value": 416 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "priority_class" + }, + "properties": [ + { + "id": "custom.width", + "value": 176 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "pod_ip" + }, + "properties": [ + { + "id": "custom.width", + "value": 157 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "created_by_kind" + }, + "properties": [ + { + "id": "custom.width", + "value": 205 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "namespace" + }, + "properties": [ + { + "id": "custom.width", + "value": 263 + } + ] + } + ] + }, + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "kube_pod_info{node=\"$node\", cluster=\"$cluster\"}", + "format": "table", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "List of pods on node ($node)", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true, + "__name__": true, + "container": true, + "created_by_kind": false, + "created_by_name": true, + "endpoint": true, + "env": true, + "host_ip": true, + "host_network": true, + "instance": true, + "job": true, + "node": true, + "project": true, + "prometheus_replica": true, + "service": true, + "uid": true + }, + "indexByName": { + "Time": 6, + "Value": 20, + "__name__": 7, + "container": 8, + "created_by_kind": 2, + "created_by_name": 9, + "endpoint": 10, + "env": 11, + "host_ip": 5, + "host_network": 12, + "instance": 13, + "job": 14, + "namespace": 1, + "node": 15, + "pod": 0, + "pod_ip": 3, + "priority_class": 4, + "project": 16, + "prometheus_replica": 17, + "service": 18, + "uid": 19 + }, + "renameByName": {} + } + }, + { + "id": "groupBy", + "options": { + "fields": { + "created_by_kind": { + "aggregations": [], + "operation": "groupby" + }, + "host_ip": { + "aggregations": [], + "operation": "groupby" + }, + "namespace": { + "aggregations": [ + "last" + ], + "operation": "groupby" + }, + "pod": { + "aggregations": [], + "operation": "groupby" + }, + "pod_ip": { + "aggregations": [], + "operation": "groupby" + }, + "priority_class": { + "aggregations": [], + "operation": "groupby" + } + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "decimals": 3, + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 0, + "y": 9 + }, + "id": 9, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(rate(node_cpu_seconds_total{mode!~\"idle|iowait|steal\", instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "instant": true, + "interval": "$resolution", + "legendFormat": "", + "refId": "A" + } + ], + "title": "CPU Used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 2, + "y": 9 + }, + "id": 11, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(machine_cpu_cores{node=\"$node\", cluster=\"$cluster\"})", + "interval": "$resolution", + "legendFormat": "", + "refId": "A" + } + ], + "title": "CPU Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 4, + "y": 9 + }, + "id": 15, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_memory_MemTotal_bytes{instance=\"$instance\", cluster=\"$cluster\"} - node_memory_MemAvailable_bytes{instance=\"$instance\", cluster=\"$cluster\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "RAM Used", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 6, + "y": 9 + }, + "id": 17, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "machine_memory_bytes{node=\"$node\", cluster=\"$cluster\"}", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "RAM Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 25228800 + }, + { + "color": "red", + "value": 31536000 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 4, + "x": 8, + "y": 9 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_time_seconds{instance=\"$instance\", cluster=\"$cluster\"} - node_boot_time_seconds{instance=\"$instance\", cluster=\"$cluster\"}", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "uptime", + "type": "stat" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 38, + "panels": [], + "title": "Resources", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 100, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 13 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "avg(rate(node_cpu_seconds_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]) * 100) by (mode)", + "hide": false, + "instant": false, + "interval": "$resolution", + "legendFormat": "{{ mode }}", + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_memory_MemTotal_bytes{instance=\"$instance\", cluster=\"$cluster\"} - node_memory_MemFree_bytes{instance=\"$instance\", cluster=\"$cluster\"} - (node_memory_Cached_bytes{instance=\"$instance\", cluster=\"$cluster\"} + node_memory_Buffers_bytes{instance=\"$instance\", cluster=\"$cluster\"})", + "instant": false, + "interval": "$resolution", + "legendFormat": "RAM Used", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_memory_MemTotal_bytes{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "RAM Total", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_memory_Cached_bytes{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "RAM Cache", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_memory_Buffers_bytes{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "RAM Buffer", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_memory_MemFree_bytes{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "RAM Free", + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_memory_SwapTotal_bytes{instance=\"$instance\", cluster=\"$cluster\"} - node_memory_SwapFree_bytes{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "SWAP Used", + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "node_memory_SwapTotal_bytes{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "SWAP Total", + "refId": "G" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU Cores", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 26, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(container_cpu_usage_seconds_total{node=\"$node\", image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }}", + "refId": "A" + } + ], + "title": "CPU usage by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{node=\"$node\", image!=\"\", cluster=\"$cluster\"}) by (pod)", + "interval": "$resolution", + "legendFormat": "{{ pod }}", + "refId": "A" + } + ], + "title": "Memory usage by Pod", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Number of times a CPU core has been throttled on an instance.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU CORES", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 66, + "options": { + "legend": { + "calcs": [ + "min", + "max", + "mean" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_cpu_core_throttles_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Nb of cpu core throttles", + "range": true, + "refId": "A" + } + ], + "title": "Number of CPU Core Throttled", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 44, + "panels": [], + "title": "System", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 48, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_load1{instance=\"$instance\", cluster=\"$cluster\"}", + "interval": "$resolution", + "legendFormat": "1m", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_load5{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "5m", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_load15{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "15m", + "range": true, + "refId": "C" + } + ], + "title": "System Load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 46, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(node_context_switches_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval])", + "interval": "$resolution", + "intervalFactor": 1, + "legendFormat": "Context switches", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(node_intr_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval])", + "hide": false, + "interval": "$resolution", + "legendFormat": "Interrupts", + "range": true, + "refId": "B" + } + ], + "title": "Context Switches & Interrupts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 47 + }, + "id": 49, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_filefd_maximum{instance=\"$instance\", cluster=\"$cluster\"}", + "instant": false, + "interval": "$resolution", + "legendFormat": "Maximum file descriptors", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_filefd_allocated{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "instant": false, + "interval": "$resolution", + "legendFormat": "Allocated file descriptors", + "refId": "B" + } + ], + "title": "File Descriptors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 50, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_timex_estimated_error_seconds{instance=\"$instance\", cluster=\"$cluster\"}", + "instant": false, + "interval": "$resolution", + "intervalFactor": 1, + "legendFormat": "Estimated error in seconds", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_timex_maxerror_seconds{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "intervalFactor": 1, + "legendFormat": "Maximum error in seconds", + "range": true, + "refId": "B" + } + ], + "title": "Time Sync", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 55 + }, + "id": 36, + "panels": [], + "title": "Network", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "BANDWIDTH", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_network_receive_bytes_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "In", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(node_network_transmit_bytes_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Out", + "range": true, + "refId": "B" + } + ], + "title": "Network usage (bytes/s)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 61, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(node_network_receive_errs_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "In", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(node_network_transmit_errs_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Out", + "range": true, + "refId": "B" + } + ], + "title": "Network errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 64 + }, + "id": 62, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(node_network_receive_packets_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "In", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "- sum(rate(node_network_transmit_packets_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Out", + "range": true, + "refId": "B" + } + ], + "title": "Network usage (packet/s)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 64 + }, + "id": 64, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(node_network_receive_drop_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "hide": false, + "interval": "$resolution", + "legendFormat": "In", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "- sum(rate(node_network_transmit_drop_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]))", + "hide": false, + "interval": "$resolution", + "legendFormat": "Out", + "refId": "B" + } + ], + "title": "Network total drops", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 72 + }, + "id": 60, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_netstat_Tcp_CurrEstab{instance=\"$instance\", cluster=\"$cluster\"}", + "instant": false, + "interval": "$resolution", + "legendFormat": "TCP Currently Established", + "refId": "A" + } + ], + "title": "TCP Currently Established", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NF Conntrack limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 72 + }, + "id": 63, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_nf_conntrack_entries{instance=\"$instance\", cluster=\"$cluster\"}", + "instant": false, + "interval": "$resolution", + "legendFormat": "NF Conntrack entries", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "node_nf_conntrack_entries_limit{instance=\"$instance\", cluster=\"$cluster\"}", + "hide": false, + "interval": "$resolution", + "legendFormat": "NF Conntrack limit", + "range": true, + "refId": "B" + } + ], + "title": "NF Conntrack", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 80 + }, + "id": 54, + "panels": [], + "title": "Kubernetes Storage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kubelet_volume_stats_used_bytes{node=\"$node\", cluster=\"$cluster\"}) by (persistentvolumeclaim) / sum(kubelet_volume_stats_capacity_bytes{node=\"$node\", cluster=\"$cluster\"}) by (persistentvolumeclaim)", + "interval": "$resolution", + "legendFormat": "{{ persistentvolumeclaim }}", + "range": true, + "refId": "A" + } + ], + "title": "Persistent Volumes - Usage in %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Used" + }, + "properties": [ + { + "id": "custom.width", + "value": 146 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "custom.width", + "value": 167 + } + ] + } + ] + }, + "gridPos": { + "h": 16, + "w": 12, + "x": 12, + "y": 81 + }, + "id": 34, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(kubelet_volume_stats_used_bytes{node=\"$node\", cluster=\"$cluster\"}) by (persistentvolumeclaim)", + "format": "table", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(kubelet_volume_stats_capacity_bytes{node=\"$node\", cluster=\"$cluster\"}) by (persistentvolumeclaim)", + "format": "table", + "hide": false, + "interval": "", + "legendFormat": "", + "refId": "B" + } + ], + "title": "Persistent Volumes - Usage in GB", + "transformations": [ + { + "id": "groupBy", + "options": { + "fields": { + "Value": { + "aggregations": [ + "lastNotNull" + ], + "operation": "aggregate" + }, + "Value #A": { + "aggregations": [ + "lastNotNull" + ], + "operation": "aggregate" + }, + "Value #B": { + "aggregations": [ + "lastNotNull" + ], + "operation": "aggregate" + }, + "persistentvolumeclaim": { + "aggregations": [], + "operation": "groupby" + } + } + } + }, + { + "id": "seriesToColumns", + "options": { + "byField": "persistentvolumeclaim" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "indexByName": {}, + "renameByName": { + "Value #A (lastNotNull)": "Used", + "Value #B (lastNotNull)": "Total", + "persistentvolumeclaim": "Persistent Volume Claim" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 89 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(kubelet_volume_stats_inodes_used{node=\"$node\", cluster=\"$cluster\"}) by (persistentvolumeclaim) / sum(kubelet_volume_stats_inodes{node=\"$node\", cluster=\"$cluster\"}) by (persistentvolumeclaim) * 100", + "interval": "$resolution", + "legendFormat": "{{ persistentvolumeclaim }}", + "range": true, + "refId": "A" + } + ], + "title": "Persistent Volumes - Inodes", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 97 + }, + "id": 42, + "panels": [], + "title": "Node Storage", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 98 + }, + "id": 33, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "100 - ((node_filesystem_avail_bytes{instance=\"$instance\", cluster=\"$cluster\"} * 100) / node_filesystem_size_bytes{instance=\"$instance\", cluster=\"$cluster\"})", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{ mountpoint }}", + "range": true, + "refId": "A" + } + ], + "title": "FS usage in %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 98 + }, + "id": 59, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "100 - (node_filesystem_files_free{instance=\"$instance\", cluster=\"$cluster\"} / node_filesystem_files{instance=\"$instance\", cluster=\"$cluster\"} * 100)", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{ mountpoint }}", + "range": true, + "refId": "A" + } + ], + "title": "FS inode usage in %", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 106 + }, + "id": 52, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(node_disk_read_bytes_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval])", + "interval": "$resolution", + "legendFormat": "{{device}}", + "range": true, + "refId": "A" + } + ], + "title": "Reads by disk (bytes)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 106 + }, + "id": 57, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(node_disk_written_bytes_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval])", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{device}}", + "range": true, + "refId": "A" + } + ], + "title": "Writes by disk (bytes)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "read/s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 114 + }, + "id": 51, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(node_disk_reads_completed_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval])", + "interval": "$resolution", + "legendFormat": "{{device}}", + "range": true, + "refId": "A" + } + ], + "title": "Completed reads by disk", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "write/s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 114 + }, + "id": 56, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(node_disk_writes_completed_total{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval])", + "hide": false, + "interval": "$resolution", + "legendFormat": "{{device}}", + "range": true, + "refId": "A" + } + ], + "title": "Completed writes by disk", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "io/s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 122 + }, + "id": 58, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "rate(node_disk_io_now{instance=\"$instance\", cluster=\"$cluster\"}[$__rate_interval]) ", + "interval": "$resolution", + "legendFormat": "{{device}}", + "range": true, + "refId": "A" + } + ], + "title": "Disk(s) io/s", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 122 + }, + "id": 55, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "right", + "showLegend": false + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(node_filesystem_device_error{instance=\"$instance\", cluster=\"$cluster\"}) by (mountpoint)", + "interval": "$resolution", + "legendFormat": "{{ mountpoint }}", + "range": true, + "refId": "A" + } + ], + "title": "FS - Device Errors", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "Kubernetes", + "Prometheus" + ], + "templating": { + "list": [ + { + "current": {}, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info{cluster=\"$cluster\"}, node)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "node", + "options": [], + "query": { + "query": "label_values(kube_node_info{cluster=\"$cluster\"}, node)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(node_uname_info{nodename=~\"(?i:($node)(.[a-z0-9.]+)?)\", cluster=\"$cluster\"}, instance)", + "hide": 2, + "includeAll": false, + "multi": false, + "name": "instance", + "options": [], + "query": { + "query": "label_values(node_uname_info{nodename=~\"(?i:($node)(.[a-z0-9.]+)?)\", cluster=\"$cluster\"}, instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes / Views / Nodes", + "uid": "k8s_views_nodes", + "version": 37, + "weekStart": "" +} diff --git a/ops/prometheus/grafana/dashboards/k8s/k8s-views-pods.json b/ops/prometheus/grafana/dashboards/k8s/k8s-views-pods.json new file mode 100644 index 0000000..83a1d31 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/k8s/k8s-views-pods.json @@ -0,0 +1,2717 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": [], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "8.3.4" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "5.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "#5c4ee5", + "name": "terraform", + "target": { + "limit": 100, + "matchAny": false, + "tags": ["terraform"], + "type": "tags" + } + }, + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": false, + "iconColor": "red", + "name": "oncall", + "target": { + "limit": 100, + "matchAny": false, + "tags": ["oncall"], + "type": "tags" + } + } + ] + }, + "description": "This is a modern 'Pods View' dashboard for your Kubernetes cluster(s). Made for kube-prometheus-stack and take advantage of the latest Grafana features. GitHub repository: https://github.com/dotdc/grafana-dashboards-kubernetes", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 43, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Information", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Panel only works when a single pod is selected.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["mean"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "kube_pod_info{namespace=\"$namespace\", pod=\"$pod\", cluster=\"$cluster\"}", + "instant": true, + "interval": "", + "legendFormat": "{{ created_by_kind }}: {{ created_by_name }}", + "refId": "A" + } + ], + "title": "Created by", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Panel only works when a single pod is selected.", + "fieldConfig": { + "defaults": { + "links": [ + { + "title": "", + "url": "/d/k8s_views_nodes/kubernetes-views-nodes?var-datasource=${datasource}&var-node=${__field.labels.node}&${cluster:queryparam}" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 33, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["mean"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "kube_pod_info{namespace=\"$namespace\", pod=\"$pod\", cluster=\"$cluster\"}", + "instant": true, + "interval": "", + "legendFormat": "{{ node }}", + "refId": "A" + } + ], + "title": "Running on", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Panel only works when a single pod is selected.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 41, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["mean"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "kube_pod_info{namespace=\"$namespace\", pod=\"$pod\", cluster=\"$cluster\"}", + "instant": true, + "interval": "", + "legendFormat": "{{ pod_ip }}", + "refId": "A" + } + ], + "title": "Pod IP", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Panel only works when a single pod is selected.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgb(255, 255, 255)", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 5, + "x": 0, + "y": 3 + }, + "id": 52, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": ["mean"], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "kube_pod_info{namespace=\"$namespace\", pod=\"$pod\", priority_class!=\"\", cluster=\"$cluster\"}", + "format": "time_series", + "instant": true, + "interval": "", + "legendFormat": "{{ priority_class }}", + "range": false, + "refId": "A" + } + ], + "title": "Priority Class", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Panel only works when a single pod is selected.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Burstable" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "BestEffort" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "orange", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 2, + "w": 7, + "x": 5, + "y": 3 + }, + "id": 53, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "kube_pod_status_qos_class{namespace=\"$namespace\", pod=\"$pod\", cluster=\"$cluster\"} > 0", + "instant": true, + "interval": "", + "legendFormat": "{{ qos_class }}", + "refId": "A" + } + ], + "title": "QOS Class", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Panel only works when a single pod is selected.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 6, + "x": 12, + "y": 3 + }, + "id": 56, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "name", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "kube_pod_container_status_last_terminated_reason{namespace=\"$namespace\", pod=\"$pod\", cluster=\"$cluster\"}", + "instant": true, + "interval": "", + "legendFormat": "{{ reason }}", + "refId": "A" + } + ], + "title": "Last Terminated Reason", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Panel only works when a single pod is selected.", + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + }, + { + "color": "red", + "value": 1 + }, + { + "color": "#EAB839", + "value": 2 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 6, + "x": 18, + "y": 3 + }, + "id": 57, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": true + }, + "showPercentChange": false, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "kube_pod_container_status_last_terminated_exitcode{namespace=\"$namespace\", pod=\"$pod\", cluster=\"$cluster\"}", + "instant": true, + "interval": "", + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Last Terminated Exit Code", + "type": "stat" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 47, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Resources", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "red", + "value": 75 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 3, + "x": 0, + "y": 6 + }, + "id": 39, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) / sum(kube_pod_container_resource_requests{namespace=\"$namespace\", pod=~\"$pod\", resource=\"cpu\", job=~\"$job\", cluster=\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "Requests", + "refId": "A" + } + ], + "title": "Total pod CPU Requests usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "red", + "value": 75 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 3, + "x": 3, + "y": 6 + }, + "id": 48, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) / sum(kube_pod_container_resource_limits{namespace=\"$namespace\", pod=~\"$pod\", resource=\"cpu\", job=~\"$job\", cluster=\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "Limits", + "refId": "A" + } + ], + "title": "Total pod CPU Limits usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "fixed" + }, + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "#EAB839", + "value": 80 + }, + { + "color": "red", + "value": 99 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 3, + "x": 6, + "y": 6 + }, + "id": 40, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(container_memory_working_set_bytes{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}) / sum(kube_pod_container_resource_requests{namespace=\"$namespace\", pod=~\"$pod\", resource=\"memory\", job=~\"$job\", cluster=\"$cluster\"})", + "instant": true, + "interval": "$resolution", + "legendFormat": "Requests", + "refId": "A" + } + ], + "title": "Total pod RAM Requests usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "red", + "value": 75 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 3, + "x": 9, + "y": 6 + }, + "id": 49, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": ["last"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(container_memory_working_set_bytes{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}) / sum(kube_pod_container_resource_limits{namespace=\"$namespace\", pod=~\"$pod\", resource=\"memory\", job=~\"$job\", cluster=\"$cluster\"}) ", + "instant": true, + "interval": "$resolution", + "legendFormat": "Limits", + "refId": "B" + } + ], + "title": "Total pod RAM Limits usage", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": false, + "inspect": false, + "minWidth": 100 + }, + "decimals": 4, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Memory Requests" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + }, + { + "id": "decimals", + "value": 2 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Memory Limits" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + }, + { + "id": "decimals", + "value": 2 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Memory Used" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 38, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "11.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(kube_pod_container_resource_requests{namespace=\"$namespace\", pod=~\"$pod\", resource=\"cpu\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(kube_pod_container_resource_limits{namespace=\"$namespace\", pod=~\"$pod\", resource=\"cpu\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(kube_pod_container_resource_requests{namespace=\"$namespace\", pod=~\"$pod\", resource=\"memory\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(kube_pod_container_resource_limits{namespace=\"$namespace\", pod=~\"$pod\", resource=\"memory\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", container!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (container)", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(container_memory_working_set_bytes{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", container!=\"\", cluster=\"$cluster\"}) by (container)", + "format": "table", + "hide": false, + "instant": true, + "range": false, + "refId": "F" + } + ], + "title": "Resources by container", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "container" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Time 1": true, + "Time 2": true, + "Time 4": true, + "__name__": true, + "__name__ 1": true, + "__name__ 2": true, + "__name__ 3": true, + "__name__ 4": true, + "container": false, + "endpoint": true, + "endpoint 2": true, + "endpoint 3": true, + "endpoint 4": true, + "instance": true, + "instance 2": true, + "instance 3": true, + "instance 4": true, + "job": true, + "job 2": true, + "job 3": true, + "job 4": true, + "namespace": true, + "namespace 2": true, + "namespace 3": true, + "namespace 4": true, + "node": true, + "node 2": true, + "node 3": true, + "node 4": true, + "pod": true, + "pod 2": true, + "pod 3": true, + "pod 4": true, + "resource 1": true, + "resource 2": true, + "resource 3": true, + "resource 4": true, + "service": true, + "service 2": true, + "service 3": true, + "service 4": true, + "uid 1": true, + "uid 2": true, + "uid 3": true, + "uid 4": true, + "unit 1": true, + "unit 2": true, + "unit 3": true, + "unit 4": true + }, + "indexByName": { + "Time 1": 7, + "Time 2": 8, + "Time 3": 9, + "Time 4": 10, + "Time 5": 11, + "Time 6": 12, + "Value #A": 2, + "Value #B": 3, + "Value #C": 5, + "Value #D": 6, + "Value #E": 1, + "Value #F": 4, + "container": 0 + }, + "renameByName": { + "Value #A": "CPU Requests", + "Value #B": "CPU Limits", + "Value #C": "Memory Requests", + "Value #D": "Memory Limits", + "Value #E": "CPU Used", + "Value #F": "Memory Used", + "container": "Container" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Percent", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 30 + }, + { + "color": "yellow", + "value": 70 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 50, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (container) / sum(kube_pod_container_resource_requests{namespace=\"$namespace\", pod=~\"$pod\", resource=\"cpu\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "interval": "$resolution", + "legendFormat": "{{ container }} REQUESTS", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (container) / sum(kube_pod_container_resource_limits{namespace=\"$namespace\", pod=~\"$pod\", resource=\"cpu\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "hide": false, + "legendFormat": "{{ container }} LIMITS", + "range": true, + "refId": "B" + } + ], + "title": "CPU Usage / Requests & Limits by container", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "thresholds" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Percent", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 30 + }, + { + "color": "#EAB839", + "value": 70 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}) by (container) / sum(kube_pod_container_resource_requests{namespace=\"$namespace\", pod=~\"$pod\", resource=\"memory\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "interval": "", + "legendFormat": "{{ container }} REQUESTS", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(container_memory_working_set_bytes{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", cluster=\"$cluster\"}) by (container) / sum(kube_pod_container_resource_limits{namespace=\"$namespace\", pod=~\"$pod\", resource=\"memory\", job=~\"$job\", cluster=\"$cluster\"}) by (container)", + "hide": false, + "legendFormat": "{{ container }} LIMITS", + "range": true, + "refId": "B" + } + ], + "title": "Memory Usage / Requests & Limits by container", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "CPU Cores", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 4, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 22 + }, + "id": 29, + "options": { + "legend": { + "calcs": ["min", "max", "mean"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_usage_seconds_total{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", container!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (container, id)", + "interval": "$resolution", + "legendFormat": "{{ container }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Usage by container", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Bytes", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 22 + }, + "id": 51, + "options": { + "legend": { + "calcs": ["min", "max", "mean"], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(container_memory_working_set_bytes{namespace=\"$namespace\", pod=~\"$pod\", image!=\"\", container!=\"\", cluster=\"$cluster\"}) by (container, id)", + "interval": "", + "legendFormat": "{{ container }}", + "range": true, + "refId": "A" + } + ], + "title": "Memory Usage by container", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "SECONDS", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 30 + }, + "id": 59, + "options": { + "legend": { + "calcs": ["min", "max", "mean"], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(rate(container_cpu_cfs_throttled_seconds_total{namespace=~\"$namespace\", pod=~\"$pod\", image!=\"\", container!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (container)", + "interval": "$resolution", + "legendFormat": "{{ container }}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Throttled seconds by container", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 62, + "panels": [], + "title": "Kubernetes", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "thresholds" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Percent", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 30 + }, + { + "color": "#EAB839", + "value": 70 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 60, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(increase(container_oom_events_total{namespace=\"${namespace}\", pod=\"${pod}\", container!=\"\", cluster=\"$cluster\"}[$__rate_interval])) by (container)", + "interval": "", + "legendFormat": "{{ container }}", + "range": true, + "refId": "A" + } + ], + "title": "OOM Events by container", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "blue", + "mode": "thresholds" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Percent", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "area" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 20 + }, + { + "color": "green", + "value": 30 + }, + { + "color": "#EAB839", + "value": 70 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 61, + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(increase(kube_pod_container_status_restarts_total{namespace=~\"${namespace}\", pod=\"${pod}\", container!=\"\", job=~\"$job\", cluster=\"$cluster\"}[$__rate_interval])) by (container)", + "interval": "", + "legendFormat": "{{ container }}", + "range": true, + "refId": "A" + } + ], + "title": "Container Restarts by container", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 47 + }, + "id": 45, + "panels": [], + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "refId": "A" + } + ], + "title": "Network", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 31, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(container_network_receive_bytes_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Received", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_bytes_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Transmitted", + "refId": "B" + } + ], + "title": "Network - Bandwidth", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(container_network_receive_packets_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Received", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_packets_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Transmitted", + "refId": "B" + } + ], + "title": "Network - Packets Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 36, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(container_network_receive_packets_dropped_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Received", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_packets_dropped_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Transmitted", + "refId": "B" + } + ], + "title": "Network - Packets Dropped", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 25, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 37, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.3.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(container_network_receive_errors_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Received", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "- sum(rate(container_network_transmit_errors_total{namespace=\"$namespace\", pod=~\"$pod\", cluster=\"$cluster\"}[$__rate_interval]))", + "interval": "$resolution", + "legendFormat": "Transmitted", + "refId": "B" + } + ], + "title": "Network - Errors", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": ["Kubernetes", "Prometheus"], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "", + "value": "" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "isNone": true, + "selected": false, + "text": "None", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_node_info,cluster)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_node_info,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "monitoring", + "value": "monitoring" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_pod_info{cluster=\"$cluster\"}, namespace)", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(kube_pod_info{cluster=\"$cluster\"}, namespace)", + "refId": "Prometheus-namespace-Variable-Query" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "", + "value": "" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_pod_info{namespace=\"$namespace\", cluster=\"$cluster\"}, pod)", + "hide": 0, + "includeAll": true, + "multi": true, + "name": "pod", + "options": [], + "query": { + "query": "label_values(kube_pod_info{namespace=\"$namespace\", cluster=\"$cluster\"}, pod)", + "refId": "Prometheus-pod-Variable-Query" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "30s", + "value": "30s" + }, + "hide": 0, + "includeAll": false, + "multi": false, + "name": "resolution", + "options": [ + { + "selected": false, + "text": "1s", + "value": "1s" + }, + { + "selected": false, + "text": "15s", + "value": "15s" + }, + { + "selected": true, + "text": "30s", + "value": "30s" + }, + { + "selected": false, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "3m", + "value": "3m" + }, + { + "selected": false, + "text": "5m", + "value": "5m" + } + ], + "query": "1s, 15s, 30s, 1m, 3m, 5m", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "kube-state-metrics", + "value": "kube-state-metrics" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_pod_info{namespace=\"$namespace\", cluster=\"$cluster\"},job)", + "hide": 0, + "includeAll": false, + "multi": true, + "name": "job", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_pod_info{namespace=\"$namespace\", cluster=\"$cluster\"},job)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Kubernetes / Views / Pods", + "uid": "k8s_views_pods", + "version": 36, + "weekStart": "" +} diff --git a/ops/prometheus/grafana/dashboards/redis/redis-dashboard.json b/ops/prometheus/grafana/dashboards/redis/redis-dashboard.json new file mode 100644 index 0000000..ebab195 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/redis/redis-dashboard.json @@ -0,0 +1,1543 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 2, + "links": [], + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "count(redis_up{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Redis Nodes", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "sum(redis_connected_clients{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Redis Connected Clients", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 0 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 40 + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "avg((redis_config_maxclients{env=~\"$env\"} - redis_connected_clients{env=~\"$env\"}) / redis_config_maxclients{env=~\"$env\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Redis Connection Usage", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "increase(redis_commands_processed_total{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "Redis Commands Executed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 0 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 4 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "sum(redis_cluster_slots_fail{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Redis Slots Failed", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "from": 0, + "result": { + "index": 0, + "text": "Master" + }, + "to": 100 + }, + "type": "range" + }, + { + "options": { + "match": "null", + "result": { + "index": 1, + "text": "Slave" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-orange", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 4 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": { + "valueSize": 30 + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "count(redis_instance_info{namespace=~\"$namespace\",service=~\"$service\",role=\"master\",env=~\"$env\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Redis Role", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 4 + }, + "id": 19, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "text": {}, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "avg(redis_rdb_changes_since_last_save{env=~\"$env\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Redis Last Changes", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 9, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "topk(5, increase(redis_commands_total{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"} [$__rate_interval]))", + "interval": "", + "legendFormat": "{{cmd}}", + "refId": "A" + } + ], + "title": "Redis Executed Commands", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "avg(irate(redis_commands_duration_seconds_total{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}[1m])) by (cmd)\n /\navg(irate(redis_commands_total{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}[1m])) by (cmd)", + "interval": "", + "legendFormat": "{{cmd}}", + "refId": "A" + } + ], + "title": "Redis Command Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "redis_db_keys{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}", + "interval": "", + "legendFormat": "{{db}}", + "refId": "A" + } + ], + "title": "Redis Keys", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 14, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "rate(redis_net_input_bytes_total{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "Redis Input Network", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "rate(redis_net_output_bytes_total{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}[$__rate_interval])", + "interval": "", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "Redis Output Network", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "redis_memory_used_bytes{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}", + "interval": "", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "Redis Used Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 16, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "redis_connected_clients{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}", + "interval": "", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "Redis Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "redis_master_repl_offset{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"} - redis_slave_repl_offset{namespace=~\"$namespace\",service=~\"$service\",env=~\"$env\"}", + "interval": "", + "legendFormat": "{{pod}}", + "refId": "A" + } + ], + "title": "Redis Replication Lag", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 20, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "8.0.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "sum(redis_cluster_slots_ok{env=~\"$env\"})", + "interval": "", + "legendFormat": "Ok", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "sum(redis_cluster_slots_fail{env=~\"$env\"})", + "hide": false, + "interval": "", + "legendFormat": "fail", + "refId": "B" + } + ], + "title": "Redis Cluster Slot", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "ctfd", + "value": "ctfd" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(kube_namespace_status_phase{job=\"kube-state-metrics\"},namespace)", + "hide": 0, + "includeAll": false, + "label": "Namespace", + "multi": false, + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(kube_namespace_status_phase{job=\"kube-state-metrics\"},namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "redis-standalone", + "value": "redis-standalone" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values({namespace=\"$namespace\", endpoint=\"redis-exporter\"},service)", + "hide": 0, + "includeAll": false, + "label": "Service", + "multi": false, + "name": "service", + "options": [], + "query": { + "qryType": 1, + "query": "label_values({namespace=\"$namespace\", endpoint=\"redis-exporter\"},service)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Data source", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": {}, + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Env", + "multi": false, + "name": "env", + "options": [], + "query": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Redis Operator | Cluster Dashboard", + "uid": "OsAINfZnk", + "version": 11, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/redis/redis-operator.json b/ops/prometheus/grafana/dashboards/redis/redis-operator.json new file mode 100644 index 0000000..bf51150 --- /dev/null +++ b/ops/prometheus/grafana/dashboards/redis/redis-operator.json @@ -0,0 +1,874 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 406435, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 9, + "panels": [], + "title": "Cluster", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "rediscluster_healthy{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "{{namespace}}/{{instance}}", + "refId": "A" + } + ], + "title": "Cluster Health", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "rate(rediscluster_rebalance_total{namespace=~\"$namespace\",instance=~\"$instance\"}[5m])", + "legendFormat": "Rebalance", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "rate(rediscluster_reshard_total{namespace=~\"$namespace\",instance=~\"$instance\"}[5m])", + "legendFormat": "Reshard", + "refId": "B" + } + ], + "title": "Cluster Rebalance / Reshard (per 5m)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 5, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "rate(rediscluster_adding_node_attempt{namespace=~\"$namespace\",instance=~\"$instance\"}[5m])", + "legendFormat": "Add Node", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "rate(rediscluster_remove_follower_attempt{namespace=~\"$namespace\",instance=~\"$instance\"}[5m])", + "legendFormat": "Remove Follower", + "refId": "B" + } + ], + "title": "Cluster Add / Remove Node Attempts (per 5m)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "rediscluster_skipreconcile{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "Cluster - {{namespace}}/{{instance}}", + "refId": "A" + } + ], + "title": "Skip Reconcile Cluster", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 10, + "panels": [], + "title": "Replication", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "redisreplication_has_master{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "{{namespace}}/{{instance}}", + "refId": "A" + } + ], + "title": "Replication Master Status", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "redisreplication_replicas_size_desired{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "Desired - {{namespace}}/{{instance}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "redisreplication_replicas_size_current{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "Current - {{namespace}}/{{instance}}", + "range": true, + "refId": "B" + } + ], + "title": "Replication Replica Counts (Desired vs Current)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "redisreplication_connected_slaves_total{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "{{namespace}}/{{instance}}", + "refId": "A" + } + ], + "title": "Replication Connected Slaves", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 7, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "redisreplication_replicas_size_mismatch{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "{{namespace}}/{{instance}}", + "refId": "A" + } + ], + "title": "Replication Replica Size Mismatch", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 15 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "expr": "redisreplication_skipreconcile{namespace=~\"$namespace\",instance=~\"$instance\"}", + "legendFormat": "Replication - {{namespace}}/{{instance}}", + "refId": "B" + } + ], + "title": "Skip Reconcile Cluster", + "type": "stat" + } + ], + "preload": false, + "refresh": "10s", + "schemaVersion": 40, + "tags": [ + "redis", + "operator", + "kubernetes" + ], + "templating": { + "list": [ + { + "current": { + "text": "Beholder-Mars", + "value": "cc291c92-8dba-4776-bb65-2ef00118db13" + }, + "label": "Datasource", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": "$datasource", + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": "label_values(rediscluster_healthy, namespace)", + "refresh": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "$datasource", + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": "label_values(rediscluster_healthy{namespace=~\"$namespace\"}, instance)", + "refresh": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Redis Operator Controller", + "uid": "redis-operator-controller", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/dashboards/traefik/traefik-custom.json b/ops/prometheus/grafana/dashboards/traefik/traefik-custom.json new file mode 100644 index 0000000..3b9b3cf --- /dev/null +++ b/ops/prometheus/grafana/dashboards/traefik/traefik-custom.json @@ -0,0 +1,1482 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Official dashboard for Standalone Traefik", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 17346, + "graphTooltip": 0, + "id": 12, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 9, + "panels": [], + "title": "General", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 5, + "x": 0, + "y": 1 + }, + "id": 13, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "count(traefik_config_reloads_total)", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Traefik Instances", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 7, + "x": 5, + "y": 1 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_entrypoint_requests_total{entrypoint=~\"$entrypoint\"}[1m])) by (entrypoint)", + "legendFormat": "{{entrypoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Requests per Entrypoint", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "https://medium.com/@tristan_96324/prometheus-apdex-alerting-d17a065e39d0", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "(sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"0.3\",code=\"200\",entrypoint=~\"$entrypoint\"}[5m])) by (method) + \n sum(rate(traefik_entrypoint_request_duration_seconds_bucket{le=\"1.2\",code=\"200\",entrypoint=~\"$entrypoint\"}[5m])) by (method)) / 2 / \n sum(rate(traefik_entrypoint_request_duration_seconds_count{code=\"200\",entrypoint=~\"$entrypoint\"}[5m])) by (method)\n", + "legendFormat": "{{method}}", + "range": true, + "refId": "A" + } + ], + "title": "Apdex score", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "Mean Distribution", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 5, + "x": 0, + "y": 3 + }, + "id": 14, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [ + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "multi", + "sort": "asc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[1m])) by (method, code)", + "legendFormat": "{{method}}[{{code}}]", + "range": true, + "refId": "A" + } + ], + "title": "Http Code ", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 23, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(15,\n label_replace(\n traefik_service_request_duration_seconds_sum{service=~\"$service.*\",protocol=\"http\"} / \n traefik_service_request_duration_seconds_count{service=~\"$service.*\",protocol=\"http\"},\n \"service\", \"$2\", \"exported_service\", \"(([^-]+)-[^-]+).*\")\n)\n\n", + "legendFormat": "{{method}}[{{code}}] on {{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Top slow services", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(15,\n label_replace(\n sum by (service,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$2\", \"exported_service\", \"(([^-]+)-[^-]+).*\")\n)", + "legendFormat": "[{{code}}] on {{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Most requested services", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 11, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 3, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"1.2\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)", + "legendFormat": "{{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Services failing SLO of 1200ms", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 18 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "label_replace(\n 1 - (sum by (service)\n (rate(traefik_service_request_duration_seconds_bucket{le=\"0.3\",service=~\"$service.*\"}[5m])) / sum by (service) \n (rate(traefik_service_request_duration_seconds_count{service=~\"$service.*\"}[5m]))\n ) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\"\n)", + "legendFormat": "{{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Services failing SLO of 300ms", + "type": "timeseries" + } + ], + "title": "SLO", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 16, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 0, + "y": 19 + }, + "id": 17, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"2..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "legendFormat": "{{method}}[{{code}}] on {{service}}", + "range": true, + "refId": "A" + } + ], + "title": "2xx over 5 min", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 8, + "y": 19 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code=~\"5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "legendFormat": "{{method}}[{{code}}] on {{service}}", + "range": true, + "refId": "A" + } + ], + "title": "5xx over 5 min", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "reqps" + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 16, + "y": 19 + }, + "id": 19, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(15,\n label_replace(\n sum by (service,method,code) \n (rate(traefik_service_requests_total{service=~\"$service.*\",code!~\"2..|5..\",protocol=\"http\"}[5m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "legendFormat": "{{method}}[{{code}}] on {{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Other codes over 5 min", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 20, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_requests_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "legendFormat": "{{method}} on {{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Requests Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 24, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "topk(15,\n label_replace(\n sum by (service,method) \n (rate(traefik_service_responses_bytes_total{service=~\"$service.*\",protocol=\"http\"}[1m])) > 0,\n \"service\", \"$1\", \"service\", \"([^-]+-[^-]+).*\")\n)", + "legendFormat": "{{method}} on {{service}}", + "range": true, + "refId": "A" + } + ], + "title": "Responses Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 21, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true, + "sortBy": "Max", + "sortDesc": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(traefik_open_connections{entrypoint=~\"$entrypoint\"}) by (entrypoint)\n", + "legendFormat": "{{entrypoint}}", + "range": true, + "refId": "A" + } + ], + "title": "Connections per Entrypoint", + "type": "timeseries" + } + ], + "title": "HTTP Details", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "datasource", + "multi": false, + "name": "DS_PROMETHEUS", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(traefik_open_connections, entrypoint)", + "hide": 0, + "includeAll": true, + "multi": false, + "name": "entrypoint", + "options": [], + "query": { + "query": "label_values(traefik_open_connections, entrypoint)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": { + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(traefik_service_requests_total, service)", + "hide": 0, + "includeAll": true, + "multi": false, + "name": "service", + "options": [], + "query": { + "query": "label_values(traefik_service_requests_total, service)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Traefik Dashboard", + "uid": "n5bu_kv46", + "version": 2, + "weekStart": "" +} \ No newline at end of file diff --git a/ops/prometheus/grafana/notification/.gitkeep b/ops/prometheus/grafana/notification/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ops/prometheus/kube_prometheus_custom_values.yaml b/ops/prometheus/kube_prometheus_custom_values.yaml new file mode 100644 index 0000000..ad2ad19 --- /dev/null +++ b/ops/prometheus/kube_prometheus_custom_values.yaml @@ -0,0 +1,147 @@ +grafana: + enabled: true + grafana.ini: + server: + root_url: "https://grafana.${cluster_dns_management}" + sidecar: + dashboards: + enabled: true + label: grafana_dashboard + folder: /tmp/dashboards + provider: + allowUiUpdates: true + foldersFromFilesStructure: true + + annotations: + k8s-sidecar-target-directory: "/tmp/dashboards/kubernetes" + alerts: + enabled: true + label: grafana_alert + labelValue: "1" + dashboardProviders: + dashboardproviders.yaml: + apiVersion: 1 + providers: + - name: "db" + orgId: 1 + folder: "db" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/db + - name: "redis" + orgId: 1 + folder: "redis" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/redis + - name: "k8s" + orgId: 1 + folder: "k8s" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/k8s + - name: "traefik" + orgId: 1 + folder: "traefik" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/traefik + - name: "ctf" + orgId: 1 + folder: "ctf" + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/ctf + dashboards: + db: + mysql-overview: + gnetId: 7362 + revision: 5 + datasource: Prometheus + mysql-replication: + gnetId: 7371 + revision: 1 + datasource: Prometheus + mariadb-galera: + gnetId: 13106 + revision: 3 + datasource: Prometheus + mysql-quickstart: + gnetId: 14057 + revision: 1 + datasource: Prometheus + redis: + redis-overview: + gnetId: 11145 + revision: 1 + datasource: Prometheus + k8s: + kubernetes-cluster: + gnetId: 6417 + revision: 1 + datasource: Prometheus + +prometheus: + prometheusSpec: + serviceMonitorSelector: + matchLabels: null + podMonitorSelector: + matchLabels: null + ruleSelector: + matchLabels: null + scrapeConfigSelector: + matchLabels: null + +kube-state-metrics: + enabled: true + metricLabelsAllowlist: + - pods=[ctfpilot.com/component,instanced.challenges.ctfpilot.com/deployment,instanced.challenges.ctfpilot.com/owner,challenges.ctfpilot.com/type,challenges.ctfpilot.com/name,challenges.ctfpilot.com/version,challenges.ctfpilot.com/configmap,challenges.ctfpilot.com/category,challenges.ctfpilot.com/enabled,challenges.ctfpilot.com/version] + - services=[ctfpilot.com/component,instanced.challenges.ctfpilot.com/deployment,instanced.challenges.ctfpilot.com/owner,challenges.ctfpilot.com/type,challenges.ctfpilot.com/name,challenges.ctfpilot.com/version,challenges.ctfpilot.com/configmap,challenges.ctfpilot.com/category,challenges.ctfpilot.com/enabled,challenges.ctfpilot.com/version] + - deployments=[ctfpilot.com/component,instanced.challenges.ctfpilot.com/deployment,instanced.challenges.ctfpilot.com/owner,challenges.ctfpilot.com/type,challenges.ctfpilot.com/name,challenges.ctfpilot.com/version,challenges.ctfpilot.com/configmap,challenges.ctfpilot.com/category,challenges.ctfpilot.com/enabled,challenges.ctfpilot.com/version] + - configmaps=[ctfpilot.com/component,instanced.challenges.ctfpilot.com/deployment,instanced.challenges.ctfpilot.com/owner,challenges.ctfpilot.com/type,challenges.ctfpilot.com/name,challenges.ctfpilot.com/version,challenges.ctfpilot.com/configmap,challenges.ctfpilot.com/category,challenges.ctfpilot.com/enabled,challenges.ctfpilot.com/version,page.ctfpilot.com/slug,page.ctfpilot.com/version,page.ctfpilot.com/enabled,page.ctfpilot.com/configmap] + - ingresses=[ctfpilot.com/component,instanced.challenges.ctfpilot.com/deployment,instanced.challenges.ctfpilot.com/owner,challenges.ctfpilot.com/type,challenges.ctfpilot.com/name,challenges.ctfpilot.com/version,challenges.ctfpilot.com/configmap,challenges.ctfpilot.com/category,challenges.ctfpilot.com/enabled,challenges.ctfpilot.com/version] + +alertmanager: + config: + route: + group_by: ["alertname"] + group_wait: 30s + group_interval: 5m + repeat_interval: 48h + # Default alerter + receiver: "null" + routes: + # "Muted" alerts, currently only false positive "Watchdog" resides here + - receiver: "null" + matchers: + - alertname =~ "Watchdog|InfoInhibitor" + # Nosiy alerts + - receiver: "null" + matchers: + - alertname =~ "UnusedCpu|UnusedMemory|KubeSchedulerDown|KubeControllerManagerDown|KubeProxyDown" + + # Matches anything + - receiver: "discord" + receivers: + - name: discord + discord_configs: + - webhook_url: "${discord_webhook_url}" + - name: "null" +# prometheus-node-exporter: +# hostRootFsMount: +# enabled: true +# tolerations: [] +# resources: {} +# securityContext: +# runAsUser: 0 +# privileged: true diff --git a/ops/providers.tf b/ops/providers.tf new file mode 100644 index 0000000..61e00d5 --- /dev/null +++ b/ops/providers.tf @@ -0,0 +1,81 @@ +# ---------------------- +# Terraform Configuration +# ---------------------- + +terraform { + required_version = ">= 1.9.5" + + backend "s3" {} + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.32.0" + } + + kubectl = { + source = "alekc/kubectl" + version = ">= 2.0.2" + } + + helm = { + source = "hashicorp/helm" + version = ">= 3.0.2" + } + + htpasswd = { + source = "loafoe/htpasswd" + } + + http = { + source = "hashicorp/http" + } + } +} + +# ---------------------- +# Providers +# ---------------------- + +locals { + kube_config = yamldecode(base64decode(var.kubeconfig)) +} + +provider "kubernetes" { + host = local.kube_config.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kube_config.clusters[0].cluster.certificate-authority-data) + + client_certificate = base64decode(local.kube_config.users[0].user.client-certificate-data) + client_key = base64decode(local.kube_config.users[0].user.client-key-data) +} + +provider "kubectl" { + load_config_file = false + + host = local.kube_config.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kube_config.clusters[0].cluster.certificate-authority-data) + + client_certificate = base64decode(local.kube_config.users[0].user.client-certificate-data) + client_key = base64decode(local.kube_config.users[0].user.client-key-data) +} + +provider "helm" { + kubernetes = { + host = local.kube_config.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kube_config.clusters[0].cluster.certificate-authority-data) + + client_certificate = base64decode(local.kube_config.users[0].user.client-certificate-data) + client_key = base64decode(local.kube_config.users[0].user.client-key-data) + } +} + +provider "http" { +} + +provider "htpasswd" { +} + +resource "random_password" "salt" { + length = 8 + special = true +} diff --git a/ops/redis-operator.tf b/ops/redis-operator.tf new file mode 100644 index 0000000..5ca4adb --- /dev/null +++ b/ops/redis-operator.tf @@ -0,0 +1,25 @@ +resource "kubernetes_namespace_v1" "redis" { + metadata { + name = "redis-operator" + } +} + +resource "helm_release" "redis-operator" { + name = "redis-operator" + repository = "https://ot-container-kit.github.io/helm-charts/" + namespace = kubernetes_namespace_v1.redis.metadata.0.name + create_namespace = false + + chart = "redis-operator" + version = var.redis_operator_version + + // Force use of longhorn storage class + # set = [{ + # name = "redis-operator.storageClass" + # value = "longhorn" + # }] + + depends_on = [ + kubernetes_namespace_v1.redis + ] +} diff --git a/ops/tfvars/.gitignore b/ops/tfvars/.gitignore new file mode 100644 index 0000000..8147f77 --- /dev/null +++ b/ops/tfvars/.gitignore @@ -0,0 +1 @@ +!template.tfvars diff --git a/ops/tfvars/template.tfvars b/ops/tfvars/template.tfvars new file mode 100644 index 0000000..d04ea07 --- /dev/null +++ b/ops/tfvars/template.tfvars @@ -0,0 +1,76 @@ +# ------------------------ +# Kubernetes variables +# ------------------------ +kubeconfig = "AA==" # The base64 encoded kubeconfig file (base64 -w 0 ) + +# ------------------------ +# Generic information +# ------------------------ +environment = "test" # Deployment environment name for the CTF (i.e. prod, staging, dev, test) +cluster_dns_management = "" # The domain name to use for the DNS records for the management part of the cluster +cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster +email = "" # Email to use for the ACME certificate +discord_webhook_url = "" # Discord webhook URL for sending alerts and notifications + +# ------------------------ +# Cloudflare variables +# ------------------------ +cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) +cloudflare_dns_management = "" # The top level domain (TLD) to use for the DNS records for the management part of the cluster +cloudflare_dns_platform = "" # The top level domain (TLD) to use for the DNS records for the platform part of the cluster +cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF challenges part of the cluster +cluster_dns_management = "" # The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management` + +# ---------------------- +# Filebeat configuration +# ---------------------- +filebeat_elasticsearch_host = "" # The hostname of the Elasticsearch instance for Filebeat to send logs to. Must be a https 443 endpoint. +filebeat_elasticsearch_username = "" # The username for the Elasticsearch instance +filebeat_elasticsearch_password = "" # The password for the Elasticsearch instance + +# ---------------------- +# Prometheus configuration +# ---------------------- +prometheus_storage_size = "15Gi" # The size of the persistent volume claim for Prometheus data storage. Format: (e.g., 20Gi, 100Gi) + +# ---------------------- +# Management configuration +# ---------------------- +# The following is the configuration for the management part of the cluster. + +# ArgoCD password +argocd_admin_password = "" # The password for the ArgoCD admin user +argocd_github_secret = "" # The GitHub secret for ArgoCD webhooks - Send webhook to /api/webhook with this secret as the secret header. This is used to trigger ArgoCD to sync the repositories. + +# Grafana password +grafana_admin_password = "" # The password for the Grafana admin user + +# Alert endpoints +discord_webhook_url = "" # Discord webhook URL for notifications + +# Username and password for basic auth (used for some management services) +# user: The username for the basic auth +# password: The password for the basic auth +traefik_basic_auth = { user = "", password = "" } + +# ---------------------- +# Docker images +# ---------------------- +# Values are maintained in the variables.tf file. +# You can override these values by uncommenting and setting your own images here. + +# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:latest" # The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback +# image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # The docker image for Filebeat + +# ---------------------- +# Versions +# ---------------------- +# Values are maintained in the variables.tf file. +# You can override these values by uncommenting and setting your own versions here. + +# argocd_version = "8.2.5" # The version of the ArgoCD Helm chart to deploy. More information at https://github.com/argoproj/argo-helm +# cert_manager_version = "1.17.1" # The version of the Cert-Manager Helm chart to deploy. More information at https://github.com/cert-manager/cert-manager +# descheduler_version = "1.34" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler +# mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator Helm chart to deploy. More information at https://github.com/mariadb-operator/mariadb-operator +# kube_prometheus_stack_version = "62.3.1" # The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/ +# redis_operator_version = "0.22.2" # The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator diff --git a/ops/traefik.tf b/ops/traefik.tf new file mode 100644 index 0000000..4e39a81 --- /dev/null +++ b/ops/traefik.tf @@ -0,0 +1,246 @@ +resource "kubernetes_service" "traefik_dashboard" { + metadata { + name = "traefik-dashboard" + namespace = var.traefik_namespace + labels = { + app = "traefik" + release = "traefik" + role = "dashboard" + } + } + + spec { + selector = { + "app.kubernetes.io/name" = "traefik" + } + + port { + name = "dashboard" + port = 8080 + target_port = 8080 + } + } +} + +resource "kubernetes_ingress_v1" "traefik-dashboard-ingress" { + metadata { + name = "traefik-dashboard-ingress" + namespace = var.traefik_namespace + + # Basic auth + annotations = { + "cert-manager.io/cluster-issuer" = module.cert_manager.cluster_issuer_name + "ingress.kubernetes.io/auth-realm" = "traefik" + "ingress.kubernetes.io/auth-type" = "basic" + "ingress.kubernetes.io/auth-secret" = kubernetes_secret.traefik_basic_auth.metadata.0.name + "traefik.ingress.kubernetes.io/router.middlewares" = "${var.traefik_namespace}-${kubernetes_secret.traefik_basic_auth.metadata.0.name}@kubernetescrd,errors-errors@kubernetescrd" + } + } + + spec { + default_backend { + service { + name = "traefik-dashboard" + port { + number = 8080 + } + } + } + + rule { + host = "traefik.${var.cluster_dns_management}" + http { + path { + backend { + service { + name = "traefik-dashboard" + port { + number = 8080 + } + } + } + } + } + } + + tls { + hosts = [ + "traefik.${var.cluster_dns_management}" + ] + secret_name = "traefik-dashboard-tls-cert" + } + } + + depends_on = [ + kubernetes_secret.traefik_basic_auth, + kubernetes_service.traefik_dashboard + ] +} + +resource "kubernetes_service" "traefik_metrics" { + metadata { + name = "traefik-metrics" + namespace = var.traefik_namespace + labels = { + app = "traefik" + role = "metrics" + release = "prometheus" + } + } + + spec { + selector = { + "app.kubernetes.io/name" = "traefik" + } + + port { + name = "metrics" + port = 9100 + target_port = 9100 + } + } +} + + + +resource "kubernetes_config_map_v1" "ctfd_filebeat_config" { + metadata { + name = "ctfd-filebeat-config" + namespace = var.traefik_namespace + } + + data = { + "filebeat.yml" = <<-EOF + filebeat.inputs: + - type: filestream + paths: + - /var/log/traefik/*.log + processors: + - add_fields: + target: '' + fields: + cluster_dns: "${var.cluster_dns_management}" + - decode_json_fields: + fields: ["message"] + process_array: false + max_depth: 1 + target: "traefik" + overwrite_keys: false + - drop_fields: + fields: ["ecs.version"] + + output.elasticsearch: + hosts: ["https://${var.filebeat_elasticsearch_host}:443"] + username: "${var.filebeat_elasticsearch_username}" + password: "${var.filebeat_elasticsearch_password}" + protocol: https + ssl.verification_mode: "full" + index: filebeat-${var.environment}-access + + setup: + template: + name: "filebeat-${var.environment}-access" + pattern: "filebeat-${var.environment}-access*" + overwrite: false + ilm: + enabled: true + policy_name: "filebeat" + EOF + } +} + +resource "kubernetes_manifest" "traefik-additional-config" { + manifest = { + apiVersion = "helm.cattle.io/v1" + kind = "HelmChartConfig" + metadata = { + name = "traefik" + namespace = "kube-system" + } + + # This amends the Helm chart for the traefik ingress controller which is included with k3s. + # https://github.com/traefik/traefik-helm-chart/blob/master/traefik/values.yaml + spec = { + valuesContent = <<-EOF + autoscaling: + enabled: true + minReplicas: 3 + maxReplicas: 50 + resources: + requests: + cpu: "500m" + memory: "100Mi" + limits: + cpu: "2000m" + memory: "1Gi" + tolerations: + - key: "cluster.ctfpilot.com/node" + value: "scaler" + effect: "PreferNoSchedule" + logs: + access: + enabled: true + format: json + filePath: "/var/log/traefik/access.log" + bufferingSize: 1000 + fields: + headers: + defaultmode: keep + names: + Accept: drop + Connection: drop + Authorization: redact + env: + - name: TZ + value: "Europe/Copenhagen" + deployment: + initContainers: + - name: fix-permissions + image: busybox:latest + command: ["sh", "-c", "mkdir -p /usr/share/filebeat/data"] + securityContext: + fsGroup: 1000 + volumeMounts: + - name: filebeat-data + mountPath: /usr/share/filebeat/data + additionalContainers: + - image: ${var.image_filebeat} + imagePullPolicy: Always + name: traefik-stream-accesslog + volumeMounts: + - name: logs + mountPath: /var/log/traefik + - name: ctfd-filebeat-config + mountPath: /usr/share/filebeat/filebeat.yml + subPath: filebeat.yml + - name: filebeat-data + mountPath: /usr/share/filebeat/data + resources: + requests: + cpu: "10m" + memory: "56M" + limits: + cpu: "100m" + memory: "256M" + additionalVolumes: + - name: logs + - name: ctfd-filebeat-config + configMap: + name: ctfd-filebeat-config + - name: filebeat-data + emptyDir: {} + additionalVolumeMounts: + - name: logs + mountPath: /var/log/traefik + hub: + redis: + cluster: true + endpoints: redis-cluster-leaders:6379 + EOF + } + } + + depends_on = [ + kubernetes_config_map_v1.ctfd_filebeat_config + ] +} diff --git a/ops/variables.tf b/ops/variables.tf new file mode 100644 index 0000000..f00eadc --- /dev/null +++ b/ops/variables.tf @@ -0,0 +1,169 @@ +# ------------------------ +# Variables +# ------------------------ + +variable "kubeconfig" { + type = string + description = "Base64 encoded kubeconfig file" + sensitive = true +} + +variable "environment" { + type = string + description = "Deployment environment name for the CTF (i.e. prod, staging, dev, test)" + default = "test" +} + +variable "email" { + description = "Email to use for the ACME certificate" +} + +variable "cloudflare_api_token" { + sensitive = true # Requires terraform >= 0.14 + type = string + description = "Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains)" +} + +variable "cloudflare_dns_management" { + type = string + description = "The top level domain (TLD) to use for the DNS records for the management part of the cluster" +} + +variable "cloudflare_dns_platform" { + type = string + description = "The top level domain (TLD) to use for the DNS records for the platform part of the cluster" +} + +variable "cloudflare_dns_ctf" { + type = string + description = "The top level domain (TLD) to use for the DNS records for the CTF challenges part of the cluster" +} + +variable "cluster_dns_management" { + type = string + description = "The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management`" +} + +variable "traefik_namespace" { + type = string + default = "traefik" + description = "The Kubernetes namespace where Traefik is deployed" +} + +variable "traefik_basic_auth" { + type = map(string) + default = { + "user" = "admin" + "password" = "admin" + } + sensitive = true + description = "Username and password for basic auth. Format: { user = \"username\", password = \"password\" }" +} + +variable "filebeat_elasticsearch_host" { + type = string + nullable = false + description = "The hostname of the Elasticsearch instance for Filebeat to send logs to. Must be a https 443 endpoint." +} + +variable "filebeat_elasticsearch_username" { + type = string + nullable = false + description = "The username for Elasticsearch authentication." +} + +variable "filebeat_elasticsearch_password" { + type = string + nullable = false + description = "The password for Elasticsearch authentication." +} + +variable "prometheus_storage_size" { + type = "string" + default = "15Gi" + description = "The size of the persistent volume claim for Prometheus data storage. Format: (e.g., 20Gi, 100Gi)" +} + +variable "discord_webhook_url" { + type = string + description = "Discord webhook URL for notifications" + sensitive = true +} + +variable "ghcr_username" { + description = "GitHub Container Registry username" + type = string +} + +variable "ghcr_token" { + description = "GitHub Container Registry token. This token is used to pull images from the GitHub Container Registry. Only let this token have registry read access" + type = string + sensitive = true +} + +variable "argocd_admin_password" { + sensitive = true + type = string + description = "The password for the ArgoCD admin user" +} + +variable "argocd_github_secret" { + sensitive = true + type = string + description = "The GitHub secret for ArgoCD webhooks - Send webhook to /api/webhook with this secret as the secret header. This is used to trigger ArgoCD to sync the repositories." +} + +variable "grafana_admin_password" { + sensitive = true + type = string + description = "The password for the Grafana admin user" +} + +variable "image_error_fallback" { + type = string + description = "The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback" + default = "ghcr.io/ctfpilot/error-fallback:latest" +} + +variable "image_filebeat" { + type = string + description = "The docker image for Filebeat" + default = "docker.elastic.co/beats/filebeat:8.19.0" +} + +# Variables +variable "argocd_version" { + type = string + description = "The version of ArgoCD Helm chart to deploy. More information at https://github.com/argoproj/argo-helm" + default = "8.2.5" +} + +variable "cert_manager_version" { + type = string + description = "The version of cert-manager Helm chart to deploy. More information at https://github.com/cert-manager/cert-manager" + default = "1.17.1" +} + +variable "descheduler_version" { + type = string + description = "The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler" + default = "1.34" +} + +variable "mariadb_operator_version" { + type = string + description = "The version of the MariaDB Operator Helm chart to deploy. More information at https://github.com/mariadb-operator/mariadb-operator" + default = "25.8.1" +} + +variable "kube_prometheus_stack_version" { + type = string + description = "The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/" + default = "62.3.1" +} + +variable "redis_operator_version" { + type = string + description = "The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator" + default = "0.22.2" +} \ No newline at end of file From 23248a1d37f1b427883b5acd648417be619e27da Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 21:28:32 +0100 Subject: [PATCH 006/148] refactor: add kube_hetzner_version variable and update module version reference --- cluster/kube.tf | 2 +- cluster/tfvars/template.tfvars | 8 ++++++++ cluster/variables.tf | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cluster/kube.tf b/cluster/kube.tf index 213869e..48bcab7 100644 --- a/cluster/kube.tf +++ b/cluster/kube.tf @@ -16,7 +16,7 @@ module "kube-hetzner" { source = "kube-hetzner/kube-hetzner/hcloud" # When using the terraform registry as source, you can optionally specify a version number. # See https://registry.terraform.io/modules/kube-hetzner/kube-hetzner/hcloud for the available versions - version = "2.18.2" + version = var.kube_hetzner_version # 2. For local dev, path to the git repo # source = "../../kube-hetzner/" # 3. If you want to use the latest master branch (see https://developer.hashicorp.com/terraform/language/modules/sources#github), use diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index d165c49..1bfc52b 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -75,3 +75,11 @@ challs_count = 0 # Number of challenge nodes. scale_max = 0 # Maximum number of scale nodes. Set to 0 to disable autoscaling. load_balancer_type = "lb11" # Load balancer type, see https://www.hetzner.com/cloud/load-balancer + +# ---------------------- +# Versions +# ---------------------- +# Values are maintained in the variables.tf file. +# You can override these values by uncommenting and setting your own versions here. + +# kube_hetzner_version = "2.18.2" # The version of the Kube-Hetzner module to use. More information at https://github.com/mysticaltech/terraform-hcloud-kube-hetzner diff --git a/cluster/variables.tf b/cluster/variables.tf index 808dd0c..7f18625 100644 --- a/cluster/variables.tf +++ b/cluster/variables.tf @@ -2,6 +2,12 @@ # Variables # ---------------------- +variable "kube_hetzner_version" { + type = string + description = "The version of the Kube-Hetzner module to use. More information at https://github.com/mysticaltech/terraform-hcloud-kube-hetzner" + default = "2.18.2" +} + # Hetzner variable "hcloud_token" { sensitive = true From 0ab7df631dd348de3451043b99d585b19bb324c2 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 21:42:42 +0100 Subject: [PATCH 007/148] docs: update README with additional services included in Ops --- ops/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ops/README.md b/ops/README.md index 44dfd4a..8c9fb64 100644 --- a/ops/README.md +++ b/ops/README.md @@ -13,6 +13,14 @@ Ops contians elements, that needs to be properly configured and deployed, before The following applications/services are included in the Ops: - [ArgoCD](https://argo-cd.readthedocs.io/) - GitOps continuous delivery tool, used to deploy and manage applications within the Kubernetes cluster. +- [Cert manager](https://cert-manager.io/) - Certificate management +- [Descheduler](https://github.com/kubernetes-sigs/descheduler) - Continuously rebalance the cluster +- [Error fallback](https://github.com/ctfpilot/error-fallback) - CTF Pilot's Error Fallback page +- [Filebeat](https://www.elastic.co/beats/filebeat) - Log offload to Elasticseach +- [MariaDB Operator](https://github.com/mariadb-operator/mariadb-operator) - Operator to manage MariaDB within the cluster +- [Prometheus & Grafana stack](https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack) - Prometheus and Grafana stack for monitoring +- [Redis operator](https://github.com/OT-CONTAINER-KIT/redis-operator) - Redis operator to manage Redis within the cluster +- [Traefik](https://traefik.io/traefik) - Configuration of Traefik. This project only deploys additional Helm chart configuration. ## Pre-requisites @@ -21,7 +29,7 @@ The following software needs to be installed on your local machine: - [Terraform](https://www.terraform.io/downloads.html) / [OpenTofu](https://opentofu.org) - [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) (For interacting with the Kubernetes cluster) -The following services are required, in order to deploy the Kubernetes cluster: +The following services are required, in order to deploy the services to the cluster: - A Kubernetes cluster (Deployed using the [CTF Pilot's Kubernetes Cluster on Hetzner Cloud](../cluster/README.md) guide or other means) - [Cloudflare](https://www.cloudflare.com/) account From cb8b8e676bb6d4d3ea655d299db9fe8be2f5cdc0 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 21:43:29 +0100 Subject: [PATCH 008/148] docs: update backend config reference in README for ops setup --- ops/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ops/README.md b/ops/README.md index 8c9fb64..02ecc7d 100644 --- a/ops/README.md +++ b/ops/README.md @@ -54,7 +54,7 @@ The [`tfvars/template.tfvars`](tfvars/template.tfvars) file contains further inf Run the following command to apply the ressources to the Kubernetes cluster: ```bash -tofu init -backend-config=../backend/generated/cluster.hcl +tofu init -backend-config=../backend/generated/ops.hcl tofu apply --var-file tfvars/data.tfvars ``` From 9f6aabad4a27280b500a722e6096b289b798fd55 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 21:47:15 +0100 Subject: [PATCH 009/148] docs: update README links to refer to CTFp instead of full name --- cluster/README.md | 2 +- ops/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cluster/README.md b/cluster/README.md index fc51275..ac978bd 100644 --- a/cluster/README.md +++ b/cluster/README.md @@ -4,7 +4,7 @@ > You are leaving the automated CTF Pilot setup and entering a more advanced manual setup. > This requires knowledge of Kubernetes, Terraform/OpenTofu, and cloud infrastructure management. > If you are not comfortable with these technologies, it is recommended to use the automated setup provided by CTF Pilot. -> Learn more about the automated setup in the [CTF Pilot's CTF Platform main README](../README.md). +> Learn more about the automated setup in the [CTFp main README](../README.md). This setup uses [Terraform](https://www.terraform.io/) / [OpenTofu](https://opentofu.org) to create and manage a Kubernetes cluster on [Hetzner Cloud](https://www.hetzner.com/cloud), using the [kube-hetzner](https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner). The cluster is configured to use [Cloudflare](https://www.cloudflare.com/) for DNS management. diff --git a/ops/README.md b/ops/README.md index 02ecc7d..78fd6d6 100644 --- a/ops/README.md +++ b/ops/README.md @@ -4,7 +4,7 @@ > You are leaving the automated CTF Pilot setup and entering a more advanced manual setup. > This requires knowledge of Kubernetes, Terraform/OpenTofu, and cloud infrastructure management. > If you are not comfortable with these technologies, it is recommended to use the automated setup provided by CTF Pilot. -> Learn more about the automated setup in the [CTF Pilot's CTF Platform main README](../README.md). +> Learn more about the automated setup in the [CTFp main README](../README.md). This directory contains various operational applications, services and configurations, deployed as a base on top of the Kubernetes cluster. From 83b911d82d90de217ca42ded5edcb8e777afca54 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 22:59:03 +0100 Subject: [PATCH 010/148] feat: add CTFp platform setup --- platform/.gitignore | 37 +++ platform/.terraform.lock.hcl | 136 +++++++++++ platform/README.md | 55 +++++ platform/configure-ctfd.tf | 102 ++++++++ platform/ctfd-manager.tf | 411 ++++++++++++++++++++++++++++++++ platform/ctfd.tf | 306 ++++++++++++++++++++++++ platform/data/logo.png | Bin 0 -> 10158 bytes platform/errors.tf | 25 ++ platform/metrics.tf | 168 +++++++++++++ platform/pages.tf | 68 ++++++ platform/providers.tf | 80 +++++++ platform/tfvars/.gitignore | 1 + platform/tfvars/template.tfvars | 115 +++++++++ platform/traefik-monitoring.tf | 34 +++ platform/variables.tf | 273 +++++++++++++++++++++ platform/variables_ctfd.tf | 158 ++++++++++++ 16 files changed, 1969 insertions(+) create mode 100644 platform/.gitignore create mode 100644 platform/.terraform.lock.hcl create mode 100644 platform/README.md create mode 100644 platform/configure-ctfd.tf create mode 100644 platform/ctfd-manager.tf create mode 100644 platform/ctfd.tf create mode 100644 platform/data/logo.png create mode 100644 platform/errors.tf create mode 100644 platform/metrics.tf create mode 100644 platform/pages.tf create mode 100644 platform/providers.tf create mode 100644 platform/tfvars/.gitignore create mode 100644 platform/tfvars/template.tfvars create mode 100644 platform/traefik-monitoring.tf create mode 100644 platform/variables.tf create mode 100644 platform/variables_ctfd.tf diff --git a/platform/.gitignore b/platform/.gitignore new file mode 100644 index 0000000..2faf43d --- /dev/null +++ b/platform/.gitignore @@ -0,0 +1,37 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/platform/.terraform.lock.hcl b/platform/.terraform.lock.hcl new file mode 100644 index 0000000..ee17999 --- /dev/null +++ b/platform/.terraform.lock.hcl @@ -0,0 +1,136 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/alekc/kubectl" { + version = "2.1.3" + constraints = ">= 2.0.2" + hashes = [ + "h1:AymCb0DCWzmyLqn1qEhVs2pcFUZGT/kxPK+I/BObFH8=", + "zh:0e601ae36ebc32eb8c10aff4c48c1125e471fa09f5668465af7581c9057fa22c", + "zh:1773f08a412d1a5f89bac174fe1efdfd255ecdda92d31a2e31937e4abf843a2f", + "zh:1da2db1f940c5d34e31c2384c7bd7acba68725cc1d3ba6db0fec42efe80dbfb7", + "zh:20dc810fb09031bcfea4f276e1311e8286d8d55705f55433598418b7bcc76357", + "zh:326a01c86ba90f6c6eb121bacaabb85cfa9059d6587aea935a9bbb6d3d8e3f3f", + "zh:5a3737ea1e08421fe3e700dc833c6fd2c7b8c3f32f5444e844b3fe0c2352757b", + "zh:5f490acbd0348faefea273cb358db24e684cbdcac07c71002ee26b6cfd2c54a0", + "zh:777688cda955213ba637e2ac6b1994e438a5af4d127a34ecb9bb010a8254f8a8", + "zh:7acc32371053592f55ee0bcbbc2f696a8466415dea7f4bc5a6573f03953fc926", + "zh:81f0108e2efe5ae71e651a8826b61d0ce6918811ccfdc0e5b81b2cfb0f7f57fe", + "zh:88b785ea7185720cf40679cb8fa17e57b8b07fd6322cf2d4000b835282033d81", + "zh:89d833336b5cd027e671b46f9c5bc7d10c5109e95297639bbec8001da89aa2f7", + "zh:df108339a89d4372e5b13f77bd9d53c02a04362fb5d85e1d9b6b47292e30821c", + "zh:e8a2e3a5c50ca124e6014c361d72a9940d8e815f37ae2d1e9487ac77c3043013", + ] +} + +provider "registry.opentofu.org/hashicorp/http" { + version = "3.5.0" + hashes = [ + "h1:yvwvVZ0vdbsTUMru+7Cr0On1FVgDJHAaC6TNvy/OWzM=", + "zh:0a2b33494eec6a91a183629cf217e073be063624c5d3f70870456ddb478308e9", + "zh:180f40124fa01b98b3d2f79128646b151818e09d6a1a9ca08e0b032a0b1e9cb1", + "zh:3e29e1de149dc10bf78620526c7cb8c62cd76087f5630dfaba0e93cda1f3aa7b", + "zh:4420950200cf86042ec940d0e2c9b7c89966bf556bf8038ba36217eae663bca5", + "zh:5d1f7d02109b2e2dca7ec626e5563ee765583792d0fd64081286f16f9433bd0d", + "zh:8500b138d338b1994c4206aa577b5c44e1d7260825babcf43245a7075bfa52a5", + "zh:b42165a6c4cfb22825938272d12b676e4a6946ac4e750f85df870c947685df2d", + "zh:b919bf3ee8e3b01051a0da3433b443a925e272893d3724ee8fc0f666ec7012c9", + "zh:d13b81ea6755cae785b3e11634936cdff2dc1ec009dc9610d8e3c7eb32f42e69", + "zh:f1c9d2eb1a6b618ae77ad86649679241bd8d6aacec06d0a68d86f748687f4eb3", + ] +} + +provider "registry.opentofu.org/hashicorp/kubernetes" { + version = "2.38.0" + constraints = ">= 2.32.0" + hashes = [ + "h1:nY7J9jFXcsRINog0KYagiWZw1GVYF9D2JmtIB7Wnrao=", + "zh:1096b41c4e5b2ee6c1980916fb9a8579bc1892071396f7a9432be058aabf3cbc", + "zh:2959fde9ae3d1deb5e317df0d7b02ea4977951ee6b9c4beb083c148ca8f3681c", + "zh:5082f98fcb3389c73339365f7df39fc6912bf2bd1a46d5f97778f441a67fd337", + "zh:620fd5d0fbc2d7a24ac6b420a4922e6093020358162a62fa8cbd37b2bac1d22e", + "zh:7f47c2de179bba35d759147c53082cad6c3449d19b0ec0c5a4ca8db5b06393e1", + "zh:89c3aa2a87e29febf100fd21cead34f9a4c0e6e7ae5f383b5cef815c677eb52a", + "zh:96eecc9f94938a0bc35b8a63d2c4a5f972395e44206620db06760b730d0471fc", + "zh:e15567c1095f898af173c281b66bffdc4f3068afdd9f84bb5b5b5521d9f29584", + "zh:ecc6b912629734a9a41a7cf1c4c73fb13b4b510afc9e7b2e0011d290bcd6d77f", + ] +} + +provider "registry.opentofu.org/hashicorp/local" { + version = "2.5.3" + hashes = [ + "h1:mC9+u1eaUILTjxey6Ivyf/3djm//RNNze9kBVX/trng=", + "zh:32e1d4b0595cea6cda4ca256195c162772ddff25594ab4008731a2ec7be230bf", + "zh:48c390af0c87df994ec9796f04ec2582bcac581fb81ed6bb58e0671da1c17991", + "zh:4be7289c969218a57b40902e2f359914f8d35a7f97b439140cb711aa21e494bd", + "zh:4cf958e631e99ed6c8b522c9b22e1f1b568c0bdadb01dd002ca7dffb1c927764", + "zh:7a0132c0faca4c4c96aa70808effd6817e28712bf5a39881666ac377b4250acf", + "zh:7d60de08fac427fb045e4590d1b921b6778498eee9eb16f78c64d4c577bde096", + "zh:91003bee5981e99ec3925ce2f452a5f743827f9d0e131a86613549c1464796f0", + "zh:9fe2fe75977c8149e2515fb30c6cc6cfd57b225d4ce592c570d81a3831d7ffa3", + "zh:e210e6be54933ce93e03d0994e520ba289aa01b2c1f70e77afb8f2ee796b0fe3", + "zh:e8793e5f9422f2b31a804e51806595f335b827c9a38db18766960464566f21d5", + ] +} + +provider "registry.opentofu.org/hashicorp/null" { + version = "3.2.4" + hashes = [ + "h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=", + "zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3", + "zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb", + "zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2", + "zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4", + "zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d", + "zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6", + "zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072", + "zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447", + "zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58", + "zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80", + ] +} + +provider "registry.opentofu.org/loafoe/htpasswd" { + version = "1.2.1" + hashes = [ + "h1:W1euQGM6t+QlB6Rq4fDbRKRHmeCIyYdIYdHrxL97BeE=", + "zh:14460c85ddc40a9ecadf583c22a7de91b83798a8ca4843949d50c3288c6f5bdd", + "zh:1af9416e28dd0a77c5d2c685561c4f60e19e2d606df0477ebc18eaa110c77807", + "zh:2245325864faaf027701ab12a04d641359a0dc439dd23c6e8f768407b78a5c18", + "zh:3813ff98198405d7c467565b52c7f0ad4533f43957da6390477dc898f8ed02c2", + "zh:3c0658e132232a181223f7ff65678d99cd2e8431c317f72281b67464e5e16892", + "zh:43505c0f42bc7635ec7c1fe5043c502f9b00ae4b5e74b81464bc494936643fc1", + "zh:52efdabb0abba99a33fd3ed981610f13c99bb383f94e997f90d95441d8558177", + "zh:75b5d9b4a610dfd0ff4dfb4039f61e79a0e56338e0a4cd45e0bc0edec34dfa62", + "zh:7aee5df091672d29f29dda57382a41d771fa21740cef6bb9a1b15afc6d84ffa4", + "zh:7ff618706e2953a21a22c7555e11f5cbe8e95c171704fcfdc6beedb0c25e49c0", + "zh:94e8a15c83a1a5a60ff1b58938dd9692d800fe05c5d8269e0916b5de03d89d3a", + "zh:c1ace4f322f9ec4956e4f30086da5b6a73f4d05e1266047d629b14a485c5a76d", + "zh:d4570075de49e3ee98494f7c44eab12e964c9776029ed536fd9352c3203cc635", + "zh:d99403b843de5939ea2e54b3ca46fd901d5c5b7fe34f44b8aeb8b38f4f792df6", + ] +} + +provider "registry.opentofu.org/mastercard/restapi" { + version = "2.0.1" + constraints = "2.0.1" + hashes = [ + "h1:B9x7Fql5sPqIHYSjEvQRXGOcOIUhvjV6RHKfPBUvSK8=", + "zh:09438372b8569003dabaf2fc3a98591bb9ec2505a599a37383e908432be8bed7", + "zh:0f6008de6fdbc92ee2408a34c485bf4de4bf8f46b80f9c54947c9ab89a195704", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:2171088aca38b049705bf7052c1cc0a370dddbe1850f2efee88304b819e8966f", + "zh:2a249e06ccbd13c652676f200de6dc9347d6319fd888476e6a807e11bad8c8bd", + "zh:2a306c68bca64dd63e7269de0d4131dd8de5f5f34f9958c0cf10a937ceb89757", + "zh:36c35b155157cffe590d8acd02d6540c2171f02995d7aa7c9802d5a57973ac2c", + "zh:401d28cad51efdf1b8e1b8fdbb91b0e905eea5dfc4a96baf0e270dcd84cf7a03", + "zh:6db051e5ff4b947bdd1428f555d50b7b5157e47bc72a489f8e7b60c31cb233ef", + "zh:791cac45de5b056babcc78c8ec1996666be5fbaabd770cf619ddc7679533c003", + "zh:a0ab80133a55ec19369841d82285c6603c7b140acfd5298eb3e535444c971055", + "zh:bd72f18bcf74fcfce132dc45e4cb372bbdf7a4459cc55c29aa51b5511c8985ea", + "zh:c6b96d5b075cbbd62274a69f625f0371f3c93604b8358d18be66c4b4063bef1b", + "zh:d275ba2d17d3cac3f4b55829fffe25257f89449459c44b058a58d4521f2a481e", + "zh:f38998efd8e051e433e5aee941e835418e24bd2dc02c85be9cd7cee8455f9b9d", + ] +} diff --git a/platform/README.md b/platform/README.md new file mode 100644 index 0000000..2f9b57d --- /dev/null +++ b/platform/README.md @@ -0,0 +1,55 @@ +# CTF Pilot's Kubernetes Platform + +> [!IMPORTANT] +> You are leaving the automated CTF Pilot setup and entering a more advanced manual setup. +> This requires knowledge of Kubernetes, Terraform/OpenTofu, and cloud infrastructure management. +> If you are not comfortable with these technologies, it is recommended to use the automated setup provided by CTF Pilot. +> Learn more about the automated setup in the [CTFp main README](../README.md). + +This directory contains deployment configuration for the scoreboard and related services, such as [ctfd](https://github.com/ctfpilot/ctfd) and [ctfd-manager](https://github.com/ctfpilot/ctfd-manager) + +## Pre-requisites + +The following software needs to be installed on your local machine: + +- [Terraform](https://www.terraform.io/downloads.html) / [OpenTofu](https://opentofu.org) +- [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) (For interacting with the Kubernetes cluster) + +The following services are required, in order to deploy the services to the cluster: + +- A Kubernetes cluster (Deployed using the [CTF Pilot's Kubernetes Cluster on Hetzner Cloud](../cluster/README.md) guide or other means) +- Correctly deployed [ArgoCD](https://argo-cd.readthedocs.io/) within the Kubernetes cluster. + +> [!NOTE] +> The platform has only been tested within the CTFp system. +> We recommend at least having [the Ops](../ops/README.md) deployed, as this project relies on those configurations. + +## Setup + +Copy the `tfvars/template.tfvars` file to `tfvars/data.tfvars` and edit the file with your own values. +The [`tfvars/template.tfvars`](tfvars/template.tfvars) file contains further information on each variable. + +> [!IMPORTANT] +> Make sure you generate the backend configuration file before creating the cluster. +> See the [backend generation instructions](../backend/README.md) for more information. +> +> You will also need to set the following environment variables for authentication to the S3 backend: +> - `AWS_ACCESS_KEY_ID` +> - `AWS_SECRET_ACCESS_KEY` +> +> See [OpenTofub backend S3 configuration](https://opentofu.org/docs/language/settings/backends/s3/) for more information. + +Run the following command to apply the ressources to the Kubernetes cluster: + +```bash +tofu init -backend-config=../backend/generated/platform.hcl +tofu apply --var-file tfvars/data.tfvars +``` + +### Destroying the platform + +To destroy the deployed platform, run the following command: + +```bash +tofu destroy --var-file tfvars/data.tfvars +``` diff --git a/platform/configure-ctfd.tf b/platform/configure-ctfd.tf new file mode 100644 index 0000000..1dc5b1c --- /dev/null +++ b/platform/configure-ctfd.tf @@ -0,0 +1,102 @@ +# Wait for the CTFd URL to be reachable +resource "null_resource" "wait_for_url" { + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = < 0 ? var.ctf_team_size : null + brackets = length(var.ctf_brackets) > 0 ? var.ctf_brackets : null + mail_server = var.ctf_mail_server + mail_port = var.ctf_mail_port + mail_username = var.ctf_mail_username + mail_password = var.ctf_mail_password + mail_tls = var.ctf_mail_tls + mail_from = var.ctf_mail_from + registration_code = var.ctf_registration_code + ctf_logo = { + name = "logo.png" + data = base64encode(filebase64("${path.module}/${var.ctf_logo_path}")) + } + } +} + +# Write payload to local file +resource "local_file" "ctfd_config" { + content = jsonencode(local.configure_ctfd_payload) + filename = "${path.module}/ctfd_config.json" + + depends_on = [ + null_resource.wait_for_url, + module.ctfd-ingress, + module.ctfd-manager-ingress, + ] +} + +resource "null_resource" "configure-ctfd" { + depends_on = [ + null_resource.wait_for_url, + module.ctfd-ingress, + module.ctfd-manager-ingress, + local_file.ctfd_config, + ] + + provisioner "local-exec" { + command = <RHhQ;>%@eWbV-@NWiv!;Sz1QLsn}=L-WnTT# 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 literal 0 HcmV?d00001 diff --git a/platform/errors.tf b/platform/errors.tf new file mode 100644 index 0000000..c3cf2ae --- /dev/null +++ b/platform/errors.tf @@ -0,0 +1,25 @@ +resource "kubernetes_manifest" "ctfd-errors-middleware" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "ctfd" + namespace = "errors" + } + spec = { + errors = { + status = [ + "500", + "502", + "503", + "504" + ] + query = "/{status}.html" + service = { + name = "errors" + port = 80 + } + } + } + } +} diff --git a/platform/metrics.tf b/platform/metrics.tf new file mode 100644 index 0000000..bb1bca5 --- /dev/null +++ b/platform/metrics.tf @@ -0,0 +1,168 @@ +resource "kubernetes_secret_v1" "ctfd_exporter" { + metadata { + name = "ctfd-exporter" + namespace = kubernetes_namespace_v1.challenge-config.metadata.0.name + + labels = { + app = "ctfd-exporter" + } + } + + data = { + CTFD_URL = "http://ctfd.${kubernetes_namespace_v1.ctfd.metadata[0].name}.svc.cluster.local" + POLL_RATE = "30" + } + + depends_on = [ + kubernetes_namespace_v1.challenge-config, + null_resource.configure-ctfd, + ] +} + +resource "kubernetes_deployment_v1" "ctfd_exporter" { + metadata { + name = "ctfd-exporter" + namespace = kubernetes_namespace_v1.challenge-config.metadata.0.name # Placed in challenge-config namespace as it has access to CTFd access token + labels = { + app = "ctfd-exporter" + } + } + + spec { + replicas = 1 + + selector { + match_labels = { + app = "ctfd-exporter" + } + } + + template { + metadata { + labels = { + app = "ctfd-exporter" + } + } + + spec { + container { + name = "ctfd-exporter" + image = var.image_ctfd_exporter + + env { + name = "CTFD_API" + value_from { + config_map_key_ref { + name = kubernetes_config_map_v1.ctfd-access-token.metadata.0.name + key = "access_token" + } + } + } + + env { + name = "CTFD_URL" + value_from { + secret_key_ref { + name = kubernetes_secret_v1.ctfd_exporter.metadata.0.name + key = "CTFD_URL" + } + } + } + + env { + name = "POLL_RATE" + value_from { + secret_key_ref { + name = kubernetes_secret_v1.ctfd_exporter.metadata.0.name + key = "POLL_RATE" + } + } + } + + port { + container_port = 2112 + } + + resources { + limits = { + cpu = "250m" + memory = "256Mi" + } + requests = { + cpu = "10m" + memory = "128Mi" + } + } + } + } + } + } + + depends_on = [ + kubernetes_namespace_v1.challenge-config, + null_resource.configure-ctfd, + kubernetes_secret_v1.ctfd_exporter + ] +} + +resource "kubernetes_service_v1" "ctfd_exporter" { + metadata { + name = "ctfd-exporter" + namespace = kubernetes_namespace_v1.challenge-config.metadata.0.name + labels = { + app = "ctfd-exporter" + role = "metrics" + } + } + + spec { + selector = { + app = "ctfd-exporter" + } + + port { + name = "http" + port = 2112 + target_port = 2112 + } + } + + depends_on = [ + kubernetes_namespace_v1.challenge-config, + kubernetes_deployment_v1.ctfd_exporter, + kubernetes_secret_v1.ctfd_exporter + ] +} + +resource "kubernetes_manifest" "ctfd_exporter_service_monitor" { + manifest = { + apiVersion = "monitoring.coreos.com/v1" + kind = "ServiceMonitor" + metadata = { + name = "ctfd-exporter" + namespace = kubernetes_namespace_v1.challenge-config.metadata.0.name + } + spec = { + selector = { + matchLabels = { + app = "ctfd-exporter" + role = "metrics" + } + } + namespaceSelector = { + matchNames = [kubernetes_namespace_v1.challenge-config.metadata.0.name] + } + endpoints = [{ + port = "http" + interval = "30s" + }] + } + } + + depends_on = [ + null_resource.configure-ctfd, + kubernetes_deployment_v1.ctfd_exporter, + kubernetes_service_v1.ctfd_exporter, + kubernetes_secret_v1.ctfd_exporter + ] +} diff --git a/platform/pages.tf b/platform/pages.tf new file mode 100644 index 0000000..8b7dfa4 --- /dev/null +++ b/platform/pages.tf @@ -0,0 +1,68 @@ +locals { + pages = var.pages + page_project = "ctfd-pages" + pages_repo = var.pages_repository + pages_branch = var.pages_branch == "" ? local.env_branch : var.pages_branch +} + +module "argocd_project_pages" { + source = "../tf-modules/argocd/project" + + argocd_namespace = var.argocd_namespace + project_name = local.page_project + project_destinations = [ + { + namespace = kubernetes_namespace_v1.challenge-config.metadata[0].name + server = "*" + } + ] + + depends_on = [ + kubernetes_namespace_v1.challenge-config, + null_resource.configure-ctfd + ] +} + +module "repo_access_config" { + source = "../tf-modules/private-repo" + + name = local.page_project + argocd_namespace = var.argocd_namespace + ghcr_username = var.ghcr_username + git_token = var.git_token + git_repo = local.pages_repo + argocd_project = local.page_project + + depends_on = [ + kubernetes_namespace_v1.challenge-config, + module.argocd_project_pages, + null_resource.configure-ctfd + ] +} + +module "argocd-challenge-config" { + for_each = toset(local.pages) + + source = "../tf-modules/argocd/application" + + argocd_namespace = var.argocd_namespace + application_namespace = kubernetes_namespace_v1.challenge-config.metadata[0].name + application_name = "ctfd-page-${each.value}" + application_repo_url = local.pages_repo + application_repo_path = "pages/${each.value}/k8s" + application_repo_revision = local.pages_branch + application_project = local.page_project + + argocd_labels = { + "part-of" = "ctfpilot" + "component" = "ctfd-pages" + # "version" = local.branch + } + + depends_on = [ + kubernetes_namespace_v1.challenge-config, + module.argocd_project_pages, + module.repo_access_config, + null_resource.configure-ctfd + ] +} diff --git a/platform/providers.tf b/platform/providers.tf new file mode 100644 index 0000000..81ddcc1 --- /dev/null +++ b/platform/providers.tf @@ -0,0 +1,80 @@ +# ---------------------- +# Terraform Configuration +# ---------------------- + +terraform { + required_version = ">= 1.9.5" + + backend "s3" {} + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.32.0" + } + + kubectl = { + source = "alekc/kubectl" + version = ">= 2.0.2" + } + + htpasswd = { + source = "loafoe/htpasswd" + } + + http = { + source = "hashicorp/http" + } + + restapi = { + source = "Mastercard/restapi" + version = "2.0.1" + } + } +} + +# ---------------------- +# Providers +# ---------------------- + +# Configure the REST API provider for CTFd Manager +provider "restapi" { + uri = "https://themikkel.dk" + + headers = { + "Authorization" = "Bearer ${var.ctfd_manager_password}" + "Content-Type" = "application/json" + } + + write_returns_object = true + + create_method = "GET" + update_method = null + destroy_method = null +} + +locals { + kube_config = yamldecode(base64decode(var.kubeconfig)) +} + +provider "kubernetes" { + host = local.kube_config.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kube_config.clusters[0].cluster.certificate-authority-data) + + client_certificate = base64decode(local.kube_config.users[0].user.client-certificate-data) + client_key = base64decode(local.kube_config.users[0].user.client-key-data) +} + +provider "kubectl" { + load_config_file = false + + host = local.kube_config.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kube_config.clusters[0].cluster.certificate-authority-data) + + client_certificate = base64decode(local.kube_config.users[0].user.client-certificate-data) + client_key = base64decode(local.kube_config.users[0].user.client-key-data) +} + +locals { + env_branch = var.environment == "prod" ? "main" : "develop" +} \ No newline at end of file diff --git a/platform/tfvars/.gitignore b/platform/tfvars/.gitignore new file mode 100644 index 0000000..8147f77 --- /dev/null +++ b/platform/tfvars/.gitignore @@ -0,0 +1 @@ +!template.tfvars diff --git a/platform/tfvars/template.tfvars b/platform/tfvars/template.tfvars new file mode 100644 index 0000000..4212828 --- /dev/null +++ b/platform/tfvars/template.tfvars @@ -0,0 +1,115 @@ +# ------------------------ +# Kubernetes variables +# ------------------------ +kubeconfig = "AA==" # Base64 encoded kubeconfig file + +# ------------------------ +# Generic information +# ------------------------ +environment = "test" # Environment name for the CTF +cluster_dns_management = "" # The specific domain name to use for the DNS records for the management part of the cluster +cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster + +# ------------------------ +# GitHub variables +# ------------------------ +ghcr_username = "" # GitHub Container Registry username +ghcr_token = "" # GitHub Container Registry token. This token is used to pull images from the GitHub Container Registry. Only let this token have registry read access +git_token = "" # GitHub repo token. Only let this token have read access to the needed repositories. + +# ---------------------- +# Filebeat configuration +# ---------------------- +filebeat_elasticsearch_host = "" # The hostname of the Elasticsearch instance for Filebeat to send logs to. Must be a https 443 endpoint. +filebeat_elasticsearch_username = "" # The username for Elasticsearch authentication. +filebeat_elasticsearch_password = "" # The password for Elasticsearch authentication. + +# ---------------------- +# CTF configuration +# ---------------------- +kubectf_auth_secret = "" # The secret to use for the authSecret in the CTF configuration + + +# ------------------------ +# DB configuration +# ------------------------ +db_root_password = "" # Root password for the MariaDB cluster +db_user = "ctfd" # Database user +db_password = "password" # Database password + +# S3 backup +s3_bucket = "" # S3 bucket name for backups +s3_region = "" # S3 region for backups +s3_endpoint = "" # S3 endpoint for backups +s3_access_key = "" # Access key for S3 for backups +s3_secret_key = "" # Secret key for S3 for backups + +# ------------------------ +# CTFd Manager configuration +# ------------------------ +ctfd_manager_password = "" # Password for the CTFd Manager + +# ------------------------ +# CTFd configuration +# ------------------------ +ctf_name = "" # Name of the CTF event +ctf_description = "" # Description of the CTF event +ctf_user_mode = "" # User mode for CTFd (e.g., "teams") +ctf_challenge_visibility = "" # Challenge visibility (e.g., "public") +ctf_account_visibility = "" # Account visibility (e.g., "private") +ctf_score_visibility = "" # Score visibility (e.g., "public") +ctf_registration_visibility = "" # Registration visibility (e.g., "public") +ctf_verify_emails = true # Whether to verify emails +ctf_team_size = 0 # Team size for the CTF. 0 means no limit +ctf_brackets = [] # List of brackets, optional +ctf_theme = "" # Theme for CTFd +ctf_admin_name = "" # Name of the admin user +ctf_admin_email = "" # Email of the admin user +ctf_admin_password = "" # Password for the admin user +ctf_registration_code = "" # Registration code for the CTF + +ctf_mail_server = "" # Mail server for CTFd +ctf_mail_port = 465 # Mail server port +ctf_mail_username = "" # Mail server username +ctf_mail_password = "" # Mail server password +ctf_mail_tls = true # Whether to use TLS for the mail server +ctf_mail_from = "" # From address for the mail server + +ctf_logo_path = "data/logo.png" # Path to the CTF logo file (e.g., "ctf-logo.png") + +ctfd_secret_key = "" # Secret key for CTFd + +# CTFd S3 Configuration +ctf_s3_bucket = "" # S3 bucket name for CTFd files +ctf_s3_region = "" # S3 region for CTFd files +ctf_s3_endpoint = "" # S3 endpoint for CTFd files +ctf_s3_access_key = "" # Access key for S3 for CTFd files +ctf_s3_secret_key = "" # Secret key for S3 for CTFd files +ctf_s3_prefix = "ctfd/" # S3 prefix for CTFd files, e.g., 'ctfd/dev/' + +# CTFd Plugin Configuration +ctfd_plugin_first_blood_limit_url = "" # Webhook URL for the First Blood plugin +ctfd_plugin_first_blood_limit = "1" # Limit configuration for the First Blood plugin +ctfd_plugin_first_blood_message = ":drop_of_blood: First blood for **{challenge}** goes to **{user}**! :drop_of_blood:" # Message configuration for the First Blood plugin + +# Pages Configuration +pages = [] # List of pages to deploy to CTFd +pages_repository = "" # Repository URL for pages +pages_branch = "" # Git branch for pages. Leave empty for environment based branch (environment == prod ? main : develop) + +# CTFd Deployment Configuration +ctfd_k8s_deployment_repository = "https://github.com/ctfpilot/ctfd" # Repository URL for CTFd deployment files +ctfd_k8s_deployment_path = "k8s" # Path for CTFd deployment files within the git repository +ctfd_k8s_deployment_branch = "" # Git branch for CTFd deployment files. Leave empty for environment based branch (environment == prod ? main : develop) + +# ---------------------- +# Docker images +# ---------------------- +# Values are maintained in the variables.tf file. +# You can override these values by uncommenting and setting your own images here. + +# image_ctfd_manager = "ghcr.io/ctfpilot/ctfd-manager:1.0.1" # Docker image for the CTFd Manager deployment +# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:latest" # Docker image for the error fallback deployment +# image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # Docker image for Filebeat +# image_ctfd_exporter = "ghcr.io/the0mikkel/ctfd-exporter:1.1.1" # Docker image for the CTFd Exporter + diff --git a/platform/traefik-monitoring.tf b/platform/traefik-monitoring.tf new file mode 100644 index 0000000..70abd6c --- /dev/null +++ b/platform/traefik-monitoring.tf @@ -0,0 +1,34 @@ +resource "kubernetes_manifest" "traefik_servicemonitor" { + manifest = { + apiVersion = "monitoring.coreos.com/v1" + kind = "ServiceMonitor" + metadata = { + name = "traefik" + namespace = "traefik" + } + spec = { + selector = { + matchLabels = { + app = "traefik" + role = "metrics" + } + } + namespaceSelector = { + matchNames = ["traefik"] + } + endpoints = [ + { + port = "metrics" + interval = "30s" + } + ] + } + } +} + +module "traefik-redis" { + source = "../tf-modules/redis" + + namespace = "traefik" + redis_password = "" +} diff --git a/platform/variables.tf b/platform/variables.tf new file mode 100644 index 0000000..49bc1a9 --- /dev/null +++ b/platform/variables.tf @@ -0,0 +1,273 @@ +# ------------------------ +# Variables +# ------------------------ + +variable "kubeconfig" { + type = string + description = "Base64 encoded kubeconfig file" + sensitive = true + nullable = false +} + +variable "environment" { + type = string + description = "Environment name for the CTF" + default = "test" + nullable = false +} + +variable "cluster_dns_management" { + type = string + description = "The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management`" + nullable = false +} + +variable "cluster_dns_platform" { + type = string + description = "The domain name to use for the DNS records for the platform part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_platform`" + nullable = false +} + +variable "ghcr_username" { + description = "GitHub Container Registry username" + type = string + nullable = false +} + +variable "ghcr_token" { + description = "GitHub Container Registry token. This token is used to pull images from the GitHub Container Registry. Only let this token have registry read access" + type = string + sensitive = true + nullable = false +} + +variable "git_token" { + description = "GitHub repo token. Only let this token have read access to the needed repositories." + type = string + sensitive = true + nullable = false +} + +variable "fluentd_elasticsearch_host" { + type = string + nullable = false + description = "The hostname of the Elasticsearch instance for Fluentd to send logs to. Must be a https 443 endpoint." +} + +variable "fluentd_elasticsearch_username" { + type = string + nullable = false + description = "The username for Elasticsearch authentication." +} + +variable "fluentd_elasticsearch_password" { + type = string + nullable = false + description = "The password for Elasticsearch authentication." +} + +variable "argocd_namespace" { + description = "Namespace for ArgoCD" + type = string + default = "argocd" +} + +variable "kubectf_auth_secret" { + type = string + nullable = false + description = "The secret to use for the authSecret in the CTF configuration" + sensitive = true +} + +variable "db_root_password" { + type = string + description = "Root password for the MariaDB cluster" + sensitive = true + nullable = false +} + +variable "db_user" { + type = string + description = "Database user" + default = "ctfd" + nullable = false +} + +variable "db_password" { + type = string + description = "Database password" + sensitive = true + default = "password" + nullable = false +} + +variable "ctfd_secret_key" { + type = string + description = "Secret key for CTFd" + sensitive = true + nullable = false +} + +variable "ctfd_manager_password" { + type = string + description = "Password for the CTFd manager" + sensitive = true + default = "password" + nullable = false +} + +variable "s3_bucket" { + description = "S3 bucket name for backups" + type = string + nullable = false +} + +variable "s3_region" { + description = "S3 region for backups" + type = string + nullable = false +} + +variable "s3_endpoint" { + description = "S3 endpoint for backups" + type = string + nullable = false +} + +variable "s3_access_key" { + description = "Access key for S3 for backups" + type = string + nullable = false +} + +variable "s3_secret_key" { + description = "Secret key for S3 for backups" + type = string + nullable = false +} + +variable "ctf_s3_bucket" { + description = "S3 bucket name for CTFd files" + type = string + nullable = false +} + +variable "ctf_s3_region" { + description = "S3 region for CTFd files" + type = string + nullable = false +} + +variable "ctf_s3_endpoint" { + description = "S3 endpoint for CTFd files" + type = string +} + +variable "ctf_s3_access_key" { + description = "Access key for S3 for CTFd files" + type = string + nullable = false +} + +variable "ctf_s3_secret_key" { + description = "Secret key for S3 for CTFd files" + type = string + nullable = false +} + +variable "ctf_s3_prefix" { + description = "S3 prefix for CTFd files, e.g., 'ctfd/dev/'" + type = string + default = "ctfd/" + nullable = false +} + +variable "ctfd_k8s_deployment_repository" { + type = string + description = "Repository URL for CTFd deployment files. Example: https://github.com/ctfpilot/ctfd" + default = "https://github.com/ctfpilot/ctfd" +} + +variable "ctfd_k8s_deployment_path" { + type = string + description = "Path for CTFd deployment files within the git repository (i.e `k8s`)" + default = "k8s" +} + +variable "ctfd_k8s_deployment_branch" { + type = string + description = "Git branch for CTFd deployment files. Leave empty for environment based branch (environment == prod ? main : develop)" + default = "" +} + +variable "ctfd_plugin_first_blood_limit_url" { + description = "CTFd Plugin configuration: First blood (ctfd-discord-webhook-plugin). Webhook url configuration (url)." + type = string + nullable = false +} + +variable "ctfd_plugin_first_blood_limit" { + type = string + description = "CTFd Plugin configuration: First blood (ctfd-discord-webhook-plugin). Limit configuration (limit)." + default = "1" +} + +variable "ctfd_plugin_first_blood_message" { + type = string + description = "CTFd Plugin configuration: First blood (ctfd-discord-webhook-plugin). Message configuration (message)." + default = ":drop_of_blood: First blood for **{challenge}** goes to **{user}**! :drop_of_blood:" +} + +variable "ctfd_manager_github_repo" { + type = string + description = "Github repository used in the CTFd Manager. Env variable GITHUB_REPO. See https://github.com/ctfpilot/ctfd-manager" + nullable = false +} + +variable "ctfd_manager_github_branch" { + type = string + description = "Github branch used in the CTFd Manager. Leave empty for environment based branch (environment == prod ? main : develop). Env variable GITHUB_BRANCH. See https://github.com/ctfpilot/ctfd-manager" + default = "" +} + +variable "pages" { + type = list(string) + description = "List of pages to deploy to CTFd. Needs to be the slugs available in the `pages` directory in the `pages_repository`" + default = [] +} + +variable "pages_repository" { + type = string + description = "Repository URL for pages, generated using the challenge-toolkit. See https://github.com/ctfpilot/challenge-toolkit" + nullable = false +} + +variable "pages_branch" { + type = string + description = "Git branch for pages. Leave empty for environment based branch (environment == prod ? main : develop)" + default = "" +} + +variable "image_ctfd_manager" { + type = string + description = "The docker image for the ctfd-manager deployment. See https://github.com/ctfpilot/ctfd-manager" + default = "ghcr.io/ctfpilot/ctfd-manager:1.0.1" +} + +variable "image_error_fallback" { + type = string + description = "The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback" + default = "ghcr.io/ctfpilot/error-fallback:latest" +} + +variable "image_filebeat" { + type = string + description = "The docker image for Filebeat" + default = "docker.elastic.co/beats/filebeat:8.19.0" +} + +variable "image_ctfd_exporter" { + type = string + description = "The docker image for CTFd Exporter" + default = "ghcr.io/the0mikkel/ctfd-exporter:1.1.1" +} diff --git a/platform/variables_ctfd.tf b/platform/variables_ctfd.tf new file mode 100644 index 0000000..cdbe65f --- /dev/null +++ b/platform/variables_ctfd.tf @@ -0,0 +1,158 @@ +variable "ctf_name" { + description = "Name of the CTF event" + type = string +} + +variable "ctf_description" { + description = "Description of the CTF event" + type = string +} + +variable "ctf_start_time" { + description = "Start time of the CTF event in ISO 8601 format (e.g., '2023-10-01T00:00:00Z')" + type = string + default = "" +} + +variable "ctf_end_time" { + description = "End time of the CTF event in ISO 8601 format (e.g., '2023-10-31T23:59:59Z')" + type = string + default = "" +} + +variable "ctf_user_mode" { + description = "User mode for CTFd (e.g., 'teams')" + type = string + validation { + condition = contains(["teams", "users"], var.ctf_user_mode) + error_message = "ctf_user_mode must be either 'teams' or 'users'." + } +} + +variable "ctf_challenge_visibility" { + description = "Challenge visibility (e.g., 'public')" + type = string + validation { + condition = contains(["public", "private", "admins"], var.ctf_challenge_visibility) + error_message = "ctf_challenge_visibility must be either 'public', 'private', or 'admins'." + } +} + +variable "ctf_account_visibility" { + description = "Account visibility (e.g., 'private')" + type = string + validation { + condition = contains(["public", "private", "admins"], var.ctf_account_visibility) + error_message = "ctf_account_visibility must be either 'public', 'private', or 'admins'." + } +} + +variable "ctf_score_visibility" { + description = "Score visibility (e.g., 'public')" + type = string + validation { + condition = contains(["public", "private", "hidden", "admins"], var.ctf_score_visibility) + error_message = "ctf_score_visibility must be either 'public', 'private', 'hidden', or 'admins'." + } +} + +variable "ctf_registration_visibility" { + description = "Registration visibility (e.g., 'public')" + type = string + validation { + condition = contains(["public", "private", "Mmlc"], var.ctf_registration_visibility) + error_message = "ctf_registration_visibility must be either 'public', 'private', or 'Mmlc'." + } +} + +variable "ctf_verify_emails" { + description = "Whether to verify emails" + type = bool + default = true +} + +variable "ctf_team_size" { + description = "Team size for the CTF. 0 means no limit" + type = number + default = 5 +} + +variable "ctf_brackets" { + description = "List of brackets for the CTF (optional)" + type = list(object({ + name = string + description = optional(string) + type = optional(string) # empty, "users" or "teams" + })) + default = [] + validation { + condition = alltrue([for b in var.ctf_brackets : contains(["", "users", "teams"], b.type)]) + error_message = "Each bracket type must be either '', 'users', or 'teams'." + } +} + +variable "ctf_theme" { + description = "Theme for CTFd" + type = string + default = "core-kubectf" +} + +variable "ctf_admin_name" { + description = "Name of the admin user" + type = string +} + +variable "ctf_admin_email" { + description = "Email of the admin user" + type = string +} + +variable "ctf_admin_password" { + description = "Password for the admin user" + type = string + sensitive = true +} + +variable "ctf_registration_code" { + description = "Registration code for the CTF" + type = string +} + +variable "ctf_mail_server" { + description = "Mail server for CTFd" + type = string +} + +variable "ctf_mail_port" { + description = "Mail server port" + type = number + default = 465 +} + +variable "ctf_mail_username" { + description = "Mail server username" + type = string +} + +variable "ctf_mail_password" { + description = "Mail server password" + type = string + sensitive = true +} + +variable "ctf_mail_tls" { + description = "Whether to use TLS for the mail server" + type = bool + default = true +} + +variable "ctf_mail_from" { + description = "From address for the mail server" + type = string +} + +variable "ctf_logo_path" { + description = "Path to the CTF logo file from the platform directory (e.g., 'data/logo.png')" + type = string + default = "data/logo.png" +} From f14bf8d310f5a92733610a514d5282ae40c0a909 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 22:59:43 +0100 Subject: [PATCH 011/148] refactor: add Terraform S3 bucket credentials environment file templates --- ops/.env.example | 2 ++ platform/.env.example | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 ops/.env.example create mode 100644 platform/.env.example diff --git a/ops/.env.example b/ops/.env.example new file mode 100644 index 0000000..5fe1f9d --- /dev/null +++ b/ops/.env.example @@ -0,0 +1,2 @@ +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= \ No newline at end of file diff --git a/platform/.env.example b/platform/.env.example new file mode 100644 index 0000000..5fe1f9d --- /dev/null +++ b/platform/.env.example @@ -0,0 +1,2 @@ +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= \ No newline at end of file From ed877a800894f6261ba22c44f5bfc4266132ee83 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:31:55 +0100 Subject: [PATCH 012/148] feat: add CTFp challenges setup --- challenges/.env.example | 2 + challenges/.gitignore | 37 +++++ challenges/.terraform.lock.hcl | 79 +++++++++++ challenges/README.md | 55 ++++++++ challenges/challenge/challenge.tf | 79 +++++++++++ challenges/challenges-config.tf | 16 +++ challenges/challenges-deployment.tf | 200 ++++++++++++++++++++++++++++ challenges/challenges/challenges.tf | 115 ++++++++++++++++ challenges/config/config.tf | 70 ++++++++++ challenges/kube-ctf.tf | 28 ++++ challenges/providers.tf | 59 ++++++++ challenges/tfvars/.gitignore | 1 + challenges/tfvars/template.tfvars | 50 +++++++ challenges/variables.tf | 116 ++++++++++++++++ challenges/whitelist.tf | 63 +++++++++ 15 files changed, 970 insertions(+) create mode 100644 challenges/.env.example create mode 100644 challenges/.gitignore create mode 100644 challenges/.terraform.lock.hcl create mode 100644 challenges/README.md create mode 100644 challenges/challenge/challenge.tf create mode 100644 challenges/challenges-config.tf create mode 100644 challenges/challenges-deployment.tf create mode 100644 challenges/challenges/challenges.tf create mode 100644 challenges/config/config.tf create mode 100644 challenges/kube-ctf.tf create mode 100644 challenges/providers.tf create mode 100644 challenges/tfvars/.gitignore create mode 100644 challenges/tfvars/template.tfvars create mode 100644 challenges/variables.tf create mode 100644 challenges/whitelist.tf diff --git a/challenges/.env.example b/challenges/.env.example new file mode 100644 index 0000000..5fe1f9d --- /dev/null +++ b/challenges/.env.example @@ -0,0 +1,2 @@ +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= \ No newline at end of file diff --git a/challenges/.gitignore b/challenges/.gitignore new file mode 100644 index 0000000..2faf43d --- /dev/null +++ b/challenges/.gitignore @@ -0,0 +1,37 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/challenges/.terraform.lock.hcl b/challenges/.terraform.lock.hcl new file mode 100644 index 0000000..df63395 --- /dev/null +++ b/challenges/.terraform.lock.hcl @@ -0,0 +1,79 @@ +# This file is maintained automatically by "tofu init". +# Manual edits may be lost in future updates. + +provider "registry.opentofu.org/alekc/kubectl" { + version = "2.1.3" + constraints = ">= 2.0.2" + hashes = [ + "h1:AymCb0DCWzmyLqn1qEhVs2pcFUZGT/kxPK+I/BObFH8=", + "zh:0e601ae36ebc32eb8c10aff4c48c1125e471fa09f5668465af7581c9057fa22c", + "zh:1773f08a412d1a5f89bac174fe1efdfd255ecdda92d31a2e31937e4abf843a2f", + "zh:1da2db1f940c5d34e31c2384c7bd7acba68725cc1d3ba6db0fec42efe80dbfb7", + "zh:20dc810fb09031bcfea4f276e1311e8286d8d55705f55433598418b7bcc76357", + "zh:326a01c86ba90f6c6eb121bacaabb85cfa9059d6587aea935a9bbb6d3d8e3f3f", + "zh:5a3737ea1e08421fe3e700dc833c6fd2c7b8c3f32f5444e844b3fe0c2352757b", + "zh:5f490acbd0348faefea273cb358db24e684cbdcac07c71002ee26b6cfd2c54a0", + "zh:777688cda955213ba637e2ac6b1994e438a5af4d127a34ecb9bb010a8254f8a8", + "zh:7acc32371053592f55ee0bcbbc2f696a8466415dea7f4bc5a6573f03953fc926", + "zh:81f0108e2efe5ae71e651a8826b61d0ce6918811ccfdc0e5b81b2cfb0f7f57fe", + "zh:88b785ea7185720cf40679cb8fa17e57b8b07fd6322cf2d4000b835282033d81", + "zh:89d833336b5cd027e671b46f9c5bc7d10c5109e95297639bbec8001da89aa2f7", + "zh:df108339a89d4372e5b13f77bd9d53c02a04362fb5d85e1d9b6b47292e30821c", + "zh:e8a2e3a5c50ca124e6014c361d72a9940d8e815f37ae2d1e9487ac77c3043013", + ] +} + +provider "registry.opentofu.org/hashicorp/http" { + version = "3.5.0" + hashes = [ + "h1:yvwvVZ0vdbsTUMru+7Cr0On1FVgDJHAaC6TNvy/OWzM=", + "zh:0a2b33494eec6a91a183629cf217e073be063624c5d3f70870456ddb478308e9", + "zh:180f40124fa01b98b3d2f79128646b151818e09d6a1a9ca08e0b032a0b1e9cb1", + "zh:3e29e1de149dc10bf78620526c7cb8c62cd76087f5630dfaba0e93cda1f3aa7b", + "zh:4420950200cf86042ec940d0e2c9b7c89966bf556bf8038ba36217eae663bca5", + "zh:5d1f7d02109b2e2dca7ec626e5563ee765583792d0fd64081286f16f9433bd0d", + "zh:8500b138d338b1994c4206aa577b5c44e1d7260825babcf43245a7075bfa52a5", + "zh:b42165a6c4cfb22825938272d12b676e4a6946ac4e750f85df870c947685df2d", + "zh:b919bf3ee8e3b01051a0da3433b443a925e272893d3724ee8fc0f666ec7012c9", + "zh:d13b81ea6755cae785b3e11634936cdff2dc1ec009dc9610d8e3c7eb32f42e69", + "zh:f1c9d2eb1a6b618ae77ad86649679241bd8d6aacec06d0a68d86f748687f4eb3", + ] +} + +provider "registry.opentofu.org/hashicorp/kubernetes" { + version = "2.38.0" + constraints = ">= 2.32.0" + hashes = [ + "h1:nY7J9jFXcsRINog0KYagiWZw1GVYF9D2JmtIB7Wnrao=", + "zh:1096b41c4e5b2ee6c1980916fb9a8579bc1892071396f7a9432be058aabf3cbc", + "zh:2959fde9ae3d1deb5e317df0d7b02ea4977951ee6b9c4beb083c148ca8f3681c", + "zh:5082f98fcb3389c73339365f7df39fc6912bf2bd1a46d5f97778f441a67fd337", + "zh:620fd5d0fbc2d7a24ac6b420a4922e6093020358162a62fa8cbd37b2bac1d22e", + "zh:7f47c2de179bba35d759147c53082cad6c3449d19b0ec0c5a4ca8db5b06393e1", + "zh:89c3aa2a87e29febf100fd21cead34f9a4c0e6e7ae5f383b5cef815c677eb52a", + "zh:96eecc9f94938a0bc35b8a63d2c4a5f972395e44206620db06760b730d0471fc", + "zh:e15567c1095f898af173c281b66bffdc4f3068afdd9f84bb5b5b5521d9f29584", + "zh:ecc6b912629734a9a41a7cf1c4c73fb13b4b510afc9e7b2e0011d290bcd6d77f", + ] +} + +provider "registry.opentofu.org/loafoe/htpasswd" { + version = "1.2.1" + hashes = [ + "h1:W1euQGM6t+QlB6Rq4fDbRKRHmeCIyYdIYdHrxL97BeE=", + "zh:14460c85ddc40a9ecadf583c22a7de91b83798a8ca4843949d50c3288c6f5bdd", + "zh:1af9416e28dd0a77c5d2c685561c4f60e19e2d606df0477ebc18eaa110c77807", + "zh:2245325864faaf027701ab12a04d641359a0dc439dd23c6e8f768407b78a5c18", + "zh:3813ff98198405d7c467565b52c7f0ad4533f43957da6390477dc898f8ed02c2", + "zh:3c0658e132232a181223f7ff65678d99cd2e8431c317f72281b67464e5e16892", + "zh:43505c0f42bc7635ec7c1fe5043c502f9b00ae4b5e74b81464bc494936643fc1", + "zh:52efdabb0abba99a33fd3ed981610f13c99bb383f94e997f90d95441d8558177", + "zh:75b5d9b4a610dfd0ff4dfb4039f61e79a0e56338e0a4cd45e0bc0edec34dfa62", + "zh:7aee5df091672d29f29dda57382a41d771fa21740cef6bb9a1b15afc6d84ffa4", + "zh:7ff618706e2953a21a22c7555e11f5cbe8e95c171704fcfdc6beedb0c25e49c0", + "zh:94e8a15c83a1a5a60ff1b58938dd9692d800fe05c5d8269e0916b5de03d89d3a", + "zh:c1ace4f322f9ec4956e4f30086da5b6a73f4d05e1266047d629b14a485c5a76d", + "zh:d4570075de49e3ee98494f7c44eab12e964c9776029ed536fd9352c3203cc635", + "zh:d99403b843de5939ea2e54b3ca46fd901d5c5b7fe34f44b8aeb8b38f4f792df6", + ] +} diff --git a/challenges/README.md b/challenges/README.md new file mode 100644 index 0000000..0adcc3b --- /dev/null +++ b/challenges/README.md @@ -0,0 +1,55 @@ +# CTF Pilot's Kubernetes Challenges + +> [!IMPORTANT] +> You are leaving the automated CTF Pilot setup and entering a more advanced manual setup. +> This requires knowledge of Kubernetes, Terraform/OpenTofu, and cloud infrastructure management. +> If you are not comfortable with these technologies, it is recommended to use the automated setup provided by CTF Pilot. +> Learn more about the automated setup in the [CTFp main README](../README.md). + +This directory contains deployment configuration for the challenges within the CTFp system. + +## Pre-requisites + +The following software needs to be installed on your local machine: + +- [Terraform](https://www.terraform.io/downloads.html) / [OpenTofu](https://opentofu.org) +- [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) (For interacting with the Kubernetes cluster) + +The following services are required, in order to deploy the services to the cluster: + +- A Kubernetes cluster (Deployed using the [CTF Pilot's Kubernetes Cluster on Hetzner Cloud](../cluster/README.md) guide or other means) +- Correctly deployed [ArgoCD](https://argo-cd.readthedocs.io/) within the Kubernetes cluster. +- Correctly deployed [CTF Pilot's Kubernetes Platform](../platform/README.md) within the Kubernetes cluster. + +> [!NOTE] +> The challenges has only been tested within the CTFp system. + +## Setup + +Copy the `tfvars/template.tfvars` file to `tfvars/data.tfvars` and edit the file with your own values. +The [`tfvars/template.tfvars`](tfvars/template.tfvars) file contains further information on each variable. + +> [!IMPORTANT] +> Make sure you generate the backend configuration file before creating the cluster. +> See the [backend generation instructions](../backend/README.md) for more information. +> +> You will also need to set the following environment variables for authentication to the S3 backend: +> - `AWS_ACCESS_KEY_ID` +> - `AWS_SECRET_ACCESS_KEY` +> +> See [OpenTofub backend S3 configuration](https://opentofu.org/docs/language/settings/backends/s3/) for more information. + +Run the following command to apply the ressources to the Kubernetes cluster: + +```bash +tofu init -backend-config=../backend/generated/challenges.hcl +tofu apply --var-file tfvars/data.tfvars +``` + +### Destroying the challenges + +To destroy the deployed challenges, run the following command: + +```bash +tofu destroy --var-file tfvars/data.tfvars +``` diff --git a/challenges/challenge/challenge.tf b/challenges/challenge/challenge.tf new file mode 100644 index 0000000..22c3d61 --- /dev/null +++ b/challenges/challenge/challenge.tf @@ -0,0 +1,79 @@ +variable "enabled" { + description = "Enable or disable the challenge deployment" + default = true + nullable = false +} + +variable "revision" { + description = "The revision of the repository to use" + default = "main" + nullable = false +} + +variable "category" { + description = "The category of the challenge" +} + +variable "identifier" { + description = "The identifier of the challenge" +} + +variable "path" { + description = "The path to the challenge" + default = null +} + +variable "argocd_project" { + description = "The ArgoCD project to use" + nullable = false +} + +variable "argocd_namespace" { + description = "The namespace where ArgoCD is installed" + default = "argocd" +} + +variable "challenge_namespace" { + description = "The namespace where the challenge will be deployed" + nullable = false +} + +variable "application_name" { + description = "The name of the application" + default = null +} + +variable "application_repo_url" { + description = "The URL of the repository where the application manifests are stored" + nullable = false +} + +variable "helm" { + description = "Helm chart configuration" + type = any + default = null +} + +module "argocd-challenge" { + source = "../../tf-modules/argocd/application" + + count = var.enabled ? 1 : 0 + + argocd_namespace = var.argocd_namespace + application_namespace = var.challenge_namespace + application_name = var.application_name != null ? var.application_name : "${var.category}-${var.identifier}" + application_repo_url = var.application_repo_url + application_repo_path = var.path != null ? var.path : "challenges/${var.category}/${var.identifier}/k8s/challenge" + application_repo_revision = var.revision + application_project = var.argocd_project + helm = var.helm + + argocd_labels = { + "part-of" = "ctfpilot" + "component" = "challenge" + "version" = var.revision + "category" = var.category + "instance" = var.identifier + } +} + diff --git a/challenges/challenges-config.tf b/challenges/challenges-config.tf new file mode 100644 index 0000000..b880461 --- /dev/null +++ b/challenges/challenges-config.tf @@ -0,0 +1,16 @@ +locals { + instanced_challenges = var.challenges_instanced + shared_challenges = var.challenges_static + static_challenges = var.challenges_static + + challenges_branch = var.challenges_branch == "" ? local.env_branch : var.challenges_branch + + challenge_repo_url = var.challenges_repository + branch = local.challenges_branch + + argocd_project_instanced = "instanced-challenges" + argocd_project_shared = "shared-challenges" + argocd_project_static = "static-challenges" + + config_namespace = "challenge-config" +} diff --git a/challenges/challenges-deployment.tf b/challenges/challenges-deployment.tf new file mode 100644 index 0000000..3b76fcd --- /dev/null +++ b/challenges/challenges-deployment.tf @@ -0,0 +1,200 @@ +locals { + categories_standard = keys(local.shared_challenges) + categories_isolated = keys(local.instanced_challenges) + categories_config = keys(local.static_challenges) +} + +module "argocd_project_shared" { + source = "../tf-modules/argocd/project" + + argocd_namespace = var.argocd_namespace + project_name = local.argocd_project_shared + project_destinations = [ + { + namespace = module.kube_ctf.namespace_standard_challenges + server = "*" + } + ] + + depends_on = [ + module.kube_ctf + ] +} + +module "argocd_project_instanced" { + source = "../tf-modules/argocd/project" + + argocd_namespace = var.argocd_namespace + project_name = local.argocd_project_instanced + project_destinations = [ + { + namespace = module.kube_ctf.namespace_instanced_challenges + server = "*" + } + ] + + depends_on = [ + module.kube_ctf + ] +} + +module "argocd_project_static" { + source = "../tf-modules/argocd/project" + + argocd_namespace = var.argocd_namespace + project_name = local.argocd_project_static + project_destinations = [ + { + namespace = local.config_namespace + server = "*" + } + ] + + depends_on = [ + module.kube_ctf + ] +} + +module "repo_access_standard" { + source = "../tf-modules/private-repo" + + name = local.argocd_project_shared + argocd_namespace = var.argocd_namespace + ghcr_username = var.ghcr_username + git_token = var.git_token + git_repo = local.challenge_repo_url + argocd_project = local.argocd_project_shared + + depends_on = [ + module.kube_ctf, + module.argocd_project_shared + ] +} + +module "repo_access_isolated" { + source = "../tf-modules/private-repo" + + name = local.argocd_project_instanced + argocd_namespace = var.argocd_namespace + ghcr_username = var.ghcr_username + git_token = var.git_token + git_repo = local.challenge_repo_url + argocd_project = local.argocd_project_instanced + + depends_on = [ + module.kube_ctf, + module.argocd_project_instanced + ] +} + +module "repo_access_config" { + source = "../tf-modules/private-repo" + + name = local.argocd_project_static + argocd_namespace = var.argocd_namespace + ghcr_username = var.ghcr_username + git_token = var.git_token + git_repo = local.challenge_repo_url + argocd_project = local.argocd_project_static + + depends_on = [ + module.kube_ctf, + module.argocd_project_static + ] +} + +module "shared_challenges" { + source = "./challenges" + + for_each = toset(local.categories_standard) + + revision = local.branch + category = each.key + challenges = local.shared_challenges[each.key] + + config_only = false + argocd_project = local.argocd_project_shared + argocd_config_project = local.argocd_project_static + argocd_namespace = var.argocd_namespace + application_repo_url = local.challenge_repo_url + challenge_namespace = module.kube_ctf.namespace_standard_challenges + config_namespace = local.config_namespace + helm = { + valuesObject = { + kubectf = { + host = "challs.${var.cluster_dns_ctf}" + } + } + } + + depends_on = [ + module.kube_ctf, + module.argocd_project_static, + module.argocd_project_shared, + module.repo_access_standard + ] +} + +module "instanced_challenges" { + source = "./challenges" + + for_each = toset(local.categories_isolated) + + revision = local.branch + category = each.key + challenges = local.instanced_challenges[each.key] + + config_only = false + argocd_project = local.argocd_project_instanced + argocd_config_project = local.argocd_project_static + argocd_namespace = var.argocd_namespace + application_repo_url = local.challenge_repo_url + challenge_namespace = module.kube_ctf.namespace_instanced_challenges + config_namespace = local.config_namespace + helm = { + valuesObject = { + kubectf = { + host = "challs.${var.cluster_dns_ctf}" + } + } + } + config_helm_only = true + + depends_on = [ + module.kube_ctf, + module.argocd_project_static, + module.argocd_project_instanced, + module.repo_access_isolated + ] +} + +module "static_challenges" { + source = "./challenges" + + for_each = toset(local.categories_config) + + revision = local.branch + category = each.key + challenges = local.static_challenges[each.key] + + config_only = true + argocd_project = local.argocd_project_static + argocd_config_project = local.argocd_project_static + argocd_namespace = var.argocd_namespace + application_repo_url = local.challenge_repo_url + challenge_namespace = local.config_namespace + config_namespace = local.config_namespace + helm = { + valuesObject = { + kubectf = { + host = "challs.${var.cluster_dns_ctf}" + } + } + } + + depends_on = [ + module.kube_ctf, + module.argocd_project_static, + module.repo_access_config + ] +} diff --git a/challenges/challenges/challenges.tf b/challenges/challenges/challenges.tf new file mode 100644 index 0000000..34853d9 --- /dev/null +++ b/challenges/challenges/challenges.tf @@ -0,0 +1,115 @@ +variable "config_only" { + description = "Should challenges only be deployed with config" + type = bool + default = false +} + +variable "revision" { + description = "The revision of the repository to use" + default = "main" + nullable = false +} + +variable "category" { + description = "The category of the challenge" +} + +variable "challenges" { + description = "The challenges to deploy in a given category" + type = list(string) + default = [] +} + +variable "path" { + description = "The path to the challenge" + default = null +} + +variable "path_config" { + description = "The path to the challenge config" + default = null +} + +variable "argocd_project" { + description = "The ArgoCD project to use" + nullable = false +} + +variable "argocd_config_project" { + description = "The ArgoCD project to use for config only challenges" + nullable = false +} + +variable "argocd_namespace" { + description = "The namespace where ArgoCD is installed" + default = "argocd" +} + +variable "challenge_namespace" { + description = "The namespace where the challenge will be deployed" + nullable = false +} + +variable "config_namespace" { + description = "The namespace where the challenge config will be deployed" + nullable = false +} + +variable "application_name" { + description = "The name of the application" + default = null +} + +variable "application_repo_url" { + description = "The URL of the repository where the application manifests are stored" + nullable = false +} + +variable "helm" { + description = "Helm chart configuration" + type = any + default = null +} + +variable "config_helm_only" { + description = "Helm chart configuration for config only challenges" + type = bool + default = false +} + +module "argocd-challenge" { + source = "../challenge" + + for_each = toset(var.challenges) + enabled = !var.config_only + + identifier = each.value + + revision = var.revision + category = var.category + argocd_project = var.argocd_project + argocd_namespace = var.argocd_namespace + application_repo_url = var.application_repo_url + challenge_namespace = var.challenge_namespace + application_name = var.application_name + path = var.path + helm = var.config_helm_only ? null : var.helm +} + +module "argocd-challenge-config" { + source = "../config" + + for_each = toset(var.challenges) + + identifier = each.value + + revision = var.revision + category = var.category + argocd_project = var.argocd_config_project + argocd_namespace = var.argocd_namespace + application_repo_url = var.application_repo_url + config_namespace = var.config_namespace + application_name = var.application_name + path = var.path_config + helm = var.helm +} diff --git a/challenges/config/config.tf b/challenges/config/config.tf new file mode 100644 index 0000000..172524e --- /dev/null +++ b/challenges/config/config.tf @@ -0,0 +1,70 @@ +variable "revision" { + description = "The revision of the repository to use" + default = "main" + nullable = false +} + +variable "category" { + description = "The category of the challenge" +} + +variable "identifier" { + description = "The identifier of the challenge" +} + +variable "path" { + description = "The path to the challenge config" + default = null +} + +variable "argocd_project" { + description = "The ArgoCD project to use" + nullable = false +} + +variable "argocd_namespace" { + description = "The namespace where ArgoCD is installed" + default = "argocd" +} + +variable "config_namespace" { + description = "The namespace where the challenge config will be deployed" + nullable = false +} + +variable "application_name" { + description = "The name of the application" + default = null +} + +variable "application_repo_url" { + description = "The URL of the repository where the application manifests are stored" + nullable = false +} + +variable "helm" { + description = "Helm chart configuration" + type = any + default = null +} + +module "argocd-challenge-config" { + source = "../../tf-modules/argocd/application" + + argocd_namespace = var.argocd_namespace + application_namespace = var.config_namespace + application_name = var.application_name != null ? "${var.application_name}-config" : "${var.category}-${var.identifier}-config" + application_repo_url = var.application_repo_url + application_repo_path = var.path != null ? var.path : "challenges/${var.category}/${var.identifier}/k8s/config" + application_repo_revision = var.revision + application_project = var.argocd_project + helm = var.helm + + argocd_labels = { + "part-of" = "ctfpilot" + "component" = "challenge-config" + "version" = var.revision + "category" = var.category + "instance" = var.identifier + } +} diff --git a/challenges/kube-ctf.tf b/challenges/kube-ctf.tf new file mode 100644 index 0000000..954b433 --- /dev/null +++ b/challenges/kube-ctf.tf @@ -0,0 +1,28 @@ +module "kube_ctf" { + source = "../tf-modules/kubectf" + + challenge_dns = var.cluster_dns_ctf + management_dns = var.cluster_dns_ctf + + org_name = "io" + cert_manager = "cert-manager-global" + + management_auth_secret = var.kubectf_auth_secret + container_secret = var.kubectf_container_secret + + image_landing = var.image_instancing_fallback + image_challenge_manager = var.image_kubectf + registry_prefix = "docker.io" # Optional, used in rendering templates + + ghcr_username = var.ghcr_username + ghcr_token = var.ghcr_token + + max_instances = 6 +} + +output "Hosts" { + value = { + "Challenges" = module.kube_ctf.challenge_host + "Management" = module.kube_ctf.challenge_manager_host + } +} diff --git a/challenges/providers.tf b/challenges/providers.tf new file mode 100644 index 0000000..75c454e --- /dev/null +++ b/challenges/providers.tf @@ -0,0 +1,59 @@ +# ---------------------- +# Terraform Configuration +# ---------------------- + +terraform { + required_version = ">= 1.9.5" + + backend "s3" {} + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.32.0" + } + + kubectl = { + source = "alekc/kubectl" + version = ">= 2.0.2" + } + + htpasswd = { + source = "loafoe/htpasswd" + } + + http = { + source = "hashicorp/http" + } + } +} + +# ---------------------- +# Providers +# ---------------------- + +locals { + kube_config = yamldecode(base64decode(var.kubeconfig)) +} + +provider "kubernetes" { + host = local.kube_config.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kube_config.clusters[0].cluster.certificate-authority-data) + + client_certificate = base64decode(local.kube_config.users[0].user.client-certificate-data) + client_key = base64decode(local.kube_config.users[0].user.client-key-data) +} + +provider "kubectl" { + load_config_file = false + + host = local.kube_config.clusters[0].cluster.server + cluster_ca_certificate = base64decode(local.kube_config.clusters[0].cluster.certificate-authority-data) + + client_certificate = base64decode(local.kube_config.users[0].user.client-certificate-data) + client_key = base64decode(local.kube_config.users[0].user.client-key-data) +} + +locals { + env_branch = var.environment == "prod" ? "main" : "develop" +} diff --git a/challenges/tfvars/.gitignore b/challenges/tfvars/.gitignore new file mode 100644 index 0000000..8147f77 --- /dev/null +++ b/challenges/tfvars/.gitignore @@ -0,0 +1 @@ +!template.tfvars diff --git a/challenges/tfvars/template.tfvars b/challenges/tfvars/template.tfvars new file mode 100644 index 0000000..2ab6f38 --- /dev/null +++ b/challenges/tfvars/template.tfvars @@ -0,0 +1,50 @@ +# ------------------------ +# Kubernetes variables +# ------------------------ +kubeconfig = "AA==" # Base64 encoded kubeconfig file + +# ------------------------ +# Generic information +# ------------------------ +environment = "test" # Environment name for the CTF +cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster + +# ------------------------ +# GitHub variables +# ------------------------ +ghcr_username = "" # GitHub Container Registry username +ghcr_token = "" # GitHub Container Registry token. This token is used to pull images from the GitHub Container Registry. Only let this token have registry read access +git_token = "" # GitHub repo token. Only let this token have read access to the needed repositories. + +# ---------------------- +# CTF configuration +# ---------------------- +kubectf_auth_secret = "" # The secret to use for the authSecret in the CTF configuration +kubectf_container_secret = "" # The secret to use for the containerSecret in the CTF configuration + +# ------------------------ +# Challenges configuration +# ------------------------ +chall_whitelist_ips = ["", ""] # List of IPs to whitelist for challenge access + +challenges_static = { + "" = ["", ""], +} # List of static challenges to deploy. Needs to be the slugs of the challenges +challenges_shared = { + "" = ["", ""], +} # List of shared challenges to deploy. Needs to be the slugs of the challenges +challenges_instanced = { + "" = ["", ""], +} # List of instanced challenges to deploy. Needs to be the slugs of the challenges + +challenges_repository = "" # URL of the Git repository containing the challenge definitions +challenges_branch = "" # Branch of the Git repository to use for the challenge definitions. Leave empty for environment based branch (environment == prod ? main : develop) + +# ---------------------- +# Docker images +# ---------------------- +# Values are maintained in the variables.tf file. +# You can override these values by uncommenting and setting your own images here. + +# image_instancing_fallback = "ghcr.io/ctfpilot/instancing-fallback:1.0.2" # The docker image for the instancing fallback deployment. See https://github.com/ctfpilot/instancing-fallback +# image_kubectf = "ghcr.io/ctfpilot/kube-ctf:1.0.1" # The docker image for the kube-ctf deployment. See https://github.com/ctfpilot/kube-ctf diff --git a/challenges/variables.tf b/challenges/variables.tf new file mode 100644 index 0000000..beee62b --- /dev/null +++ b/challenges/variables.tf @@ -0,0 +1,116 @@ +# ------------------------ +# Variables +# ------------------------ + +variable "kubeconfig" { + type = string + description = "Base64 encoded kubeconfig file" + sensitive = true + nullable = false +} + +variable "environment" { + type = string + description = "Environment name for the CTF" + default = "test" + nullable = false +} + +variable "cluster_dns_ctf" { + type = string + description = "The domain name to use for the DNS records for the CTF challenges part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf`" + nullable = false +} + +variable "ghcr_username" { + description = "GitHub Container Registry username" + type = string + nullable = false +} + +variable "ghcr_token" { + description = "GitHub Container Registry token. This token is used to pull images from the GitHub Container Registry. Only let this token have registry read access" + type = string + sensitive = true + nullable = false +} + +variable "git_token" { + description = "GitHub repo token. Only let this token have read access to the needed repositories." + type = string + sensitive = true + nullable = false +} + +variable "argocd_namespace" { + description = "Namespace for ArgoCD" + type = string + default = "argocd" +} + +variable "kubectf_auth_secret" { + type = string + nullable = false + description = "The secret to use for the authSecret in the CTF configuration" + sensitive = true +} + +variable "kubectf_container_secret" { + type = string + nullable = false + description = "The secret to use for the containerSecret in the CTF configuration" + sensitive = true +} + +variable "chall_whitelist_ips" { + type = list(string) + description = "List of IPs to whitelist for challenges, e.g., [ \"\", \"\" ]" + default = [] + sensitive = true + validation { + condition = length(var.chall_whitelist_ips) > 0 + error_message = "At least one IP address must be whitelisted" + } +} + +variable "challenges_static" { + type = map(list(string)) + description = "List of static challenges to deploy. In the format { \"\" = [\"\", \"\"] }" + default = [] +} + +variable "challenges_shared" { + type = map(list(string)) + description = "List of shared challenges to deploy. In the format { \"\" = [\"\", \"\"] }" + default = [] +} + +variable "challenges_instanced" { + type = map(list(string)) + description = "List of instanced challenges to deploy. In the format { \"\" = [\"\", \"\"] }" + default = [] +} + +variable "challenges_repository" { + type = string + description = "Repository URL for challenges, generated using the challenge-toolkit. See https://github.com/ctfpilot/challenge-toolkit" + nullable = false +} + +variable "challenges_branch" { + type = string + description = "Git branch for challenges. Leave empty for environment based branch (environment == prod ? main : develop)" + default = "" +} + +variable "image_instancing_fallback" { + type = string + description = "The docker image for the instancing fallback deployment. See https://github.com/ctfpilot/instancing-fallback" + default = "ghcr.io/ctfpilot/instancing-fallback:1.0.2" +} + +variable "image_kubectf" { + type = string + description = "The docker image for the kube-ctf deployment. See https://github.com/ctfpilot/kube-ctf" + default = "ghcr.io/ctfpilot/kube-ctf:1.0.1" +} diff --git a/challenges/whitelist.tf b/challenges/whitelist.tf new file mode 100644 index 0000000..7dee1eb --- /dev/null +++ b/challenges/whitelist.tf @@ -0,0 +1,63 @@ +resource "kubernetes_manifest" "ip_whitelist_web" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "challenge-ipwhitelist-web" + namespace = "ctfpilot-challenges" + } + spec = { + ipAllowList = { + sourceRange = var.chall_whitelist_ips + } + } + } +} + +resource "kubernetes_manifest" "ip_whitelist_instanced_web" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "challenge-ipwhitelist-web" + namespace = "ctfpilot-challenges-instanced" + } + spec = { + ipAllowList = { + sourceRange = var.chall_whitelist_ips + } + } + } +} + +resource "kubernetes_manifest" "ip_whitelist_tcp" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "MiddlewareTCP" + metadata = { + name = "challenge-ipwhitelist-tcp" + namespace = "ctfpilot-challenges" + } + spec = { + ipAllowList = { + sourceRange = var.chall_whitelist_ips + } + } + } +} + +resource "kubernetes_manifest" "ip_whitelist_instanced_tcp" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "MiddlewareTCP" + metadata = { + name = "challenge-ipwhitelist-tcp" + namespace = "ctfpilot-challenges-instanced" + } + spec = { + ipAllowList = { + sourceRange = var.chall_whitelist_ips + } + } + } +} From e44a09a7b8a82f179262eb8fd4cc618854d705d4 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:32:08 +0100 Subject: [PATCH 013/148] refactor: update error fallback image version to 1.2.1 in platform and ops configurations --- ops/tfvars/template.tfvars | 2 +- ops/variables.tf | 2 +- platform/tfvars/template.tfvars | 2 +- platform/variables.tf | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ops/tfvars/template.tfvars b/ops/tfvars/template.tfvars index d04ea07..6148d25 100644 --- a/ops/tfvars/template.tfvars +++ b/ops/tfvars/template.tfvars @@ -59,7 +59,7 @@ traefik_basic_auth = { user = "", password = "" # Values are maintained in the variables.tf file. # You can override these values by uncommenting and setting your own images here. -# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:latest" # The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback +# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:1.2.1" # The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback # image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # The docker image for Filebeat # ---------------------- diff --git a/ops/variables.tf b/ops/variables.tf index f00eadc..5da30b0 100644 --- a/ops/variables.tf +++ b/ops/variables.tf @@ -122,7 +122,7 @@ variable "grafana_admin_password" { variable "image_error_fallback" { type = string description = "The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback" - default = "ghcr.io/ctfpilot/error-fallback:latest" + default = "ghcr.io/ctfpilot/error-fallback:1.2.1" } variable "image_filebeat" { diff --git a/platform/tfvars/template.tfvars b/platform/tfvars/template.tfvars index 4212828..4a32f19 100644 --- a/platform/tfvars/template.tfvars +++ b/platform/tfvars/template.tfvars @@ -109,7 +109,7 @@ ctfd_k8s_deployment_branch = "" # Git branch # You can override these values by uncommenting and setting your own images here. # image_ctfd_manager = "ghcr.io/ctfpilot/ctfd-manager:1.0.1" # Docker image for the CTFd Manager deployment -# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:latest" # Docker image for the error fallback deployment +# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:1.2.1" # Docker image for the error fallback deployment # image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # Docker image for Filebeat # image_ctfd_exporter = "ghcr.io/the0mikkel/ctfd-exporter:1.1.1" # Docker image for the CTFd Exporter diff --git a/platform/variables.tf b/platform/variables.tf index 49bc1a9..89dcdc3 100644 --- a/platform/variables.tf +++ b/platform/variables.tf @@ -257,7 +257,7 @@ variable "image_ctfd_manager" { variable "image_error_fallback" { type = string description = "The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback" - default = "ghcr.io/ctfpilot/error-fallback:latest" + default = "ghcr.io/ctfpilot/error-fallback:1.2.1" } variable "image_filebeat" { From 32b6fda68e46053c9479bd14d2a3ba19b1e6812d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:34:24 +0100 Subject: [PATCH 014/148] feat: add initial ArgoCD application and project configurations --- .../argocd/application/argocd-application.tf | 98 +++++++++++++++++++ tf-modules/argocd/project/argocd-project.tf | 46 +++++++++ 2 files changed, 144 insertions(+) create mode 100644 tf-modules/argocd/application/argocd-application.tf create mode 100644 tf-modules/argocd/project/argocd-project.tf diff --git a/tf-modules/argocd/application/argocd-application.tf b/tf-modules/argocd/application/argocd-application.tf new file mode 100644 index 0000000..d32f40c --- /dev/null +++ b/tf-modules/argocd/application/argocd-application.tf @@ -0,0 +1,98 @@ +variable "argocd_namespace" { + description = "The namespace where ArgoCD is installed" + default = "argocd" +} + +variable "application_namespace" { + description = "The namespace where the application will be deployed" +} + +variable "application_name" { + description = "The name of the application" +} + +variable "application_repo_url" { + description = "The URL of the repository where the application manifests are stored" +} + +variable "application_repo_path" { + description = "The path within the repository where the application manifests are stored" +} + +variable "application_repo_revision" { + description = "The revision of the repository to use" +} + +variable "application_project" { + description = "The ArgoCD project to use" + default = "default" + + validation { + error_message = "The project name must be lowercase" + condition = can(regex("^[a-z0-9-]*$", var.application_project)) + } +} + +variable "argocd_labels" { + description = "The labels to apply to the ArgoCD Application" + type = map(string) + default = {} + +} + +variable "argocd_finalizers" { + description = "The finalizers to apply to the ArgoCD Application" + type = list(string) + default = ["resources-finalizer.argocd.argoproj.io"] +} + +variable "helm" { + description = "Helm chart configuration" + type = any + default = null +} + +locals { + argocd_source = merge( + { + repoURL = var.application_repo_url + path = var.application_repo_path + targetRevision = var.application_repo_revision + }, + var.helm != null ? { helm = var.helm } : {} + ) +} + +resource "kubernetes_manifest" "application" { + manifest = { + apiVersion = "argoproj.io/v1alpha1" + kind = "Application" + metadata = { + name = var.application_name + namespace = var.argocd_namespace + + labels = merge( + { + "managed-by" = "terraform" + }, + var.argocd_labels + ) + finalizers = length(var.argocd_finalizers) > 0 ? var.argocd_finalizers : null + } + spec = { + project = var.application_project + source = local.argocd_source + destination = { + namespace = var.application_namespace + server = "https://kubernetes.default.svc" + } + + syncPolicy = { + automated = { + prune = true + selfHeal = true + } + } + } + } +} diff --git a/tf-modules/argocd/project/argocd-project.tf b/tf-modules/argocd/project/argocd-project.tf new file mode 100644 index 0000000..c353e1f --- /dev/null +++ b/tf-modules/argocd/project/argocd-project.tf @@ -0,0 +1,46 @@ +variable "argocd_namespace" { + description = "The namespace where ArgoCD is installed" + default = "argocd" +} + +variable "project_name" { + description = "The name of the project" + + validation { + error_message = "The project name must be lowercase" + condition = can(regex("^[a-z0-9-]*$", var.project_name)) + } +} + +variable "project_destinations" { + description = "The destinations for the project" + type = list(object({ + namespace = string + server = string + })) + + default = [{ + namespace = "*" + server = "*" + }] +} + +resource "kubernetes_manifest" "project" { + manifest = { + apiVersion = "argoproj.io/v1alpha1" + kind = "AppProject" + metadata = { + name = var.project_name + namespace = var.argocd_namespace + } + spec = { + destinations = var.project_destinations + clusterResourceWhitelist = [ + { + group = "*" + kind = "*" + } + ] + } + } +} From 70c6aacfd8fd357ee6bd36f1032a518529750c6d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:35:02 +0100 Subject: [PATCH 015/148] feat: add pull-secret Terraform module --- tf-modules/pull-secret/pull-secret.tf | 22 ++++++++++++++++++++++ tf-modules/pull-secret/variables.tf | 11 +++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tf-modules/pull-secret/pull-secret.tf create mode 100644 tf-modules/pull-secret/variables.tf diff --git a/tf-modules/pull-secret/pull-secret.tf b/tf-modules/pull-secret/pull-secret.tf new file mode 100644 index 0000000..967f7b9 --- /dev/null +++ b/tf-modules/pull-secret/pull-secret.tf @@ -0,0 +1,22 @@ +resource "kubernetes_secret" "pull-secret" { + metadata { + name = "dockerconfigjson-github-com" + namespace = var.namespace + } + + type = "kubernetes.io/dockerconfigjson" + + data = { + ".dockerconfigjson" = jsonencode({ + "auths" = { + "ghcr.io" = { + "auth" = base64encode("${var.ghcr_username}:${var.ghcr_token}") + } + } + }) + } +} + +output "pull-secret" { + value = kubernetes_secret.pull-secret.metadata.0.name +} diff --git a/tf-modules/pull-secret/variables.tf b/tf-modules/pull-secret/variables.tf new file mode 100644 index 0000000..84ef639 --- /dev/null +++ b/tf-modules/pull-secret/variables.tf @@ -0,0 +1,11 @@ +variable "namespace" { + description = "The namespace where the secret will be created in" +} + +variable "ghcr_username" { + description = "The username for the GitHub Container Registry" +} + +variable "ghcr_token" { + description = "The token for the GitHub Container Registry" +} From c6ef892efbb1da29287f655435b62ca2441c7f7f Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:35:26 +0100 Subject: [PATCH 016/148] feat: add private-repo Terraform module --- tf-modules/private-repo/private-repo.tf | 17 +++++++++++++++ tf-modules/private-repo/variables.tf | 29 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 tf-modules/private-repo/private-repo.tf create mode 100644 tf-modules/private-repo/variables.tf diff --git a/tf-modules/private-repo/private-repo.tf b/tf-modules/private-repo/private-repo.tf new file mode 100644 index 0000000..a9407d6 --- /dev/null +++ b/tf-modules/private-repo/private-repo.tf @@ -0,0 +1,17 @@ +resource "kubernetes_secret" "private_repo" { + metadata { + name = "${var.name}-private-repo-access" + namespace = var.argocd_namespace + labels = { + "argocd.argoproj.io/secret-type" = "repository" + } + } + + data = { + type = "git" + url = var.git_repo + password = var.git_token + username = var.ghcr_username + project = var.argocd_project + } +} diff --git a/tf-modules/private-repo/variables.tf b/tf-modules/private-repo/variables.tf new file mode 100644 index 0000000..28e6ed2 --- /dev/null +++ b/tf-modules/private-repo/variables.tf @@ -0,0 +1,29 @@ +variable "name" { + description = "Unique name for the secret" +} + +variable "argocd_namespace" { + description = "The namespace where the secret will be created in, should be the same as the ArgoCD namespace" +} + +variable "ghcr_username" { + description = "The username for the GitHub repository" +} + +variable "git_token" { + description = "The token for the GitHub repository" +} + +variable "git_repo" { + description = "The git repository to give access to" +} + +variable "argocd_project" { + description = "The ArgoCD project to use" + default = "default" + + validation { + error_message = "The project name must be lowercase" + condition = can(regex("^[a-z0-9-]*$", var.argocd_project)) + } +} \ No newline at end of file From eaa83a8dac23e13e1a867094ca3416aa5a8960df Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:35:39 +0100 Subject: [PATCH 017/148] feat: add ingress Terraform module --- tf-modules/kubernetes/ingress/ingress.tf | 82 ++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tf-modules/kubernetes/ingress/ingress.tf diff --git a/tf-modules/kubernetes/ingress/ingress.tf b/tf-modules/kubernetes/ingress/ingress.tf new file mode 100644 index 0000000..ecf4b19 --- /dev/null +++ b/tf-modules/kubernetes/ingress/ingress.tf @@ -0,0 +1,82 @@ +variable "hostname" { + description = "The hostname to route traffic to" +} + +variable "namespace" { + description = "The namespace to deploy the ingress to" +} + +variable "service_name" { + description = "The name of the service to route traffic to" +} + +variable "ingress_name" { + description = "The name of the ingress resource" + default = null +} + +variable "service_port" { + description = "The port of the service to route traffic to" + default = 80 +} + +variable "cluster_issuer_name" { + description = "The name of the cluster issuer to use for the ingress" + default = "cert-manager-global" +} + +variable "traefik_middleware" { + description = "The name of the Traefik middleware to use" + default = "errors-errors@kubernetescrd" +} + + + +resource "kubernetes_ingress_v1" "ingress" { + metadata { + name = var.ingress_name != null ? var.ingress_name : var.service_name + namespace = var.namespace + + annotations = { + "cert-manager.io/cluster-issuer" = var.cluster_issuer_name + "traefik.ingress.kubernetes.io/router.middlewares" = var.traefik_middleware + } + } + + spec { + ingress_class_name = "traefik" + default_backend { + service { + name = var.service_name + port { + number = 80 + } + } + } + + rule { + host = var.hostname + http { + path { + path = "/" + backend { + service { + name = var.service_name + port { + number = var.service_port + } + } + } + } + } + } + + tls { + hosts = [ + "${var.hostname}" + ] + + secret_name = "${var.hostname}-cert" + } + } +} From a1f6a8bcd68bfa81ef66283daa676888bf86c4b2 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:36:20 +0100 Subject: [PATCH 018/148] feat: add database Terraform module --- .../database/database-user/database-user.tf | 159 ++++++++++++++++++ tf-modules/database/database/database.tf | 97 +++++++++++ tf-modules/database/user/user.tf | 85 ++++++++++ 3 files changed, 341 insertions(+) create mode 100644 tf-modules/database/database-user/database-user.tf create mode 100644 tf-modules/database/database/database.tf create mode 100644 tf-modules/database/user/user.tf diff --git a/tf-modules/database/database-user/database-user.tf b/tf-modules/database/database-user/database-user.tf new file mode 100644 index 0000000..11594d2 --- /dev/null +++ b/tf-modules/database/database-user/database-user.tf @@ -0,0 +1,159 @@ +# ---------- +# Required +# ---------- + +variable "db_name" { + type = string + description = "The name of the database to create" +} + +variable "namespace" { + type = string + description = "The namespace to deploy the database to" +} + +variable "mariadb_cluster" { + type = string + description = "The name of the MariaDB cluster to connect to" +} + +variable "mariadb_cluster_namespace" { + type = string + description = "The namespace of the MariaDB cluster to connect to" +} + +variable "password" { + type = string + description = "The password for the user" + sensitive = true +} + +# ---------- +# Optional +# ---------- + +variable "db_connection_secret_name" { + type = string + description = "The name of the secret to create for the database connection" + default = "db-connection" +} + +variable "db_connection_format" { + type = string + description = "The format of the connection string" + default = "mysql://{{ .Username }}:{{ .Password }}@{{ .Host }}:{{ .Port }}/{{ .Database }}{{ .Params }}" +} + +# ---------- +# Resources +# ---------- + +resource "null_resource" "replace-trigger" { + triggers = { + "db_name" = var.db_name + "namespace" = var.namespace + "maria_db_cluster" = var.mariadb_cluster + "maria_db_cluster_namespace" = var.mariadb_cluster_namespace + } +} + + +module "database" { + source = "../database" + + db_name = var.db_name + mariadb_cluster = var.mariadb_cluster + mariadb_cluster_namespace = var.mariadb_cluster_namespace + namespace = var.namespace +} + +module "user" { + source = "../user" + + user_name = var.db_name + password = var.password + mariadb_cluster = var.mariadb_cluster + mariadb_cluster_namespace = var.mariadb_cluster_namespace + namespace = var.namespace + + depends_on = [ + module.database + ] +} + +resource "kubernetes_manifest" "connection" { + manifest = { + apiVersion = "k8s.mariadb.com/v1alpha1" + kind = "Connection" + metadata = { + name = "connection" + namespace = var.namespace + } + + spec = { + mariaDbRef = { + name = var.mariadb_cluster + namespace = var.mariadb_cluster_namespace + } + username = var.db_name + passwordSecretKeyRef = { + name = "user-${var.db_name}" + key = "password" + } + database = var.db_name + secretName = var.db_connection_secret_name + + secretTemplate = { + key = "dsn" + format = var.db_connection_format + usernameKey = "username" + passwordKey = "password" + hostKey = "host" + portKey = "port" + databaseKey = "database" + } + + serviceName = var.mariadb_cluster + } + } + + lifecycle { + replace_triggered_by = [ + null_resource.replace-trigger + ] + } +} + +resource "kubernetes_manifest" "grant" { + manifest = { + apiVersion = "k8s.mariadb.com/v1alpha1" + kind = "Grant" + metadata = { + name = "${var.db_name}" + namespace = var.namespace + } + spec = { + mariaDbRef = { + name = var.mariadb_cluster + namespace = var.mariadb_cluster_namespace + } + privileges = [ + "ALL PRIVILEGES" + ] + database = var.db_name + table = "*" + username = var.db_name + grantOption = true + host = "%" + cleanupPolicy = "Delete" + requeueInterval = "30s" + retryInterval = "5s" + } + } + + lifecycle { + replace_triggered_by = [ + null_resource.replace-trigger + ] + } +} diff --git a/tf-modules/database/database/database.tf b/tf-modules/database/database/database.tf new file mode 100644 index 0000000..8c50282 --- /dev/null +++ b/tf-modules/database/database/database.tf @@ -0,0 +1,97 @@ +# ---------- +# Required +# ---------- + +variable "db_name" { + type = string + description = "The name of the database to create" +} + +variable "namespace" { + type = string + description = "The namespace to deploy the database to" +} + +variable "mariadb_cluster" { + type = string + description = "The name of the MariaDB cluster to connect to" +} + +variable "mariadb_cluster_namespace" { + type = string + description = "The namespace of the MariaDB cluster to connect to" +} + +# ---------- +# Optional +# ---------- + +variable "db_character_set" { + type = string + description = "The character set to use for the database" + default = "utf8mb4" +} + +variable "db_collate" { + type = string + description = "The collation to use for the database" + default = "utf8mb4_0900_ai_ci" +} + +variable "db_cleanup_policy" { + type = string + description = "The cleanup policy to use for the database - Is it deleted or retained when this resource is deleted" + default = "Delete" +} + +variable "db_requeue_interval" { + type = string + description = "The requeue interval to use for the database" + default = "30s" +} + +variable "db_retry_interval" { + type = string + description = "The retry interval to use for the database" + default = "5s" +} + +# ---------- +# Resources +# ---------- + +resource "null_resource" "replace-trigger" { + triggers = { + "db_name" = var.db_name + "namespace" = var.namespace + "maria_db_cluster" = var.mariadb_cluster + "maria_db_cluster_namespace" = var.mariadb_cluster_namespace + } +} + +resource "kubernetes_manifest" "database" { + manifest = { + apiVersion = "k8s.mariadb.com/v1alpha1" + kind = "Database" + metadata = { + name = "${var.db_name}" + namespace = var.namespace + } + spec = { + name = var.db_name + mariaDbRef = { + name = var.mariadb_cluster + namespace = var.mariadb_cluster_namespace + } + characterSet = var.db_character_set + collate = var.db_collate + cleanupPolicy = var.db_cleanup_policy + requeueInterval = var.db_requeue_interval + retryInterval = var.db_retry_interval + } + } + + depends_on = [ + null_resource.replace-trigger + ] +} diff --git a/tf-modules/database/user/user.tf b/tf-modules/database/user/user.tf new file mode 100644 index 0000000..452f711 --- /dev/null +++ b/tf-modules/database/user/user.tf @@ -0,0 +1,85 @@ +# ---------- +# Required +# ---------- + +variable "user_name" { + type = string + description = "The name of the user to create" +} + +variable "namespace" { + type = string + description = "The namespace to deploy the user to" +} + +variable "mariadb_cluster" { + type = string + description = "The name of the MariaDB cluster to connect to" +} + +variable "mariadb_cluster_namespace" { + type = string + description = "The namespace of the MariaDB cluster to connect to" +} + +variable "password" { + type = string + description = "The password for the user" + sensitive = true +} + +# ---------- +# Resources +# ---------- + +resource "kubernetes_secret" "database_user_password" { + metadata { + name = "user-${var.user_name}" + namespace = var.namespace + } + + data = { + password = var.password + } +} + +resource "null_resource" "replace-trigger" { + triggers = { + "user_name" = var.user_name + "namespace" = var.namespace + "maria_db_cluster" = var.mariadb_cluster + "maria_db_cluster_namespace" = var.mariadb_cluster_namespace + } +} + + +resource "kubernetes_manifest" "database_user" { + manifest = { + apiVersion = "k8s.mariadb.com/v1alpha1" + kind = "User" + metadata = { + name = "${var.user_name}" + namespace = var.namespace + } + spec = { + mariaDbRef = { + name = var.mariadb_cluster + namespace = var.mariadb_cluster_namespace + } + passwordSecretKeyRef = { + name = kubernetes_secret.database_user_password.metadata[0].name + key = "password" + } + maxUserConnections = 2000 + host = "%" # Allow connections from any host + } + } + + lifecycle { + # Replace if any variables change + replace_triggered_by = [ + null_resource.replace-trigger, + kubernetes_secret.database_user_password + ] + } +} From 4e49a9f51bec8d2a49051b6b5fd9e377567a474f Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:37:48 +0100 Subject: [PATCH 019/148] refactor: update database name to match standard naming convention --- platform/ctfd.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform/ctfd.tf b/platform/ctfd.tf index 55aa86b..36bf7cd 100644 --- a/platform/ctfd.tf +++ b/platform/ctfd.tf @@ -45,7 +45,7 @@ module "ctfd-pull-secret" { } locals { - db_name = "ctfd-db-2" + db_name = "ctfd-db" } module "db-cluster" { From cacdf128ec64907a1d5f4f4a69a67a6a25029cee Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:43:38 +0100 Subject: [PATCH 020/148] feat: add mariadb cluster Terraform module --- platform/ctfd.tf | 2 + platform/tfvars/template.tfvars | 7 + platform/variables.tf | 6 + tf-modules/mariadb-cluster/backup.tf | 81 ++++++ tf-modules/mariadb-cluster/database.tf | 249 ++++++++++++++++++ tf-modules/mariadb-cluster/mariadb-cluster.tf | 10 + tf-modules/mariadb-cluster/variables.tf | 54 ++++ 7 files changed, 409 insertions(+) create mode 100644 tf-modules/mariadb-cluster/backup.tf create mode 100644 tf-modules/mariadb-cluster/database.tf create mode 100644 tf-modules/mariadb-cluster/mariadb-cluster.tf create mode 100644 tf-modules/mariadb-cluster/variables.tf diff --git a/platform/ctfd.tf b/platform/ctfd.tf index 36bf7cd..ef7b510 100644 --- a/platform/ctfd.tf +++ b/platform/ctfd.tf @@ -61,6 +61,8 @@ module "db-cluster" { s3_access_key = var.s3_access_key s3_secret_key = var.s3_secret_key + mariadb_version = var.mariadb_version + depends_on = [ kubernetes_namespace_v1.ctfd ] diff --git a/platform/tfvars/template.tfvars b/platform/tfvars/template.tfvars index 4a32f19..1a48321 100644 --- a/platform/tfvars/template.tfvars +++ b/platform/tfvars/template.tfvars @@ -113,3 +113,10 @@ ctfd_k8s_deployment_branch = "" # Git branch # image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # Docker image for Filebeat # image_ctfd_exporter = "ghcr.io/the0mikkel/ctfd-exporter:1.1.1" # Docker image for the CTFd Exporter +# ---------------------- +# Versions +# ---------------------- +# Values are maintained in the variables.tf file. +# You can override these values by uncommenting and setting your own versions here. + +# mariadb_version = "25.8.1" # The version of MariaDB deploy. More information at https://github.com/mariadb-operator/mariadb-operator diff --git a/platform/variables.tf b/platform/variables.tf index 89dcdc3..1546127 100644 --- a/platform/variables.tf +++ b/platform/variables.tf @@ -271,3 +271,9 @@ variable "image_ctfd_exporter" { description = "The docker image for CTFd Exporter" default = "ghcr.io/the0mikkel/ctfd-exporter:1.1.1" } + +variable "mariadb_version" { + type = string + description = "The version of MariaDB deploy. More information at https://github.com/mariadb-operator/mariadb-operator" + nullable = false +} diff --git a/tf-modules/mariadb-cluster/backup.tf b/tf-modules/mariadb-cluster/backup.tf new file mode 100644 index 0000000..49a868a --- /dev/null +++ b/tf-modules/mariadb-cluster/backup.tf @@ -0,0 +1,81 @@ +resource "kubernetes_secret_v1" "s3_access" { + metadata { + name = "s3-access-${var.cluster_name}" + namespace = var.namespace + } + + data = { + accessKey = var.s3_access_key + secretKey = var.s3_secret_key + } +} + +resource "kubernetes_manifest" "mariadb-cluster-backup" { + manifest = { + apiVersion = "k8s.mariadb.com/v1alpha1" + kind = "Backup" + + metadata = { + name = "db-backup-${var.cluster_name}" + namespace = var.namespace + } + + spec = { + mariaDbRef = { + name = var.cluster_name + namespace = var.namespace + } + + maxRetention = "720h" # 30 days + timeZone = "Europe/Copenhagen" + compression = "gzip" + + schedule = { + cron = "*/15 * * * *" # Every 15 minutes + suspend = false + } + + storage = { + s3 = { + bucket = var.s3_bucket + region = var.s3_region + prefix = "backups/mariadb/${var.cluster_name}" + endpoint = var.s3_endpoint + accessKeyIdSecretKeyRef = { + name = "s3-access-${var.cluster_name}" + key = "accessKey" + } + secretAccessKeySecretKeyRef = { + name = "s3-access-${var.cluster_name}" + key = "secretKey" + } + tls = { + enabled = true + } + } + } + + # args = [ + # "--single-transaction", + # "--all-databases" + # ] + + logLevel = "info" + + resources = { + requests = { + cpu = "100m" + memory = "128Mi" + } + limits = { + cpu = "300m" + memory = "1Gi" + } + } + } + } + + depends_on = [ + kubernetes_manifest.mariadb-cluster + ] +} diff --git a/tf-modules/mariadb-cluster/database.tf b/tf-modules/mariadb-cluster/database.tf new file mode 100644 index 0000000..864f29f --- /dev/null +++ b/tf-modules/mariadb-cluster/database.tf @@ -0,0 +1,249 @@ +resource "kubernetes_manifest" "maxscale" { + manifest = { + apiVersion = "k8s.mariadb.com/v1alpha1" + kind = "MaxScale" + + metadata = { + name = var.cluster_name + namespace = var.namespace + } + + spec = { + + mariaDbRef = { + name = var.cluster_name + } + + kubernetesService = { + type = "ClusterIP" + } + + guiKubernetesService = { + type = "ClusterIP" + } + + metrics = { + enabled = true + } + } + } +} + +resource "kubernetes_manifest" "mariadb-cluster" { + manifest = { + apiVersion = "k8s.mariadb.com/v1alpha1" + kind = "MariaDB" + + metadata = { + name = var.cluster_name + namespace = var.namespace + } + + spec = { + storage = { + size = "5Gi" + + # # Use cluster longhorn storage class + # storageClassName = "longhorn" + } + + replicas = 3 + + maxScaleRef = { + name = kubernetes_manifest.maxscale.manifest.metadata.name + } + + galera = { + enabled = true + primary = { + podIndex = 0 + automaticFailover = true + } + + sst = "mariabackup" + availableWhenDonor = false + galeraLibPath = "/usr/lib/galera/libgalera_smm.so" + replicaThreads = 4 + + agent = { + image = "docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:${var.mariadb_version}" + port = 5555 + kubernetesAuth = { + enabled = true + } + gracefulShutdownTimeout = "1s" + } + + + recovery = { + enabled = true + minClusterSize = 1 + forceClusterBootstrapInPod = "${var.cluster_name}-0" + clusterMonitorInterval = "10s" + clusterHealthyTimeout = "30s" + clusterBootstrapTimeout = "10m0s" + podRecoveryTimeout = "5m0s" + podSyncTimeout = "5m0s" + + job = { + metadata = { + labels = { + "sidecar.istio.io/inject" = "false" + } + } + resources = { + requests = { + cpu = "50m" + memory = "128Mi" + } + limits = { + cpu = "500m" + memory = "512Mi" + } + } + } + } + + initContainer = { + image = "docker-registry3.mariadb.com/mariadb-operator/mariadb-operator:${var.mariadb_version}" + } + + initJob = { + metadata = { + labels = { + "sidecar.istio.io/inject" = "false" + } + } + resources = { + requests = { + cpu = "100m" + memory = "128Mi" + } + limits = { + cpu = "500m" + memory = "512Mi" + } + } + } + + config = { + volumeClaimTemplate = { + # storageClassName = "longhorn" + resources = { + requests = { + storage = "2Gi" + } + } + accessModes = [ + "ReadWriteOnce" + ] + } + } + } + + service = { + type = "ClusterIP" + } + + connection = { + secretName = "${var.cluster_name}-conn" + secretTemplate = { + key = "dsn" + } + } + + primaryService = { + type = "ClusterIP" + } + + primaryConnection = { + secretName = "${var.cluster_name}-conn-primary" + secretTemplate = { + key = "dsn" + } + } + + secondaryService = { + type = "ClusterIP" + } + + secondaryConnection = { + secretName = "${var.cluster_name}-conn-secondary" + secretTemplate = { + key = "dsn" + } + } + + affinity = { + antiAffinityEnabled = true + } + + tolerations = [ + { + key = "k8s.mariadb.com/ha" + operator = "Exists" + effect = "NoSchedule" + } + ] + + podDisruptionBudget = { + maxUnavailable = "66%" + } + + updateStrategy = { + autoUpdateDataPlane = true + type = "RollingUpdate" + rollingUpdate = { + maxUnavailable = 1 + } + } + + priorityClassName = "system-node-critical" + + myCnf = < Date: Mon, 8 Dec 2025 23:44:17 +0100 Subject: [PATCH 021/148] feat: add redis Terraform module --- .../redis/config/redis-cluster-monitor.yml | 19 ++++ tf-modules/redis/config/redis-cluster.yml | 99 +++++++++++++++++++ .../config/redis-replication-monitor.yml | 18 ++++ tf-modules/redis/config/redis-replication.yml | 65 ++++++++++++ .../redis/config/redis-sentinel-monitor.yml | 19 ++++ tf-modules/redis/config/redis-sentinel.yml | 61 ++++++++++++ .../redis/config/redis-standalone-monitor.yml | 18 ++++ tf-modules/redis/config/redis-standalone.yml | 42 ++++++++ tf-modules/redis/redis-svc.tf | 53 ++++++++++ tf-modules/redis/redis.tf | 65 ++++++++++++ 10 files changed, 459 insertions(+) create mode 100644 tf-modules/redis/config/redis-cluster-monitor.yml create mode 100644 tf-modules/redis/config/redis-cluster.yml create mode 100644 tf-modules/redis/config/redis-replication-monitor.yml create mode 100644 tf-modules/redis/config/redis-replication.yml create mode 100644 tf-modules/redis/config/redis-sentinel-monitor.yml create mode 100644 tf-modules/redis/config/redis-sentinel.yml create mode 100644 tf-modules/redis/config/redis-standalone-monitor.yml create mode 100644 tf-modules/redis/config/redis-standalone.yml create mode 100644 tf-modules/redis/redis-svc.tf create mode 100644 tf-modules/redis/redis.tf diff --git a/tf-modules/redis/config/redis-cluster-monitor.yml b/tf-modules/redis/config/redis-cluster-monitor.yml new file mode 100644 index 0000000..6c8b849 --- /dev/null +++ b/tf-modules/redis/config/redis-cluster-monitor.yml @@ -0,0 +1,19 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: redis-monitoring-cluster-${namespace} + namespace: prometheus + labels: + release: prometheus + redis-operator: "true" +spec: + selector: + matchLabels: + redis_setup_type: cluster + endpoints: + - port: redis-exporter + interval: 30s + scrapeTimeout: 10s + namespaceSelector: + matchNames: + - ${namespace} diff --git a/tf-modules/redis/config/redis-cluster.yml b/tf-modules/redis/config/redis-cluster.yml new file mode 100644 index 0000000..cd6a762 --- /dev/null +++ b/tf-modules/redis/config/redis-cluster.yml @@ -0,0 +1,99 @@ +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisCluster +metadata: + name: redis-cluster + namespace: ${namespace} +spec: + clusterSize: 3 + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + kubernetesConfig: + image: quay.io/opstree/redis:v7.0.15 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 101m + memory: 128Mi + limits: + cpu: 500m + memory: 1Gi + # redisSecret: + # name: redis-secret + # key: password + redisExporter: + enabled: true + image: quay.io/opstree/redis-exporter:v1.48.0 + imagePullPolicy: Always + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + cpu: 750m + memory: 128Mi + env: + - name: REDIS_EXPORTER_INCL_SYSTEM_METRICS + value: "true" + redisLeader: + readinessProbe: + failureThreshold: 5 + initialDelaySeconds: 30 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + livenessProbe: + failureThreshold: 5 + initialDelaySeconds: 30 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - redis-cluster-leader + topologyKey: "kubernetes.io/hostname" + redisFollower: + readinessProbe: + failureThreshold: 5 + initialDelaySeconds: 30 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + livenessProbe: + failureThreshold: 5 + initialDelaySeconds: 30 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: app + operator: In + values: + - redis-cluster-follower + topologyKey: "kubernetes.io/hostname" + storage: + volumeClaimTemplate: + spec: + # storageClassName: longhorn + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi + nodeConfVolume: true + nodeConfVolumeClaimTemplate: + spec: + # storageClassName: longhorn + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi diff --git a/tf-modules/redis/config/redis-replication-monitor.yml b/tf-modules/redis/config/redis-replication-monitor.yml new file mode 100644 index 0000000..1566ed1 --- /dev/null +++ b/tf-modules/redis/config/redis-replication-monitor.yml @@ -0,0 +1,18 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: redis-monitoring-replication + namespace: prometheus + labels: + release: prometheus +spec: + selector: + matchLabels: + app: redis-replication + endpoints: + - port: redis-exporter + interval: 30s + scrapeTimeout: 10s + namespaceSelector: + matchNames: + - ${namespace} diff --git a/tf-modules/redis/config/redis-replication.yml b/tf-modules/redis/config/redis-replication.yml new file mode 100644 index 0000000..1e99ed1 --- /dev/null +++ b/tf-modules/redis/config/redis-replication.yml @@ -0,0 +1,65 @@ +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisReplication +metadata: + name: redis-replication + namespace: ${namespace} +spec: + clusterSize: 3 + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + kubernetesConfig: + image: quay.io/opstree/redis:v7.0.15 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 101m + memory: 128Mi + limits: + cpu: 500m + memory: 1Gi + updateStrategy: + type: RollingUpdate + # redisSecret: + # name: redis-secret + # key: password + redisExporter: + enabled: true + image: quay.io/opstree/redis-exporter:v1.48.0 + imagePullPolicy: Always + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + env: + - name: REDIS_EXPORTER_INCL_SYSTEM_METRICS + value: "true" + readinessProbe: + failureThreshold: 5 + initialDelaySeconds: 30 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + livenessProbe: + failureThreshold: 5 + initialDelaySeconds: 30 + periodSeconds: 15 + successThreshold: 1 + timeoutSeconds: 5 + storage: + volumeClaimTemplate: + spec: + # storageClassName: longhorn + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 10Gi + sentinel: + customConfig: + - down-after-milliseconds mymaster 2000 + - failover-timeout mymaster 10000 + - parallel-syncs mymaster 1 + diff --git a/tf-modules/redis/config/redis-sentinel-monitor.yml b/tf-modules/redis/config/redis-sentinel-monitor.yml new file mode 100644 index 0000000..edb1dfb --- /dev/null +++ b/tf-modules/redis/config/redis-sentinel-monitor.yml @@ -0,0 +1,19 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: redis-monitoring-sentinel-${namespace} + namespace: prometheus + labels: + release: prometheus + redis-operator: "true" +spec: + selector: + matchLabels: + redis_setup_type: sentinel + endpoints: + - port: redis-exporter + interval: 30s + scrapeTimeout: 10s + namespaceSelector: + matchNames: + - ${namespace} diff --git a/tf-modules/redis/config/redis-sentinel.yml b/tf-modules/redis/config/redis-sentinel.yml new file mode 100644 index 0000000..b7bceae --- /dev/null +++ b/tf-modules/redis/config/redis-sentinel.yml @@ -0,0 +1,61 @@ +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: RedisSentinel +metadata: + name: redis-sentinel + namespace: ${namespace} +spec: + clusterSize: 3 + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + pdb: + enabled: false + minAvailable: 1 + redisSentinelConfig: + redisReplicationName: redis-replication + kubernetesConfig: + image: quay.io/opstree/redis-sentinel:v7.0.15 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 101m + memory: 128Mi + limits: + cpu: 500m + memory: 1Gi + redisExporter: + enabled: true + image: quay.io/opstree/redis-exporter:v1.48.0 + imagePullPolicy: Always + resources: + requests: + cpu: 10m + memory: 128Mi + limits: + cpu: 750m + memory: 128Mi + env: + - name: REDIS_EXPORTER_INCL_SYSTEM_METRICS + value: "true" + # affinity: + # podAntiAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # - labelSelector: + # matchExpressions: + # - key: app + # operator: In + # values: + # - redis-sentinel-sentinel + # topologyKey: "kubernetes.io/hostname" + # readinessProbe: + # failureThreshold: 5 + # initialDelaySeconds: 30 + # periodSeconds: 15 + # successThreshold: 1 + # timeoutSeconds: 5 + # livenessProbe: + # failureThreshold: 5 + # initialDelaySeconds: 30 + # periodSeconds: 15 + # successThreshold: 1 + # timeoutSeconds: 5 diff --git a/tf-modules/redis/config/redis-standalone-monitor.yml b/tf-modules/redis/config/redis-standalone-monitor.yml new file mode 100644 index 0000000..698bef5 --- /dev/null +++ b/tf-modules/redis/config/redis-standalone-monitor.yml @@ -0,0 +1,18 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: redis-monitoring-standalone + namespace: prometheus + labels: + release: prometheus +spec: + selector: + matchLabels: + app: redis-standalone + endpoints: + - port: redis-exporter + interval: 30s + scrapeTimeout: 10s + namespaceSelector: + matchNames: + - ${namespace} diff --git a/tf-modules/redis/config/redis-standalone.yml b/tf-modules/redis/config/redis-standalone.yml new file mode 100644 index 0000000..3528c88 --- /dev/null +++ b/tf-modules/redis/config/redis-standalone.yml @@ -0,0 +1,42 @@ +apiVersion: redis.redis.opstreelabs.in/v1beta2 +kind: Redis +metadata: + name: redis-standalone + namespace: ${namespace} +spec: + podSecurityContext: + runAsUser: 1000 + fsGroup: 1000 + kubernetesConfig: + image: quay.io/opstree/redis:v7.0.12 + imagePullPolicy: IfNotPresent + resources: + requests: + cpu: 101m + memory: 128Mi + limits: + cpu: 101m + memory: 128Mi + redisExporter: + enabled: true + image: quay.io/opstree/redis-exporter:v1.48.0 + imagePullPolicy: Always + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 100m + memory: 128Mi + env: + - name: REDIS_EXPORTER_INCL_SYSTEM_METRICS + value: "true" + storage: + volumeClaimTemplate: + spec: + # storageClassName: longhorn + storageClassName: hcloud-volumes + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi diff --git a/tf-modules/redis/redis-svc.tf b/tf-modules/redis/redis-svc.tf new file mode 100644 index 0000000..bb98ac4 --- /dev/null +++ b/tf-modules/redis/redis-svc.tf @@ -0,0 +1,53 @@ +resource "kubernetes_service" "redis_cluster_leaders" { + metadata { + name = "redis-cluster-leaders" + namespace = var.namespace + labels = { + app = "redis-cluster-leader" + redis_setup_type = "cluster" + role = "leader" + } + } + + spec { + selector = { + app = "redis-cluster-leader" + redis_setup_type = "cluster" + role = "leader" + } + port { + name = "redis-client" + port = 6379 + target_port = 6379 + protocol = "TCP" + } + # type = "NodePort" + } +} + +resource "kubernetes_service" "redis_cluster_followers" { + metadata { + name = "redis-cluster-followers" + namespace = var.namespace + labels = { + app = "redis-cluster-follower" + redis_setup_type = "cluster" + role = "follower" + } + } + + spec { + selector = { + app = "redis-cluster-follower" + redis_setup_type = "cluster" + role = "follower" + } + port { + name = "redis-client" + port = 6379 + target_port = 6379 + protocol = "TCP" + } + # type = "NodePort" + } +} diff --git a/tf-modules/redis/redis.tf b/tf-modules/redis/redis.tf new file mode 100644 index 0000000..d4ff9d2 --- /dev/null +++ b/tf-modules/redis/redis.tf @@ -0,0 +1,65 @@ +variable "namespace" { + description = "Namespace for the Redis cluster" + type = string +} + +variable "redis_password" { + description = "Password for the Redis cluster" + type = string + sensitive = true + +} + +resource "kubernetes_secret_v1" "redis_secret" { + metadata { + name = "redis-secret" + namespace = var.namespace + } + + data = { + "password" = var.redis_password + } +} + +resource "kubernetes_manifest" "redis-cluster" { + manifest = yamldecode(templatefile("${path.module}/config/redis-cluster.yml", { + namespace = var.namespace + })) +} + +resource "kubernetes_manifest" "redis-cluster-monitor" { + manifest = yamldecode(templatefile("${path.module}/config/redis-cluster-monitor.yml", { + namespace = var.namespace + })) +} + +# resource "kubernetes_manifest" "redis_replication" { +# manifest = yamldecode(templatefile("${path.module}/config/redis-replication.yml", { +# namespace = var.namespace +# })) +# } + +# resource "kubernetes_manifest" "redis_sentinel" { +# manifest = yamldecode(templatefile("${path.module}/config/redis-sentinel.yml", { +# namespace = var.namespace +# })) +# } + +# resource "kubernetes_manifest" "redis_sentinel_monitor" { +# manifest = yamldecode(templatefile("${path.module}/config/redis-sentinel-monitor.yml", { +# namespace = var.namespace +# })) +# } + +# resource "kubernetes_manifest" "redis_standalone" { +# manifest = yamldecode(templatefile("${path.module}/config/redis-standalone.yml", { +# namespace = var.namespace +# })) +# } + +# resource "kubernetes_manifest" "redis_standalone_monitoring" { +# manifest = yamldecode(templatefile("${path.module}/config/redis-standalone-monitor.yml", { +# namespace = var.namespace +# })) +# } + From 102c39a3ed1ee19e7ed6478fc24e6dfaad83a9f9 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 8 Dec 2025 23:54:05 +0100 Subject: [PATCH 022/148] feat: add kubectf Terraform module --- challenges/kube-ctf.tf | 4 +- challenges/tfvars/template.tfvars | 1 + challenges/variables.tf | 6 + tf-modules/kubectf/challenge-manager.tf | 361 ++++++++++++++++++++++++ tf-modules/kubectf/crd.tf | 54 ++++ tf-modules/kubectf/kube-janitor.tf | 187 ++++++++++++ tf-modules/kubectf/landing.tf | 211 ++++++++++++++ tf-modules/kubectf/namespaces.tf | 101 +++++++ tf-modules/kubectf/networkpolicy.tf | 119 ++++++++ tf-modules/kubectf/providers.tf | 83 ++++++ tf-modules/kubectf/traefik.tf | 30 ++ 11 files changed, 1155 insertions(+), 2 deletions(-) create mode 100644 tf-modules/kubectf/challenge-manager.tf create mode 100644 tf-modules/kubectf/crd.tf create mode 100644 tf-modules/kubectf/kube-janitor.tf create mode 100644 tf-modules/kubectf/landing.tf create mode 100644 tf-modules/kubectf/namespaces.tf create mode 100644 tf-modules/kubectf/networkpolicy.tf create mode 100644 tf-modules/kubectf/providers.tf create mode 100644 tf-modules/kubectf/traefik.tf diff --git a/challenges/kube-ctf.tf b/challenges/kube-ctf.tf index 954b433..837c0e9 100644 --- a/challenges/kube-ctf.tf +++ b/challenges/kube-ctf.tf @@ -2,9 +2,9 @@ module "kube_ctf" { source = "../tf-modules/kubectf" challenge_dns = var.cluster_dns_ctf - management_dns = var.cluster_dns_ctf + management_dns = var.cluster_dns_management - org_name = "io" + org_name = "ctfpilot.com" cert_manager = "cert-manager-global" management_auth_secret = var.kubectf_auth_secret diff --git a/challenges/tfvars/template.tfvars b/challenges/tfvars/template.tfvars index 2ab6f38..38808ab 100644 --- a/challenges/tfvars/template.tfvars +++ b/challenges/tfvars/template.tfvars @@ -7,6 +7,7 @@ kubeconfig = "AA==" # Base64 encoded kubeconfig file # Generic information # ------------------------ environment = "test" # Environment name for the CTF +cluster_dns_management = "" # The specific domain name to use for the DNS records for the management part of the cluster cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster # ------------------------ diff --git a/challenges/variables.tf b/challenges/variables.tf index beee62b..a0418dc 100644 --- a/challenges/variables.tf +++ b/challenges/variables.tf @@ -16,6 +16,12 @@ variable "environment" { nullable = false } +variable "cluster_dns_management" { + type = string + description = "The specific domain name to use for the DNS records for the management part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_management`" + nullable = false +} + variable "cluster_dns_ctf" { type = string description = "The domain name to use for the DNS records for the CTF challenges part of the cluster. Must be the TLD or subdomain of `cloudflare_dns_ctf`" diff --git a/tf-modules/kubectf/challenge-manager.tf b/tf-modules/kubectf/challenge-manager.tf new file mode 100644 index 0000000..d8e0ca8 --- /dev/null +++ b/tf-modules/kubectf/challenge-manager.tf @@ -0,0 +1,361 @@ +resource "kubernetes_secret_v1" "challenge-manager" { + metadata { + name = "challenge-manager" + namespace = local.management_namespace + } + + data = { + "auth" = var.management_auth_secret + "container" = var.container_secret + } + + depends_on = [ + kubernetes_namespace.management + ] +} + +resource "kubernetes_cluster_role_binding_v1" "challenge-management" { + metadata { + name = "kubectf-challenge-manager-read-instanced-challenges" + } + + role_ref { + kind = "ClusterRole" + name = kubernetes_cluster_role_v1.challenge-management.metadata.0.name + api_group = "rbac.authorization.k8s.io" + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account_v1.challenge-manager.metadata.0.name + namespace = local.management_namespace + } + + depends_on = [ + kubernetes_cluster_role_v1.challenge-management, + kubernetes_service_account_v1.challenge-manager + ] +} + +resource "kubernetes_role_binding_v1" "challenge-management" { + metadata { + name = "challenge-manager" + namespace = local.instanced_challenge_namespace + } + + role_ref { + kind = "Role" + name = kubernetes_role_v1.challenge-management.metadata.0.name + api_group = "rbac.authorization.k8s.io" + } + + subject { + kind = "ServiceAccount" + name = kubernetes_service_account_v1.challenge-manager.metadata.0.name + namespace = local.management_namespace + } + + depends_on = [ + kubernetes_role_v1.challenge-management + ] +} + +resource "kubernetes_cluster_role_v1" "challenge-management" { + metadata { + name = "kubectf-read-instanced-challenges" + } + + rule { + api_groups = ["kube-ctf.${var.org_name}"] + resources = ["instanced-challenges"] + verbs = ["get", "list"] + } +} + +resource "kubernetes_role_v1" "challenge-management" { + metadata { + name = "challenge-manager" + namespace = local.instanced_challenge_namespace + } + + rule { + api_groups = ["*"] + resources = [ + "ingresses", + "ingressroutes", + "ingressroutetcps", + "pods", + "deployments", + "services", + "namespaces", + "secrets", + "networkpolicies" + ] + verbs = [ + "create", + "delete", + "get", + "list", + "patch", + "update", + "watch" + ] + } + + depends_on = [ + kubernetes_namespace.management + ] +} + +resource "kubernetes_service_account_v1" "challenge-manager" { + metadata { + name = "challenge-manager" + namespace = local.management_namespace + + labels = { + system = "kube-ctf" + org = var.org_name + } + } + + depends_on = [ + kubernetes_namespace.management + ] +} + +locals { + management_dns = "manager.${var.management_dns}" +} + +resource "kubernetes_deployment_v1" "challenge-manager" { + metadata { + name = "challenge-manager" + namespace = local.management_namespace + + labels = { + system = "kube-ctf" + org = var.org_name + + "app.kubernetes.io/name" = "kube-ctf-challenge-manager" + "app.kubernetes.io/instance" = "kubectf" + "app.kubernetes.io/component" = "challenge-manager" + + "kube-ctf.${var.org_name}/service" = "challenge-manager" + } + } + + spec { + replicas = var.services_replicas + + selector { + match_labels = { + "kube-ctf.${var.org_name}/service" = "challenge-manager" + } + } + + template { + metadata { + labels = { + "kube-ctf.${var.org_name}/service" = "challenge-manager" + } + } + + spec { + service_account_name = kubernetes_service_account_v1.challenge-manager.metadata.0.name + + image_pull_secrets { + name = var.ghcr_token != "" ? module.pull-secret[local.management_namespace].pull-secret : "" + } + + container { + name = "challenge-manager" + image = var.image_challenge_manager + image_pull_policy = "Always" + + + port { + container_port = 3000 + } + + readiness_probe { + http_get { + path = "/healthz" + port = 3000 + } + initial_delay_seconds = 10 + period_seconds = 10 + } + + liveness_probe { + http_get { + path = "/healthz" + port = 3000 + } + initial_delay_seconds = 30 + period_seconds = 10 + } + + env { + name = "KUBECTF_BASE_DOMAIN" + value = local.challenges_host + } + + env { + name = "KUBECTF_API_DOMAIN" + value = local.management_dns + } + + env { + name = "KUBECTF_NAMESPACE" + value = local.instanced_challenge_namespace + } + + env { + name = "KUBECTF_MAX_OWNER_DEPLOYMENTS" + value = var.max_instances + } + + env { + name = "KUBECTF_REGISTRY_PREFIX" + value = var.registry_prefix + } + + env { + name = "KUBECTF_AUTH_SECRET" + value_from { + secret_key_ref { + name = kubernetes_secret_v1.challenge-manager.metadata.0.name + key = "auth" + } + } + } + + env { + name = "KUBECTF_CONTAINER_SECRET" + value_from { + secret_key_ref { + name = kubernetes_secret_v1.challenge-manager.metadata.0.name + key = "container" + } + } + } + + resources { + limits = { + cpu = "250m" + memory = "512Mi" + } + requests = { + cpu = "10m" + memory = "128Mi" + } + } + } + } + } + } + + depends_on = [ + kubernetes_service_account_v1.challenge-manager, + kubernetes_secret_v1.challenge-manager, + kubernetes_cluster_role_binding_v1.challenge-management, + kubernetes_role_binding_v1.challenge-management, + kubernetes_cluster_role_v1.challenge-management, + kubernetes_role_v1.challenge-management, + module.pull-secret, + local.challenges_host + ] +} + +resource "kubernetes_service_v1" "challenge-manager" { + metadata { + name = "challenge-manager" + namespace = local.management_namespace + + labels = { + system = "kube-ctf" + org = var.org_name + + "app.kubernetes.io/name" = "kube-ctf-challenge-manager-service" + "app.kubernetes.io/instance" = "kubectf" + "app.kubernetes.io/component" = "challenge-manager" + + "kube-ctf.${var.org_name}/service" = "challenge-manager" + } + } + + spec { + selector = { + "kube-ctf.${var.org_name}/service" = "challenge-manager" + } + + port { + port = 3000 + } + } + + depends_on = [ + kubernetes_deployment_v1.challenge-manager + ] +} + +resource "kubernetes_ingress_v1" "challenge-manager" { + metadata { + name = "challenge-manager" + namespace = local.management_namespace + + labels = { + system = "kube-ctf" + org = var.org_name + + "app.kubernetes.io/name" = "kube-ctf-challenge-manager-ingress" + "app.kubernetes.io/instance" = "kubectf" + "app.kubernetes.io/component" = "challenge-manager" + + "kube-ctf.${var.org_name}/service" = "challenge-manager" + } + + annotations = { + "cert-manager.io/cluster-issuer" = var.cert_manager + "traefik.ingress.kubernetes.io/router.priority" = "10" + "traefik.ingress.kubernetes.io/router.middlewares" = "errors-errors@kubernetescrd" + } + } + + spec { + tls { + hosts = [ + local.management_dns + ] + + secret_name = "kubectf-cert-challenge-manager" + } + + rule { + host = local.management_dns + + http { + path { + path = "/" + path_type = "Prefix" + backend { + service { + name = kubernetes_service_v1.challenge-manager.metadata.0.name + port { + number = 3000 + } + } + } + } + } + } + } + + depends_on = [ + kubernetes_service_v1.challenge-manager + ] +} + +output "challenge_manager_host" { + value = local.management_dns +} diff --git a/tf-modules/kubectf/crd.tf b/tf-modules/kubectf/crd.tf new file mode 100644 index 0000000..3871e3b --- /dev/null +++ b/tf-modules/kubectf/crd.tf @@ -0,0 +1,54 @@ +resource "kubernetes_manifest" "crd" { + manifest = { + apiVersion = "apiextensions.k8s.io/v1" + kind = "CustomResourceDefinition" + metadata = { + name = "instanced-challenges.kube-ctf.${var.org_name}" + } + spec = { + group = "kube-ctf.${var.org_name}" + names = { + + plural = "instanced-challenges" + singular = "isolated-challenge" + kind = "IsolatedChallenge" + shortNames = [ + "isolated-challenge" + ] + } + versions = [ + { + name = "v1" + served = true + storage = true + schema = { + openAPIV3Schema = { + type = "object" + properties = { + spec = { + type = "object" + properties = { + expires = { + type = "integer" + } + available_at = { + type = "integer" + } + type = { + type = "string" + } + template = { + type = "string" + type = "string" + } + } + } + } + } + } + } + ] + scope = "Cluster" + } + } +} diff --git a/tf-modules/kubectf/kube-janitor.tf b/tf-modules/kubectf/kube-janitor.tf new file mode 100644 index 0000000..f9b331a --- /dev/null +++ b/tf-modules/kubectf/kube-janitor.tf @@ -0,0 +1,187 @@ +resource "kubernetes_config_map_v1" "kube-janitor" { + metadata { + name = "kube-janitor" + namespace = local.management_namespace + + labels = { + system = "kube-ctf" + org = var.org_name + + "app.kubernetes.io/name" = "kube-ctf-kube-janitor-config" + "app.kubernetes.io/instance" = "kubectf" + "app.kubernetes.io/component" = "kube-janitor" + } + } + + data = { + "rules.yaml" = < Date: Tue, 9 Dec 2025 00:00:06 +0100 Subject: [PATCH 023/148] docs: add warning about system publishing status --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 351c2b4..0655eea 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # CTFp - CTF Pilot's CTF Platform +> [!WARNING] +> +> We are currently in the process of publishing the CTFp system. +> Meanwhile, some components may not be present or fully functional. + ## Contributing We welcome contributions of all kinds, from **code** and **documentation** to **bug reports** and **feedback**! From 248d0a049d793ab8649afc765340c875545eb7cd Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Thu, 11 Dec 2025 11:20:08 +0100 Subject: [PATCH 024/148] Add template for automated setup configuration in terraform variables --- .env.example | 2 + cli.py | 1076 +++++++++++++++++++++++++++++++++++++ template.automated.tfvars | 220 ++++++++ 3 files changed, 1298 insertions(+) create mode 100644 .env.example create mode 100644 cli.py create mode 100644 template.automated.tfvars diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5fe1f9d --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= \ No newline at end of file diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..637f744 --- /dev/null +++ b/cli.py @@ -0,0 +1,1076 @@ +import os +import sys +import argparse +import time +import subprocess + +AUTO_APPLY = True +ENVIRONMENTS = ["test", "dev", "prod"] +FLAVOR = "tofu" # Can be "terraform" or "tofu" + +CLUSTER_TFVARS = [ + # Server configuration + "region_1", + "region_2", + "region_3", + "control_plane_type_1", + "control_plane_type_2", + "control_plane_type_3", + "agent_type_1", + "agent_type_2", + "agent_type_3", + "scale_type", + "control_plane_count_1", + "control_plane_count_2", + "control_plane_count_3", + "agent_count_1", + "agent_count_2", + "agent_count_3", + "scale_count", + "scale_min", + "load_balancer_type", + + # Cluster configuration + "hcloud_token", + "ssh_key_private_base64", + "ssh_key_public_base64", + "cloudflare_api_token", + "cloudflare_dns_management", + "cloudflare_dns_ctf", + "cloudflare_dns_platform", + "cluster_dns_management", + "cluster_dns_ctf", + "cluster_dns_platform", +] +CONTENT_TFVARS = [ + "cloudflare_api_token", + "cloudflare_dns_management", + "cloudflare_dns_ctf", + "cloudflare_dns_platform", + "cluster_dns_management", + "cluster_dns_ctf", + "email", + "argocd_github_secret", + "argocd_admin_password", + "grafana_admin_password", + "discord_webhook_url", + "traefik_basic_auth", + + "filebeat_elasticsearch_host", + "filebeat_elasticsearch_username", + "filebeat_elasticsearch_password", + + "ghcr_username", + "ghcr_token", +] +PLATFORM_TFVARS = [ + "cluster_dns_ctf", + "cluster_dns_platform", + "ghcr_username", + "ghcr_token", + "git_token", + "kubectf_auth_secret", + "db_root_password", + "db_user", + "db_password", + "ctfd_manager_password", + + # S3 configuration + "s3_bucket", + "s3_region", + "s3_endpoint", + "s3_access_key", + "s3_secret_key", + + # Elasticsearch configuration + "filebeat_elasticsearch_host", + "filebeat_elasticsearch_username", + "filebeat_elasticsearch_password", + + # CTFd configuration + "ctfd_secret_key", + "ctf_name", + "ctf_description", + "ctf_start_time", + "ctf_end_time", + "ctf_user_mode", + "ctf_challenge_visibility", + "ctf_account_visibility", + "ctf_score_visibility", + "ctf_registration_visibility", + "ctf_verify_emails", + "ctf_team_size", + "ctf_brackets", + "ctf_theme", + "ctf_admin_name", + "ctf_admin_email", + "ctf_admin_password", + "ctf_registration_code", + "ctf_mail_server", + "ctf_mail_port", + "ctf_mail_username", + "ctf_mail_password", + "ctf_mail_tls", + "ctf_mail_from", + "ctf_logo_path", + "ctfd_discord_webhook_url", + "ctf_s3_bucket", + "ctf_s3_region", + "ctf_s3_endpoint", + "ctf_s3_access_key", + "ctf_s3_secret_key", + "ctf_s3_prefix", +] +CHALLENGES_TFVARS = [ + "cluster_dns_ctf", + "ghcr_username", + "ghcr_token", + "git_token", + "kubectf_auth_secret", + "kubectf_container_secret", + "chall_whitelist_ips", +] + +# Load env from .env +if os.path.exists(".env"): + with open(".env", "r") as f: + for line in f: + if line.strip() and not line.startswith("#"): + key, value = line.strip().split("=", 1) + os.environ[key.strip()] = value.strip() + +def run(cmd, shell=True): + """ + Run a subprocess in a new process group and forward KeyboardInterrupt (SIGINT) to it. + Returns the process returncode. + """ + import signal + proc = subprocess.Popen( + cmd, + shell=shell, + preexec_fn=os.setsid + ) + try: + proc.wait() + except KeyboardInterrupt: + os.killpg(proc.pid, signal.SIGINT) + proc.wait() + return proc.returncode + +class Args: + command = None + parser = None + + def __init__(self): + self.parser = argparse.ArgumentParser(description="Platform CLI") + + def print_help(self): + if self.parser is None: + Logger.error("Parser is not initialized") + exit(1) + + self.parser.print_help() + +class Utils: + @staticmethod + def get_path_to_script(): + path = os.path.dirname(os.path.realpath(__file__)) + + # Check if the path contains spaces + if " " in path: + Logger.error("Path to script contains spaces. Please move the script to a path without spaces") + exit(1) + + return path + + @staticmethod + def extract_tuple_from_list(list, key): + for item in list: + if key in item: + return item + return None + +class Logger: + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + RESET = "\033[0m" + + @staticmethod + def error(message): + print(f"{Logger.RED}Error: {message}{Logger.RESET}") + exit(1) + + @staticmethod + def info(message): + print(f"{Logger.BLUE}Info: {message}{Logger.RESET}") + + @staticmethod + def success(message): + print(f"{Logger.GREEN}Success: {message}{Logger.RESET}") + + @staticmethod + def warning(message): + print(f"{Logger.YELLOW}Warning: {message}{Logger.RESET}") + + @staticmethod + def debug(message): + print(f"{Logger.BLUE}Debug: {message}{Logger.RESET}") + + @staticmethod + def space(): + print("") + +''' +Subcommand pattern +''' +class Command: + name = "Command" + help = "Command" + description = "Command" + + def __init__(self, subparser): + self.subparser = subparser.add_parser(self.name, help=self.help, description=self.description) + self.subparser.set_defaults(func=self.run) + + def register_subcommand(self): + raise NotImplementedError + + def run(self, args): + raise NotImplementedError + +class GenerateImages(Command): + name = "generate-images" + help = "Generate server images" + description = "Generate server images" + + def register_subcommand(self): + # No arguments to register + return + + def run(self, args): + Logger.info("Generating server images") + path = Utils.get_path_to_script() + try: + rc = run(f"cd {path}/cluster && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Failed to generate images") + Logger.success("Images generated successfully") + + +''' +Initialize automated.tfvars with the template +''' +class InitializeTFVars(Command): + name = "init" + help = "Initialize automated.tfvars" + description = "Initialize automated.tfvars" + environment = "test" # Default environment + + def register_subcommand(self): + self.subparser.add_argument("--force", action="store_true", help="Force overwrite automated.tfvars") + self.subparser.add_argument("--test", action="store_true", help="Work with TEST cluster (default)") + self.subparser.add_argument("--dev", action="store_true", help="Work with DEV cluster") + self.subparser.add_argument("--prod", action="store_true", help="Work with PROD cluster") + return + + def run(self, args): + if [args.test, args.dev, args.prod].count(True) > 1: + Logger.error("Please specify only one environment: --test, --dev or --prod") + exit(1) + + self.environment = "test" + if args.dev: + self.environment = "dev" + elif args.prod: + self.environment = "prod" + + Logger.info(f"Initializing {self.get_filename_tfvars()} (ENV: {self.environment})") + path = Utils.get_path_to_script() + template = f"{path}/template.{self.get_filename_tfvars()}" + destination = f"{path}/{self.get_filename_tfvars()}" + + # Check if destination file already exists + if os.path.exists(destination) and not args.force: + Logger.warning(f"{self.get_filename_tfvars()} already exists") + + # Ask user if they want to overwrite the file + response = input("Do you want to overwrite the file? (y/N): ") + if response.lower() != "y": + Logger.info("Exiting") + exit(0) + + # Clone the template to the destination + try: + os_output = os.system(f"cp {template} {destination}") + if os_output != 0: + raise Exception + except: + Logger.error(f"Failed to initialize {self.get_filename_tfvars()}") + Logger.success(f"{self.get_filename_tfvars()} initialized successfully") + + def get_filename_tfvars(self): + return TFVARS.get_filename_tfvars(self.environment) + +''' +Generate RSA keys +''' +class GenerateKeys(Command): + name = "generate-keys" + help = "Generate RSA keys" + description = "Generate RSA keys" + environment = "test" # Default environment + + def register_subcommand(self): + self.subparser.add_argument("--insert", action="store_true", help="Insert keys into automated.tfvars") + self.subparser.add_argument("--test", action="store_true", help="Work with TEST cluster (default)") + self.subparser.add_argument("--dev", action="store_true", help="Work with DEV cluster") + self.subparser.add_argument("--prod", action="store_true", help="Work with PROD cluster") + return + + def run(self, args): + if [args.test, args.dev, args.prod].count(True) > 1: + Logger.error("Please specify only one environment: --test, --dev or --prod") + exit(1) + + self.environment = "test" + if args.dev: + self.environment = "dev" + elif args.prod: + self.environment = "prod" + + Logger.info("Generating RSA keys") + path = Utils.get_path_to_script() + try: + rc = run([f"{path}/data/keys/create.sh"], shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Failed to generate keys") + + Logger.success("Keys generated successfully in data/keys/ using ed25519") + Logger.info("Public key: data/keys/k8s.pub") + Logger.info("Private key: data/keys/k8s") + + # Insert keys into automated.tfvars + if args.insert: + TFVARS.insert_keys(self.environment) + Logger.success(f"Keys inserted successfully into {TFVARS.get_filename_tfvars(self.environment)}") + +''' +Insert SSH keys into automated.tfvars +''' +class InsertKeys(Command): + name = "insert-keys" + help = "Insert SSH keys into automated.tfvars" + description = "Insert SSH keys into automated.tfvars" + + def register_subcommand(self): + self.subparser.add_argument("--test", action="store_true", help="Works with TEST cluster (default)") + self.subparser.add_argument("--dev", action="store_true", help="Works with DEV cluster") + self.subparser.add_argument("--prod", action="store_true", help="Works with PROD cluster") + return + + def run(self, args): + if [args.test, args.dev, args.prod].count(True) > 1: + Logger.error("Please specify only one environment: --test, --dev or --prod") + exit(1) + + self.environment = "test" + if args.dev: + self.environment = "dev" + elif args.prod: + self.environment = "prod" + + Logger.info(f"Inserting SSH keys into {TFVARS.get_filename_tfvars(self.environment)}") + TFVARS.insert_keys(self.environment) + Logger.success(f"Keys inserted successfully into {TFVARS.get_filename_tfvars(self.environment)}") + +''' +Deploy the platform +''' +class Deploy(Command): + name = "deploy" + help = "Deploy the platform" + description = "Deploy the platform" + times = [] + environment = "test" # Default environment + + def register_subcommand(self): + # Only run listed parts of the deployment + self.subparser.add_argument("--cluster", action="store_true", help="Deploy the cluster") + self.subparser.add_argument("--content", action="store_true", help="Deploy the content") + self.subparser.add_argument("--platform", action="store_true", help="Deploy the platform") + self.subparser.add_argument("--challenges", action="store_true", help="Deploy the challenges") + self.subparser.add_argument("--all", action="store_true", help="Deploy all parts of the platform") + self.subparser.add_argument("--test", action="store_true", help="Deploy TEST cluster (default)") + self.subparser.add_argument("--dev", action="store_true", help="Deploy DEV cluster") + self.subparser.add_argument("--prod", action="store_true", help="Deploy PROD cluster") + return + + def run(self, args): + if not args.cluster and not args.content and not args.platform and not args.challenges and not args.all: + Logger.error("Please specify which part of the platform to deploy") + exit(1) + + if args.all and (args.cluster or args.content or args.platform or args.challenges): + Logger.error("Please specify only --all or individual parts of the platform") + exit(1) + + if [args.test, args.dev, args.prod].count(True) > 1: + Logger.error("Please specify only one environment: --test, --dev or --prod") + exit(1) + + if args.prod: + AUTO_APPLY = False # Disable auto-apply for production environment + + deploy_all = args.all + deploy_cluster = args.cluster or deploy_all + deploy_content = args.content or deploy_all + deploy_platform = args.platform or deploy_all + deploy_challenges = args.challenges or deploy_all + + self.environment = "test" + if args.dev: + self.environment = "dev" + elif args.prod: + self.environment = "prod" + + self.times.append(("start", time.time())) + Logger.info("Deploying " + (self.environment.upper() if self.environment != "test" else "TEST") + " environment") + self.check_values() + Logger.space() + + if deploy_cluster: + start_time = time.time() + self.cluster_deploy() + self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + if deploy_content: + start_time = time.time() + self.content_deploy() + self.times.append(("content", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + if deploy_platform: + start_time = time.time() + self.platform_deploy() + self.times.append(("platform", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + if deploy_challenges: + start_time = time.time() + self.challenges_deploy() + self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + Logger.success("Platform deployed") + self.times.append(("end", time.time())) + + Logger.info(f"Time taken: {str(round(Utils.extract_tuple_from_list(self.times, 'end')[1] - Utils.extract_tuple_from_list(self.times, 'start')[1], 2))} seconds") + + if deploy_cluster: + Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") + if deploy_content: + Logger.info(f"Content time: {str(round(Utils.extract_tuple_from_list(self.times, 'content')[3], 2))} seconds") + if deploy_platform: + Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") + if deploy_challenges: + Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") + + ''' + Initialize Terraform to a given environment (workspace) + ''' + def init_terraform(self, path): + Logger.info("Initializing Terraform") + current_dir = os.getcwd() + os.chdir(path) + + try: + # Create workspaces + Logger.info("Creating workspaces if they do not exist") + for env in ENVIRONMENTS: + subprocess.run([FLAVOR, "workspace", "new", env], check=False) + + # Initialize the backend (if not already done for this project) + Logger.info("Running terraform init") + rc = run(f"{FLAVOR} init", shell=True) + if rc != 0: + raise Exception + + # Select the workspace based on the environment + Logger.info(f"Selecting workspace: {self.environment}") + rc = run(f"{FLAVOR} workspace select {self.environment}", shell=True) + if rc != 0: + raise Exception + except subprocess.CalledProcessError as e: + Logger.error("Terraform initialization failed") + raise e + finally: + os.chdir(current_dir) # Always change back to the original directory + Logger.success("Terraform initialized successfully") + + def get_filename_tfvars(self): + return TFVARS.get_filename_tfvars(self.environment) + + def get_path_tfvars(self): + path = Utils.get_path_to_script() + return f"{path}/{self.get_filename_tfvars()}" + + ''' + Validate automated.tfvars is set, and values are set + ''' + def check_values(self): + # Check if automated.tfvars exists + tfvars_path = self.get_path_tfvars() + if not os.path.exists(tfvars_path): + Logger.error(f"{self.get_filename_tfvars()} not found. Please create the file and try again") + exit(1) + + # Ensure no < or > are present in the file + with open(tfvars_path, "r") as file: + for line in file: + if "<" in line or ">" in line: + Logger.error(f"{self.get_filename_tfvars()} does not seem to be filled out. Please fill out all fields and try again") + exit(1) + + Logger.info(f"{self.get_filename_tfvars()} is filled out correctly") + + def cluster_deploy(self): + path = Utils.get_path_to_script() + Logger.info("Deploying the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars") + tfvars.create(CLUSTER_TFVARS) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{path}/cluster") + cmd = f"cd {path}/cluster && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}" + rc = run(cmd, shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Cluster terraform failed") + Logger.success("Cluster terraform applied successfully") + # Export kubeconfig + self.export_kubeconfig() + Logger.success("Cluster deployed successfully") + + def export_kubeconfig(self): + path = Utils.get_path_to_script() + Logger.info("Exporting kubeconfig") + + # Export kubeconfig + try: + rc = run(f"cd {path}/cluster && {FLAVOR} output --raw kubeconfig > {path}/kube-config/kube-config.{self.environment}.yml") + if rc != 0: + raise Exception + rc = run(f"cat {path}/kube-config/kube-config.{self.environment}.yml | base64 -w0 > {path}/kube-config/kube-config.{self.environment}.b64") + if rc != 0: + raise Exception + except: + Logger.error("Failed to export kubeconfig") + Logger.success("Kubeconfig exported") + + def get_kubeconfig_b64(self): + path = Utils.get_path_to_script() + with open(f"{path}/kube-config/kube-config.{self.environment}.b64", "r") as file: + return file.read() + + def content_deploy(self): + path = Utils.get_path_to_script() + Logger.info("Deploying the content on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/content/data.auto.tfvars") + tfvars.create(CONTENT_TFVARS) + tfvars.add("kubeconfig", self.get_kubeconfig_b64()) + tfvars.add("environment", self.environment) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{path}/content") + rc = run(f"cd {path}/content && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Content apply failed") + Logger.success("Content deployed successfully") + + def platform_deploy(self): + path = Utils.get_path_to_script() + Logger.info("Deploying the platform on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars") + tfvars.create(PLATFORM_TFVARS) + tfvars.add("kubeconfig", self.get_kubeconfig_b64()) + tfvars.add("environment", self.environment) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{path}/platform") + rc = run(f"cd {path}/platform && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Platform apply failed") + Logger.success("Platform deployed successfully") + + def challenges_deploy(self): + path = Utils.get_path_to_script() + Logger.info("Deploying the challenges on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars") + tfvars.create(CHALLENGES_TFVARS) + tfvars.add("kubeconfig", self.get_kubeconfig_b64()) + tfvars.add("environment", self.environment) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{path}/challenges") + rc = run(f"cd {path}/challenges && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Challenges apply failed") + Logger.success("Challenges deployed successfully") + +''' +Destroy the platform +''' +class Destroy(Command): + name = "destroy" + help = "Destroy the platform" + description = "Destroy the platform" + times = [] + environment = "test" # Default environment + + def register_subcommand(self): + # Only run listed parts of the destruction + self.subparser.add_argument("--cluster", action="store_true", help="Destroy the cluster") + self.subparser.add_argument("--content", action="store_true", help="Destroy the content") + self.subparser.add_argument("--platform", action="store_true", help="Destroy the platform") + self.subparser.add_argument("--challenges", action="store_true", help="Destroy the challenges") + self.subparser.add_argument("--all", action="store_true", help="Destroy all parts of the platform") + self.subparser.add_argument("--test", action="store_true", help="Destroy TEST cluster (default)") + self.subparser.add_argument("--dev", action="store_true", help="Destroy DEV cluster") + self.subparser.add_argument("--prod", action="store_true", help="Destroy PROD cluster") + return + + def run(self, args): + if not args.cluster and not args.content and not args.platform and not args.challenges and not args.all: + Logger.error("Please specify which part of the platform to destroy") + exit(1) + + if args.all and (args.cluster or args.content or args.platform or args.challenges): + Logger.error("Please specify only --all or individual parts of the platform") + exit(1) + + if [args.test, args.dev, args.prod].count(True) > 1: + Logger.error("Please specify only one environment: --test, --dev or --prod") + exit(1) + + if args.prod: + AUTO_APPLY = False # Disable auto-apply for production environment + + destroy_all = args.all + destroy_cluster = args.cluster or destroy_all + destroy_content = args.content or destroy_all + destroy_platform = args.platform or destroy_all + destroy_challenges = args.challenges or destroy_all + + self.environment = "test" + if args.dev: + self.environment = "dev" + elif args.prod: + self.environment = "prod" + + self.times.append(("start", time.time())) + Logger.info("Destroying " + (self.environment.upper() if self.environment != "test" else "TEST") + " environment") + Logger.space() + + if destroy_challenges: + start_time = time.time() + self.challenges_destroy() + self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + if destroy_platform: + start_time = time.time() + self.platform_destroy() + self.times.append(("platform", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + if destroy_content: + start_time = time.time() + self.content_destroy() + self.times.append(("content", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + if destroy_cluster: + start_time = time.time() + self.cluster_destroy() + self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) + Logger.space() + Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.space() + + Logger.success("Destroyed action") + + self.times.append(("end", time.time())) + + Logger.info(f"Time taken: {str(round(Utils.extract_tuple_from_list(self.times, 'end')[1] - Utils.extract_tuple_from_list(self.times, 'start')[1], 2))} seconds") + + if destroy_cluster: + Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") + if destroy_content: + Logger.info(f"Content time: {str(round(Utils.extract_tuple_from_list(self.times, 'content')[3], 2))} seconds") + if destroy_platform: + Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") + if destroy_challenges: + Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") + + ''' + Initialize Terraform to a given environment (workspace) + ''' + def init_terraform(self, path): + Logger.info("Initializing Terraform") + current_dir = os.getcwd() + os.chdir(path) + + try: + # Create workspaces + Logger.info("Creating workspaces if they do not exist") + for env in ENVIRONMENTS: + subprocess.run([FLAVOR, "workspace", "new", env], check=False) + + # Initialize the backend (if not already done for this project) + Logger.info("Running terraform init") + rc = run(f"{FLAVOR} init", shell=True) + if rc != 0: + raise Exception + + # Select the workspace based on the environment + Logger.info(f"Selecting workspace: {self.environment}") + rc = run(f"{FLAVOR} workspace select {self.environment}", shell=True) + if rc != 0: + raise Exception + except subprocess.CalledProcessError as e: + Logger.error("Terraform initialization failed") + raise e + finally: + os.chdir(current_dir) # Always change back to the original directory + Logger.success("Terraform initialized successfully") + + def get_filename_tfvars(self): + return TFVARS.get_filename_tfvars(self.environment) + + def get_path_tfvars(self): + path = Utils.get_path_to_script() + return f"{path}/{self.get_filename_tfvars()}" + + def get_kubeconfig_b64(self): + path = Utils.get_path_to_script() + with open(f"{path}/kube-config/kube-config.{self.environment}.b64", "r") as file: + return file.read() + + def cluster_destroy(self): + path = Utils.get_path_to_script() + Logger.info("Destroying the cluster") + + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars") + tfvars.create(CLUSTER_TFVARS) + Logger.space() + + # Destroy the cluster + try: + self.init_terraform(f"{path}/cluster") + rc = run(f"cd {path}/cluster && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Cluster terraform destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars").destroy() + + Logger.success("Cluster terraform destroy applied successfully") + + # remove kubeconfig + self.remove_kubeconfig() + + def remove_kubeconfig(self): + path = Utils.get_path_to_script() + Logger.info("Removing kubeconfig") + + # Remove kubeconfig + try: + rc = run(f"rm {path}/kube-config/kube-config.{self.environment}.yml", shell=True) + if rc != 0: + raise Exception + rc = run(f"rm {path}/kube-config/kube-config.{self.environment}.b64", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Failed to remove kubeconfig") + Logger.success("Kubeconfig removed") + + def content_destroy(self): + path = Utils.get_path_to_script() + Logger.info("Destroying the content on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/content/data.auto.tfvars") + tfvars.create(CONTENT_TFVARS) + tfvars.add("kubeconfig", self.get_kubeconfig_b64()) + tfvars.add("environment", self.environment) + Logger.space() + + # Destroy the content + try: + self.init_terraform(f"{path}/content") + rc = run(f"cd {path}/content && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Content destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{path}/content/data.auto.tfvars").destroy() + + Logger.success("Content destroyed successfully") + + def platform_destroy(self): + path = Utils.get_path_to_script() + Logger.info("Destroying the platform on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars") + tfvars.create(PLATFORM_TFVARS) + tfvars.add("kubeconfig", self.get_kubeconfig_b64()) + tfvars.add("environment", self.environment) + Logger.space() + + # Destroy the platform + try: + self.init_terraform(f"{path}/platform") + rc = run(f"cd {path}/platform && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Platform destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars").destroy() + + Logger.success("Platform destroyed successfully") + + def challenges_destroy(self): + path = Utils.get_path_to_script() + Logger.info("Destroying the challenges on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars") + tfvars.create(CHALLENGES_TFVARS) + tfvars.add("kubeconfig", self.get_kubeconfig_b64()) + tfvars.add("environment", self.environment) + Logger.space() + + # Destroy the challenges + try: + self.init_terraform(f"{path}/challenges") + rc = run(f"cd {path}/challenges && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + if rc != 0: + raise Exception + except: + Logger.error("Challenges destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars").destroy() + + Logger.success("Challenges destroyed successfully") + +''' +TFVars handler class +''' +class TFVARS: + def __init__(self, root, destination): + self.root = root + self.destination = destination + + @staticmethod + def get_filename_tfvars(environment="test"): + prefix = "" + if environment != "test": + prefix = f"{environment}." + + return f"automated.{prefix}tfvars" + + def create(self, fields=[]): + # Check if destination exists + exists = os.path.exists(self.destination) + + # Create the file or empty it + with open(self.destination, "w") as file: + if exists: + Logger.info(f"Overwriting {self.destination}") + else: + Logger.info(f"Creating {self.destination}") + + file.write("") + + # Parse the root file into key-value pairs + key_value_pairs = {} + with open(self.root, "r") as root: + for line in root: + line = line.strip() + if "=" in line and not line.startswith("#"): + key, value = map(str.strip, line.split("=", 1)) + key_value_pairs[key] = value + + # Filter and write only the specified fields to the destination file + with open(self.destination, "w") as file: + for field in fields: + if field in key_value_pairs: + file.write(f"{field} = {key_value_pairs[field]}\n") + else: + Logger.warning(f"Field '{field}' not found in {self.root}") + + def add(self, key, value): + # Check if destionation exists + exists = os.path.exists(self.destination) + if not exists: + Logger.error(f"{self.destination} does not exist") + exit(1) + + # Overwrite line if it exists or append to the end + with open(self.destination, "r") as file: + lines = file.readlines() + + with open(self.destination, "w") as file: + found = False + for line in lines: + if key in line: + file.write(f'{key} = "{value}"\n') + found = True + else: + file.write(line) + if not found: + file.write(f'{key} = "{value}"\n') + + def destroy(self): + # Check if destionation exists + exists = os.path.exists(self.destination) + + # Remove the file + if exists: + Logger.info(f"Removing {self.destination}") + os.remove(self.destination) + else: + Logger.info(f"{self.destination} does not exist") + + @staticmethod + def insert_keys(environment="test"): + path = Utils.get_path_to_script() + + # Read the keys + public_key = "" + private_key = "" + with open(f"{path}/data/keys/k8s.pub.b64", "r") as file: + public_key = file.read() + with open(f"{path}/data/keys/k8s.b64", "r") as file: + private_key = file.read() + + # Insert the keys into automated.tfvars + with open(f"{path}/{TFVARS.get_filename_tfvars(environment)}", "r") as file: + lines = file.readlines() + with open(f"{path}/{TFVARS.get_filename_tfvars(environment)}", "w") as file: + for line in lines: + if "ssh_key_public_base64" in line: + file.write(f'ssh_key_public_base64 = "{public_key}"\n') + elif "ssh_key_private_base64" in line: + file.write(f'ssh_key_private_base64 = "{private_key}"\n') + else: + file.write(line) + +''' +CLI tool +''' +class CLI: + def run(self): + self.platform_check() + + args = Args() + if args.parser is None: + Logger.error("Failed to initialize argument parser") + exit(1) + + subparser = args.parser.add_subparsers(dest="command", help="Subcommand to run", title="subcommands") + + # Commands + commands = [ + InitializeTFVars(subparser), + GenerateImages(subparser), + GenerateKeys(subparser), + InsertKeys(subparser), + Deploy(subparser), + Destroy(subparser), + ] + for command in commands: + command.register_subcommand() + + # Get arguments + namespace = args.parser.parse_args() + + # Fallback to help if no subcommand is provided + if not hasattr(namespace, "func"): + args.print_help() + exit(1) + + # Run the subcommand + try: + namespace.func(namespace) + except Exception as e: + Logger.error(f"Failed to run subcommand: {e}") + + def platform_check(self): + # Check if system is linux + if sys.platform != "linux": + Logger.error("This script is only supported on Linux") + exit(1) + + # Check if user has bash + if not os.path.exists("/bin/bash"): + Logger.error("This script requires bash") + exit(1) + +if __name__ == "__main__": + CLI().run() diff --git a/template.automated.tfvars b/template.automated.tfvars new file mode 100644 index 0000000..5c95baa --- /dev/null +++ b/template.automated.tfvars @@ -0,0 +1,220 @@ +# Template for the automated setup process. +# Clone this file to `automated.tfvars` and fill in the values. +# This file (`template.automated.tfvars`) is git tracked, and MUST NOT be changed in the repository to include sensitive information. + +# ------------------------ +# IMPORTANT INFORMATION +# ------------------------ +# FORMAT: key = "value" +# It is important keys and equal signs have AT LEAST one space between them. +# The values MUST be in quotes. +# Value MUST NOT be multiline. + +# ------------------------ +# Cluster configuration +# ------------------------ +# WARNING: Changing region while the cluster is running will cause all servers in the group to be destroyed and recreated. +# For uptimal performance, it is recommended to use the same region for all servers. +# Region 1 is used for scale nodes and loadbalancer. +# Possible values: fsn1, hel1, nbg1 +region_1 = "fsn1" # Region for subgroup 1 +region_2 = "fsn1" # Region for subgroup 2 +region_3 = "fsn1" # Region for subgroup 3 + +# Servers +# Server definitions are split into three groups: Control Plane, Agents, and Scale. Control plane and agents has three groups each, and scale has one group. +# Each group can be scaled and defined independently, to allow for smooth transitions between different server types and sizes. +# Control planes are the servers that run the Kubernetes control plane, and are responsible for managing the cluster. +# Agents are the servers that run the workloads, and scale is used to scale the cluster up or down dynamically. +# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically. + +# Server types (e.g., "cx32", "cx42", "cx22") See https://www.hetzner.com/cloud +control_plane_type_1 = "cx32" # Control plane group 1 +control_plane_type_2 = "cx32" # Control plane group 2 +control_plane_type_3 = "cx32" # Control plane group 3 +agent_type_1 = "cx32" # Agent group 1 +agent_type_2 = "cx32" # Agent group 2 +agent_type_3 = "cx32" # Agent group 3 +scale_type = "cx32" # Scale group + +# Server count +# Minimum of 1 control plane across all groups. 1 in each group is recommended for HA. +control_plane_count_1 = 1 # Number of control plane nodes in group 1 +control_plane_count_2 = 1 # Number of control plane nodes in group 2 +control_plane_count_3 = 1 # Number of control plane nodes in group 3 +# Minimum of 1 agent across all groups. 1 in each group is recommended for HA. +agent_count_1 = 1 # Number of agent nodes in group 1 +agent_count_2 = 1 # Number of agent nodes in group 2 +agent_count_3 = 1 # Number of agent nodes in group 3 +# Optional - 0 means no scale nodes available to the autoscaler. +scale_count = 0 +# Minimum number of scale nodes - Only applicable if scale_count > 0 +scale_min = 0 + +load_balancer_type = "lb11" # Load balancer type, see https://www.hetzner.com/cloud/load-balancer + +# ------------------------ +# Hetzner +# ------------------------ +hcloud_token = "" # Hetzner cloud project token (obtained from a specific project in Hetzner cloud) + +# ------------------------ +# SSH +# ------------------------ +# The following tokens are base64 encoded public and private keys. +# To generate these, leave the template as is, and run the following commands to fill in the values: +# $ python3 cli.py generate-keys --insert +ssh_key_private_base64 = "" # The private key to use for SSH access to the servers (base64 encoded) +ssh_key_public_base64 = "" # The public key to use for SSH access to the servers (base64 encoded) + + +# ------------------------ +# Cloudflare variables +# ------------------------ +# The cluster uses two domains for the management and CTF parts of the cluster. +# This is to sepearte the two parts of the cluster, and to allow for different DNS records for the two parts. It may be the same domain. The specific subdomains is set later. +cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) +cloudflare_dns_management = "" # The top level domain (TLD) to use for the DNS records for the management part of the cluster +cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF part of the cluster +cloudflare_dns_platform = "" # The top level domain (TLD) to use for the DNS records for the platform part of the cluster + +# ------------------------ +# DNS information +# ------------------------ +# The cluster uses two domains for the management and CTF parts of the cluster. +# The following is the actually used subdomains for the two parts of the cluster. They may be either TLD or subdomains. +cluster_dns_management = "" # The specific domain name to use for the DNS records for the management part of the cluster +cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster +cluster_dns_platform = "" # The domain name to use for the DNS records for the platform part of the cluster + +# The following is used for the ACME certificate (https) for the cluster. +email = "" # Email to use for the ACME certificate + + +# ---------------------- +# Management configuration +# ---------------------- +# The following is the configuration for the management part of the cluster. + +# ArgoCD password +argocd_admin_password = "" # The password for the ArgoCD admin user +argocd_github_secret = "" # The GitHub secret for ArgoCD webhooks - Send webhook to /api/webhook with this secret as the secret header. This is used to trigger ArgoCD to sync the repositories. + +# Grafana password +grafana_admin_password = "" # The password for the Grafana admin user + +# Alert endpoints +discord_webhook_url = "" # Discord webhook URL for notifications + +# Username and password for basic auth (used for some management services) +# The following MUST BE ONE LINE +# user: The username for the basic auth +# password: The password for the basic auth +traefik_basic_auth = { user = "", password = "" } + +# ---------------------- +# Filebeat configuration +# ---------------------- +filebeat_elasticsearch_host = "" # The hostname of the Elasticsearch instance for Filebeat to send logs to. Must be a https 443 endpoint. +filebeat_elasticsearch_username = "" # The username for the Elasticsearch instance +filebeat_elasticsearch_password = "" # The password for the Elasticsearch instance + + +# ---------------------- +# Github configuration +# ---------------------- +# The following configures the cluster access to Github and needed Github repositories. +ghcr_username = "" # GitHub Container Registry username +ghcr_token = "" # GitHub Container Registry token. This token is used to pull images from the GitHub Container Registry. Only let this token have registry read access +git_token = "" # GitHub repo token. Only let this token have read access to the needed repositories. + +# ---------------------- +# CTF configuration +# ---------------------- +# The following is the configuration for the instanced challenge management system. +# They should be unique and strong passwords. +kubectf_auth_secret = "" # The secret to use for the authSecret in the CTF configuration +kubectf_container_secret = "" # The secret to use for the containerSecret in the CTF configuration + +# ------------------------ +# DB configuration +# ------------------------ +# DB configuration for the MariaDB cluster, used for the CTFd instance. +db_root_password = "" # Root password for the MariaDB cluster +db_user = "" # Database user +db_password = "" # Database password + +# ------------------------ +# S3 configuration (for backups) +# ------------------------ +s3_bucket = "" # S3 bucket name for backups +s3_region = "" # S3 region for backups +s3_endpoint = "" # S3 endpoint for backups +s3_access_key = "" # Access key for S3 for backups +s3_secret_key = "" # Secret key for S3 for backups + +# ------------------------ +# CTFd Manager configuration +# ------------------------ +# The following is the configuration for the CTFd manager. +ctfd_manager_password = "" # Password for the CTFd Manager +# The CTFd manager is used to manage the CTFd instance, and is not used for the CTFd instance itself. +ctfd_secret_key = "" # Secret key for CTFd, used for the CTFd instance itself. This is used to sign cookies and other sensitive data. It should be a long, random string. + +# ------------------------ +# CTFd configuration +# ------------------------ +ctf_name = "" # Name of the CTF event +ctf_description = "" # Description of the CTF event +ctf_start_time = "" # Start time of the CTF event (ISO 8601 format, e.g., "2023-10-01T00:00:00Z") +ctf_end_time = "" # End time of the CTF event +ctf_user_mode = "" # User mode for CTFd (e.g., "teams") +ctf_challenge_visibility = "" # Challenge visibility (e.g., "public") +ctf_account_visibility = "" # Account visibility (e.g., "private") +ctf_score_visibility = "" # Score visibility (e.g., "public") +ctf_registration_visibility = "" # Registration visibility (e.g., "public") +ctf_verify_emails = true # Whether to verify emails +ctf_team_size = 0 # Team size for the CTF. 0 means no limit +ctf_brackets = [] # List of brackets, optional - Must be formatted as one line. +ctf_theme = "" # Theme for CTFd +ctf_admin_name = "" # Name of the admin user +ctf_admin_email = "" # Email of the admin user +ctf_admin_password = "" # Password for the admin user +ctf_registration_code = "" # Registration code for the CTF + +ctf_mail_server = "" # Mail server for CTFd +ctf_mail_port = 465 # Mail server port +ctf_mail_username = "" # Mail server username +ctf_mail_password = "" # Mail server password +ctf_mail_tls = true # Whether to use TLS for the mail server +ctf_mail_from = "" # From address for the mail server + +ctf_s3_bucket = "" # S3 bucket name for CTFd files +ctf_s3_region = "" # S3 region for CTF +ctf_s3_endpoint = "" # S3 endpoint for CTFd files +ctf_s3_access_key = "" # Access key for S3 for CTFd files +ctf_s3_secret_key = "" # Secret key for S3 for CTFd files +ctf_s3_prefix = "ctfd//" # S3 prefix for CTFd files, e.g., "ctfd/dev/" + +ctf_logo_path = "data/logo.png" # Path to the CTF logo file (e.g., "ctf-logo.png") + +ctfd_plugin_first_blood_limit_url = "" # Discord webhook URL for First blood notifications + +chall_whitelist_ips = ["", ""] # List of IPs to whitelist for challenges, e.g., [ "0.0.0.0/0" ] + +# ---------------------- +# Docker images +# ---------------------- +# Values are maintained within each component as defaults. +# You can override these values by uncommenting and setting your own images here. + +# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:1.2.1" # The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback +# image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # The docker image for Filebeat + +# ---------------------- +# Versions +# ---------------------- +# Values are maintained within each component as defaults. +# You can override these values by uncommenting and setting your own versions here. + +# mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator to deploy. More information at https://github.com/mariadb-operator/mariadb-operator From 0b249caef66346fc1707bca7633021314170aba2 Mon Sep 17 00:00:00 2001 From: Mikkel Albrechtsem Date: Thu, 18 Dec 2025 21:03:07 +0100 Subject: [PATCH 025/148] Renamed CLI to CTFp --- cli.py => ctfp.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename cli.py => ctfp.py (99%) diff --git a/cli.py b/ctfp.py similarity index 99% rename from cli.py rename to ctfp.py index 637f744..6813b22 100644 --- a/cli.py +++ b/ctfp.py @@ -1,3 +1,8 @@ +# CTFp CLI tool +# Licensed under PolyForm Noncommercial License 1.0.0. +# See LICENSE file in the project root for full license information. +# This file must not be distributed without the LICENSE file. + import os import sys import argparse @@ -162,7 +167,7 @@ class Args: parser = None def __init__(self): - self.parser = argparse.ArgumentParser(description="Platform CLI") + self.parser = argparse.ArgumentParser(description="CTFp CLI") def print_help(self): if self.parser is None: From aed4732b90bdf24b3ed3e4adce6c0991b2895ee5 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Thu, 18 Dec 2025 21:47:47 +0100 Subject: [PATCH 026/148] chore: add copyright notice to the top of ctfp.py --- ctfp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ctfp.py b/ctfp.py index 6813b22..ffc6f13 100644 --- a/ctfp.py +++ b/ctfp.py @@ -2,6 +2,7 @@ # Licensed under PolyForm Noncommercial License 1.0.0. # See LICENSE file in the project root for full license information. # This file must not be distributed without the LICENSE file. +# Required Notice: Copyright Mikkel Albrechtsen () import os import sys From c65402fc9865c84a8798a728f72f2cf3ec887be3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Thu, 18 Dec 2025 23:04:44 +0100 Subject: [PATCH 027/148] Add updated tfvars variable list --- ctfp.py | 138 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 106 insertions(+), 32 deletions(-) diff --git a/ctfp.py b/ctfp.py index ffc6f13..d734189 100644 --- a/ctfp.py +++ b/ctfp.py @@ -15,10 +15,29 @@ FLAVOR = "tofu" # Can be "terraform" or "tofu" CLUSTER_TFVARS = [ - # Server configuration + # Hetzner + "hcloud_token", + + # SSH + "ssh_key_private_base64", + "ssh_key_public_base64", + + # Cloudflare variables + "cloudflare_api_token", + "cloudflare_dns_management", + "cloudflare_dns_platform", + "cloudflare_dns_ctf", + + # DNS information + "cluster_dns_management", + "cluster_dns_platform", + "cluster_dns_ctf", + + # Cluster configuration "region_1", "region_2", "region_3", + "network_zone", "control_plane_type_1", "control_plane_type_2", "control_plane_type_3", @@ -32,67 +51,87 @@ "agent_count_1", "agent_count_2", "agent_count_3", - "scale_count", - "scale_min", + "challs_count", + "scale_max", "load_balancer_type", - # Cluster configuration - "hcloud_token", - "ssh_key_private_base64", - "ssh_key_public_base64", - "cloudflare_api_token", - "cloudflare_dns_management", - "cloudflare_dns_ctf", - "cloudflare_dns_platform", - "cluster_dns_management", - "cluster_dns_ctf", - "cluster_dns_platform", + # Versions + "kube_hetzner_version", ] -CONTENT_TFVARS = [ +OPS_TFVARS = [ + # Generic information + "email", + "discord_webhook_url", + + # Cloudflare variables "cloudflare_api_token", "cloudflare_dns_management", - "cloudflare_dns_ctf", "cloudflare_dns_platform", + "cloudflare_dns_ctf", "cluster_dns_management", - "cluster_dns_ctf", - "email", - "argocd_github_secret", - "argocd_admin_password", - "grafana_admin_password", - "discord_webhook_url", - "traefik_basic_auth", + # Filebeat configuration "filebeat_elasticsearch_host", "filebeat_elasticsearch_username", "filebeat_elasticsearch_password", + # Prometheus configuration + "prometheus_storage_size", + + # Management configuration + "argocd_github_secret", + "argocd_admin_password", + "grafana_admin_password", + "traefik_basic_auth", + + # GitHub variables "ghcr_username", "ghcr_token", + + # Docker images + "image_error_fallback", + "image_filebeat", + + # Versions + "argocd_version", + "cert_manager_version", + "descheduler_version", + "mariadb_operator_version", + "kube_prometheus_stack_version", + "redis_operator_version", ] PLATFORM_TFVARS = [ + # Generic information + "cluster_dns_management", "cluster_dns_ctf", - "cluster_dns_platform", + + # GitHub variables "ghcr_username", "ghcr_token", "git_token", + + # Filebeat configuration + "filebeat_elasticsearch_host", + "filebeat_elasticsearch_username", + "filebeat_elasticsearch_password", + + # CTF configuration "kubectf_auth_secret", + + # DB configuration "db_root_password", "db_user", "db_password", - "ctfd_manager_password", - - # S3 configuration + # DB backupo configuration "s3_bucket", "s3_region", "s3_endpoint", "s3_access_key", "s3_secret_key", - # Elasticsearch configuration - "filebeat_elasticsearch_host", - "filebeat_elasticsearch_username", - "filebeat_elasticsearch_password", - + # CTFd Manager configuration + "ctfd_manager_password", + # CTFd configuration "ctfd_secret_key", "ctf_name", @@ -126,15 +165,50 @@ "ctf_s3_access_key", "ctf_s3_secret_key", "ctf_s3_prefix", + "ctfd_plugin_first_blood_limit_url", + "ctfd_plugin_first_blood_limit", + "ctfd_plugin_first_blood_message", + "pages", + "pages_repository", + "pages_branch", + "ctfd_k8s_deployment_repository", + "ctfd_k8s_deployment_path", + "ctfd_k8s_deployment_branch", + + # Docker images + "image_ctfd_manager", + "image_error_fallback", + "image_filebeat", + "image_ctfd_exporter", + + # Versions + "mariadb_version", ] CHALLENGES_TFVARS = [ + # Generic information + "cluster_dns_management", "cluster_dns_ctf", + + # GitHub variables "ghcr_username", "ghcr_token", "git_token", + + # CTF configuration "kubectf_auth_secret", "kubectf_container_secret", + + # Challenges configuration "chall_whitelist_ips", + "challenges_static", + "challenges_shared", + "challenges_instanced", + "challenges_repository", + "challenges_branch", + + # Docker images + "image_instancing_fallback", + "image_kubectf", ] # Load env from .env From 53410184450ec4e19be88e07e89b23dadc7623f0 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Thu, 18 Dec 2025 23:05:34 +0100 Subject: [PATCH 028/148] Remove unused DNS variables and add GitHub Container Registry credentials in template.tfvars --- ops/tfvars/template.tfvars | 8 ++++++-- platform/tfvars/template.tfvars | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ops/tfvars/template.tfvars b/ops/tfvars/template.tfvars index 6148d25..fd4043b 100644 --- a/ops/tfvars/template.tfvars +++ b/ops/tfvars/template.tfvars @@ -7,8 +7,6 @@ kubeconfig = "AA==" # The base64 encoded kubeconfig file (base64 -w 0 ) # Generic information # ------------------------ environment = "test" # Deployment environment name for the CTF (i.e. prod, staging, dev, test) -cluster_dns_management = "" # The domain name to use for the DNS records for the management part of the cluster -cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster email = "" # Email to use for the ACME certificate discord_webhook_url = "" # Discord webhook URL for sending alerts and notifications @@ -28,6 +26,12 @@ filebeat_elasticsearch_host = "" # The hostname of the Elasticsear filebeat_elasticsearch_username = "" # The username for the Elasticsearch instance filebeat_elasticsearch_password = "" # The password for the Elasticsearch instance +# ------------------------ +# GitHub variables +# ------------------------ +ghcr_username = "" # GitHub Container Registry username +ghcr_token = "" # GitHub Container Registry token. This token is used to pull images from the GitHub Container Registry. Only let this token have registry read access + # ---------------------- # Prometheus configuration # ---------------------- diff --git a/platform/tfvars/template.tfvars b/platform/tfvars/template.tfvars index 1a48321..6d3b7ed 100644 --- a/platform/tfvars/template.tfvars +++ b/platform/tfvars/template.tfvars @@ -29,7 +29,6 @@ filebeat_elasticsearch_password = "" # The password for Elasticsearch # ---------------------- kubectf_auth_secret = "" # The secret to use for the authSecret in the CTF configuration - # ------------------------ # DB configuration # ------------------------ From dd1133a2a914cb37eead8d46308a1b36d257d0f8 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Thu, 18 Dec 2025 23:05:52 +0100 Subject: [PATCH 029/148] Move to correct HCL2 language parsing for TFVARS interactions --- ctfp.py | 251 +++++++++++++++++++++++++------------- requirements.txt | 1 + template.automated.tfvars | 25 ++-- 3 files changed, 183 insertions(+), 94 deletions(-) create mode 100644 requirements.txt diff --git a/ctfp.py b/ctfp.py index d734189..2b2082e 100644 --- a/ctfp.py +++ b/ctfp.py @@ -10,6 +10,9 @@ import time import subprocess +# Terraform parser - https://github.com/amplify-education/python-hcl2 +import hcl2 + AUTO_APPLY = True ENVIRONMENTS = ["test", "dev", "prod"] FLAVOR = "tofu" # Can be "terraform" or "tofu" @@ -482,7 +485,7 @@ class Deploy(Command): def register_subcommand(self): # Only run listed parts of the deployment self.subparser.add_argument("--cluster", action="store_true", help="Deploy the cluster") - self.subparser.add_argument("--content", action="store_true", help="Deploy the content") + self.subparser.add_argument("--ops", action="store_true", help="Deploy the ops") self.subparser.add_argument("--platform", action="store_true", help="Deploy the platform") self.subparser.add_argument("--challenges", action="store_true", help="Deploy the challenges") self.subparser.add_argument("--all", action="store_true", help="Deploy all parts of the platform") @@ -492,11 +495,11 @@ def register_subcommand(self): return def run(self, args): - if not args.cluster and not args.content and not args.platform and not args.challenges and not args.all: + if not args.cluster and not args.ops and not args.platform and not args.challenges and not args.all: Logger.error("Please specify which part of the platform to deploy") exit(1) - if args.all and (args.cluster or args.content or args.platform or args.challenges): + if args.all and (args.cluster or args.ops or args.platform or args.challenges): Logger.error("Please specify only --all or individual parts of the platform") exit(1) @@ -509,7 +512,7 @@ def run(self, args): deploy_all = args.all deploy_cluster = args.cluster or deploy_all - deploy_content = args.content or deploy_all + deploy_ops = args.ops or deploy_all deploy_platform = args.platform or deploy_all deploy_challenges = args.challenges or deploy_all @@ -532,10 +535,10 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if deploy_content: + if deploy_ops: start_time = time.time() - self.content_deploy() - self.times.append(("content", start_time, time.time(), time.time() - start_time)) + self.ops_deploy() + self.times.append(("ops", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() @@ -563,8 +566,8 @@ def run(self, args): if deploy_cluster: Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") - if deploy_content: - Logger.info(f"Content time: {str(round(Utils.extract_tuple_from_list(self.times, 'content')[3], 2))} seconds") + if deploy_ops: + Logger.info(f"Ops time: {str(round(Utils.extract_tuple_from_list(self.times, 'ops')[3], 2))} seconds") if deploy_platform: Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") if deploy_challenges: @@ -622,7 +625,7 @@ def check_values(self): # Ensure no < or > are present in the file with open(tfvars_path, "r") as file: for line in file: - if "<" in line or ">" in line: + if "<" in line and ">" in line: Logger.error(f"{self.get_filename_tfvars()} does not seem to be filled out. Please fill out all fields and try again") exit(1) @@ -635,6 +638,7 @@ def cluster_deploy(self): # Configure tfvars file tfvars = TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars") tfvars.create(CLUSTER_TFVARS) + # tfvars.add("environment", self.environment) Logger.space() # Deploy the cluster @@ -672,26 +676,28 @@ def get_kubeconfig_b64(self): with open(f"{path}/kube-config/kube-config.{self.environment}.b64", "r") as file: return file.read() - def content_deploy(self): + def ops_deploy(self): path = Utils.get_path_to_script() - Logger.info("Deploying the content on the cluster") + Logger.info("Deploying the ops on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/content/data.auto.tfvars") - tfvars.create(CONTENT_TFVARS) - tfvars.add("kubeconfig", self.get_kubeconfig_b64()) - tfvars.add("environment", self.environment) + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/ops/data.auto.tfvars") + tfvars.create(OPS_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) Logger.space() # Deploy the cluster try: - self.init_terraform(f"{path}/content") - rc = run(f"cd {path}/content && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{path}/ops") + rc = run(f"cd {path}/ops && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: - Logger.error("Content apply failed") - Logger.success("Content deployed successfully") + Logger.error("Ops apply failed") + Logger.success("Ops deployed successfully") def platform_deploy(self): path = Utils.get_path_to_script() @@ -700,8 +706,10 @@ def platform_deploy(self): # Configure tfvars file tfvars = TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars") tfvars.create(PLATFORM_TFVARS) - tfvars.add("kubeconfig", self.get_kubeconfig_b64()) - tfvars.add("environment", self.environment) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) Logger.space() # Deploy the cluster @@ -721,8 +729,10 @@ def challenges_deploy(self): # Configure tfvars file tfvars = TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars") tfvars.create(CHALLENGES_TFVARS) - tfvars.add("kubeconfig", self.get_kubeconfig_b64()) - tfvars.add("environment", self.environment) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) Logger.space() # Deploy the cluster @@ -748,7 +758,7 @@ class Destroy(Command): def register_subcommand(self): # Only run listed parts of the destruction self.subparser.add_argument("--cluster", action="store_true", help="Destroy the cluster") - self.subparser.add_argument("--content", action="store_true", help="Destroy the content") + self.subparser.add_argument("--ops", action="store_true", help="Destroy the ops") self.subparser.add_argument("--platform", action="store_true", help="Destroy the platform") self.subparser.add_argument("--challenges", action="store_true", help="Destroy the challenges") self.subparser.add_argument("--all", action="store_true", help="Destroy all parts of the platform") @@ -758,11 +768,11 @@ def register_subcommand(self): return def run(self, args): - if not args.cluster and not args.content and not args.platform and not args.challenges and not args.all: + if not args.cluster and not args.ops and not args.platform and not args.challenges and not args.all: Logger.error("Please specify which part of the platform to destroy") exit(1) - if args.all and (args.cluster or args.content or args.platform or args.challenges): + if args.all and (args.cluster or args.ops or args.platform or args.challenges): Logger.error("Please specify only --all or individual parts of the platform") exit(1) @@ -775,7 +785,7 @@ def run(self, args): destroy_all = args.all destroy_cluster = args.cluster or destroy_all - destroy_content = args.content or destroy_all + destroy_ops = args.ops or destroy_all destroy_platform = args.platform or destroy_all destroy_challenges = args.challenges or destroy_all @@ -805,10 +815,10 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if destroy_content: + if destroy_ops: start_time = time.time() - self.content_destroy() - self.times.append(("content", start_time, time.time(), time.time() - start_time)) + self.ops_destroy() + self.times.append(("ops", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() @@ -829,8 +839,8 @@ def run(self, args): if destroy_cluster: Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") - if destroy_content: - Logger.info(f"Content time: {str(round(Utils.extract_tuple_from_list(self.times, 'content')[3], 2))} seconds") + if destroy_ops: + Logger.info(f"Ops time: {str(round(Utils.extract_tuple_from_list(self.times, 'ops')[3], 2))} seconds") if destroy_platform: Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") if destroy_challenges: @@ -888,6 +898,7 @@ def cluster_destroy(self): # Configure tfvars file tfvars = TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars") tfvars.create(CLUSTER_TFVARS) + # tfvars.add("environment", self.environment) Logger.space() # Destroy the cluster @@ -923,30 +934,32 @@ def remove_kubeconfig(self): Logger.error("Failed to remove kubeconfig") Logger.success("Kubeconfig removed") - def content_destroy(self): + def ops_destroy(self): path = Utils.get_path_to_script() - Logger.info("Destroying the content on the cluster") + Logger.info("Destroying the ops on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/content/data.auto.tfvars") - tfvars.create(CONTENT_TFVARS) - tfvars.add("kubeconfig", self.get_kubeconfig_b64()) - tfvars.add("environment", self.environment) + tfvars = TFVARS(self.get_path_tfvars(), f"{path}/ops/data.auto.tfvars") + tfvars.create(OPS_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) Logger.space() - # Destroy the content + # Destroy the ops try: - self.init_terraform(f"{path}/content") - rc = run(f"cd {path}/content && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{path}/ops") + rc = run(f"cd {path}/ops && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: - Logger.error("Content destroy failed") + Logger.error("Ops destroy failed") # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{path}/content/data.auto.tfvars").destroy() + TFVARS(self.get_path_tfvars(), f"{path}/ops/data.auto.tfvars").destroy() - Logger.success("Content destroyed successfully") + Logger.success("Ops destroyed successfully") def platform_destroy(self): path = Utils.get_path_to_script() @@ -955,8 +968,10 @@ def platform_destroy(self): # Configure tfvars file tfvars = TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars") tfvars.create(PLATFORM_TFVARS) - tfvars.add("kubeconfig", self.get_kubeconfig_b64()) - tfvars.add("environment", self.environment) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) Logger.space() # Destroy the platform @@ -980,8 +995,10 @@ def challenges_destroy(self): # Configure tfvars file tfvars = TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars") tfvars.create(CHALLENGES_TFVARS) - tfvars.add("kubeconfig", self.get_kubeconfig_b64()) - tfvars.add("environment", self.environment) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) Logger.space() # Destroy the challenges @@ -1002,18 +1019,76 @@ def challenges_destroy(self): TFVars handler class ''' class TFVARS: + root: str + destination: str + def __init__(self, root, destination): self.root = root self.destination = destination @staticmethod def get_filename_tfvars(environment="test"): + ''' + Get the filename for the tfvars file based on the environment + + :param environment: The environment name (test, dev, prod) + :return: The filename for the tfvars file + ''' + prefix = "" if environment != "test": prefix = f"{environment}." return f"automated.{prefix}tfvars" + @staticmethod + def load_tfvars(file_path: str): + ''' + Load a tfvars file and return its contents as a dictionary + + :param file_path: The path to the tfvars file + :return: A dictionary containing the tfvars key-value pairs + ''' + + with open(file_path, "r") as tfvars_file: + tfvars = hcl2.api.load(tfvars_file) + return tfvars + + @staticmethod + def safe_load_tfvars(file_path: str): + ''' + Safely load a tfvars file and handle errors by exiting the program + + :param file_path: The path to the tfvars file + :return: A dictionary containing the tfvars key-value pairs + ''' + + try: + return TFVARS.load_tfvars(file_path) + except Exception as e: + print(f"Error loading tfvars file: {e}") + exit(1) + + @staticmethod + def safe_write_tfvars(file_path: str, data: dict): + ''' + Safely write a dictionary to a tfvars file and handle errors by exiting the program + + :param file_path: The path to the tfvars file + :param data: A dictionary containing the tfvars key-value pairs + :return: None + ''' + + try: + tree = hcl2.api.reverse_transform(data) + formatted_data = hcl2.api.writes(tree) + + with open(file_path, "w") as tfvars_file: + tfvars_file.write(formatted_data) + except Exception as e: + print(f"Error writing tfvars file: {e}") + exit(1) + def create(self, fields=[]): # Check if destination exists exists = os.path.exists(self.destination) @@ -1028,43 +1103,54 @@ def create(self, fields=[]): file.write("") # Parse the root file into key-value pairs - key_value_pairs = {} - with open(self.root, "r") as root: - for line in root: - line = line.strip() - if "=" in line and not line.startswith("#"): - key, value = map(str.strip, line.split("=", 1)) - key_value_pairs[key] = value + key_value_pairs = TFVARS.safe_load_tfvars(self.root) # Filter and write only the specified fields to the destination file - with open(self.destination, "w") as file: - for field in fields: - if field in key_value_pairs: - file.write(f"{field} = {key_value_pairs[field]}\n") - else: - Logger.warning(f"Field '{field}' not found in {self.root}") + filtered_values = {} + for field in fields: + if field in key_value_pairs: + filtered_values[field] = key_value_pairs[field] + else: + Logger.warning(f"Field '{field}' not found in {self.root}") + TFVARS.safe_write_tfvars(self.destination, filtered_values) def add(self, key, value): + ''' + Add a key-value pair to the tfvars file + + :param key: The key to add + :param value: The value to add + :return: None + ''' + # Check if destionation exists exists = os.path.exists(self.destination) if not exists: Logger.error(f"{self.destination} does not exist") exit(1) - # Overwrite line if it exists or append to the end - with open(self.destination, "r") as file: - lines = file.readlines() + data = TFVARS.safe_load_tfvars(self.destination) + data[key] = value + TFVARS.safe_write_tfvars(self.destination, data) + + def add_dict(self, dict_data): + ''' + Add multiple key-value pairs from a dictionary to the tfvars file - with open(self.destination, "w") as file: - found = False - for line in lines: - if key in line: - file.write(f'{key} = "{value}"\n') - found = True - else: - file.write(line) - if not found: - file.write(f'{key} = "{value}"\n') + :param dict_data: A dictionary containing the key-value pairs to add + :return: None + ''' + + # Check if destionation exists + exists = os.path.exists(self.destination) + if not exists: + Logger.error(f"{self.destination} does not exist") + exit(1) + + data = TFVARS.safe_load_tfvars(self.destination) + for key, value in dict_data.items(): + data[key] = value + TFVARS.safe_write_tfvars(self.destination, data) def destroy(self): # Check if destionation exists @@ -1089,17 +1175,10 @@ def insert_keys(environment="test"): with open(f"{path}/data/keys/k8s.b64", "r") as file: private_key = file.read() - # Insert the keys into automated.tfvars - with open(f"{path}/{TFVARS.get_filename_tfvars(environment)}", "r") as file: - lines = file.readlines() - with open(f"{path}/{TFVARS.get_filename_tfvars(environment)}", "w") as file: - for line in lines: - if "ssh_key_public_base64" in line: - file.write(f'ssh_key_public_base64 = "{public_key}"\n') - elif "ssh_key_private_base64" in line: - file.write(f'ssh_key_private_base64 = "{private_key}"\n') - else: - file.write(line) + data = TFVARS.safe_load_tfvars(f"{path}/{TFVARS.get_filename_tfvars(environment)}") + data["ssh_key_public_base64"] = public_key + data["ssh_key_private_base64"] = private_key + TFVARS.safe_write_tfvars(f"{path}/{TFVARS.get_filename_tfvars(environment)}", data) ''' CLI tool diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c5cefba --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python-hcl2 >= 7.3.1 diff --git a/template.automated.tfvars b/template.automated.tfvars index 5c95baa..6401588 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -2,14 +2,6 @@ # Clone this file to `automated.tfvars` and fill in the values. # This file (`template.automated.tfvars`) is git tracked, and MUST NOT be changed in the repository to include sensitive information. -# ------------------------ -# IMPORTANT INFORMATION -# ------------------------ -# FORMAT: key = "value" -# It is important keys and equal signs have AT LEAST one space between them. -# The values MUST be in quotes. -# Value MUST NOT be multiline. - # ------------------------ # Cluster configuration # ------------------------ @@ -202,6 +194,23 @@ ctfd_plugin_first_blood_limit_url = "" # Discord webhook URL for Fi chall_whitelist_ips = ["", ""] # List of IPs to whitelist for challenges, e.g., [ "0.0.0.0/0" ] +# ------------------------ +# Challenges configuration +# ------------------------ +challenges_static = { + "" = ["", ""], +} # List of static challenges to deploy. Needs to be the slugs of the challenges +challenges_shared = { + "" = ["", ""], +} # List of shared challenges to deploy. Needs to be the slugs of the challenges +challenges_instanced = { + "" = ["", ""], +} # List of instanced challenges to deploy. Needs to be the slugs of the challenges + +challenges_repository = "" # URL of the Git repository containing the challenge definitions +challenges_branch = "" # Branch of the Git repository to use for the challenge definitions. Leave empty for environment based branch (environment == prod ? main : develop) + + # ---------------------- # Docker images # ---------------------- From d15371bf152f520163ebbeafec8e11dfb77377d5 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Thu, 18 Dec 2025 23:33:27 +0100 Subject: [PATCH 030/148] Update tfvars templates for improved configuration and resource allocation --- cluster/tfvars/template.tfvars | 19 +++--- platform/tfvars/template.tfvars | 2 +- template.automated.tfvars | 117 +++++++++++++++++++------------- 3 files changed, 81 insertions(+), 57 deletions(-) diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index 1bfc52b..54ce649 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -12,7 +12,6 @@ hcloud_token = "" # Hetzner cloud project token (obtained from a ssh_key_private_base64 = "" # The private key to use for SSH access to the servers (base64 encoded) ssh_key_public_base64 = "" # The public key to use for SSH access to the servers (base64 encoded) - # ------------------------ # Cloudflare variables # ------------------------ @@ -51,14 +50,14 @@ network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central # Agents are the servers that run the workloads, and scale is used to scale the cluster up or down dynamically. # Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically. -# Server types (e.g., "cx32", "cx42", "cx22") See https://www.hetzner.com/cloud -control_plane_type_1 = "cx32" # Control plane group 1 -control_plane_type_2 = "cx32" # Control plane group 2 -control_plane_type_3 = "cx32" # Control plane group 3 -agent_type_1 = "cx32" # Agent group 1 -agent_type_2 = "cx32" # Agent group 2 -agent_type_3 = "cx32" # Agent group 3 -scale_type = "cx32" # Scale group +# Server types. See https://www.hetzner.com/cloud +control_plane_type_1 = "cx23" # Control plane group 1 +control_plane_type_2 = "cx23" # Control plane group 2 +control_plane_type_3 = "cx23" # Control plane group 3 +agent_type_1 = "cx33" # Agent group 1 +agent_type_2 = "cx33" # Agent group 2 +agent_type_3 = "cx33" # Agent group 3 +scale_type = "cx33" # Scale group # Server count # Minimum of 1 control plane across all groups. 1 in each group is recommended for HA. @@ -70,7 +69,7 @@ agent_count_1 = 1 # Number of agent nodes in group 1 agent_count_2 = 1 # Number of agent nodes in group 2 agent_count_3 = 1 # Number of agent nodes in group 3 # Challenge nodes - Nodes dedicated to running CTF challenges. These nodes are tainted to only run challenge workloads. -challs_count = 0 # Number of challenge nodes. +challs_count = 1 # Number of challenge nodes. # Scale nodes - Nodes that are automatically scaled by the cluster autoscaler. These nodes are used to scale the cluster up or down dynamically. scale_max = 0 # Maximum number of scale nodes. Set to 0 to disable autoscaling. diff --git a/platform/tfvars/template.tfvars b/platform/tfvars/template.tfvars index 6d3b7ed..acc1041 100644 --- a/platform/tfvars/template.tfvars +++ b/platform/tfvars/template.tfvars @@ -108,7 +108,7 @@ ctfd_k8s_deployment_branch = "" # Git branch # You can override these values by uncommenting and setting your own images here. # image_ctfd_manager = "ghcr.io/ctfpilot/ctfd-manager:1.0.1" # Docker image for the CTFd Manager deployment -# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:1.2.1" # Docker image for the error fallback deployment +# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:1.2.1" # Docker image for the error fallback deployment # image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # Docker image for Filebeat # image_ctfd_exporter = "ghcr.io/the0mikkel/ctfd-exporter:1.1.1" # Docker image for the CTFd Exporter diff --git a/template.automated.tfvars b/template.automated.tfvars index 6401588..77fbda9 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -9,9 +9,10 @@ # For uptimal performance, it is recommended to use the same region for all servers. # Region 1 is used for scale nodes and loadbalancer. # Possible values: fsn1, hel1, nbg1 -region_1 = "fsn1" # Region for subgroup 1 -region_2 = "fsn1" # Region for subgroup 2 -region_3 = "fsn1" # Region for subgroup 3 +region_1 = "fsn1" # Region for subgroup 1 +region_2 = "fsn1" # Region for subgroup 2 +region_3 = "fsn1" # Region for subgroup 3 +network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central", "us-east", "us-west", "ap-southeast". Regions must be within the network zone. # Servers # Server definitions are split into three groups: Control Plane, Agents, and Scale. Control plane and agents has three groups each, and scale has one group. @@ -20,14 +21,14 @@ region_3 = "fsn1" # Region for subgroup 3 # Agents are the servers that run the workloads, and scale is used to scale the cluster up or down dynamically. # Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically. -# Server types (e.g., "cx32", "cx42", "cx22") See https://www.hetzner.com/cloud -control_plane_type_1 = "cx32" # Control plane group 1 -control_plane_type_2 = "cx32" # Control plane group 2 -control_plane_type_3 = "cx32" # Control plane group 3 -agent_type_1 = "cx32" # Agent group 1 -agent_type_2 = "cx32" # Agent group 2 -agent_type_3 = "cx32" # Agent group 3 -scale_type = "cx32" # Scale group +# Server types. See https://www.hetzner.com/cloud +control_plane_type_1 = "cx23" # Control plane group 1 +control_plane_type_2 = "cx23" # Control plane group 2 +control_plane_type_3 = "cx23" # Control plane group 3 +agent_type_1 = "cx33" # Agent group 1 +agent_type_2 = "cx33" # Agent group 2 +agent_type_3 = "cx33" # Agent group 3 +scale_type = "cx33" # Scale group # Server count # Minimum of 1 control plane across all groups. 1 in each group is recommended for HA. @@ -38,10 +39,10 @@ control_plane_count_3 = 1 # Number of control plane nodes in group 3 agent_count_1 = 1 # Number of agent nodes in group 1 agent_count_2 = 1 # Number of agent nodes in group 2 agent_count_3 = 1 # Number of agent nodes in group 3 -# Optional - 0 means no scale nodes available to the autoscaler. -scale_count = 0 -# Minimum number of scale nodes - Only applicable if scale_count > 0 -scale_min = 0 +# Challenge nodes - Nodes dedicated to running CTF challenges. These nodes are tainted to only run challenge workloads. +challs_count = 1 # Number of challenge nodes. +# Scale nodes - Nodes that are automatically scaled by the cluster autoscaler. These nodes are used to scale the cluster up or down dynamically. +scale_max = 0 # Maximum number of scale nodes. Set to 0 to disable autoscaling. load_balancer_type = "lb11" # Load balancer type, see https://www.hetzner.com/cloud/load-balancer @@ -59,7 +60,6 @@ hcloud_token = "" # Hetzner cloud project token (obtained from a ssh_key_private_base64 = "" # The private key to use for SSH access to the servers (base64 encoded) ssh_key_public_base64 = "" # The public key to use for SSH access to the servers (base64 encoded) - # ------------------------ # Cloudflare variables # ------------------------ @@ -67,8 +67,8 @@ ssh_key_public_base64 = "" # The public key to use for SSH access # This is to sepearte the two parts of the cluster, and to allow for different DNS records for the two parts. It may be the same domain. The specific subdomains is set later. cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) cloudflare_dns_management = "" # The top level domain (TLD) to use for the DNS records for the management part of the cluster -cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF part of the cluster cloudflare_dns_platform = "" # The top level domain (TLD) to use for the DNS records for the platform part of the cluster +cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF part of the cluster # ------------------------ # DNS information @@ -76,8 +76,8 @@ cloudflare_dns_platform = "" # The top level domain (TLD) t # The cluster uses two domains for the management and CTF parts of the cluster. # The following is the actually used subdomains for the two parts of the cluster. They may be either TLD or subdomains. cluster_dns_management = "" # The specific domain name to use for the DNS records for the management part of the cluster -cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster cluster_dns_platform = "" # The domain name to use for the DNS records for the platform part of the cluster +cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster # The following is used for the ACME certificate (https) for the cluster. email = "" # Email to use for the ACME certificate @@ -99,7 +99,6 @@ grafana_admin_password = "" # The password for the Grafana adm discord_webhook_url = "" # Discord webhook URL for notifications # Username and password for basic auth (used for some management services) -# The following MUST BE ONE LINE # user: The username for the basic auth # password: The password for the basic auth traefik_basic_auth = { user = "", password = "" } @@ -111,6 +110,10 @@ filebeat_elasticsearch_host = "" # The hostname of the Elasticsear filebeat_elasticsearch_username = "" # The username for the Elasticsearch instance filebeat_elasticsearch_password = "" # The password for the Elasticsearch instance +# ---------------------- +# Prometheus configuration +# ---------------------- +prometheus_storage_size = "15Gi" # The size of the persistent volume claim for Prometheus data storage. Format: (e.g., 20Gi, 100Gi) # ---------------------- # Github configuration @@ -136,22 +139,18 @@ db_root_password = "" # Root password for the MariaDB cluster db_user = "" # Database user db_password = "" # Database password -# ------------------------ -# S3 configuration (for backups) -# ------------------------ -s3_bucket = "" # S3 bucket name for backups -s3_region = "" # S3 region for backups -s3_endpoint = "" # S3 endpoint for backups -s3_access_key = "" # Access key for S3 for backups -s3_secret_key = "" # Secret key for S3 for backups +# S3 backup +s3_bucket = "" # S3 bucket name for backups +s3_region = "" # S3 region for backups +s3_endpoint = "" # S3 endpoint for backups +s3_access_key = "" # Access key for S3 for backups +s3_secret_key = "" # Secret key for S3 for backups # ------------------------ # CTFd Manager configuration # ------------------------ -# The following is the configuration for the CTFd manager. -ctfd_manager_password = "" # Password for the CTFd Manager # The CTFd manager is used to manage the CTFd instance, and is not used for the CTFd instance itself. -ctfd_secret_key = "" # Secret key for CTFd, used for the CTFd instance itself. This is used to sign cookies and other sensitive data. It should be a long, random string. +ctfd_manager_password = "" # Password for the CTFd Manager # ------------------------ # CTFd configuration @@ -167,7 +166,7 @@ ctf_score_visibility = "" # Score visibility (e.g., "public" ctf_registration_visibility = "" # Registration visibility (e.g., "public") ctf_verify_emails = true # Whether to verify emails ctf_team_size = 0 # Team size for the CTF. 0 means no limit -ctf_brackets = [] # List of brackets, optional - Must be formatted as one line. +ctf_brackets = [] # List of brackets, optional. ctf_theme = "" # Theme for CTFd ctf_admin_name = "" # Name of the admin user ctf_admin_email = "" # Email of the admin user @@ -181,22 +180,38 @@ ctf_mail_password = "" # Mail server password ctf_mail_tls = true # Whether to use TLS for the mail server ctf_mail_from = "" # From address for the mail server -ctf_s3_bucket = "" # S3 bucket name for CTFd files -ctf_s3_region = "" # S3 region for CTF -ctf_s3_endpoint = "" # S3 endpoint for CTFd files -ctf_s3_access_key = "" # Access key for S3 for CTFd files -ctf_s3_secret_key = "" # Secret key for S3 for CTFd files -ctf_s3_prefix = "ctfd//" # S3 prefix for CTFd files, e.g., "ctfd/dev/" - ctf_logo_path = "data/logo.png" # Path to the CTF logo file (e.g., "ctf-logo.png") -ctfd_plugin_first_blood_limit_url = "" # Discord webhook URL for First blood notifications +ctfd_secret_key = "" # Secret key for CTFd + +# CTFd S3 Configuration +ctf_s3_bucket = "" # S3 bucket name for CTFd files +ctf_s3_region = "" # S3 region for CTFd files +ctf_s3_endpoint = "" # S3 endpoint for CTFd files +ctf_s3_access_key = "" # Access key for S3 for CTFd files +ctf_s3_secret_key = "" # Secret key for S3 for CTFd files +ctf_s3_prefix = "ctfd/" # S3 prefix for CTFd files, e.g., 'ctfd/dev/' -chall_whitelist_ips = ["", ""] # List of IPs to whitelist for challenges, e.g., [ "0.0.0.0/0" ] +# CTFd Plugin Configuration +ctfd_plugin_first_blood_limit_url = "" # Webhook URL for the First Blood plugin +ctfd_plugin_first_blood_limit = "1" # Limit configuration for the First Blood plugin +ctfd_plugin_first_blood_message = ":drop_of_blood: First blood for **{challenge}** goes to **{user}**! :drop_of_blood:" # Message configuration for the First Blood plugin + +# Pages Configuration +pages = [] # List of pages to deploy to CTFd +pages_repository = "" # Repository URL for pages +pages_branch = "" # Git branch for pages. Leave empty for environment based branch (environment == prod ? main : develop) + +# CTFd Deployment Configuration +ctfd_k8s_deployment_repository = "https://github.com/" # Repository URL for CTFd deployment files +ctfd_k8s_deployment_path = "k8s" # Path for CTFd deployment files within the git repository +ctfd_k8s_deployment_branch = "" # Git branch for CTFd deployment files. Leave empty for environment based branch (environment == prod ? main : develop) # ------------------------ # Challenges configuration # ------------------------ +chall_whitelist_ips = ["", ""] # List of IPs to whitelist for challenge access + challenges_static = { "" = ["", ""], } # List of static challenges to deploy. Needs to be the slugs of the challenges @@ -207,9 +222,8 @@ challenges_instanced = { "" = ["", ""], } # List of instanced challenges to deploy. Needs to be the slugs of the challenges -challenges_repository = "" # URL of the Git repository containing the challenge definitions -challenges_branch = "" # Branch of the Git repository to use for the challenge definitions. Leave empty for environment based branch (environment == prod ? main : develop) - +challenges_repository = "https://github.com/" # URL of the Git repository containing the challenge definitions +challenges_branch = "" # Branch of the Git repository to use for the challenge definitions. Leave empty for environment based branch (environment == prod ? main : develop) # ---------------------- # Docker images @@ -217,8 +231,12 @@ challenges_branch = "" # Branch of the Git repository to use for the # Values are maintained within each component as defaults. # You can override these values by uncommenting and setting your own images here. -# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:1.2.1" # The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback -# image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # The docker image for Filebeat +# image_error_fallback = "ghcr.io/ctfpilot/error-fallback:1.2.1" # The docker image for the error fallback deployment. See https://github.com/ctfpilot/error-fallback +# image_filebeat = "docker.elastic.co/beats/filebeat:8.19.0" # The docker image for Filebeat +# image_ctfd_manager = "ghcr.io/ctfpilot/ctfd-manager:1.0.1" # Docker image for the CTFd Manager deployment +# image_ctfd_exporter = "ghcr.io/the0mikkel/ctfd-exporter:1.1.1" # Docker image for the CTFd Exporter +# image_instancing_fallback = "ghcr.io/ctfpilot/instancing-fallback:1.0.2" # The docker image for the instancing fallback deployment. See https://github.com/ctfpilot/instancing-fallback +# image_kubectf = "ghcr.io/ctfpilot/kube-ctf:1.0.1" # The docker image for the kube-ctf deployment. See https://github.com/ctfpilot/kube-ctf # ---------------------- # Versions @@ -226,4 +244,11 @@ challenges_branch = "" # Branch of the Git repository to use for the # Values are maintained within each component as defaults. # You can override these values by uncommenting and setting your own versions here. -# mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator to deploy. More information at https://github.com/mariadb-operator/mariadb-operator +# kube_hetzner_version = "2.18.2" # The version of the Kube-Hetzner module to use. More information at https://github.com/mysticaltech/terraform-hcloud-kube-hetzner +# argocd_version = "8.2.5" # The version of the ArgoCD Helm chart to deploy. More information at https://github.com/argoproj/argo-helm +# cert_manager_version = "1.17.1" # The version of the Cert-Manager Helm chart to deploy. More information at https://github.com/cert-manager/cert-manager +# descheduler_version = "1.34" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler +# mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator Helm chart to deploy. More information at https://github.com/mariadb-operator/mariadb-operator +# kube_prometheus_stack_version = "62.3.1" # The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/ +# redis_operator_version = "0.22.2" # The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator +# mariadb_version = "25.8.1" # The version of MariaDB deploy. More information at https://github.com/mariadb-operator/mariadb-operator From 04b015acde24af17cbd5a0936feeb73d0e97d52d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Thu, 18 Dec 2025 23:33:36 +0100 Subject: [PATCH 031/148] Rename template file to use automated.tfvars extension for clarity --- ctfp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctfp.py b/ctfp.py index 2b2082e..c35f516 100644 --- a/ctfp.py +++ b/ctfp.py @@ -373,7 +373,7 @@ def run(self, args): Logger.info(f"Initializing {self.get_filename_tfvars()} (ENV: {self.environment})") path = Utils.get_path_to_script() - template = f"{path}/template.{self.get_filename_tfvars()}" + template = f"{path}/template.automated.tfvars" destination = f"{path}/{self.get_filename_tfvars()}" # Check if destination file already exists From 9db4442c25433c20e80f9d0296a6ef1dbb9e43b1 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 00:09:37 +0100 Subject: [PATCH 032/148] refactor: update backend generator to work with ctfp cli command system --- backend/generate.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/backend/generate.py b/backend/generate.py index 3baec0d..ef408d2 100644 --- a/backend/generate.py +++ b/backend/generate.py @@ -86,24 +86,23 @@ def run(self): class Generator: args = None - parent_parser = None - - def __init__(self, parent_parser = None): - self.parent_parser = parent_parser + + def __init__(self, subparser = None): + if not subparser: + self.subparser = argparse.ArgumentParser(description="Backend generator for Terraform") + self.subparser.set_defaults(func=self.run) + return + + self.subparser = subparser.add_parser("generate-backend", help="Generate Terraform backend configuration", description="Generate Terraform backend configuration for specified component") + self.subparser.set_defaults(func=self.run) def register_subcommand(self): - self.args = Args(self.parent_parser) + self.subparser.add_argument("component", help="Component to generate backend for", choices=["cluster", "ops", "platform", "challenges"]) + self.subparser.add_argument("bucket", help="S3 bucket name for Terraform state storage") + self.subparser.add_argument("region", help="Region for S3 bucket") + self.subparser.add_argument("endpoint", help="Endpoint URL for S3-compatible storage") - def run(self): - if not self.args: - arguments = Args(self.parent_parser) - arguments.parse() - self.args = arguments - else: - self.args.parse() - - args = self.args - + def run(self, args): template = Template( component=args.component, bucket=args.bucket, @@ -113,4 +112,14 @@ def run(self): template.run() if __name__ == "__main__": - Generator().run() \ No newline at end of file + args = Args() + if args.parser is None: + print("Failed to initialize argument parser") + exit(1) + + generator = Generator() + generator.register_subcommand() + + namespace = args.parser.parse_args() + + generator.run(namespace) From c7357d9e52fc3fdc1cf341c6cd7a0fbc91c76ffb Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 00:10:05 +0100 Subject: [PATCH 033/148] Integrate backend generator into CLI commands --- ctfp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ctfp.py b/ctfp.py index c35f516..79d3605 100644 --- a/ctfp.py +++ b/ctfp.py @@ -13,6 +13,8 @@ # Terraform parser - https://github.com/amplify-education/python-hcl2 import hcl2 +import backend.generate as backend_generate + AUTO_APPLY = True ENVIRONMENTS = ["test", "dev", "prod"] FLAVOR = "tofu" # Can be "terraform" or "tofu" @@ -1202,6 +1204,7 @@ def run(self): InsertKeys(subparser), Deploy(subparser), Destroy(subparser), + backend_generate.Generator(subparser) ] for command in commands: command.register_subcommand() From 04b0514af75a0f31f2020ad0f15558b33ac18aee Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 00:36:51 +0100 Subject: [PATCH 034/148] refactor: correct variable type to not use string based type --- ops/variables.tf | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ops/variables.tf b/ops/variables.tf index 5da30b0..e0f786a 100644 --- a/ops/variables.tf +++ b/ops/variables.tf @@ -79,8 +79,8 @@ variable "filebeat_elasticsearch_password" { } variable "prometheus_storage_size" { - type = "string" - default = "15Gi" + type = string + default = "15Gi" description = "The size of the persistent volume claim for Prometheus data storage. Format: (e.g., 20Gi, 100Gi)" } @@ -159,11 +159,11 @@ variable "mariadb_operator_version" { variable "kube_prometheus_stack_version" { type = string description = "The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/" - default = "62.3.1" + default = "62.3.1" } variable "redis_operator_version" { - type = string + type = string description = "The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator" - default = "0.22.2" -} \ No newline at end of file + default = "0.22.2" +} From 8f2238a0b120c6b0b39f7b4fdd718d8efe851f0d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 00:42:00 +0100 Subject: [PATCH 035/148] fix: update descheduler chart version and correct Helm chart path --- ops/descheduler.tf | 2 +- ops/tfvars/template.tfvars | 2 +- ops/variables.tf | 2 +- template.automated.tfvars | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ops/descheduler.tf b/ops/descheduler.tf index 84b34e0..ffde222 100644 --- a/ops/descheduler.tf +++ b/ops/descheduler.tf @@ -7,7 +7,7 @@ resource "kubernetes_namespace_v1" "descheduler" { resource "helm_release" "descheduler" { name = "descheduler" repository = "https://kubernetes-sigs.github.io/descheduler/" - chart = "descheduler/descheduler" + chart = "descheduler" version = var.descheduler_version namespace = kubernetes_namespace_v1.descheduler.metadata.0.name diff --git a/ops/tfvars/template.tfvars b/ops/tfvars/template.tfvars index fd4043b..6096e37 100644 --- a/ops/tfvars/template.tfvars +++ b/ops/tfvars/template.tfvars @@ -74,7 +74,7 @@ traefik_basic_auth = { user = "", password = "" # argocd_version = "8.2.5" # The version of the ArgoCD Helm chart to deploy. More information at https://github.com/argoproj/argo-helm # cert_manager_version = "1.17.1" # The version of the Cert-Manager Helm chart to deploy. More information at https://github.com/cert-manager/cert-manager -# descheduler_version = "1.34" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler +# descheduler_version = "0.34.0" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler # mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator Helm chart to deploy. More information at https://github.com/mariadb-operator/mariadb-operator # kube_prometheus_stack_version = "62.3.1" # The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/ # redis_operator_version = "0.22.2" # The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator diff --git a/ops/variables.tf b/ops/variables.tf index e0f786a..221a4e6 100644 --- a/ops/variables.tf +++ b/ops/variables.tf @@ -147,7 +147,7 @@ variable "cert_manager_version" { variable "descheduler_version" { type = string description = "The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler" - default = "1.34" + default = "0.34.0" } variable "mariadb_operator_version" { diff --git a/template.automated.tfvars b/template.automated.tfvars index 77fbda9..6a4d76f 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -247,7 +247,7 @@ challenges_branch = "" # Branch of the Git reposito # kube_hetzner_version = "2.18.2" # The version of the Kube-Hetzner module to use. More information at https://github.com/mysticaltech/terraform-hcloud-kube-hetzner # argocd_version = "8.2.5" # The version of the ArgoCD Helm chart to deploy. More information at https://github.com/argoproj/argo-helm # cert_manager_version = "1.17.1" # The version of the Cert-Manager Helm chart to deploy. More information at https://github.com/cert-manager/cert-manager -# descheduler_version = "1.34" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler +# descheduler_version = "0.34.0" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler # mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator Helm chart to deploy. More information at https://github.com/mariadb-operator/mariadb-operator # kube_prometheus_stack_version = "62.3.1" # The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/ # redis_operator_version = "0.22.2" # The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator From ae2dbe065eaefcdec6825678f98c22b7d2c2cb96 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 00:45:04 +0100 Subject: [PATCH 036/148] fix: update namespace reference in errors pull secret module --- ops/errors.tf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ops/errors.tf b/ops/errors.tf index fc115e9..561e705 100644 --- a/ops/errors.tf +++ b/ops/errors.tf @@ -10,9 +10,13 @@ resource "kubernetes_namespace" "errors" { module "errors-pull-secret" { source = "../tf-modules/pull-secret" - namespace = "errors" + namespace = kubernetes_namespace.errors.metadata[0].name ghcr_token = var.ghcr_token ghcr_username = var.ghcr_username + + depends_on = [ + kubernetes_namespace.errors + ] } resource "kubernetes_deployment_v1" "errors" { From b0834f5bcd8f03ea6c667c5404d15a75cddcf58b Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:14:07 +0100 Subject: [PATCH 037/148] refactor: change default value of challenge variables from list to map --- challenges/variables.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/challenges/variables.tf b/challenges/variables.tf index a0418dc..dbb955a 100644 --- a/challenges/variables.tf +++ b/challenges/variables.tf @@ -82,19 +82,19 @@ variable "chall_whitelist_ips" { variable "challenges_static" { type = map(list(string)) description = "List of static challenges to deploy. In the format { \"\" = [\"\", \"\"] }" - default = [] + default = {} } variable "challenges_shared" { type = map(list(string)) description = "List of shared challenges to deploy. In the format { \"\" = [\"\", \"\"] }" - default = [] + default = {} } variable "challenges_instanced" { type = map(list(string)) description = "List of instanced challenges to deploy. In the format { \"\" = [\"\", \"\"] }" - default = [] + default = {} } variable "challenges_repository" { From 539109e983ea452e9746a820108b83d36d2fb599 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:15:00 +0100 Subject: [PATCH 038/148] feat: add kubectl setup script for configuring kubectl context for CTFp cluster --- kube-config/.gitignore | 2 ++ kubectl.sh | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 kube-config/.gitignore create mode 100644 kubectl.sh diff --git a/kube-config/.gitignore b/kube-config/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/kube-config/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/kubectl.sh b/kubectl.sh new file mode 100644 index 0000000..3d44912 --- /dev/null +++ b/kubectl.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Select environment between test, dev or prod +# Usage: ./kubectl-setup.sh [test|dev|prod] +set -e +CTFP_EXECUTE=TRUE +if [ -z "$1" ]; then + echo "Usage: $0 [test|dev|prod]" + CTFP_EXECUTE=FALSE +fi +set +e + +if [ "$CTFP_EXECUTE" = TRUE ]; then + CTFP_ENVIRONMENT=$1 + echo "Setting up kubectl for environment: $CTFP_ENVIRONMENT" + + # Check if the kube-config directory exists, if not create it + if [ ! -d "./kube-config" ]; then + mkdir -p ./kube-config + fi + + # Check if the kube-config file exists, if not then fail + if [ -f "./kube-config/kube-config.$CTFP_ENVIRONMENT.yml" ]; then + export KUBECONFIG=${KUBECONFIG:-~/.kube/config}:$(pwd)/kube-config/kube-config.$CTFP_ENVIRONMENT.yml + kubectl config use-context k3s + echo "KUBECONFIG set to k3s" + else + echo "Kube-config file not found!" + fi +fi From 78f19ee0d1991221b344b21297e54507690e17a8 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:15:23 +0100 Subject: [PATCH 039/148] refactor: update CTF configuration with platform DNS and GitHub manager settings --- platform/tfvars/template.tfvars | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/platform/tfvars/template.tfvars b/platform/tfvars/template.tfvars index acc1041..4b04782 100644 --- a/platform/tfvars/template.tfvars +++ b/platform/tfvars/template.tfvars @@ -8,7 +8,7 @@ kubeconfig = "AA==" # Base64 encoded kubeconfig file # ------------------------ environment = "test" # Environment name for the CTF cluster_dns_management = "" # The specific domain name to use for the DNS records for the management part of the cluster -cluster_dns_ctf = "" # The domain name to use for the DNS records for the CTF part of the cluster +cluster_dns_platform = "" # The domain name to use for the DNS records for the platform part of the cluster # ------------------------ # GitHub variables @@ -46,7 +46,9 @@ s3_secret_key = "" # Secret key for S3 for backups # ------------------------ # CTFd Manager configuration # ------------------------ -ctfd_manager_password = "" # Password for the CTFd Manager +ctfd_manager_password = "" # Password for the CTFd Manager +ctfd_manager_github_repo = "" # Github repository used in the CTFd Manager. Env variable GITHUB_REPO. See https://github.com/ctfpilot/ctfd-manager +ctfd_manager_github_branch = "" # Github branch used in the CTFd Manager. Leave empty for environment based branch (environment == prod ? main : develop). Env variable GITHUB_BRANCH. See https://github.com/ctfpilot/ctfd-manager # ------------------------ # CTFd configuration From 284d4026735bc0f6f82207fec7366ef3d580e8d2 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:15:38 +0100 Subject: [PATCH 040/148] fix: update Elasticsearch configuration for Filebeat integration --- platform/ctfd.tf | 6 +++--- platform/variables.tf | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/platform/ctfd.tf b/platform/ctfd.tf index ef7b510..88199de 100644 --- a/platform/ctfd.tf +++ b/platform/ctfd.tf @@ -231,9 +231,9 @@ resource "kubernetes_secret_v1" "ctfd-filebeat-config" { file: "registrations.log" output.elasticsearch: - hosts: ["https://${var.fluentd_elasticsearch_host}:443"] - username: "${var.fluentd_elasticsearch_username}" - password: "${var.fluentd_elasticsearch_password}" + hosts: ["https://${var.filebeat_elasticsearch_host}:443"] + username: "${var.filebeat_elasticsearch_username}" + password: "${var.filebeat_elasticsearch_password}" protocol: https ssl.verification_mode: "full" index: filebeat-${var.environment}-ctfd diff --git a/platform/variables.tf b/platform/variables.tf index 1546127..ad9a069 100644 --- a/platform/variables.tf +++ b/platform/variables.tf @@ -48,19 +48,19 @@ variable "git_token" { nullable = false } -variable "fluentd_elasticsearch_host" { +variable "filebeat_elasticsearch_host" { type = string nullable = false - description = "The hostname of the Elasticsearch instance for Fluentd to send logs to. Must be a https 443 endpoint." + description = "The hostname of the Elasticsearch instance for Filebeat to send logs to. Must be a https 443 endpoint." } -variable "fluentd_elasticsearch_username" { +variable "filebeat_elasticsearch_username" { type = string nullable = false description = "The username for Elasticsearch authentication." } -variable "fluentd_elasticsearch_password" { +variable "filebeat_elasticsearch_password" { type = string nullable = false description = "The password for Elasticsearch authentication." @@ -276,4 +276,5 @@ variable "mariadb_version" { type = string description = "The version of MariaDB deploy. More information at https://github.com/mariadb-operator/mariadb-operator" nullable = false + default = "25.8.1" } From 3afd0a8bca97a055ca4cdc5d33606c03180b66c0 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:32:46 +0100 Subject: [PATCH 041/148] fix: correct shared_challenges variable assignment in challenges-config --- challenges/challenges-config.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/challenges/challenges-config.tf b/challenges/challenges-config.tf index b880461..0baf95d 100644 --- a/challenges/challenges-config.tf +++ b/challenges/challenges-config.tf @@ -1,6 +1,6 @@ locals { instanced_challenges = var.challenges_instanced - shared_challenges = var.challenges_static + shared_challenges = var.challenges_shared static_challenges = var.challenges_static challenges_branch = var.challenges_branch == "" ? local.env_branch : var.challenges_branch From ce8ff4ca76272fa75ce8d57e02fa632bde004ec6 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:32:55 +0100 Subject: [PATCH 042/148] feat: add GitHub user to ctfd-manager secret and deployment environment --- platform/ctfd-manager.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/platform/ctfd-manager.tf b/platform/ctfd-manager.tf index 251ceab..4e8f178 100644 --- a/platform/ctfd-manager.tf +++ b/platform/ctfd-manager.tf @@ -92,6 +92,7 @@ resource "kubernetes_secret_v1" "ctfd-manager-secret" { } data = { + "github-user" = var.ghcr_username "github-token" = var.git_token "password" = var.ctfd_manager_password } @@ -286,6 +287,16 @@ resource "kubernetes_deployment_v1" "ctfd-manager" { value = local.ctfd_manager_gh_branch } + env { + name = "GITHUB_USER" + value_from { + secret_key_ref { + name = kubernetes_secret_v1.ctfd-manager-secret.metadata.0.name + key = "github-user" + } + } + } + env { name = "GITHUB_TOKEN" value_from { From ce081583eca5ee38dba38371fe704792463130f6 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:33:11 +0100 Subject: [PATCH 043/148] refactor: update region settings and add GitHub repository configurations in automated setup template --- template.automated.tfvars | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/template.automated.tfvars b/template.automated.tfvars index 6a4d76f..be97fab 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -9,9 +9,9 @@ # For uptimal performance, it is recommended to use the same region for all servers. # Region 1 is used for scale nodes and loadbalancer. # Possible values: fsn1, hel1, nbg1 -region_1 = "fsn1" # Region for subgroup 1 -region_2 = "fsn1" # Region for subgroup 2 -region_3 = "fsn1" # Region for subgroup 3 +region_1 = "nbg1" # Region for subgroup 1 +region_2 = "nbg1" # Region for subgroup 2 +region_3 = "nbg1" # Region for subgroup 3 network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central", "us-east", "us-west", "ap-southeast". Regions must be within the network zone. # Servers @@ -151,6 +151,8 @@ s3_secret_key = "" # Secret key for S3 for backups # ------------------------ # The CTFd manager is used to manage the CTFd instance, and is not used for the CTFd instance itself. ctfd_manager_password = "" # Password for the CTFd Manager +ctfd_manager_github_repo = "" # Github repository used in the CTFd Manager. Env variable GITHUB_REPO. See https://github.com/ctfpilot/ctfd-manager +ctfd_manager_github_branch = "" # Github branch used in the CTFd Manager. Leave empty for environment based branch (environment == prod ? main : develop). Env variable GITHUB_BRANCH. See https://github.com/ctfpilot/ctfd-manager # ------------------------ # CTFd configuration @@ -180,7 +182,7 @@ ctf_mail_password = "" # Mail server password ctf_mail_tls = true # Whether to use TLS for the mail server ctf_mail_from = "" # From address for the mail server -ctf_logo_path = "data/logo.png" # Path to the CTF logo file (e.g., "ctf-logo.png") +ctf_logo_path = "data/logo.png" # Path to the CTF logo file (e.g., "ctf-logo.png"). Path from `platform/` directory. ctfd_secret_key = "" # Secret key for CTFd @@ -198,9 +200,9 @@ ctfd_plugin_first_blood_limit = "1" ctfd_plugin_first_blood_message = ":drop_of_blood: First blood for **{challenge}** goes to **{user}**! :drop_of_blood:" # Message configuration for the First Blood plugin # Pages Configuration -pages = [] # List of pages to deploy to CTFd -pages_repository = "" # Repository URL for pages -pages_branch = "" # Git branch for pages. Leave empty for environment based branch (environment == prod ? main : develop) +pages = [] # List of pages to deploy to CTFd +pages_repository = "https://github.com/" # Repository URL for pages +pages_branch = "" # Git branch for pages. Leave empty for environment based branch (environment == prod ? main : develop) # CTFd Deployment Configuration ctfd_k8s_deployment_repository = "https://github.com/" # Repository URL for CTFd deployment files From 1311cae8ba98bfdb4dfff957de68eaa631bad3f3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 01:33:55 +0100 Subject: [PATCH 044/148] refactor: tfvars handling and values --- ctfp.py | 205 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 110 insertions(+), 95 deletions(-) diff --git a/ctfp.py b/ctfp.py index 79d3605..4e85389 100644 --- a/ctfp.py +++ b/ctfp.py @@ -108,7 +108,7 @@ PLATFORM_TFVARS = [ # Generic information "cluster_dns_management", - "cluster_dns_ctf", + "cluster_dns_platform", # GitHub variables "ghcr_username", @@ -136,6 +136,8 @@ # CTFd Manager configuration "ctfd_manager_password", + "ctfd_manager_github_repo", + "ctfd_manager_github_branch", # CTFd configuration "ctfd_secret_key", @@ -163,7 +165,6 @@ "ctf_mail_tls", "ctf_mail_from", "ctf_logo_path", - "ctfd_discord_webhook_url", "ctf_s3_bucket", "ctf_s3_region", "ctf_s3_endpoint", @@ -216,6 +217,12 @@ "image_kubectf", ] +PATH = os.path.dirname(os.path.realpath(__file__)) +# Check if the path contains spaces +if " " in PATH: + print("Path to script contains spaces. Please move the script to a path without spaces") + exit(1) + # Load env from .env if os.path.exists(".env"): with open(".env", "r") as f: @@ -256,24 +263,26 @@ def print_help(self): self.parser.print_help() -class Utils: - @staticmethod - def get_path_to_script(): - path = os.path.dirname(os.path.realpath(__file__)) - - # Check if the path contains spaces - if " " in path: - Logger.error("Path to script contains spaces. Please move the script to a path without spaces") - exit(1) - - return path - +class Utils: @staticmethod def extract_tuple_from_list(list, key): for item in list: if key in item: return item return None + +class TFBackend: + @staticmethod + def get_backend_filename(component): + return f"{component}.hcl" + + @staticmethod + def get_backend_path(component): + return f"{PATH}/backend/generated/{TFBackend.get_backend_filename(component)}" + + @staticmethod + def backend_exists(component): + return os.path.exists(TFBackend.get_backend_path(component)) class Logger: RED = "\033[91m" @@ -336,9 +345,8 @@ def register_subcommand(self): def run(self, args): Logger.info("Generating server images") - path = Utils.get_path_to_script() try: - rc = run(f"cd {path}/cluster && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"", shell=True) + rc = run(f"cd {PATH}/cluster && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"", shell=True) if rc != 0: raise Exception except: @@ -374,9 +382,8 @@ def run(self, args): self.environment = "prod" Logger.info(f"Initializing {self.get_filename_tfvars()} (ENV: {self.environment})") - path = Utils.get_path_to_script() - template = f"{path}/template.automated.tfvars" - destination = f"{path}/{self.get_filename_tfvars()}" + template = f"{PATH}/template.automated.tfvars" + destination = f"{PATH}/{self.get_filename_tfvars()}" # Check if destination file already exists if os.path.exists(destination) and not args.force: @@ -428,9 +435,8 @@ def run(self, args): self.environment = "prod" Logger.info("Generating RSA keys") - path = Utils.get_path_to_script() try: - rc = run([f"{path}/data/keys/create.sh"], shell=True) + rc = run([f"{PATH}/data/keys/create.sh"], shell=True) if rc != 0: raise Exception except: @@ -578,22 +584,27 @@ def run(self, args): ''' Initialize Terraform to a given environment (workspace) ''' - def init_terraform(self, path): + def init_terraform(self, path, components: str = ""): Logger.info("Initializing Terraform") current_dir = os.getcwd() os.chdir(path) try: - # Create workspaces - Logger.info("Creating workspaces if they do not exist") - for env in ENVIRONMENTS: - subprocess.run([FLAVOR, "workspace", "new", env], check=False) + # Check if backend config exists + if not TFBackend.backend_exists(components): + Logger.error(f"Backend configuration for {components} does not exist. Please generate it first.") + raise Exception # Initialize the backend (if not already done for this project) Logger.info("Running terraform init") - rc = run(f"{FLAVOR} init", shell=True) + rc = run(f"{FLAVOR} init -backend-config={TFBackend.get_backend_path(components)}", shell=True) if rc != 0: raise Exception + + # Create workspaces + Logger.info("Creating workspaces if they do not exist") + for env in ENVIRONMENTS: + subprocess.run([FLAVOR, "workspace", "new", env], check=False) # Select the workspace based on the environment Logger.info(f"Selecting workspace: {self.environment}") @@ -611,8 +622,7 @@ def get_filename_tfvars(self): return TFVARS.get_filename_tfvars(self.environment) def get_path_tfvars(self): - path = Utils.get_path_to_script() - return f"{path}/{self.get_filename_tfvars()}" + return f"{PATH}/{self.get_filename_tfvars()}" ''' Validate automated.tfvars is set, and values are set @@ -623,30 +633,43 @@ def check_values(self): if not os.path.exists(tfvars_path): Logger.error(f"{self.get_filename_tfvars()} not found. Please create the file and try again") exit(1) - - # Ensure no < or > are present in the file - with open(tfvars_path, "r") as file: - for line in file: - if "<" in line and ">" in line: - Logger.error(f"{self.get_filename_tfvars()} does not seem to be filled out. Please fill out all fields and try again") - exit(1) + + # Load tfvars file + tfvars_data = TFVARS.safe_load_tfvars(tfvars_path) + + # Check if fields include "<" or ">" + def check_placeholders(value): + if isinstance(value, str) and "<" in value and ">" in value: + return True + elif isinstance(value, dict): + for v in value.values(): + if check_placeholders(v): + return True + elif isinstance(value, list): + for item in value: + if check_placeholders(item): + return True + return False + for key, value in tfvars_data.items(): + if check_placeholders(value): + Logger.error(f"{self.get_filename_tfvars()} does not seem to be filled out (see field '{key}'). Please fill out all fields and try again") + exit(1) Logger.info(f"{self.get_filename_tfvars()} is filled out correctly") def cluster_deploy(self): - path = Utils.get_path_to_script() Logger.info("Deploying the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars") tfvars.create(CLUSTER_TFVARS) # tfvars.add("environment", self.environment) Logger.space() # Deploy the cluster try: - self.init_terraform(f"{path}/cluster") - cmd = f"cd {path}/cluster && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}" + self.init_terraform(f"{PATH}/cluster", "cluster") + cmd = f"cd {PATH}/cluster && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}" rc = run(cmd, shell=True) if rc != 0: raise Exception @@ -658,15 +681,14 @@ def cluster_deploy(self): Logger.success("Cluster deployed successfully") def export_kubeconfig(self): - path = Utils.get_path_to_script() Logger.info("Exporting kubeconfig") # Export kubeconfig try: - rc = run(f"cd {path}/cluster && {FLAVOR} output --raw kubeconfig > {path}/kube-config/kube-config.{self.environment}.yml") + rc = run(f"cd {PATH}/cluster && {FLAVOR} output --raw kubeconfig > {PATH}/kube-config/kube-config.{self.environment}.yml") if rc != 0: raise Exception - rc = run(f"cat {path}/kube-config/kube-config.{self.environment}.yml | base64 -w0 > {path}/kube-config/kube-config.{self.environment}.b64") + rc = run(f"cat {PATH}/kube-config/kube-config.{self.environment}.yml | base64 -w0 > {PATH}/kube-config/kube-config.{self.environment}.b64") if rc != 0: raise Exception except: @@ -674,16 +696,14 @@ def export_kubeconfig(self): Logger.success("Kubeconfig exported") def get_kubeconfig_b64(self): - path = Utils.get_path_to_script() - with open(f"{path}/kube-config/kube-config.{self.environment}.b64", "r") as file: + with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: return file.read() def ops_deploy(self): - path = Utils.get_path_to_script() Logger.info("Deploying the ops on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/ops/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars") tfvars.create(OPS_TFVARS) tfvars.add_dict({ "kubeconfig": self.get_kubeconfig_b64(), @@ -693,8 +713,8 @@ def ops_deploy(self): # Deploy the cluster try: - self.init_terraform(f"{path}/ops") - rc = run(f"cd {path}/ops && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{PATH}/ops", "ops") + rc = run(f"cd {PATH}/ops && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -702,11 +722,10 @@ def ops_deploy(self): Logger.success("Ops deployed successfully") def platform_deploy(self): - path = Utils.get_path_to_script() Logger.info("Deploying the platform on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars") tfvars.create(PLATFORM_TFVARS) tfvars.add_dict({ "kubeconfig": self.get_kubeconfig_b64(), @@ -716,8 +735,8 @@ def platform_deploy(self): # Deploy the cluster try: - self.init_terraform(f"{path}/platform") - rc = run(f"cd {path}/platform && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{PATH}/platform", "platform") + rc = run(f"cd {PATH}/platform && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -725,11 +744,10 @@ def platform_deploy(self): Logger.success("Platform deployed successfully") def challenges_deploy(self): - path = Utils.get_path_to_script() Logger.info("Deploying the challenges on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars") tfvars.create(CHALLENGES_TFVARS) tfvars.add_dict({ "kubeconfig": self.get_kubeconfig_b64(), @@ -739,8 +757,8 @@ def challenges_deploy(self): # Deploy the cluster try: - self.init_terraform(f"{path}/challenges") - rc = run(f"cd {path}/challenges && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{PATH}/challenges", "challenges") + rc = run(f"cd {PATH}/challenges && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -851,22 +869,27 @@ def run(self, args): ''' Initialize Terraform to a given environment (workspace) ''' - def init_terraform(self, path): + def init_terraform(self, path, components: str = ""): Logger.info("Initializing Terraform") current_dir = os.getcwd() os.chdir(path) try: - # Create workspaces - Logger.info("Creating workspaces if they do not exist") - for env in ENVIRONMENTS: - subprocess.run([FLAVOR, "workspace", "new", env], check=False) + # Check if backend config exists + if not TFBackend.backend_exists(components): + Logger.error(f"Backend configuration for {components} does not exist. Please generate it first.") + raise Exception # Initialize the backend (if not already done for this project) Logger.info("Running terraform init") - rc = run(f"{FLAVOR} init", shell=True) + rc = run(f"{FLAVOR} init -backend-config={TFBackend.get_backend_path(components)}", shell=True) if rc != 0: raise Exception + + # Create workspaces + Logger.info("Creating workspaces if they do not exist") + for env in ENVIRONMENTS: + subprocess.run([FLAVOR, "workspace", "new", env], check=False) # Select the workspace based on the environment Logger.info(f"Selecting workspace: {self.environment}") @@ -884,36 +907,33 @@ def get_filename_tfvars(self): return TFVARS.get_filename_tfvars(self.environment) def get_path_tfvars(self): - path = Utils.get_path_to_script() - return f"{path}/{self.get_filename_tfvars()}" + return f"{PATH}/{self.get_filename_tfvars()}" def get_kubeconfig_b64(self): - path = Utils.get_path_to_script() - with open(f"{path}/kube-config/kube-config.{self.environment}.b64", "r") as file: + with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: return file.read() def cluster_destroy(self): - path = Utils.get_path_to_script() Logger.info("Destroying the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars") tfvars.create(CLUSTER_TFVARS) # tfvars.add("environment", self.environment) Logger.space() # Destroy the cluster try: - self.init_terraform(f"{path}/cluster") - rc = run(f"cd {path}/cluster && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{PATH}/challenges", "challenges") + rc = run(f"cd {PATH}/cluster && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: Logger.error("Cluster terraform destroy failed") # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{path}/cluster/data.auto.tfvars").destroy() + TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars").destroy() Logger.success("Cluster terraform destroy applied successfully") @@ -921,15 +941,14 @@ def cluster_destroy(self): self.remove_kubeconfig() def remove_kubeconfig(self): - path = Utils.get_path_to_script() Logger.info("Removing kubeconfig") # Remove kubeconfig try: - rc = run(f"rm {path}/kube-config/kube-config.{self.environment}.yml", shell=True) + rc = run(f"rm {PATH}/kube-config/kube-config.{self.environment}.yml", shell=True) if rc != 0: raise Exception - rc = run(f"rm {path}/kube-config/kube-config.{self.environment}.b64", shell=True) + rc = run(f"rm {PATH}/kube-config/kube-config.{self.environment}.b64", shell=True) if rc != 0: raise Exception except: @@ -937,11 +956,10 @@ def remove_kubeconfig(self): Logger.success("Kubeconfig removed") def ops_destroy(self): - path = Utils.get_path_to_script() Logger.info("Destroying the ops on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/ops/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars") tfvars.create(OPS_TFVARS) tfvars.add_dict({ "kubeconfig": self.get_kubeconfig_b64(), @@ -951,24 +969,23 @@ def ops_destroy(self): # Destroy the ops try: - self.init_terraform(f"{path}/ops") - rc = run(f"cd {path}/ops && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{PATH}/ops", "ops") + rc = run(f"cd {PATH}/ops && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: Logger.error("Ops destroy failed") # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{path}/ops/data.auto.tfvars").destroy() + TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars").destroy() Logger.success("Ops destroyed successfully") def platform_destroy(self): - path = Utils.get_path_to_script() Logger.info("Destroying the platform on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars") tfvars.create(PLATFORM_TFVARS) tfvars.add_dict({ "kubeconfig": self.get_kubeconfig_b64(), @@ -978,24 +995,23 @@ def platform_destroy(self): # Destroy the platform try: - self.init_terraform(f"{path}/platform") - rc = run(f"cd {path}/platform && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{PATH}/platform", "platform") + rc = run(f"cd {PATH}/platform && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: Logger.error("Platform destroy failed") # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{path}/platform/data.auto.tfvars").destroy() + TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars").destroy() Logger.success("Platform destroyed successfully") def challenges_destroy(self): - path = Utils.get_path_to_script() Logger.info("Destroying the challenges on the cluster") # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars") + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars") tfvars.create(CHALLENGES_TFVARS) tfvars.add_dict({ "kubeconfig": self.get_kubeconfig_b64(), @@ -1005,15 +1021,15 @@ def challenges_destroy(self): # Destroy the challenges try: - self.init_terraform(f"{path}/challenges") - rc = run(f"cd {path}/challenges && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + self.init_terraform(f"{PATH}/challenges", "challenges") + rc = run(f"cd {PATH}/challenges && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: Logger.error("Challenges destroy failed") # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{path}/challenges/data.auto.tfvars").destroy() + TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars").destroy() Logger.success("Challenges destroyed successfully") @@ -1167,20 +1183,19 @@ def destroy(self): @staticmethod def insert_keys(environment="test"): - path = Utils.get_path_to_script() # Read the keys public_key = "" private_key = "" - with open(f"{path}/data/keys/k8s.pub.b64", "r") as file: + with open(f"{PATH}/data/keys/k8s.pub.b64", "r") as file: public_key = file.read() - with open(f"{path}/data/keys/k8s.b64", "r") as file: + with open(f"{PATH}/data/keys/k8s.b64", "r") as file: private_key = file.read() - data = TFVARS.safe_load_tfvars(f"{path}/{TFVARS.get_filename_tfvars(environment)}") + data = TFVARS.safe_load_tfvars(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}") data["ssh_key_public_base64"] = public_key data["ssh_key_private_base64"] = private_key - TFVARS.safe_write_tfvars(f"{path}/{TFVARS.get_filename_tfvars(environment)}", data) + TFVARS.safe_write_tfvars(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}", data) ''' CLI tool From e39c26641c54d0a39e885521f71ca9c23fc6f2df Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 19 Dec 2025 22:17:42 +0100 Subject: [PATCH 045/148] Correct typos and improve descriptions in configuration files --- cluster/tfvars/template.tfvars | 6 +++--- cluster/variables.tf | 6 +++--- ctfp.py | 8 ++++---- notes.md | 13 +++++++++++++ ops/tfvars/template.tfvars | 4 ++-- ops/variables.tf | 4 ++-- template.automated.tfvars | 10 +++++----- 7 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 notes.md diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index 54ce649..0f9813c 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -16,8 +16,8 @@ ssh_key_public_base64 = "" # The public key to use for SSH access # Cloudflare variables # ------------------------ # The cluster uses two domains for the management and CTF parts of the cluster. -# This is to sepearte the two parts of the cluster, and to allow for different DNS records for the two parts. It may be the same domain. The specific subdomains is set later. -cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) +# This is to separate the two parts of the cluster, and to allow for different DNS records for the two parts. It may be the same domain. The specific subdomains is set later. +cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zone.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) cloudflare_dns_management = "" # The top level domain (TLD) to use for the DNS records for the management part of the cluster cloudflare_dns_platform = "" # The top level domain (TLD) to use for the DNS records for the platform part of the cluster cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF part of the cluster @@ -35,7 +35,7 @@ cluster_dns_ctf = "" # The domain name to use for # Cluster configuration # ------------------------ # WARNING: Changing region while the cluster is running will cause all servers in the group to be destroyed and recreated. -# For uptimal performance, it is recommended to use the same region for all servers. +# For optimal performance, it is recommended to use the same region for all servers. # Region 1 is used for scale nodes and loadbalancer. # Possible values: fsn1, hel1, nbg1 region_1 = "fsn1" # Region for subgroup 1 diff --git a/cluster/variables.tf b/cluster/variables.tf index 7f18625..d90bc6e 100644 --- a/cluster/variables.tf +++ b/cluster/variables.tf @@ -3,9 +3,9 @@ # ---------------------- variable "kube_hetzner_version" { - type = string + type = string description = "The version of the Kube-Hetzner module to use. More information at https://github.com/mysticaltech/terraform-hcloud-kube-hetzner" - default = "2.18.2" + default = "2.18.2" } # Hetzner @@ -28,7 +28,7 @@ variable "ssh_key_public_base64" { variable "cloudflare_api_token" { sensitive = true # Requires terraform >= 0.14 type = string - description = "Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains)" + description = "Cloudflare API Token for updating the DNS records (Zone.Zone.Read and Zone.DNS.Edit permissions required for the two following domains)" } variable "cloudflare_dns_management" { diff --git a/ctfp.py b/ctfp.py index 4e85389..db91702 100644 --- a/ctfp.py +++ b/ctfp.py @@ -127,7 +127,7 @@ "db_root_password", "db_user", "db_password", - # DB backupo configuration + # DB backup configuration "s3_bucket", "s3_region", "s3_endpoint", @@ -1141,7 +1141,7 @@ def add(self, key, value): :return: None ''' - # Check if destionation exists + # Check if destination exists exists = os.path.exists(self.destination) if not exists: Logger.error(f"{self.destination} does not exist") @@ -1159,7 +1159,7 @@ def add_dict(self, dict_data): :return: None ''' - # Check if destionation exists + # Check if destination exists exists = os.path.exists(self.destination) if not exists: Logger.error(f"{self.destination} does not exist") @@ -1171,7 +1171,7 @@ def add_dict(self, dict_data): TFVARS.safe_write_tfvars(self.destination, data) def destroy(self): - # Check if destionation exists + # Check if destination exists exists = os.path.exists(self.destination) # Remove the file diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..c2e97d0 --- /dev/null +++ b/notes.md @@ -0,0 +1,13 @@ +KubeCTF > CTF Pilot +Kubectf > CTFPilot +kubectf > ctfpilot +kubectf-overview > ctfpilot-overview +kubectf-challenges-isolated > ctfpilot-challenges-instanced +kubectf-challenges > ctfpilot-challenges +kube-ctf.io/node > cluster.ctfpilot.com/node +label_isolated_challenge_kube_ctf_io_ > label_instanced_challenges_ctfpilot_com_ +isolated.challenge.kube.ctf.io > instanced.challenges.ctfpilot.com +label_challenges_kube_ctf_io_ > label_challenges_ctfpilot_com_ +challenges.kube.ctf.io > challenges.ctfpilot.com +label_kube_ctf_io_ > label_ctfpilot_com_ +kube_ctf.io > ctfpilot.com diff --git a/ops/tfvars/template.tfvars b/ops/tfvars/template.tfvars index 6096e37..1ae78fe 100644 --- a/ops/tfvars/template.tfvars +++ b/ops/tfvars/template.tfvars @@ -13,7 +13,7 @@ discord_webhook_url = "" # Discord webhook URL for sending alerts and # ------------------------ # Cloudflare variables # ------------------------ -cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) +cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zone.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) cloudflare_dns_management = "" # The top level domain (TLD) to use for the DNS records for the management part of the cluster cloudflare_dns_platform = "" # The top level domain (TLD) to use for the DNS records for the platform part of the cluster cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF challenges part of the cluster @@ -77,4 +77,4 @@ traefik_basic_auth = { user = "", password = "" # descheduler_version = "0.34.0" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler # mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator Helm chart to deploy. More information at https://github.com/mariadb-operator/mariadb-operator # kube_prometheus_stack_version = "62.3.1" # The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/ -# redis_operator_version = "0.22.2" # The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator +# redis_operator_version = "0.22.2" # The version of the Redis Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator diff --git a/ops/variables.tf b/ops/variables.tf index 221a4e6..57222d8 100644 --- a/ops/variables.tf +++ b/ops/variables.tf @@ -21,7 +21,7 @@ variable "email" { variable "cloudflare_api_token" { sensitive = true # Requires terraform >= 0.14 type = string - description = "Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains)" + description = "Cloudflare API Token for updating the DNS records (Zone.Zone.Read and Zone.DNS.Edit permissions required for the two following domains)" } variable "cloudflare_dns_management" { @@ -164,6 +164,6 @@ variable "kube_prometheus_stack_version" { variable "redis_operator_version" { type = string - description = "The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator" + description = "The version of the Redis Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator" default = "0.22.2" } diff --git a/template.automated.tfvars b/template.automated.tfvars index be97fab..cdebe65 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -6,7 +6,7 @@ # Cluster configuration # ------------------------ # WARNING: Changing region while the cluster is running will cause all servers in the group to be destroyed and recreated. -# For uptimal performance, it is recommended to use the same region for all servers. +# For optimal performance, it is recommended to use the same region for all servers. # Region 1 is used for scale nodes and loadbalancer. # Possible values: fsn1, hel1, nbg1 region_1 = "nbg1" # Region for subgroup 1 @@ -64,8 +64,8 @@ ssh_key_public_base64 = "" # The public key to use for SSH access # Cloudflare variables # ------------------------ # The cluster uses two domains for the management and CTF parts of the cluster. -# This is to sepearte the two parts of the cluster, and to allow for different DNS records for the two parts. It may be the same domain. The specific subdomains is set later. -cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zne.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) +# This is to separate the two parts of the cluster, and to allow for different DNS records for the two parts. It may be the same domain. The specific subdomains is set later. +cloudflare_api_token = "" # Cloudflare API Token for updating the DNS records (Zone.Zone.Read and Zone.DNS.Edit permissions required for the two following domains) cloudflare_dns_management = "" # The top level domain (TLD) to use for the DNS records for the management part of the cluster cloudflare_dns_platform = "" # The top level domain (TLD) to use for the DNS records for the platform part of the cluster cloudflare_dns_ctf = "" # The top level domain (TLD) to use for the DNS records for the CTF part of the cluster @@ -150,7 +150,7 @@ s3_secret_key = "" # Secret key for S3 for backups # CTFd Manager configuration # ------------------------ # The CTFd manager is used to manage the CTFd instance, and is not used for the CTFd instance itself. -ctfd_manager_password = "" # Password for the CTFd Manager +ctfd_manager_password = "" # Password for the CTFd Manager ctfd_manager_github_repo = "" # Github repository used in the CTFd Manager. Env variable GITHUB_REPO. See https://github.com/ctfpilot/ctfd-manager ctfd_manager_github_branch = "" # Github branch used in the CTFd Manager. Leave empty for environment based branch (environment == prod ? main : develop). Env variable GITHUB_BRANCH. See https://github.com/ctfpilot/ctfd-manager @@ -252,5 +252,5 @@ challenges_branch = "" # Branch of the Git reposito # descheduler_version = "0.34.0" # The version of descheduler Helm chart to deploy. More information at https://github.com/kubernetes-sigs/descheduler # mariadb_operator_version = "25.8.1" # The version of the MariaDB Operator Helm chart to deploy. More information at https://github.com/mariadb-operator/mariadb-operator # kube_prometheus_stack_version = "62.3.1" # The version of the kube-prometheus-stack Helm chart to deploy. More information at https://github.com/prometheus-community/helm-charts/ -# redis_operator_version = "0.22.2" # The version of the Redus Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator +# redis_operator_version = "0.22.2" # The version of the Redis Operator Helm chart to deploy. More information at https://github.com/OT-CONTAINER-KIT/redis-operator # mariadb_version = "25.8.1" # The version of MariaDB deploy. More information at https://github.com/mariadb-operator/mariadb-operator From 4bf3715b0badbaf6edef0e444c0ed879e169fe50 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:14:39 +0100 Subject: [PATCH 046/148] declare AUTO_APPLY as global in Deploy and Destroy classes --- ctfp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ctfp.py b/ctfp.py index db91702..10fb986 100644 --- a/ctfp.py +++ b/ctfp.py @@ -503,6 +503,8 @@ def register_subcommand(self): return def run(self, args): + global AUTO_APPLY + if not args.cluster and not args.ops and not args.platform and not args.challenges and not args.all: Logger.error("Please specify which part of the platform to deploy") exit(1) @@ -788,6 +790,8 @@ def register_subcommand(self): return def run(self, args): + global AUTO_APPLY + if not args.cluster and not args.ops and not args.platform and not args.challenges and not args.all: Logger.error("Please specify which part of the platform to destroy") exit(1) From ed87279755fc07f031b76a1eca070f2744e3bffb Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:14:51 +0100 Subject: [PATCH 047/148] Update path in init_terraform call for cluster destruction --- ctfp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctfp.py b/ctfp.py index 10fb986..07b8dd2 100644 --- a/ctfp.py +++ b/ctfp.py @@ -929,7 +929,7 @@ def cluster_destroy(self): # Destroy the cluster try: - self.init_terraform(f"{PATH}/challenges", "challenges") + self.init_terraform(f"{PATH}/cluster", "cluster") rc = run(f"cd {PATH}/cluster && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception From bf0aac5c3e08c9bf9c1675989ea8b8c8e27cc4b4 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:18:02 +0100 Subject: [PATCH 048/148] refactor: improve subprocess handling in run function for better compatibility --- ctfp.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/ctfp.py b/ctfp.py index 07b8dd2..47675da 100644 --- a/ctfp.py +++ b/ctfp.py @@ -232,20 +232,31 @@ os.environ[key.strip()] = value.strip() def run(cmd, shell=True): - """ - Run a subprocess in a new process group and forward KeyboardInterrupt (SIGINT) to it. - Returns the process returncode. - """ + ''' + Run a subprocess in a new process group (where supported) and forward + KeyboardInterrupt (SIGINT) to it. Returns the process returncode. + ''' import signal + # Use os.setsid only on platforms where it is available (POSIX). + preexec = os.setsid if hasattr(os, "setsid") else None proc = subprocess.Popen( cmd, shell=shell, - preexec_fn=os.setsid + preexec_fn=preexec ) try: proc.wait() except KeyboardInterrupt: - os.killpg(proc.pid, signal.SIGINT) + # On POSIX, if we created a new process group, send SIGINT to the group. + if preexec is not None and hasattr(os, "killpg"): + os.killpg(proc.pid, signal.SIGINT) + else: + # Fallback for non-POSIX: send SIGINT directly to the child. + try: + proc.send_signal(signal.SIGINT) + except Exception: + # As a last resort, terminate the process. + proc.terminate() proc.wait() return proc.returncode From d99ad49d859e3f0fbfe4cadcf5600e0c31281005 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:30:04 +0100 Subject: [PATCH 049/148] refactor: sanitize script path and handle special characters in commands --- ctfp.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/ctfp.py b/ctfp.py index 47675da..f264ada 100644 --- a/ctfp.py +++ b/ctfp.py @@ -218,10 +218,13 @@ ] PATH = os.path.dirname(os.path.realpath(__file__)) -# Check if the path contains spaces -if " " in PATH: - print("Path to script contains spaces. Please move the script to a path without spaces") - exit(1) +# Sanitize path +PATH = PATH.replace(" ", "\\ ").replace("\"", "\\\"").replace("'", "\\'") +# Check if PATH contains special characters +for char in ['&', ';', '$', '>', '<', '|', '`', '!', '*', '?', '(', ')', '[', ']', '{', '}', '~']: + if char in PATH: + print(f"Path to script contains special character '{char}'. Please move the script to a path without special characters") + exit(1) # Load env from .env if os.path.exists(".env"): @@ -357,7 +360,7 @@ def register_subcommand(self): def run(self, args): Logger.info("Generating server images") try: - rc = run(f"cd {PATH}/cluster && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"", shell=True) + rc = run(f"cd \"{PATH}/cluster\" && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"", shell=True) if rc != 0: raise Exception except: @@ -447,7 +450,7 @@ def run(self, args): Logger.info("Generating RSA keys") try: - rc = run([f"{PATH}/data/keys/create.sh"], shell=True) + rc = run([f"\"{PATH}\"/data/keys/create.sh"], shell=True) if rc != 0: raise Exception except: @@ -610,7 +613,7 @@ def init_terraform(self, path, components: str = ""): # Initialize the backend (if not already done for this project) Logger.info("Running terraform init") - rc = run(f"{FLAVOR} init -backend-config={TFBackend.get_backend_path(components)}", shell=True) + rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) if rc != 0: raise Exception @@ -682,7 +685,7 @@ def cluster_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/cluster", "cluster") - cmd = f"cd {PATH}/cluster && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}" + cmd = f"cd \"{PATH}/cluster\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}" rc = run(cmd, shell=True) if rc != 0: raise Exception @@ -698,10 +701,10 @@ def export_kubeconfig(self): # Export kubeconfig try: - rc = run(f"cd {PATH}/cluster && {FLAVOR} output --raw kubeconfig > {PATH}/kube-config/kube-config.{self.environment}.yml") + rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} output --raw kubeconfig > \"{PATH}\"/kube-config/kube-config.{self.environment}.yml") if rc != 0: raise Exception - rc = run(f"cat {PATH}/kube-config/kube-config.{self.environment}.yml | base64 -w0 > {PATH}/kube-config/kube-config.{self.environment}.b64") + rc = run(f"cat \"{PATH}\"/kube-config/kube-config.{self.environment}.yml | base64 -w0 > \"{PATH}\"/kube-config/kube-config.{self.environment}.b64") if rc != 0: raise Exception except: @@ -727,7 +730,7 @@ def ops_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd {PATH}/ops && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -749,7 +752,7 @@ def platform_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd {PATH}/platform && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -771,7 +774,7 @@ def challenges_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd {PATH}/challenges && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -897,7 +900,7 @@ def init_terraform(self, path, components: str = ""): # Initialize the backend (if not already done for this project) Logger.info("Running terraform init") - rc = run(f"{FLAVOR} init -backend-config={TFBackend.get_backend_path(components)}", shell=True) + rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) if rc != 0: raise Exception @@ -941,7 +944,7 @@ def cluster_destroy(self): # Destroy the cluster try: self.init_terraform(f"{PATH}/cluster", "cluster") - rc = run(f"cd {PATH}/cluster && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -960,10 +963,10 @@ def remove_kubeconfig(self): # Remove kubeconfig try: - rc = run(f"rm {PATH}/kube-config/kube-config.{self.environment}.yml", shell=True) + rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.yml", shell=True) if rc != 0: raise Exception - rc = run(f"rm {PATH}/kube-config/kube-config.{self.environment}.b64", shell=True) + rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.b64", shell=True) if rc != 0: raise Exception except: @@ -985,7 +988,7 @@ def ops_destroy(self): # Destroy the ops try: self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd {PATH}/ops && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -1011,7 +1014,7 @@ def platform_destroy(self): # Destroy the platform try: self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd {PATH}/platform && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: @@ -1037,7 +1040,7 @@ def challenges_destroy(self): # Destroy the challenges try: self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd {PATH}/challenges && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception except: From 6dbd7f7d3fb98cc933623088c1dd486391fa71d2 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:34:00 +0100 Subject: [PATCH 050/148] Add better error handling for file handling --- ctfp.py | 112 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 43 deletions(-) diff --git a/ctfp.py b/ctfp.py index f264ada..69ab119 100644 --- a/ctfp.py +++ b/ctfp.py @@ -218,12 +218,46 @@ ] PATH = os.path.dirname(os.path.realpath(__file__)) + +class Logger: + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + RESET = "\033[0m" + + @staticmethod + def error(message): + print(f"{Logger.RED}Error: {message}{Logger.RESET}") + exit(1) + + @staticmethod + def info(message): + print(f"{Logger.BLUE}Info: {message}{Logger.RESET}") + + @staticmethod + def success(message): + print(f"{Logger.GREEN}Success: {message}{Logger.RESET}") + + @staticmethod + def warning(message): + print(f"{Logger.YELLOW}Warning: {message}{Logger.RESET}") + + @staticmethod + def debug(message): + print(f"{Logger.BLUE}Debug: {message}{Logger.RESET}") + + @staticmethod + def space(): + print("") + + # Sanitize path PATH = PATH.replace(" ", "\\ ").replace("\"", "\\\"").replace("'", "\\'") # Check if PATH contains special characters for char in ['&', ';', '$', '>', '<', '|', '`', '!', '*', '?', '(', ')', '[', ']', '{', '}', '~']: if char in PATH: - print(f"Path to script contains special character '{char}'. Please move the script to a path without special characters") + Logger.error(f"Path to script contains special character '{char}'. Please move the script to a path without special characters") exit(1) # Load env from .env @@ -298,38 +332,6 @@ def get_backend_path(component): def backend_exists(component): return os.path.exists(TFBackend.get_backend_path(component)) -class Logger: - RED = "\033[91m" - GREEN = "\033[92m" - YELLOW = "\033[93m" - BLUE = "\033[94m" - RESET = "\033[0m" - - @staticmethod - def error(message): - print(f"{Logger.RED}Error: {message}{Logger.RESET}") - exit(1) - - @staticmethod - def info(message): - print(f"{Logger.BLUE}Info: {message}{Logger.RESET}") - - @staticmethod - def success(message): - print(f"{Logger.GREEN}Success: {message}{Logger.RESET}") - - @staticmethod - def warning(message): - print(f"{Logger.YELLOW}Warning: {message}{Logger.RESET}") - - @staticmethod - def debug(message): - print(f"{Logger.BLUE}Debug: {message}{Logger.RESET}") - - @staticmethod - def space(): - print("") - ''' Subcommand pattern ''' @@ -712,8 +714,15 @@ def export_kubeconfig(self): Logger.success("Kubeconfig exported") def get_kubeconfig_b64(self): - with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: - return file.read() + try: + with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: + return file.read() + except FileNotFoundError: + Logger.error("Kubeconfig file not found. Please deploy the cluster first.") + exit(1) + except OSError as e: + Logger.error(f"Failed to read kubeconfig file: {e}") + exit(1) def ops_deploy(self): Logger.info("Deploying the ops on the cluster") @@ -928,8 +937,15 @@ def get_path_tfvars(self): return f"{PATH}/{self.get_filename_tfvars()}" def get_kubeconfig_b64(self): - with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: - return file.read() + try: + with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: + return file.read() + except FileNotFoundError: + Logger.error("Kubeconfig file not found. Please deploy the cluster first.") + exit(1) + except OSError as e: + Logger.error(f"Failed to read kubeconfig file: {e}") + exit(1) def cluster_destroy(self): Logger.info("Destroying the cluster") @@ -1101,8 +1117,11 @@ def safe_load_tfvars(file_path: str): try: return TFVARS.load_tfvars(file_path) + except FileNotFoundError: + Logger.error("tfvars file not found. Please create the file and try again.") + exit(1) except Exception as e: - print(f"Error loading tfvars file: {e}") + Logger.error(f"Error loading tfvars file: {e}") exit(1) @staticmethod @@ -1122,7 +1141,7 @@ def safe_write_tfvars(file_path: str, data: dict): with open(file_path, "w") as tfvars_file: tfvars_file.write(formatted_data) except Exception as e: - print(f"Error writing tfvars file: {e}") + Logger.error(f"Error writing tfvars file: {e}") exit(1) def create(self, fields=[]): @@ -1205,10 +1224,17 @@ def insert_keys(environment="test"): # Read the keys public_key = "" private_key = "" - with open(f"{PATH}/data/keys/k8s.pub.b64", "r") as file: - public_key = file.read() - with open(f"{PATH}/data/keys/k8s.b64", "r") as file: - private_key = file.read() + try: + with open(f"{PATH}/data/keys/k8s.pub.b64", "r") as file: + public_key = file.read() + with open(f"{PATH}/data/keys/k8s.b64", "r") as file: + private_key = file.read() + except FileNotFoundError: + Logger.error("SSH keys not found. Please run 'generate-keys' first.") + exit(1) + except OSError as e: + Logger.error(f"Failed to read SSH key files: {e}") + exit(1) data = TFVARS.safe_load_tfvars(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}") data["ssh_key_public_base64"] = public_key From 6e434642c87000a258c8180610c0117d327e9398 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:34:41 +0100 Subject: [PATCH 051/148] refactor: rename parameter in extract_tuple_from_list for clarity --- ctfp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ctfp.py b/ctfp.py index 69ab119..c5a107a 100644 --- a/ctfp.py +++ b/ctfp.py @@ -313,8 +313,8 @@ def print_help(self): class Utils: @staticmethod - def extract_tuple_from_list(list, key): - for item in list: + def extract_tuple_from_list(tuple_list, key): + for item in tuple_list: if key in item: return item return None From e5a96778dd30aa6c54530ff79b3b88dd02fe1a5d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:35:42 +0100 Subject: [PATCH 052/148] refactor: specify Exception in error handling for clarity --- ctfp.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/ctfp.py b/ctfp.py index c5a107a..053ecec 100644 --- a/ctfp.py +++ b/ctfp.py @@ -365,7 +365,7 @@ def run(self, args): rc = run(f"cd \"{PATH}/cluster\" && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Failed to generate images") Logger.success("Images generated successfully") @@ -416,7 +416,7 @@ def run(self, args): os_output = os.system(f"cp {template} {destination}") if os_output != 0: raise Exception - except: + except Exception: Logger.error(f"Failed to initialize {self.get_filename_tfvars()}") Logger.success(f"{self.get_filename_tfvars()} initialized successfully") @@ -455,7 +455,7 @@ def run(self, args): rc = run([f"\"{PATH}\"/data/keys/create.sh"], shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Failed to generate keys") Logger.success("Keys generated successfully in data/keys/ using ed25519") @@ -709,7 +709,7 @@ def export_kubeconfig(self): rc = run(f"cat \"{PATH}\"/kube-config/kube-config.{self.environment}.yml | base64 -w0 > \"{PATH}\"/kube-config/kube-config.{self.environment}.b64") if rc != 0: raise Exception - except: + except Exception: Logger.error("Failed to export kubeconfig") Logger.success("Kubeconfig exported") @@ -742,7 +742,7 @@ def ops_deploy(self): rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Ops apply failed") Logger.success("Ops deployed successfully") @@ -764,7 +764,7 @@ def platform_deploy(self): rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Platform apply failed") Logger.success("Platform deployed successfully") @@ -786,7 +786,7 @@ def challenges_deploy(self): rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Challenges apply failed") Logger.success("Challenges deployed successfully") @@ -963,7 +963,7 @@ def cluster_destroy(self): rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Cluster terraform destroy failed") # Remove the tfvars file @@ -985,7 +985,7 @@ def remove_kubeconfig(self): rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.b64", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Failed to remove kubeconfig") Logger.success("Kubeconfig removed") @@ -1007,7 +1007,7 @@ def ops_destroy(self): rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Ops destroy failed") # Remove the tfvars file @@ -1033,7 +1033,7 @@ def platform_destroy(self): rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Platform destroy failed") # Remove the tfvars file @@ -1059,7 +1059,7 @@ def challenges_destroy(self): rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) if rc != 0: raise Exception - except: + except Exception: Logger.error("Challenges destroy failed") # Remove the tfvars file From 2c32a61390b463d376f8c95b66c5e324f546cc87 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:36:36 +0100 Subject: [PATCH 053/148] refactor: simplify command construction for clarity --- ctfp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctfp.py b/ctfp.py index 053ecec..96a5f8a 100644 --- a/ctfp.py +++ b/ctfp.py @@ -687,7 +687,7 @@ def cluster_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/cluster", "cluster") - cmd = f"cd \"{PATH}/cluster\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}" + cmd = f"cd \"{PATH}/cluster\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}" rc = run(cmd, shell=True) if rc != 0: raise Exception From c71b92e9d46eb2f940a54171b04f658e4ab7cdd8 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:37:35 +0100 Subject: [PATCH 054/148] refactor: simplify command construction for clarity --- ctfp.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ctfp.py b/ctfp.py index 96a5f8a..66bbf47 100644 --- a/ctfp.py +++ b/ctfp.py @@ -739,7 +739,7 @@ def ops_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) if rc != 0: raise Exception except Exception: @@ -761,7 +761,7 @@ def platform_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) if rc != 0: raise Exception except Exception: @@ -783,7 +783,7 @@ def challenges_deploy(self): # Deploy the cluster try: self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} apply {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) if rc != 0: raise Exception except Exception: @@ -960,7 +960,7 @@ def cluster_destroy(self): # Destroy the cluster try: self.init_terraform(f"{PATH}/cluster", "cluster") - rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) if rc != 0: raise Exception except Exception: @@ -1004,7 +1004,7 @@ def ops_destroy(self): # Destroy the ops try: self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) if rc != 0: raise Exception except Exception: @@ -1030,7 +1030,7 @@ def platform_destroy(self): # Destroy the platform try: self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) if rc != 0: raise Exception except Exception: @@ -1056,7 +1056,7 @@ def challenges_destroy(self): # Destroy the challenges try: self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {AUTO_APPLY and '-auto-approve' or ''}", shell=True) + rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) if rc != 0: raise Exception except Exception: From d6ebb08372a959d003a592b4868d888c5d151336 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 11:38:25 +0100 Subject: [PATCH 055/148] refactor: standardize boolean values for clarity --- kubectl.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kubectl.sh b/kubectl.sh index 3d44912..7e38071 100644 --- a/kubectl.sh +++ b/kubectl.sh @@ -2,14 +2,14 @@ # Select environment between test, dev or prod # Usage: ./kubectl-setup.sh [test|dev|prod] set -e -CTFP_EXECUTE=TRUE +CTFP_EXECUTE=true if [ -z "$1" ]; then echo "Usage: $0 [test|dev|prod]" - CTFP_EXECUTE=FALSE + CTFP_EXECUTE=false fi set +e -if [ "$CTFP_EXECUTE" = TRUE ]; then +if [ "$CTFP_EXECUTE" = true ]; then CTFP_ENVIRONMENT=$1 echo "Setting up kubectl for environment: $CTFP_ENVIRONMENT" From 62d903ed229440caab87fc6a0f76e1bacb9c8352 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 16:57:22 +0100 Subject: [PATCH 056/148] refactor: add shebang for script execution compatibility --- ctfp.py | 2 ++ 1 file changed, 2 insertions(+) mode change 100644 => 100755 ctfp.py diff --git a/ctfp.py b/ctfp.py old mode 100644 new mode 100755 index 66bbf47..7de6357 --- a/ctfp.py +++ b/ctfp.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + # CTFp CLI tool # Licensed under PolyForm Noncommercial License 1.0.0. # See LICENSE file in the project root for full license information. From 6f5419b3eee4c4add36f4d022a92e557a7e934f4 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 17:08:05 +0100 Subject: [PATCH 057/148] refactor: encapsulate deployment logic within Terraform class --- ctfp.py | 739 +++++++++++++++++++++++++++----------------------------- 1 file changed, 357 insertions(+), 382 deletions(-) diff --git a/ctfp.py b/ctfp.py index 7de6357..788c159 100755 --- a/ctfp.py +++ b/ctfp.py @@ -552,12 +552,16 @@ def run(self, args): self.times.append(("start", time.time())) Logger.info("Deploying " + (self.environment.upper() if self.environment != "test" else "TEST") + " environment") - self.check_values() + Logger.space() + + terraform = Terraform(self.environment) + + terraform.check_values() Logger.space() if deploy_cluster: start_time = time.time() - self.cluster_deploy() + terraform.cluster_deploy() self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -565,7 +569,7 @@ def run(self, args): if deploy_ops: start_time = time.time() - self.ops_deploy() + terraform.ops_deploy() self.times.append(("ops", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -573,7 +577,7 @@ def run(self, args): if deploy_platform: start_time = time.time() - self.platform_deploy() + terraform.platform_deploy() self.times.append(("platform", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -581,7 +585,7 @@ def run(self, args): if deploy_challenges: start_time = time.time() - self.challenges_deploy() + terraform.challenges_deploy() self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -600,197 +604,6 @@ def run(self, args): Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") if deploy_challenges: Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") - - ''' - Initialize Terraform to a given environment (workspace) - ''' - def init_terraform(self, path, components: str = ""): - Logger.info("Initializing Terraform") - current_dir = os.getcwd() - os.chdir(path) - - try: - # Check if backend config exists - if not TFBackend.backend_exists(components): - Logger.error(f"Backend configuration for {components} does not exist. Please generate it first.") - raise Exception - - # Initialize the backend (if not already done for this project) - Logger.info("Running terraform init") - rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) - if rc != 0: - raise Exception - - # Create workspaces - Logger.info("Creating workspaces if they do not exist") - for env in ENVIRONMENTS: - subprocess.run([FLAVOR, "workspace", "new", env], check=False) - - # Select the workspace based on the environment - Logger.info(f"Selecting workspace: {self.environment}") - rc = run(f"{FLAVOR} workspace select {self.environment}", shell=True) - if rc != 0: - raise Exception - except subprocess.CalledProcessError as e: - Logger.error("Terraform initialization failed") - raise e - finally: - os.chdir(current_dir) # Always change back to the original directory - Logger.success("Terraform initialized successfully") - - def get_filename_tfvars(self): - return TFVARS.get_filename_tfvars(self.environment) - - def get_path_tfvars(self): - return f"{PATH}/{self.get_filename_tfvars()}" - - ''' - Validate automated.tfvars is set, and values are set - ''' - def check_values(self): - # Check if automated.tfvars exists - tfvars_path = self.get_path_tfvars() - if not os.path.exists(tfvars_path): - Logger.error(f"{self.get_filename_tfvars()} not found. Please create the file and try again") - exit(1) - - # Load tfvars file - tfvars_data = TFVARS.safe_load_tfvars(tfvars_path) - - # Check if fields include "<" or ">" - def check_placeholders(value): - if isinstance(value, str) and "<" in value and ">" in value: - return True - elif isinstance(value, dict): - for v in value.values(): - if check_placeholders(v): - return True - elif isinstance(value, list): - for item in value: - if check_placeholders(item): - return True - return False - for key, value in tfvars_data.items(): - if check_placeholders(value): - Logger.error(f"{self.get_filename_tfvars()} does not seem to be filled out (see field '{key}'). Please fill out all fields and try again") - exit(1) - - Logger.info(f"{self.get_filename_tfvars()} is filled out correctly") - - def cluster_deploy(self): - Logger.info("Deploying the cluster") - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars") - tfvars.create(CLUSTER_TFVARS) - # tfvars.add("environment", self.environment) - Logger.space() - - # Deploy the cluster - try: - self.init_terraform(f"{PATH}/cluster", "cluster") - cmd = f"cd \"{PATH}/cluster\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}" - rc = run(cmd, shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Cluster terraform failed") - Logger.success("Cluster terraform applied successfully") - # Export kubeconfig - self.export_kubeconfig() - Logger.success("Cluster deployed successfully") - - def export_kubeconfig(self): - Logger.info("Exporting kubeconfig") - - # Export kubeconfig - try: - rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} output --raw kubeconfig > \"{PATH}\"/kube-config/kube-config.{self.environment}.yml") - if rc != 0: - raise Exception - rc = run(f"cat \"{PATH}\"/kube-config/kube-config.{self.environment}.yml | base64 -w0 > \"{PATH}\"/kube-config/kube-config.{self.environment}.b64") - if rc != 0: - raise Exception - except Exception: - Logger.error("Failed to export kubeconfig") - Logger.success("Kubeconfig exported") - - def get_kubeconfig_b64(self): - try: - with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: - return file.read() - except FileNotFoundError: - Logger.error("Kubeconfig file not found. Please deploy the cluster first.") - exit(1) - except OSError as e: - Logger.error(f"Failed to read kubeconfig file: {e}") - exit(1) - - def ops_deploy(self): - Logger.info("Deploying the ops on the cluster") - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars") - tfvars.create(OPS_TFVARS) - tfvars.add_dict({ - "kubeconfig": self.get_kubeconfig_b64(), - "environment": self.environment - }) - Logger.space() - - # Deploy the cluster - try: - self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Ops apply failed") - Logger.success("Ops deployed successfully") - - def platform_deploy(self): - Logger.info("Deploying the platform on the cluster") - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars") - tfvars.create(PLATFORM_TFVARS) - tfvars.add_dict({ - "kubeconfig": self.get_kubeconfig_b64(), - "environment": self.environment - }) - Logger.space() - - # Deploy the cluster - try: - self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Platform apply failed") - Logger.success("Platform deployed successfully") - - def challenges_deploy(self): - Logger.info("Deploying the challenges on the cluster") - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars") - tfvars.create(CHALLENGES_TFVARS) - tfvars.add_dict({ - "kubeconfig": self.get_kubeconfig_b64(), - "environment": self.environment - }) - Logger.space() - - # Deploy the cluster - try: - self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Challenges apply failed") - Logger.success("Challenges deployed successfully") ''' Destroy the platform @@ -848,9 +661,11 @@ def run(self, args): Logger.info("Destroying " + (self.environment.upper() if self.environment != "test" else "TEST") + " environment") Logger.space() + terraform = Terraform(self.environment) + if destroy_challenges: start_time = time.time() - self.challenges_destroy() + terraform.challenges_destroy() self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -858,7 +673,7 @@ def run(self, args): if destroy_platform: start_time = time.time() - self.platform_destroy() + terraform.platform_destroy() self.times.append(("platform", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -866,7 +681,7 @@ def run(self, args): if destroy_ops: start_time = time.time() - self.ops_destroy() + terraform.ops_destroy() self.times.append(("ops", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -874,7 +689,7 @@ def run(self, args): if destroy_cluster: start_time = time.time() - self.cluster_destroy() + terraform.cluster_destroy() self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) Logger.space() Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") @@ -895,190 +710,17 @@ def run(self, args): if destroy_challenges: Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") - ''' - Initialize Terraform to a given environment (workspace) - ''' - def init_terraform(self, path, components: str = ""): - Logger.info("Initializing Terraform") - current_dir = os.getcwd() - os.chdir(path) - - try: - # Check if backend config exists - if not TFBackend.backend_exists(components): - Logger.error(f"Backend configuration for {components} does not exist. Please generate it first.") - raise Exception - # Initialize the backend (if not already done for this project) - Logger.info("Running terraform init") - rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) - if rc != 0: - raise Exception - - # Create workspaces - Logger.info("Creating workspaces if they do not exist") - for env in ENVIRONMENTS: - subprocess.run([FLAVOR, "workspace", "new", env], check=False) - - # Select the workspace based on the environment - Logger.info(f"Selecting workspace: {self.environment}") - rc = run(f"{FLAVOR} workspace select {self.environment}", shell=True) - if rc != 0: - raise Exception - except subprocess.CalledProcessError as e: - Logger.error("Terraform initialization failed") - raise e - finally: - os.chdir(current_dir) # Always change back to the original directory - Logger.success("Terraform initialized successfully") +''' +TFVars handler class +''' +class TFVARS: + root: str + destination: str - def get_filename_tfvars(self): - return TFVARS.get_filename_tfvars(self.environment) - - def get_path_tfvars(self): - return f"{PATH}/{self.get_filename_tfvars()}" - - def get_kubeconfig_b64(self): - try: - with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: - return file.read() - except FileNotFoundError: - Logger.error("Kubeconfig file not found. Please deploy the cluster first.") - exit(1) - except OSError as e: - Logger.error(f"Failed to read kubeconfig file: {e}") - exit(1) - - def cluster_destroy(self): - Logger.info("Destroying the cluster") - - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars") - tfvars.create(CLUSTER_TFVARS) - # tfvars.add("environment", self.environment) - Logger.space() - - # Destroy the cluster - try: - self.init_terraform(f"{PATH}/cluster", "cluster") - rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Cluster terraform destroy failed") - - # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars").destroy() - - Logger.success("Cluster terraform destroy applied successfully") - - # remove kubeconfig - self.remove_kubeconfig() - - def remove_kubeconfig(self): - Logger.info("Removing kubeconfig") - - # Remove kubeconfig - try: - rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.yml", shell=True) - if rc != 0: - raise Exception - rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.b64", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Failed to remove kubeconfig") - Logger.success("Kubeconfig removed") - - def ops_destroy(self): - Logger.info("Destroying the ops on the cluster") - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars") - tfvars.create(OPS_TFVARS) - tfvars.add_dict({ - "kubeconfig": self.get_kubeconfig_b64(), - "environment": self.environment - }) - Logger.space() - - # Destroy the ops - try: - self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Ops destroy failed") - - # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars").destroy() - - Logger.success("Ops destroyed successfully") - - def platform_destroy(self): - Logger.info("Destroying the platform on the cluster") - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars") - tfvars.create(PLATFORM_TFVARS) - tfvars.add_dict({ - "kubeconfig": self.get_kubeconfig_b64(), - "environment": self.environment - }) - Logger.space() - - # Destroy the platform - try: - self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Platform destroy failed") - - # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars").destroy() - - Logger.success("Platform destroyed successfully") - - def challenges_destroy(self): - Logger.info("Destroying the challenges on the cluster") - - # Configure tfvars file - tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars") - tfvars.create(CHALLENGES_TFVARS) - tfvars.add_dict({ - "kubeconfig": self.get_kubeconfig_b64(), - "environment": self.environment - }) - Logger.space() - - # Destroy the challenges - try: - self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception - except Exception: - Logger.error("Challenges destroy failed") - - # Remove the tfvars file - TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars").destroy() - - Logger.success("Challenges destroyed successfully") - -''' -TFVars handler class -''' -class TFVARS: - root: str - destination: str - - def __init__(self, root, destination): - self.root = root - self.destination = destination + def __init__(self, root, destination): + self.root = root + self.destination = destination @staticmethod def get_filename_tfvars(environment="test"): @@ -1243,6 +885,339 @@ def insert_keys(environment="test"): data["ssh_key_private_base64"] = private_key TFVARS.safe_write_tfvars(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}", data) +''' +Terraform handler +''' +class Terraform: + environment: str + + @staticmethod + def is_installed(): + ''' + Check if Terraform is installed + + :return: True if installed, False otherwise + ''' + try: + rc = run(f"{FLAVOR} version", shell=True) + return rc == 0 + except Exception: + return False + + def __init__(self, environment="test"): + self.environment = environment + + ''' + Initialize Terraform to a given environment (workspace) + ''' + def init_terraform(self, path, components: str = ""): + Logger.info("Initializing Terraform") + current_dir = os.getcwd() + os.chdir(path) + + try: + # Check if backend config exists + if not TFBackend.backend_exists(components): + Logger.error(f"Backend configuration for {components} does not exist. Please generate it first.") + raise Exception + + # Initialize the backend (if not already done for this project) + Logger.info("Running terraform init") + rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) + if rc != 0: + raise Exception + + # Create workspaces + Logger.info("Creating workspaces if they do not exist") + for env in ENVIRONMENTS: + subprocess.run([FLAVOR, "workspace", "new", env], check=False) + + # Select the workspace based on the environment + Logger.info(f"Selecting workspace: {self.environment}") + rc = run(f"{FLAVOR} workspace select {self.environment}", shell=True) + if rc != 0: + raise Exception + except subprocess.CalledProcessError as e: + Logger.error("Terraform initialization failed") + raise e + finally: + os.chdir(current_dir) # Always change back to the original directory + Logger.success("Terraform initialized successfully") + + def get_filename_tfvars(self): + return TFVARS.get_filename_tfvars(self.environment) + + def get_path_tfvars(self): + return f"{PATH}/{self.get_filename_tfvars()}" + + + ''' + Validate automated.tfvars is set, and values are set + ''' + def check_values(self): + # Check if automated.tfvars exists + tfvars_path = self.get_path_tfvars() + if not os.path.exists(tfvars_path): + Logger.error(f"{self.get_filename_tfvars()} not found. Please create the file and try again") + exit(1) + + # Load tfvars file + tfvars_data = TFVARS.safe_load_tfvars(tfvars_path) + + # Check if fields include "<" or ">" + def check_placeholders(value): + if isinstance(value, str) and "<" in value and ">" in value: + return True + elif isinstance(value, dict): + for v in value.values(): + if check_placeholders(v): + return True + elif isinstance(value, list): + for item in value: + if check_placeholders(item): + return True + return False + for key, value in tfvars_data.items(): + if check_placeholders(value): + Logger.error(f"{self.get_filename_tfvars()} does not seem to be filled out (see field '{key}'). Please fill out all fields and try again") + exit(1) + + Logger.info(f"{self.get_filename_tfvars()} is filled out correctly") + + def cluster_deploy(self): + Logger.info("Deploying the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars") + tfvars.create(CLUSTER_TFVARS) + # tfvars.add("environment", self.environment) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{PATH}/cluster", "cluster") + cmd = f"cd \"{PATH}/cluster\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}" + rc = run(cmd, shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Cluster terraform failed") + Logger.success("Cluster terraform applied successfully") + # Export kubeconfig + self.export_kubeconfig() + Logger.success("Cluster deployed successfully") + + def export_kubeconfig(self): + Logger.info("Exporting kubeconfig") + + # Export kubeconfig + try: + rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} output --raw kubeconfig > \"{PATH}\"/kube-config/kube-config.{self.environment}.yml") + if rc != 0: + raise Exception + rc = run(f"cat \"{PATH}\"/kube-config/kube-config.{self.environment}.yml | base64 -w0 > \"{PATH}\"/kube-config/kube-config.{self.environment}.b64") + if rc != 0: + raise Exception + except Exception: + Logger.error("Failed to export kubeconfig") + Logger.success("Kubeconfig exported") + + def get_kubeconfig_b64(self): + try: + with open(f"{PATH}/kube-config/kube-config.{self.environment}.b64", "r") as file: + return file.read() + except FileNotFoundError: + Logger.error("Kubeconfig file not found. Please deploy the cluster first.") + exit(1) + except OSError as e: + Logger.error(f"Failed to read kubeconfig file: {e}") + exit(1) + + def ops_deploy(self): + Logger.info("Deploying the ops on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars") + tfvars.create(OPS_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{PATH}/ops", "ops") + rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Ops apply failed") + Logger.success("Ops deployed successfully") + + def platform_deploy(self): + Logger.info("Deploying the platform on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars") + tfvars.create(PLATFORM_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{PATH}/platform", "platform") + rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Platform apply failed") + Logger.success("Platform deployed successfully") + + def challenges_deploy(self): + Logger.info("Deploying the challenges on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars") + tfvars.create(CHALLENGES_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) + Logger.space() + + # Deploy the cluster + try: + self.init_terraform(f"{PATH}/challenges", "challenges") + rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Challenges apply failed") + Logger.success("Challenges deployed successfully") + + def cluster_destroy(self): + Logger.info("Destroying the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars") + tfvars.create(CLUSTER_TFVARS) + # tfvars.add("environment", self.environment) + Logger.space() + + # Destroy the cluster + try: + self.init_terraform(f"{PATH}/cluster", "cluster") + rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Cluster terraform destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{PATH}/cluster/data.auto.tfvars").destroy() + + Logger.success("Cluster terraform destroy applied successfully") + + # remove kubeconfig + self.remove_kubeconfig() + + def remove_kubeconfig(self): + Logger.info("Removing kubeconfig") + + # Remove kubeconfig + try: + rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.yml", shell=True) + if rc != 0: + raise Exception + rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.b64", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Failed to remove kubeconfig") + Logger.success("Kubeconfig removed") + + def ops_destroy(self): + Logger.info("Destroying the ops on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars") + tfvars.create(OPS_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) + Logger.space() + + # Destroy the ops + try: + self.init_terraform(f"{PATH}/ops", "ops") + rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Ops destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{PATH}/ops/data.auto.tfvars").destroy() + + Logger.success("Ops destroyed successfully") + + def platform_destroy(self): + Logger.info("Destroying the platform on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars") + tfvars.create(PLATFORM_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) + Logger.space() + + # Destroy the platform + try: + self.init_terraform(f"{PATH}/platform", "platform") + rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Platform destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{PATH}/platform/data.auto.tfvars").destroy() + + Logger.success("Platform destroyed successfully") + + def challenges_destroy(self): + Logger.info("Destroying the challenges on the cluster") + + # Configure tfvars file + tfvars = TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars") + tfvars.create(CHALLENGES_TFVARS) + tfvars.add_dict({ + "kubeconfig": self.get_kubeconfig_b64(), + "environment": self.environment + }) + Logger.space() + + # Destroy the challenges + try: + self.init_terraform(f"{PATH}/challenges", "challenges") + rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception + except Exception: + Logger.error("Challenges destroy failed") + + # Remove the tfvars file + TFVARS(self.get_path_tfvars(), f"{PATH}/challenges/data.auto.tfvars").destroy() + + Logger.success("Challenges destroyed successfully") + ''' CLI tool ''' From b81f0289201cada3d40e74075fb0329c6067b8cc Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 17:15:53 +0100 Subject: [PATCH 058/148] refactor: update key generation logic and add create script --- ctfp.py | 12 ++++++------ keys/.gitignore | 3 +++ keys/create.sh | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 keys/.gitignore create mode 100755 keys/create.sh diff --git a/ctfp.py b/ctfp.py index 788c159..e26b539 100755 --- a/ctfp.py +++ b/ctfp.py @@ -454,15 +454,15 @@ def run(self, args): Logger.info("Generating RSA keys") try: - rc = run([f"\"{PATH}\"/data/keys/create.sh"], shell=True) + rc = run([f"\"{PATH}\"/keys/create.sh \"{self.environment}\""], shell=True) if rc != 0: raise Exception except Exception: Logger.error("Failed to generate keys") - Logger.success("Keys generated successfully in data/keys/ using ed25519") - Logger.info("Public key: data/keys/k8s.pub") - Logger.info("Private key: data/keys/k8s") + Logger.success("Keys generated successfully in keys/ using ed25519") + Logger.info(f"Public key: keys/k8s-{self.environment}.pub") + Logger.info(f"Private key: keys/k8s-{self.environment}") # Insert keys into automated.tfvars if args.insert: @@ -869,9 +869,9 @@ def insert_keys(environment="test"): public_key = "" private_key = "" try: - with open(f"{PATH}/data/keys/k8s.pub.b64", "r") as file: + with open(f"{PATH}/keys/k8s-{environment}.pub.b64", "r") as file: public_key = file.read() - with open(f"{PATH}/data/keys/k8s.b64", "r") as file: + with open(f"{PATH}/keys/k8s-{environment}.b64", "r") as file: private_key = file.read() except FileNotFoundError: Logger.error("SSH keys not found. Please run 'generate-keys' first.") diff --git a/keys/.gitignore b/keys/.gitignore new file mode 100644 index 0000000..b2e756f --- /dev/null +++ b/keys/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!create.sh diff --git a/keys/create.sh b/keys/create.sh new file mode 100755 index 0000000..515c6fc --- /dev/null +++ b/keys/create.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Usage: ./create.sh [test|dev|prod] +set -e +CTFP_EXECUTE=true +if [ -z "$1" ]; then + echo "Usage: $0 [test|dev|prod]" + CTFP_EXECUTE=false +fi +set +e + +if [ "$CTFP_EXECUTE" = true ]; then + CTFP_ENVIRONMENT=$1 + echo "Creating SSH keys for environment: $CTFP_ENVIRONMENT" + + # Get location of this file + DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + + ssh-keygen -t ed25519 -f "$DIR/k8s-$CTFP_ENVIRONMENT" -q -N "" + + # base64 encode the keys (into single base64 string) + base64 "$DIR/k8s-$CTFP_ENVIRONMENT" -w0 > "$DIR/k8s-$CTFP_ENVIRONMENT.b64" + base64 "$DIR/k8s-$CTFP_ENVIRONMENT.pub" -w0 > "$DIR/k8s-$CTFP_ENVIRONMENT.pub.b64" +fi From bdd9873e299663f432985379db9742df843c156a Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 17:37:28 +0100 Subject: [PATCH 059/148] refactor: update insert_keys to insert in place --- ctfp.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ctfp.py b/ctfp.py index e26b539..62603ef 100755 --- a/ctfp.py +++ b/ctfp.py @@ -864,7 +864,6 @@ def destroy(self): @staticmethod def insert_keys(environment="test"): - # Read the keys public_key = "" private_key = "" @@ -878,12 +877,19 @@ def insert_keys(environment="test"): exit(1) except OSError as e: Logger.error(f"Failed to read SSH key files: {e}") - exit(1) + exit(1) - data = TFVARS.safe_load_tfvars(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}") - data["ssh_key_public_base64"] = public_key - data["ssh_key_private_base64"] = private_key - TFVARS.safe_write_tfvars(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}", data) + # Insert the keys into automated.tfvars (in place) + with open(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}", "r") as file: + lines = file.readlines() + with open(f"{PATH}/{TFVARS.get_filename_tfvars(environment)}", "w") as file: + for line in lines: + if "ssh_key_public_base64" in line: + file.write(f'ssh_key_public_base64 = "{public_key}"\n') + elif "ssh_key_private_base64" in line: + file.write(f'ssh_key_private_base64 = "{private_key}"\n') + else: + file.write(line) ''' Terraform handler From 37a60b91a947f5428f3bb6753eff4d39b87de293 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 17:39:24 +0100 Subject: [PATCH 060/148] refactor: update challenges_branch to default to empty string --- template.automated.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.automated.tfvars b/template.automated.tfvars index cdebe65..338dd71 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -225,7 +225,7 @@ challenges_instanced = { } # List of instanced challenges to deploy. Needs to be the slugs of the challenges challenges_repository = "https://github.com/" # URL of the Git repository containing the challenge definitions -challenges_branch = "" # Branch of the Git repository to use for the challenge definitions. Leave empty for environment based branch (environment == prod ? main : develop) +challenges_branch = "" # Branch of the Git repository to use for the challenge definitions. Leave empty for environment based branch (environment == prod ? main : develop) # ---------------------- # Docker images From d7df136529462aea6876f9324b8425cb26ef9ad4 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 19:17:54 +0100 Subject: [PATCH 061/148] fix: update kube-ctf image version to 1.0.2 --- challenges/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/challenges/variables.tf b/challenges/variables.tf index dbb955a..401d90a 100644 --- a/challenges/variables.tf +++ b/challenges/variables.tf @@ -118,5 +118,5 @@ variable "image_instancing_fallback" { variable "image_kubectf" { type = string description = "The docker image for the kube-ctf deployment. See https://github.com/ctfpilot/kube-ctf" - default = "ghcr.io/ctfpilot/kube-ctf:1.0.1" + default = "ghcr.io/ctfpilot/kube-ctf:1.0.2" } From d0e0afa12f8c9577f18c9f8129e525e3485cd359 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 19:18:05 +0100 Subject: [PATCH 062/148] refactor: clarify version variable usage in kube.tf comments --- cluster/kube.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cluster/kube.tf b/cluster/kube.tf index 48bcab7..a24f974 100644 --- a/cluster/kube.tf +++ b/cluster/kube.tf @@ -16,7 +16,7 @@ module "kube-hetzner" { source = "kube-hetzner/kube-hetzner/hcloud" # When using the terraform registry as source, you can optionally specify a version number. # See https://registry.terraform.io/modules/kube-hetzner/kube-hetzner/hcloud for the available versions - version = var.kube_hetzner_version + version = var.kube_hetzner_version # Not possible to make a variable when using Terraform - See https://github.com/hashicorp/terraform/issues/28912 # 2. For local dev, path to the git repo # source = "../../kube-hetzner/" # 3. If you want to use the latest master branch (see https://developer.hashicorp.com/terraform/language/modules/sources#github), use From e64f88545659c60fdda8d6926935897ce8759f5a Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 19:54:32 +0100 Subject: [PATCH 063/148] refactor: reorder local category variables and update references in challenges module --- challenges/challenges-deployment.tf | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/challenges/challenges-deployment.tf b/challenges/challenges-deployment.tf index 3b76fcd..f97cfb8 100644 --- a/challenges/challenges-deployment.tf +++ b/challenges/challenges-deployment.tf @@ -1,7 +1,7 @@ locals { - categories_standard = keys(local.shared_challenges) - categories_isolated = keys(local.instanced_challenges) - categories_config = keys(local.static_challenges) + categories_config = keys(local.static_challenges) + categories_shared = keys(local.shared_challenges) + categories_instanced = keys(local.instanced_challenges) } module "argocd_project_shared" { @@ -106,7 +106,7 @@ module "repo_access_config" { module "shared_challenges" { source = "./challenges" - for_each = toset(local.categories_standard) + for_each = toset(local.categories_shared) revision = local.branch category = each.key @@ -138,7 +138,7 @@ module "shared_challenges" { module "instanced_challenges" { source = "./challenges" - for_each = toset(local.categories_isolated) + for_each = toset(local.categories_instanced) revision = local.branch category = each.key From b48101886e43d1c01a4709d9021e18d29165b0c2 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 19:55:04 +0100 Subject: [PATCH 064/148] fix: rename kube-ctf CRD from isolated to instanced --- tf-modules/kubectf/crd.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tf-modules/kubectf/crd.tf b/tf-modules/kubectf/crd.tf index 3871e3b..6b7737a 100644 --- a/tf-modules/kubectf/crd.tf +++ b/tf-modules/kubectf/crd.tf @@ -10,10 +10,10 @@ resource "kubernetes_manifest" "crd" { names = { plural = "instanced-challenges" - singular = "isolated-challenge" - kind = "IsolatedChallenge" + singular = "instanced-challenge" + kind = "instancedChallenge" shortNames = [ - "isolated-challenge" + "instanced-challenge" ] } versions = [ From ae40e91b08e995649a54873e35caa213edeec3f1 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 19:55:21 +0100 Subject: [PATCH 065/148] refactor: add instancing fallback middleware to Traefik configuration --- tf-modules/kubectf/traefik.tf | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tf-modules/kubectf/traefik.tf b/tf-modules/kubectf/traefik.tf index 802d9f8..a4a8067 100644 --- a/tf-modules/kubectf/traefik.tf +++ b/tf-modules/kubectf/traefik.tf @@ -28,3 +28,34 @@ resource "kubernetes_manifest" "traefik-errors-middleware" { kubernetes_service_v1.landing ] } + +resource "kubernetes_manifest" "traefik-instancing-fallback-middleware" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "instancing-fallback" + namespace = local.generic_namespace + } + spec = { + errors = { + status = [ + "502", + "503", + "504" + ] + query = "/{status}.html" + service = { + name = "landing" + port = 80 + } + } + } + } + + depends_on = [ + kubernetes_namespace.generic, + kubernetes_namespace.instanced-challenges, + kubernetes_service_v1.landing + ] +} From b39e62e5b08aaba5230ca5a3e21fb7c418f25c1e Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 19:55:54 +0100 Subject: [PATCH 066/148] Add CTFd configuration files to .gitignore --- platform/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/platform/.gitignore b/platform/.gitignore index 2faf43d..12c7fe8 100644 --- a/platform/.gitignore +++ b/platform/.gitignore @@ -35,3 +35,7 @@ override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc + +# CTFd deployment +ctfd_config.json +**/ctfd_config.json \ No newline at end of file From a7cf54dbbe837eea1c39fd343a3662805dac5809 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 19:58:19 +0100 Subject: [PATCH 067/148] refactor: update Terraform execution process --- ctfp.py | 244 +++++++++++++++++++++++++------------------ terraform/.gitignore | 5 + 2 files changed, 150 insertions(+), 99 deletions(-) create mode 100644 terraform/.gitignore diff --git a/ctfp.py b/ctfp.py index 62603ef..54f0f37 100755 --- a/ctfp.py +++ b/ctfp.py @@ -17,9 +17,10 @@ import backend.generate as backend_generate -AUTO_APPLY = True +AUTO_APPLY = False ENVIRONMENTS = ["test", "dev", "prod"] -FLAVOR = "tofu" # Can be "terraform" or "tofu" +FLAVOR = "tofu" # Can be "terraform" or "tofu". Only tested with "tofu" +COMPONENTS = ["cluster", "ops", "platform", "challenges"] CLUSTER_TFVARS = [ # Hetzner @@ -319,7 +320,7 @@ def extract_tuple_from_list(tuple_list, key): for item in tuple_list: if key in item: return item - return None + return () class TFBackend: @staticmethod @@ -506,43 +507,34 @@ class Deploy(Command): help = "Deploy the platform" description = "Deploy the platform" times = [] - environment = "test" # Default environment + environment = "test" # Default environment + components = COMPONENTS + ["all"] def register_subcommand(self): - # Only run listed parts of the deployment - self.subparser.add_argument("--cluster", action="store_true", help="Deploy the cluster") - self.subparser.add_argument("--ops", action="store_true", help="Deploy the ops") - self.subparser.add_argument("--platform", action="store_true", help="Deploy the platform") - self.subparser.add_argument("--challenges", action="store_true", help="Deploy the challenges") - self.subparser.add_argument("--all", action="store_true", help="Deploy all parts of the platform") + self.subparser.add_argument("component", help="Component to deploy (cluster, ops, platform, challenges, all)", choices=self.components) self.subparser.add_argument("--test", action="store_true", help="Deploy TEST cluster (default)") self.subparser.add_argument("--dev", action="store_true", help="Deploy DEV cluster") self.subparser.add_argument("--prod", action="store_true", help="Deploy PROD cluster") + self.subparser.add_argument("--auto-apply", action="store_true", help="Automatically apply Terraform changes without prompting") return def run(self, args): global AUTO_APPLY - - if not args.cluster and not args.ops and not args.platform and not args.challenges and not args.all: - Logger.error("Please specify which part of the platform to deploy") - exit(1) - - if args.all and (args.cluster or args.ops or args.platform or args.challenges): - Logger.error("Please specify only --all or individual parts of the platform") - exit(1) + # Check component is valid + component = args.component.lower() + if component not in COMPONENTS and component != "all": + Logger.error(f"Invalid component. Please specify one of: {', '.join(self.components)}") + exit(1) + if [args.test, args.dev, args.prod].count(True) > 1: Logger.error("Please specify only one environment: --test, --dev or --prod") exit(1) - - if args.prod: - AUTO_APPLY = False # Disable auto-apply for production environment - deploy_all = args.all - deploy_cluster = args.cluster or deploy_all - deploy_ops = args.ops or deploy_all - deploy_platform = args.platform or deploy_all - deploy_challenges = args.challenges or deploy_all + if args.auto_apply: + AUTO_APPLY = True + + deploy_all = component == "all" self.environment = "test" if args.dev: @@ -555,11 +547,9 @@ def run(self, args): Logger.space() terraform = Terraform(self.environment) - - terraform.check_values() Logger.space() - if deploy_cluster: + if deploy_all or component == "cluster": start_time = time.time() terraform.cluster_deploy() self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) @@ -567,7 +557,7 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if deploy_ops: + if deploy_all or component == "ops": start_time = time.time() terraform.ops_deploy() self.times.append(("ops", start_time, time.time(), time.time() - start_time)) @@ -575,7 +565,7 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if deploy_platform: + if deploy_all or component == "platform": start_time = time.time() terraform.platform_deploy() self.times.append(("platform", start_time, time.time(), time.time() - start_time)) @@ -583,7 +573,7 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if deploy_challenges: + if deploy_all or component == "challenges": start_time = time.time() terraform.challenges_deploy() self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) @@ -596,13 +586,13 @@ def run(self, args): Logger.info(f"Time taken: {str(round(Utils.extract_tuple_from_list(self.times, 'end')[1] - Utils.extract_tuple_from_list(self.times, 'start')[1], 2))} seconds") - if deploy_cluster: + if deploy_all or component == "cluster": Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") - if deploy_ops: + if deploy_all or component == "ops": Logger.info(f"Ops time: {str(round(Utils.extract_tuple_from_list(self.times, 'ops')[3], 2))} seconds") - if deploy_platform: + if deploy_all or component == "platform": Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") - if deploy_challenges: + if deploy_all or component == "challenges": Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") ''' @@ -614,42 +604,34 @@ class Destroy(Command): description = "Destroy the platform" times = [] environment = "test" # Default environment + components = COMPONENTS + ["all"] def register_subcommand(self): # Only run listed parts of the destruction - self.subparser.add_argument("--cluster", action="store_true", help="Destroy the cluster") - self.subparser.add_argument("--ops", action="store_true", help="Destroy the ops") - self.subparser.add_argument("--platform", action="store_true", help="Destroy the platform") - self.subparser.add_argument("--challenges", action="store_true", help="Destroy the challenges") - self.subparser.add_argument("--all", action="store_true", help="Destroy all parts of the platform") + self.subparser.add_argument("component", help="Component to destroy (cluster, ops, platform, challenges, all)", choices=self.components) self.subparser.add_argument("--test", action="store_true", help="Destroy TEST cluster (default)") self.subparser.add_argument("--dev", action="store_true", help="Destroy DEV cluster") self.subparser.add_argument("--prod", action="store_true", help="Destroy PROD cluster") + self.subparser.add_argument("--auto-apply", action="store_true", help="Automatically apply Terraform changes without prompting") return def run(self, args): global AUTO_APPLY - - if not args.cluster and not args.ops and not args.platform and not args.challenges and not args.all: - Logger.error("Please specify which part of the platform to destroy") - exit(1) - - if args.all and (args.cluster or args.ops or args.platform or args.challenges): - Logger.error("Please specify only --all or individual parts of the platform") + + # Check component is valid + component = args.component.lower() + if component not in COMPONENTS and component != "all": + Logger.error(f"Invalid component. Please specify one of: {', '.join(self.components)}") exit(1) - + if [args.test, args.dev, args.prod].count(True) > 1: Logger.error("Please specify only one environment: --test, --dev or --prod") exit(1) - - if args.prod: - AUTO_APPLY = False # Disable auto-apply for production environment + + if args.auto_apply: + AUTO_APPLY = True - destroy_all = args.all - destroy_cluster = args.cluster or destroy_all - destroy_ops = args.ops or destroy_all - destroy_platform = args.platform or destroy_all - destroy_challenges = args.challenges or destroy_all + destroy_all = component == "all" self.environment = "test" if args.dev: @@ -663,7 +645,7 @@ def run(self, args): terraform = Terraform(self.environment) - if destroy_challenges: + if destroy_all or component == "challenges": start_time = time.time() terraform.challenges_destroy() self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) @@ -671,7 +653,7 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if destroy_platform: + if destroy_all or component == "platform": start_time = time.time() terraform.platform_destroy() self.times.append(("platform", start_time, time.time(), time.time() - start_time)) @@ -679,7 +661,7 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if destroy_ops: + if destroy_all or component == "ops": start_time = time.time() terraform.ops_destroy() self.times.append(("ops", start_time, time.time(), time.time() - start_time)) @@ -687,7 +669,7 @@ def run(self, args): Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") Logger.space() - if destroy_cluster: + if destroy_all or component == "cluster": start_time = time.time() terraform.cluster_destroy() self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) @@ -701,13 +683,13 @@ def run(self, args): Logger.info(f"Time taken: {str(round(Utils.extract_tuple_from_list(self.times, 'end')[1] - Utils.extract_tuple_from_list(self.times, 'start')[1], 2))} seconds") - if destroy_cluster: + if destroy_all or component == "cluster": Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") - if destroy_ops: + if destroy_all or component == "ops": Logger.info(f"Ops time: {str(round(Utils.extract_tuple_from_list(self.times, 'ops')[3], 2))} seconds") - if destroy_platform: + if destroy_all or component == "platform": Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") - if destroy_challenges: + if destroy_all or component == "challenges": Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") @@ -922,6 +904,9 @@ def init_terraform(self, path, components: str = ""): os.chdir(path) try: + # Check if tfvars file exists and is valid + self.check_values() + # Check if backend config exists if not TFBackend.backend_exists(components): Logger.error(f"Backend configuration for {components} does not exist. Please generate it first.") @@ -931,7 +916,16 @@ def init_terraform(self, path, components: str = ""): Logger.info("Running terraform init") rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) if rc != 0: - raise Exception + # Try to init with reconfigure + response = input(f"The init of the backend for {components} failed. Do you want to try to reconfigure the backend? (y/N): ") + if response.lower() != "y": + Logger.info("Exiting") + exit(0) + + Logger.warning("Reconfiguring backend") + rc = run(f"{FLAVOR} init -reconfigure -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) + if rc != 0: + raise Exception # Create workspaces Logger.info("Creating workspaces if they do not exist") @@ -956,6 +950,83 @@ def get_filename_tfvars(self): def get_path_tfvars(self): return f"{PATH}/{self.get_filename_tfvars()}" + def execute(self, component, generate_plan=True, action="apply"): + ''' + Execute Terraform action (apply or destroy) + + :param component: The component to execute + :param generate_plan: Whether to generate a plan before executing + :param action: The action to execute (apply or destroy) + ''' + if action not in ["apply", "destroy"]: + Logger.error("Invalid action. Must be 'apply' or 'destroy'") + exit(1) + + is_apply = action == "apply" + + # Initialize Terraform + component_path = f"{PATH}/{component}" + self.init_terraform(component_path, component) + + rc = 0 + if generate_plan: + # Generate plan + Logger.info("Generating Terraform plan") + rc = run(f"cd \"{component_path}\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} plan {'' if is_apply else '-destroy'} -out=\"{PATH}/terraform/{component}-{self.environment}.tfplan\"", shell=True) + if rc != 0: + raise Exception(f"Terraform plan failed for {component} ({action}), with return code: {rc}") + + # Store the plan as human-readable output (Allowing user to review it) + rc = run(f"cd \"{component_path}\" && {FLAVOR} show -no-color \"{PATH}/terraform/{component}-{self.environment}.tfplan\" > \"{PATH}/terraform/{component}-{self.environment}.plan.txt\"", shell=True) + if rc != 0: + raise Exception(f"Terraform show plan failed for {component} ({action}), with return code: {rc}") + + Logger.success(f"Terraform plan generated successfully - It can be found at terraform/{component}-{self.environment}.plan.txt") + + # Ask if user wants to proceed + if not AUTO_APPLY: + response = input(f"Do you want to apply this plan on {component} ({action} {component} in {self.environment})? (y/N): ") + if response.lower() != "y": + Logger.info(f"Exiting without applying the plan on {component} ({action})") + exit(0) + + # Run apply + rc = run(f"cd \"{component_path}\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} apply \"{PATH}/terraform/{component}-{self.environment}.tfplan\"", shell=True) + + # Remove the plan files + os.remove(f"{PATH}/terraform/{component}-{self.environment}.tfplan") + + # Move human readable plan to .old + os.rename(f"{PATH}/terraform/{component}-{self.environment}.plan.txt", f"{PATH}/terraform/{component}-{self.environment}.plan.txt.old") + else: + # Run apply directly + rc = run(f"cd \"{component_path}\" && {FLAVOR} {action} {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + if rc != 0: + raise Exception(f"Terraform {action} failed for {component}, with return code: {rc}") + + ''' + Run Terraform apply + ''' + def apply(self, component, generate_plan=True): + ''' + Run Terraform apply + + :param component: The component to apply + :param generate_plan: Whether to generate a plan before applying + ''' + self.execute(component, generate_plan, action="apply") + + ''' + Run Terraform destroy + ''' + def destroy(self, component, generate_plan=True): + ''' + Run Terraform destroy + + :param component: The component to destroy + :param generate_plan: Whether to generate a plan before destroying + ''' + self.execute(component, generate_plan, action="destroy") ''' Validate automated.tfvars is set, and values are set @@ -1001,11 +1072,7 @@ def cluster_deploy(self): # Deploy the cluster try: - self.init_terraform(f"{PATH}/cluster", "cluster") - cmd = f"cd \"{PATH}/cluster\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}" - rc = run(cmd, shell=True) - if rc != 0: - raise Exception + self.apply("cluster") except Exception: Logger.error("Cluster terraform failed") Logger.success("Cluster terraform applied successfully") @@ -1053,10 +1120,7 @@ def ops_deploy(self): # Deploy the cluster try: - self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception + self.apply("ops") except Exception: Logger.error("Ops apply failed") Logger.success("Ops deployed successfully") @@ -1075,10 +1139,7 @@ def platform_deploy(self): # Deploy the cluster try: - self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception + self.apply("platform") except Exception: Logger.error("Platform apply failed") Logger.success("Platform deployed successfully") @@ -1097,10 +1158,7 @@ def challenges_deploy(self): # Deploy the cluster try: - self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} apply {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception + self.apply("challenges") except Exception: Logger.error("Challenges apply failed") Logger.success("Challenges deployed successfully") @@ -1116,10 +1174,7 @@ def cluster_destroy(self): # Destroy the cluster try: - self.init_terraform(f"{PATH}/cluster", "cluster") - rc = run(f"cd \"{PATH}/cluster\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception + self.destroy("cluster") except Exception: Logger.error("Cluster terraform destroy failed") @@ -1160,10 +1215,7 @@ def ops_destroy(self): # Destroy the ops try: - self.init_terraform(f"{PATH}/ops", "ops") - rc = run(f"cd \"{PATH}/ops\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception + self.destroy("ops") except Exception: Logger.error("Ops destroy failed") @@ -1186,10 +1238,7 @@ def platform_destroy(self): # Destroy the platform try: - self.init_terraform(f"{PATH}/platform", "platform") - rc = run(f"cd \"{PATH}/platform\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception + self.destroy("platform") except Exception: Logger.error("Platform destroy failed") @@ -1212,10 +1261,7 @@ def challenges_destroy(self): # Destroy the challenges try: - self.init_terraform(f"{PATH}/challenges", "challenges") - rc = run(f"cd \"{PATH}/challenges\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} destroy {'-auto-approve' if AUTO_APPLY else ''}", shell=True) - if rc != 0: - raise Exception + self.destroy("challenges") except Exception: Logger.error("Challenges destroy failed") diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 0000000..e57f689 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,5 @@ +# This directory contains Terraform plans when deploying CTFp. + +* +!.gitignore +!create.sh From 83aea9cb5d7bda774d86085390ab4e4533eb209d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 20:28:39 +0100 Subject: [PATCH 068/148] Correct comment formatting for kubectf_container_secret in automated.tfvars --- template.automated.tfvars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template.automated.tfvars b/template.automated.tfvars index 338dd71..1f355ac 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -129,7 +129,7 @@ git_token = "" # GitHub repo token. Only let this token have # The following is the configuration for the instanced challenge management system. # They should be unique and strong passwords. kubectf_auth_secret = "" # The secret to use for the authSecret in the CTF configuration -kubectf_container_secret = "" # The secret to use for the containerSecret in the CTF configuration +kubectf_container_secret = "" # The secret to use for the containerSecret in the CTF configuration # ------------------------ # DB configuration From 316af2c0910ebce53bf9bf43ce5c22c00b9618f7 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 20:30:28 +0100 Subject: [PATCH 069/148] refactor: improve placeholder check in Terraform class --- ctfp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctfp.py b/ctfp.py index 54f0f37..980d22d 100755 --- a/ctfp.py +++ b/ctfp.py @@ -1043,7 +1043,7 @@ def check_values(self): # Check if fields include "<" or ">" def check_placeholders(value): - if isinstance(value, str) and "<" in value and ">" in value: + if isinstance(value, str) and value.startswith("<") and value.endswith(">"): return True elif isinstance(value, dict): for v in value.values(): From 6fd931c99a9f8d178fc87ad9f45fd20e89f62434 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 20 Dec 2025 20:33:26 +0100 Subject: [PATCH 070/148] refactor: simplify platform check for Linux and bash requirements --- ctfp.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ctfp.py b/ctfp.py index 980d22d..993ba56 100755 --- a/ctfp.py +++ b/ctfp.py @@ -1312,14 +1312,9 @@ def run(self): Logger.error(f"Failed to run subcommand: {e}") def platform_check(self): - # Check if system is linux - if sys.platform != "linux": - Logger.error("This script is only supported on Linux") - exit(1) - - # Check if user has bash - if not os.path.exists("/bin/bash"): - Logger.error("This script requires bash") + # Check if system is linux and if bash is available + if sys.platform != "linux" or not os.path.exists("/bin/bash"): + Logger.error("This script requires Linux and bash") exit(1) if __name__ == "__main__": From a65bc4306988ef630f0c7fc77f109475cb6222ab Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 10:46:56 +0100 Subject: [PATCH 071/148] refactor: remove set -e and set +e from script for improved readability --- keys/create.sh | 2 -- kubectl.sh | 2 -- 2 files changed, 4 deletions(-) diff --git a/keys/create.sh b/keys/create.sh index 515c6fc..69cb1de 100755 --- a/keys/create.sh +++ b/keys/create.sh @@ -1,13 +1,11 @@ #!/usr/bin/env bash # Usage: ./create.sh [test|dev|prod] -set -e CTFP_EXECUTE=true if [ -z "$1" ]; then echo "Usage: $0 [test|dev|prod]" CTFP_EXECUTE=false fi -set +e if [ "$CTFP_EXECUTE" = true ]; then CTFP_ENVIRONMENT=$1 diff --git a/kubectl.sh b/kubectl.sh index 7e38071..6bb997b 100644 --- a/kubectl.sh +++ b/kubectl.sh @@ -1,13 +1,11 @@ #!/usr/bin/env bash # Select environment between test, dev or prod # Usage: ./kubectl-setup.sh [test|dev|prod] -set -e CTFP_EXECUTE=true if [ -z "$1" ]; then echo "Usage: $0 [test|dev|prod]" CTFP_EXECUTE=false fi -set +e if [ "$CTFP_EXECUTE" = true ]; then CTFP_ENVIRONMENT=$1 From 5368a643dd5ec4a4dbab1590cb56d8c82e5ab2cc Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 10:48:35 +0100 Subject: [PATCH 072/148] refactor: correct kind casing for InstancedChallenge in CRD definition --- tf-modules/kubectf/crd.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tf-modules/kubectf/crd.tf b/tf-modules/kubectf/crd.tf index 6b7737a..ff0f500 100644 --- a/tf-modules/kubectf/crd.tf +++ b/tf-modules/kubectf/crd.tf @@ -11,7 +11,7 @@ resource "kubernetes_manifest" "crd" { plural = "instanced-challenges" singular = "instanced-challenge" - kind = "instancedChallenge" + kind = "InstancedChallenge" shortNames = [ "instanced-challenge" ] From 3b98f9711b3bad07086078e010612c6c5a9b41ee Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 10:50:57 +0100 Subject: [PATCH 073/148] refactor: enhance PATH validation to reject special characters that may break shell commands --- ctfp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ctfp.py b/ctfp.py index 993ba56..b96a07c 100755 --- a/ctfp.py +++ b/ctfp.py @@ -255,10 +255,8 @@ def space(): print("") -# Sanitize path -PATH = PATH.replace(" ", "\\ ").replace("\"", "\\\"").replace("'", "\\'") -# Check if PATH contains special characters -for char in ['&', ';', '$', '>', '<', '|', '`', '!', '*', '?', '(', ')', '[', ']', '{', '}', '~']: +# Validate PATH: reject if it contains special characters that may break shell commands +for char in [' ', '"', "'", '&', ';', '$', '>', '<', '|', '`', '!', '*', '?', '(', ')', '[', ']', '{', '}', '~']: if char in PATH: Logger.error(f"Path to script contains special character '{char}'. Please move the script to a path without special characters") exit(1) From 964cbda24dda2f37dd3ea3693efcff1834ff2df4 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 10:53:20 +0100 Subject: [PATCH 074/148] refactor: update key generation terminology from RSA to SSH --- ctfp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ctfp.py b/ctfp.py index b96a07c..b1b42b7 100755 --- a/ctfp.py +++ b/ctfp.py @@ -425,12 +425,12 @@ def get_filename_tfvars(self): return TFVARS.get_filename_tfvars(self.environment) ''' -Generate RSA keys +Generate SSH keys ''' class GenerateKeys(Command): name = "generate-keys" - help = "Generate RSA keys" - description = "Generate RSA keys" + help = "Generate SSH keys" + description = "Generate SSH keys" environment = "test" # Default environment def register_subcommand(self): @@ -451,7 +451,7 @@ def run(self, args): elif args.prod: self.environment = "prod" - Logger.info("Generating RSA keys") + Logger.info("Generating SSH keys") try: rc = run([f"\"{PATH}\"/keys/create.sh \"{self.environment}\""], shell=True) if rc != 0: From 336da3e7e1e1d39cbb9a56e20ebeadb32f3d5834 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 11:02:48 +0100 Subject: [PATCH 075/148] refactor: replace os.system with shutil.copyfile for file copying and add tool checks for dependencies --- ctfp.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/ctfp.py b/ctfp.py index b1b42b7..42e6f18 100755 --- a/ctfp.py +++ b/ctfp.py @@ -11,6 +11,7 @@ import argparse import time import subprocess +import shutil # Terraform parser - https://github.com/amplify-education/python-hcl2 import hcl2 @@ -414,9 +415,7 @@ def run(self, args): # Clone the template to the destination try: - os_output = os.system(f"cp {template} {destination}") - if os_output != 0: - raise Exception + shutil.copyfile(template, destination) except Exception: Logger.error(f"Failed to initialize {self.get_filename_tfvars()}") Logger.success(f"{self.get_filename_tfvars()} initialized successfully") @@ -1274,6 +1273,7 @@ def challenges_destroy(self): class CLI: def run(self): self.platform_check() + self.tool_check() args = Args() if args.parser is None: @@ -1315,5 +1315,26 @@ def platform_check(self): Logger.error("This script requires Linux and bash") exit(1) + def tool_check(self): + # Check if Terraform is installed + if not Terraform.is_installed(): + Logger.error("Terraform is not installed. Please install Terraform and try again.") + exit(1) + + # Check if curl is installed + if run("which curl", shell=True) != 0: + Logger.error("curl is not installed. Please install curl and try again.") + exit(1) + + # Check if base64 is installed + if run("which base64", shell=True) != 0: + Logger.error("base64 is not installed. Please install base64 and try again.") + exit(1) + + # Check if keygen is installed + if run("which ssh-keygen", shell=True) != 0: + Logger.error("ssh-keygen is not installed. Please install ssh-keygen and try again.") + exit(1) + if __name__ == "__main__": CLI().run() From 125d4882dfe7b1815ae369ef44d08118351bf9c0 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 11:12:35 +0100 Subject: [PATCH 076/148] refactor: change shell execution to use shell=False for improved security --- ctfp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ctfp.py b/ctfp.py index 42e6f18..a7d9faf 100755 --- a/ctfp.py +++ b/ctfp.py @@ -884,7 +884,7 @@ def is_installed(): :return: True if installed, False otherwise ''' try: - rc = run(f"{FLAVOR} version", shell=True) + rc = run(f"{FLAVOR} version", shell=False) return rc == 0 except Exception: return False @@ -1322,17 +1322,17 @@ def tool_check(self): exit(1) # Check if curl is installed - if run("which curl", shell=True) != 0: + if run("which curl", shell=False) != 0: Logger.error("curl is not installed. Please install curl and try again.") exit(1) # Check if base64 is installed - if run("which base64", shell=True) != 0: + if run("which base64", shell=False) != 0: Logger.error("base64 is not installed. Please install base64 and try again.") exit(1) # Check if keygen is installed - if run("which ssh-keygen", shell=True) != 0: + if run("which ssh-keygen", shell=False) != 0: Logger.error("ssh-keygen is not installed. Please install ssh-keygen and try again.") exit(1) From 1eb889b68e9725375e01c3aed33b0a93501e8f7e Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 11:23:37 +0100 Subject: [PATCH 077/148] refactor: remove unused Utils class and streamline command execution by eliminating shell=True --- ctfp.py | 121 +++++++++++++++++++++++++++----------------------------- 1 file changed, 58 insertions(+), 63 deletions(-) diff --git a/ctfp.py b/ctfp.py index a7d9faf..9d38352 100755 --- a/ctfp.py +++ b/ctfp.py @@ -312,14 +312,6 @@ def print_help(self): exit(1) self.parser.print_help() - -class Utils: - @staticmethod - def extract_tuple_from_list(tuple_list, key): - for item in tuple_list: - if key in item: - return item - return () class TFBackend: @staticmethod @@ -364,7 +356,7 @@ def register_subcommand(self): def run(self, args): Logger.info("Generating server images") try: - rc = run(f"cd \"{PATH}/cluster\" && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"", shell=True) + rc = run(f"cd \"{PATH}/cluster\" && tmp_script=$(mktemp) && curl -sSL -o \"${{tmp_script}}\" https://raw.githubusercontent.com/kube-hetzner/terraform-hcloud-kube-hetzner/master/scripts/create.sh && chmod +x \"${{tmp_script}}\" && \"${{tmp_script}}\" && rm \"${{tmp_script}}\"") if rc != 0: raise Exception except Exception: @@ -452,7 +444,7 @@ def run(self, args): Logger.info("Generating SSH keys") try: - rc = run([f"\"{PATH}\"/keys/create.sh \"{self.environment}\""], shell=True) + rc = run([f"\"{PATH}\"/keys/create.sh \"{self.environment}\""]) if rc != 0: raise Exception except Exception: @@ -503,7 +495,6 @@ class Deploy(Command): name = "deploy" help = "Deploy the platform" description = "Deploy the platform" - times = [] environment = "test" # Default environment components = COMPONENTS + ["all"] @@ -539,7 +530,8 @@ def run(self, args): elif args.prod: self.environment = "prod" - self.times.append(("start", time.time())) + times = {} + times["start"] = time.time() Logger.info("Deploying " + (self.environment.upper() if self.environment != "test" else "TEST") + " environment") Logger.space() @@ -547,50 +539,50 @@ def run(self, args): Logger.space() if deploy_all or component == "cluster": - start_time = time.time() + component_start = time.time() terraform.cluster_deploy() - self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) + times["cluster"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['cluster'], 2)} seconds") Logger.space() if deploy_all or component == "ops": - start_time = time.time() + component_start = time.time() terraform.ops_deploy() - self.times.append(("ops", start_time, time.time(), time.time() - start_time)) + times["ops"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['ops'], 2)} seconds") Logger.space() if deploy_all or component == "platform": - start_time = time.time() + component_start = time.time() terraform.platform_deploy() - self.times.append(("platform", start_time, time.time(), time.time() - start_time)) + times["platform"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['platform'], 2)} seconds") Logger.space() if deploy_all or component == "challenges": - start_time = time.time() + component_start = time.time() terraform.challenges_deploy() - self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) + times["challenges"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['challenges'], 2)} seconds") Logger.space() Logger.success("Platform deployed") - self.times.append(("end", time.time())) + total_time = time.time() - times["start"] - Logger.info(f"Time taken: {str(round(Utils.extract_tuple_from_list(self.times, 'end')[1] - Utils.extract_tuple_from_list(self.times, 'start')[1], 2))} seconds") + Logger.info(f"Time taken: {round(total_time, 2)} seconds") if deploy_all or component == "cluster": - Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") + Logger.info(f"Cluster time: {round(times['cluster'], 2)} seconds") if deploy_all or component == "ops": - Logger.info(f"Ops time: {str(round(Utils.extract_tuple_from_list(self.times, 'ops')[3], 2))} seconds") + Logger.info(f"Ops time: {round(times['ops'], 2)} seconds") if deploy_all or component == "platform": - Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") + Logger.info(f"Platform time: {round(times['platform'], 2)} seconds") if deploy_all or component == "challenges": - Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") + Logger.info(f"Challenges time: {round(times['challenges'], 2)} seconds") ''' Destroy the platform @@ -636,58 +628,58 @@ def run(self, args): elif args.prod: self.environment = "prod" - self.times.append(("start", time.time())) + times = {} + times["start"] = time.time() Logger.info("Destroying " + (self.environment.upper() if self.environment != "test" else "TEST") + " environment") Logger.space() terraform = Terraform(self.environment) if destroy_all or component == "challenges": - start_time = time.time() + component_start = time.time() terraform.challenges_destroy() - self.times.append(("challenges", start_time, time.time(), time.time() - start_time)) + times["challenges"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['challenges'], 2)} seconds") Logger.space() if destroy_all or component == "platform": - start_time = time.time() + component_start = time.time() terraform.platform_destroy() - self.times.append(("platform", start_time, time.time(), time.time() - start_time)) + times["platform"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['platform'], 2)} seconds") Logger.space() if destroy_all or component == "ops": - start_time = time.time() + component_start = time.time() terraform.ops_destroy() - self.times.append(("ops", start_time, time.time(), time.time() - start_time)) + times["ops"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['ops'], 2)} seconds") Logger.space() if destroy_all or component == "cluster": - start_time = time.time() + component_start = time.time() terraform.cluster_destroy() - self.times.append(("cluster", start_time, time.time(), time.time() - start_time)) + times["cluster"] = time.time() - component_start Logger.space() - Logger.info(f"Time taken: {str(round(self.times[-1][3], 2))} seconds") + Logger.info(f"Time taken: {round(times['cluster'], 2)} seconds") Logger.space() Logger.success("Destroyed action") + total_time = time.time() - times["start"] - self.times.append(("end", time.time())) - - Logger.info(f"Time taken: {str(round(Utils.extract_tuple_from_list(self.times, 'end')[1] - Utils.extract_tuple_from_list(self.times, 'start')[1], 2))} seconds") + Logger.info(f"Time taken: {round(total_time, 2)} seconds") if destroy_all or component == "cluster": - Logger.info(f"Cluster time: {str(round(Utils.extract_tuple_from_list(self.times, 'cluster')[3], 2))} seconds") + Logger.info(f"Cluster time: {round(times['cluster'], 2)} seconds") if destroy_all or component == "ops": - Logger.info(f"Ops time: {str(round(Utils.extract_tuple_from_list(self.times, 'ops')[3], 2))} seconds") + Logger.info(f"Ops time: {round(times['ops'], 2)} seconds") if destroy_all or component == "platform": - Logger.info(f"Platform time: {str(round(Utils.extract_tuple_from_list(self.times, 'platform')[3], 2))} seconds") + Logger.info(f"Platform time: {round(times['platform'], 2)} seconds") if destroy_all or component == "challenges": - Logger.info(f"Challenges time: {str(round(Utils.extract_tuple_from_list(self.times, 'challenges')[3], 2))} seconds") + Logger.info(f"Challenges time: {round(times['challenges'], 2)} seconds") ''' @@ -884,7 +876,7 @@ def is_installed(): :return: True if installed, False otherwise ''' try: - rc = run(f"{FLAVOR} version", shell=False) + rc = run(f"{FLAVOR} version") return rc == 0 except Exception: return False @@ -911,7 +903,7 @@ def init_terraform(self, path, components: str = ""): # Initialize the backend (if not already done for this project) Logger.info("Running terraform init") - rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) + rc = run(f"{FLAVOR} init -backend-config=\"{TFBackend.get_backend_path(components)}\"") if rc != 0: # Try to init with reconfigure response = input(f"The init of the backend for {components} failed. Do you want to try to reconfigure the backend? (y/N): ") @@ -920,7 +912,7 @@ def init_terraform(self, path, components: str = ""): exit(0) Logger.warning("Reconfiguring backend") - rc = run(f"{FLAVOR} init -reconfigure -backend-config=\"{TFBackend.get_backend_path(components)}\"", shell=True) + rc = run(f"{FLAVOR} init -reconfigure -backend-config=\"{TFBackend.get_backend_path(components)}\"") if rc != 0: raise Exception @@ -931,7 +923,7 @@ def init_terraform(self, path, components: str = ""): # Select the workspace based on the environment Logger.info(f"Selecting workspace: {self.environment}") - rc = run(f"{FLAVOR} workspace select {self.environment}", shell=True) + rc = run(f"{FLAVOR} workspace select {self.environment}") if rc != 0: raise Exception except subprocess.CalledProcessError as e: @@ -969,12 +961,12 @@ def execute(self, component, generate_plan=True, action="apply"): if generate_plan: # Generate plan Logger.info("Generating Terraform plan") - rc = run(f"cd \"{component_path}\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} plan {'' if is_apply else '-destroy'} -out=\"{PATH}/terraform/{component}-{self.environment}.tfplan\"", shell=True) + rc = run(f"cd \"{component_path}\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} plan {'' if is_apply else '-destroy'} -out=\"{PATH}/terraform/{component}-{self.environment}.tfplan\"") if rc != 0: raise Exception(f"Terraform plan failed for {component} ({action}), with return code: {rc}") # Store the plan as human-readable output (Allowing user to review it) - rc = run(f"cd \"{component_path}\" && {FLAVOR} show -no-color \"{PATH}/terraform/{component}-{self.environment}.tfplan\" > \"{PATH}/terraform/{component}-{self.environment}.plan.txt\"", shell=True) + rc = run(f"cd \"{component_path}\" && {FLAVOR} show -no-color \"{PATH}/terraform/{component}-{self.environment}.tfplan\" > \"{PATH}/terraform/{component}-{self.environment}.plan.txt\"") if rc != 0: raise Exception(f"Terraform show plan failed for {component} ({action}), with return code: {rc}") @@ -988,7 +980,7 @@ def execute(self, component, generate_plan=True, action="apply"): exit(0) # Run apply - rc = run(f"cd \"{component_path}\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} apply \"{PATH}/terraform/{component}-{self.environment}.tfplan\"", shell=True) + rc = run(f"cd \"{component_path}\" && {FLAVOR} workspace select {self.environment} && {FLAVOR} apply \"{PATH}/terraform/{component}-{self.environment}.tfplan\"") # Remove the plan files os.remove(f"{PATH}/terraform/{component}-{self.environment}.tfplan") @@ -997,7 +989,7 @@ def execute(self, component, generate_plan=True, action="apply"): os.rename(f"{PATH}/terraform/{component}-{self.environment}.plan.txt", f"{PATH}/terraform/{component}-{self.environment}.plan.txt.old") else: # Run apply directly - rc = run(f"cd \"{component_path}\" && {FLAVOR} {action} {'-auto-approve' if AUTO_APPLY else ''}", shell=True) + rc = run(f"cd \"{component_path}\" && {FLAVOR} {action} {'-auto-approve' if AUTO_APPLY else ''}") if rc != 0: raise Exception(f"Terraform {action} failed for {component}, with return code: {rc}") @@ -1188,10 +1180,10 @@ def remove_kubeconfig(self): # Remove kubeconfig try: - rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.yml", shell=True) + rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.yml") if rc != 0: raise Exception - rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.b64", shell=True) + rc = run(f"rm \"{PATH}\"/kube-config/kube-config.{self.environment}.b64") if rc != 0: raise Exception except Exception: @@ -1272,8 +1264,11 @@ def challenges_destroy(self): ''' class CLI: def run(self): + Logger.info("Starting CTF-Pilot CLI") + Logger.info("Checking availability of requried tools") self.platform_check() self.tool_check() + Logger.success("Required Tools are available") args = Args() if args.parser is None: @@ -1322,17 +1317,17 @@ def tool_check(self): exit(1) # Check if curl is installed - if run("which curl", shell=False) != 0: + if run("which curl") != 0: Logger.error("curl is not installed. Please install curl and try again.") exit(1) # Check if base64 is installed - if run("which base64", shell=False) != 0: + if run("which base64") != 0: Logger.error("base64 is not installed. Please install base64 and try again.") exit(1) # Check if keygen is installed - if run("which ssh-keygen", shell=False) != 0: + if run("which ssh-keygen") != 0: Logger.error("ssh-keygen is not installed. Please install ssh-keygen and try again.") exit(1) From 3f67866ef6c1eb70ad65b02d26834d8ac09bd0d3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 11:32:18 +0100 Subject: [PATCH 078/148] Remove notes --- notes.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 notes.md diff --git a/notes.md b/notes.md deleted file mode 100644 index c2e97d0..0000000 --- a/notes.md +++ /dev/null @@ -1,13 +0,0 @@ -KubeCTF > CTF Pilot -Kubectf > CTFPilot -kubectf > ctfpilot -kubectf-overview > ctfpilot-overview -kubectf-challenges-isolated > ctfpilot-challenges-instanced -kubectf-challenges > ctfpilot-challenges -kube-ctf.io/node > cluster.ctfpilot.com/node -label_isolated_challenge_kube_ctf_io_ > label_instanced_challenges_ctfpilot_com_ -isolated.challenge.kube.ctf.io > instanced.challenges.ctfpilot.com -label_challenges_kube_ctf_io_ > label_challenges_ctfpilot_com_ -challenges.kube.ctf.io > challenges.ctfpilot.com -label_kube_ctf_io_ > label_ctfpilot_com_ -kube_ctf.io > ctfpilot.com From 6be538a230dfe70973e675df50a56c5a0c713056 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 12:30:19 +0100 Subject: [PATCH 079/148] fix: update usage comment in kubectl.sh to reflect correct script name --- kubectl.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 kubectl.sh diff --git a/kubectl.sh b/kubectl.sh old mode 100644 new mode 100755 index 6bb997b..26b63de --- a/kubectl.sh +++ b/kubectl.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Select environment between test, dev or prod -# Usage: ./kubectl-setup.sh [test|dev|prod] +# Usage: ./kubectl.sh [test|dev|prod] CTFP_EXECUTE=true if [ -z "$1" ]; then echo "Usage: $0 [test|dev|prod]" From ee876dd8201c7e299a9743839e6d389b9ec22ce3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 12:30:50 +0100 Subject: [PATCH 080/148] fix: correct tfvars filename generation for test environment to include environment name --- ctfp.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ctfp.py b/ctfp.py index 9d38352..dcadd1f 100755 --- a/ctfp.py +++ b/ctfp.py @@ -701,12 +701,8 @@ def get_filename_tfvars(environment="test"): :param environment: The environment name (test, dev, prod) :return: The filename for the tfvars file ''' - - prefix = "" - if environment != "test": - prefix = f"{environment}." - return f"automated.{prefix}tfvars" + return f"automated.{environment}.tfvars" @staticmethod def load_tfvars(file_path: str): From 5d656afc270474dd8190e1f9bad914d6013e7825 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 12:31:14 +0100 Subject: [PATCH 081/148] Initial documentation --- README.md | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0655eea..04ec932 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,143 @@ # CTFp - CTF Pilot's CTF Platform -> [!WARNING] +> [!TIP] +> If you are looking for **how to build challenges for CTFp**, please check out the **[CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template)** and **[CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit)** repositories. + +CTFp (CTF Pilot's CTF Platform) is a CTF plaform designed to host large-scale Capture The Flag (CTF) competitions, with focus on scalability, resilience and ease of use. +The platform uses Kubernetes as the underlying orchestration system, where both the management, scoreboard and challenge infrastructure are deployed as Kubernetes resources. It then leverages GitOps through [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) for managing the platform's configuration and deployments, including the CTF challenges. + +CTFp acts as the orchestration layer for deploying and managing the platform, while utilizing a varirety of CTF Pilots components for providing the full functionality of the platform. + +CTFp provides a CLI tool for managing the deployment of the platform, but it is possible to use the individual Terraform components directly if desired. To further work with the platform after initial deployment, you will primarily interact with the Kubernetes cluster using `kubectl`, ArgoCD and the other monitoring systems deployed. + +> [!IMPORTANT] +> In order to run CTFp properly, you will need to have a working knowledge of **Cloud**, **Kubernetes**, **Terraform/OpenTofu**, **GitOps** and **CTFd**. +> The platform is designed to work with CTF Pilot's Challenges ecosystem, to ensure secure hosting of CTF challenges. > -> We are currently in the process of publishing the CTFp system. -> Meanwhile, some components may not be present or fully functional. +> This platform is not intended for beginners, and it is assumed that you have prior experience with these technologies and systems. +> Incorrect handling of Kubernetes resources can lead to data loss, downtime and security vulnerabilities. +> Incorrectly configured challenges may lead to security vulnerabilities or platform instability. + +This platform deploys real world infrastructure, and will incur costs when deployed. + +## Features + +CTFp offers a wide range of features to facilitate the deployment and management of CTF competitions. Below is an overview of the key features: + +- **Infrastructure & Deployment** + - **Multi-environment support** with isolated configurations for Test, Dev, and Production + - **Component-based architecture** with four deployable components: Cluster, Ops, Platform, and Challenges + - **Infrastructure as Code** using Terraform/OpenTofu with automated state management and S3 backend + - **Multi-region Kubernetes clusters** on Hetzner Cloud with configurable node types and auto-scaling + - **Custom server images** generation using Packer + - **Cloudflare DNS integration** for management, platform, and CTF zones +- **Operations & Monitoring** + - **GitOps workflow** powered by ArgoCD for automated deployments + - **Comprehensive monitoring** with Prometheus, Grafana, and metrics exporters + - **Log aggregation** via Filebeat to Elasticsearch + - **Traefik ingress controller** with SSL certificate management (cert-manager) + - **Discord webhook notifications** for platform events + - **Automated descheduling** for optimal resource distribution +- **Scoreboard** + - **Customizable CTFd scoreboard deployment** allowing for bring-your-own CTFd configuration + - **Auto deployment of CTFd configuration** providing a ready-to-use CTFd instance + - **Flexible CTF settings** supporting a large portion of CTFd's configuration options + - **S3 storage configuration** for challenge files and user uploads in CTFd + - **Clustered database setup** with MariaDB operator and automated backups to S3 + - **Redis caching** with Redis operator for ease of use + - **Automatic deployment of CTFd pages** from GitHub +- **Challenge Management** + - **Full support for CTF Pilot's Challenges ecosystem**, including KubeCTF integration + - **Support for three challenge deployment modes**: Isolated, Shared, and Instanced + - **Git-based deployment** with branch-specific configurations + - **IP whitelisting** for challenge access control + - **Custom fallback pages** for errors and instancing states +- **CLI Tool** + - **Simple command-line interface** for managing the deployment and lifecycle of the platform + - **Modular commands** for initializing, deploying, destroying, and managing components + - **Environment management** for handling multiple deployment environments (Test, Dev, Prod) + - **State management** with automated backend configuration, with states stored in S3 + - **Plan generation and review** before applying changes + - **Sub 20 minute deployment time** for the entire platform (excluding image generation) + - **Fully configured through configuration files** for easy setup and management + +## Quick start + +> [!TIP] +> **This is a quick start guide for getting the platform up and running, and acts as a quick reference guide.** +> If it is your first time working with CTFp, we recommend going through the full documentation for a more in-depth understanding of the platform and its components. + +To use the CTFp CLI tool, you first need to clone the repository: + +```bash +git clone https://github.com/ctfpilot/ctfp +cd ctfp +``` + +First you need to initialize the platform configuration for your desired environment (test, dev, prod): + +```bash +./ctfp.py init +``` + +> [!NOTE] +> You can add `--test`, `--dev` or `--prod` to specify the environment you want to initialize. +> The default environment is `test` (`--test`). + +Next, you need to fill out the configuration located in the `automated..tfvars` file. + +In order to deploy, ensure you have SSH keys created, and inserted into your configuration: + +```bash +./ctfp.py generate-keys --insert +``` + +To create the server images used for the Kubernetes cluster nodes, run: + +```bash +./ctfp.py generate-images +``` + +Finally, you can deploy the entire platform with: + +```bash +./ctfp.py deploy all +``` + +To destroy the entire platform, run: + +```bash +./ctfp.py destroy all +``` + +`all` can be replaced with any of the individual components: `cluster`, `ops`, `platform`, `challenges`. + +To interact with the cluster, run the following command to configure your `kubectl` context: + +```bash +source kubectl.sh [test|dev|prod] +``` + +*`source` is required to set the environment variables in your current shell session.* + +## Pre-requisites + +In order to even deploy the platform, the following software needs to be installed on your local machine: + +- [OpenTofu](https://opentofu.org) (Alternative version of [Terraform](https://www.terraform.io/downloads.html)) +- [Packer](https://developer.hashicorp.com/packer/tutorials/docker-get-started/get-started-install-cli#installing-packer) - For initial generation of server images +- [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - For interacting with the Kubernetes cluster +- [hcloud cli tool](https://github.com/hetznercloud/cli) - For interacting with the Hetzner Cloud API (Recommended, otherwise use the web interface) +- SSH client - For connecting to the servers + +And the following is required in order to deploy the platform: + +- [Hetzner Cloud](https://www.hetzner.com/cloud) account with a Hetzner Cloud project +- [Hetzner Cloud API Token](https://console.hetzner.cloud/projects) - For authenticating with the Hetzner Cloud API +- [Hetzner S3 buckets](https://console.hetzner.cloud/projects) - For storing the Terraform state files, backups and challenge data. We recommend using 3 separate buckets with seperate access keys for security reasons +- [Cloudflare](https://www.cloudflare.com/) account +- [Cloudflare API Token](https://dash.cloudflare.com/profile/api-tokens) - For authenticating with the Cloudflare API +- [3 Cloudflare controlled domains](https://dash.cloudflare.com/) - For allowing the system to allocate a domain for the Kubernetes cluster. Used to allocate management, platform and challenge domains. ## Contributing From e1674348bdfefe60e51c2efa70b5397cf05275f3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 12:33:41 +0100 Subject: [PATCH 082/148] docs: clarify usage of environment flags in initialization commands --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 04ec932..7288f44 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,8 @@ First you need to initialize the platform configuration for your desired environ > [!NOTE] > You can add `--test`, `--dev` or `--prod` to specify the environment you want to initialize. > The default environment is `test` (`--test`). +> +> Used in all commands, except the `generate-images` command, as it asks for the Hetzner Cloud project to use when generating images. Next, you need to fill out the configuration located in the `automated..tfvars` file. From 8dbb5026a1808edce08a86c22e3d5d8b990bc8c3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 12:44:23 +0100 Subject: [PATCH 083/148] docs: update usage instructions in kubectl.sh to reflect sourcing --- kubectl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kubectl.sh b/kubectl.sh index 26b63de..1fd9b1d 100755 --- a/kubectl.sh +++ b/kubectl.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # Select environment between test, dev or prod -# Usage: ./kubectl.sh [test|dev|prod] +# Usage: source ./kubectl.sh [test|dev|prod] CTFP_EXECUTE=true if [ -z "$1" ]; then - echo "Usage: $0 [test|dev|prod]" + echo "Usage: source $0 [test|dev|prod]" CTFP_EXECUTE=false fi From 26f8525627d2b5a62b7dc63b4ccec6fc68cfebc4 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 21 Dec 2025 22:38:18 +0100 Subject: [PATCH 084/148] Start on architecture section --- README.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/README.md b/README.md index 7288f44..6e6a665 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,41 @@ source kubectl.sh [test|dev|prod] *`source` is required to set the environment variables in your current shell session.* +## Architecture + +The CTFp platform is composed of four main components, each responsible for different aspects of the platform's functionality: + +1. **Cluster Component**: Responsible for provisioning and managing the underlying Kubernetes cluster infrastructure on Hetzner Cloud. This includes setting up the necessary servers, networking, and storage resources required for the cluster to operate. + This can be found in the [`cluster`](./cluster) directory, and as the `cluster` components in the CLI tool. +2. **Ops Component**: Focuses on deploying and managing the operational tools and monitoring systems for the platform. This includes setting up ArgoCD, monitoring, logging, ingress controllers, and other essential services that ensure the smooth operation of the platform. + This can be found in the [`ops`](./ops) directory, and as the `ops` components in the CLI tool. +3. **Platform Component**: Handles the deployment and configuration of the CTFd scoreboard and its associated services. This includes setting up the database, caching, and storage solutions required for the scoreboard to function effectively. + This can be found in the [`platform`](./platform) directory, and as the `platform` components in the CLI tool. +4. **Challenges Component**: Manages the deployment and configuration of the CTF challenges. This includes setting up the necessary resources and configurations to host and manage the challenges securely and efficiently. + This can be found in the [`challenges`](./challenges) directory, and as the `challenges` components in the CLI tool. + +Each component is designed to be modular and can be deployed independently or together, allowing for flexibility in managing the platform's infrastructure and services. + +### Directory structure + +The CTFp repository is structured as follows: + +```txt +ctfp/ +├── backend/ # Terraform backend configurations +├── keys/ # Generated SSH keys +├── terraform/ # Terraform plans +├── tf-modules/ # Reusable Terraform modules +├── cluster/ # Cluster component Terraform configurations +├── ops/ # Ops component Terraform configurations +├── platform/ # Platform component Terraform configurations +├── challenges/ # Challenges component Terraform configurations +├── ctfp.py # CTFp CLI tool +├── kubectl.sh # Script for configuring kubectl context +├── README.md # This README file +└── ... # Other files and directories +``` + ## Pre-requisites In order to even deploy the platform, the following software needs to be installed on your local machine: From ecb98bc3fcc84628c9c9e0c3d41e58e63a8b87a8 Mon Sep 17 00:00:00 2001 From: Mikkel Albrechtsem Date: Wed, 24 Dec 2025 17:04:14 +0100 Subject: [PATCH 085/148] Reorganize structure --- README.md | 92 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 6e6a665..47bea05 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,19 @@ CTFp provides a CLI tool for managing the deployment of the platform, but it is This platform deploys real world infrastructure, and will incur costs when deployed. +## Table of Contents + +- [Features](#features) +- [Quick start](#quick-start) +- [How to run](#how-to-run) + - [Pre-requisites](#pre-requisites) +- [Architecture](#architecture) + - [Directory structure](#directory-structure) +- [Contributing](#contributing) +- [Background](#background) +- [License](#license) +- [Code of Conduct](#code-of-conduct) + ## Features CTFp offers a wide range of features to facilitate the deployment and management of CTF competitions. Below is an overview of the key features: @@ -122,42 +135,9 @@ source kubectl.sh [test|dev|prod] *`source` is required to set the environment variables in your current shell session.* -## Architecture +## How to run -The CTFp platform is composed of four main components, each responsible for different aspects of the platform's functionality: - -1. **Cluster Component**: Responsible for provisioning and managing the underlying Kubernetes cluster infrastructure on Hetzner Cloud. This includes setting up the necessary servers, networking, and storage resources required for the cluster to operate. - This can be found in the [`cluster`](./cluster) directory, and as the `cluster` components in the CLI tool. -2. **Ops Component**: Focuses on deploying and managing the operational tools and monitoring systems for the platform. This includes setting up ArgoCD, monitoring, logging, ingress controllers, and other essential services that ensure the smooth operation of the platform. - This can be found in the [`ops`](./ops) directory, and as the `ops` components in the CLI tool. -3. **Platform Component**: Handles the deployment and configuration of the CTFd scoreboard and its associated services. This includes setting up the database, caching, and storage solutions required for the scoreboard to function effectively. - This can be found in the [`platform`](./platform) directory, and as the `platform` components in the CLI tool. -4. **Challenges Component**: Manages the deployment and configuration of the CTF challenges. This includes setting up the necessary resources and configurations to host and manage the challenges securely and efficiently. - This can be found in the [`challenges`](./challenges) directory, and as the `challenges` components in the CLI tool. - -Each component is designed to be modular and can be deployed independently or together, allowing for flexibility in managing the platform's infrastructure and services. - -### Directory structure - -The CTFp repository is structured as follows: - -```txt -ctfp/ -├── backend/ # Terraform backend configurations -├── keys/ # Generated SSH keys -├── terraform/ # Terraform plans -├── tf-modules/ # Reusable Terraform modules -├── cluster/ # Cluster component Terraform configurations -├── ops/ # Ops component Terraform configurations -├── platform/ # Platform component Terraform configurations -├── challenges/ # Challenges component Terraform configurations -├── ctfp.py # CTFp CLI tool -├── kubectl.sh # Script for configuring kubectl context -├── README.md # This README file -└── ... # Other files and directories -``` - -## Pre-requisites +### Pre-requisites In order to even deploy the platform, the following software needs to be installed on your local machine: @@ -176,6 +156,48 @@ And the following is required in order to deploy the platform: - [Cloudflare API Token](https://dash.cloudflare.com/profile/api-tokens) - For authenticating with the Cloudflare API - [3 Cloudflare controlled domains](https://dash.cloudflare.com/) - For allowing the system to allocate a domain for the Kubernetes cluster. Used to allocate management, platform and challenge domains. + +## Architecture + +The CTFp platform is composed of four main components, each responsible for different aspects of the platform's functionality: + +1. **Cluster**: Responsible for provisioning and managing the underlying Kubernetes cluster infrastructure on Hetzner Cloud. This includes setting up the necessary servers, networking, and storage resources required for the cluster to operate. + This can be found in the [`cluster`](./cluster) directory, and as the `cluster` component in the CLI tool. +2. **Ops** (Operations): Focuses on deploying and managing the operational tools and monitoring systems for the platform. This includes setting up ArgoCD, monitoring, logging, ingress controllers, and other essential services that ensure the smooth operation of the platform. + This can be found in the [`ops`](./ops) directory, and as the `ops` component in the CLI tool. +3. **Platform**: Handles the deployment and configuration of the CTFd scoreboard and its associated services. This includes setting up the database, caching, and storage solutions required for the scoreboard to function effectively. + This can be found in the [`platform`](./platform) directory, and as the `platform` component in the CLI tool. +4. **Challenges**: Manages the deployment and configuration of the CTF challenges. This includes setting up the necessary resources and configurations to host and manage the challenges securely and efficiently. + This can be found in the [`challenges`](./challenges) directory, and as the `challenges` component in the CLI tool. + +Each component is designed to be modular and can be deployed independently or together, allowing for flexibility in managing the platform's infrastructure and services. + +### Directory structure + +The CTFp repository is structured as follows: + +```txt +ctfp/ +├── backend/ # Terraform backend configurations +├── keys/ # Generated SSH keys +├── terraform/ # Terraform plans +├── tf-modules/ # Reusable Terraform modules +├── cluster/ # Cluster component Terraform configurations +├── ops/ # Ops component Terraform configurations +├── platform/ # Platform component Terraform configurations +├── challenges/ # Challenges component Terraform configurations +├── ctfp.py # CTFp CLI tool +├── kubectl.sh # Script for configuring kubectl context +├── README.md # This README file +├── requirements.txt # Python dependencies for the CLI tool +├── template.automated.tfvars # Template for CTFp CLI configuration +└── ... # Other files and directories, such as license, contributing guidelines, etc. +``` + +### CTFp + +### CLI Tool + ## Contributing We welcome contributions of all kinds, from **code** and **documentation** to **bug reports** and **feedback**! From b2e3a88d0054d1dc72af2720ad8f3f7ab7ab4b66 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 16:18:19 +0100 Subject: [PATCH 086/148] feat: add loading of S3 backend credentials from automated.tfvars --- ctfp.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/ctfp.py b/ctfp.py index dcadd1f..bfa7590 100755 --- a/ctfp.py +++ b/ctfp.py @@ -891,6 +891,9 @@ def init_terraform(self, path, components: str = ""): try: # Check if tfvars file exists and is valid self.check_values() + + # Load backend connection credentials + self.load_backend_credentials() # Check if backend config exists if not TFBackend.backend_exists(components): @@ -1046,6 +1049,27 @@ def check_placeholders(value): Logger.info(f"{self.get_filename_tfvars()} is filled out correctly") + + def load_backend_credentials(self): + ''' + Load S3 backend credentials from automated.tfvars, to set Terraform S3 connection credentials + ''' + + # Check if automated.tfvars exists + tfvars_path = self.get_path_tfvars() + if not os.path.exists(tfvars_path): + Logger.error(f"{self.get_filename_tfvars()} not found. Please create the file and try again") + exit(1) + + # Load tfvars file + tfvars_data = TFVARS.safe_load_tfvars(tfvars_path) + + # Set environment variables for S3 backend + os.environ["AWS_ACCESS_KEY_ID"] = tfvars_data.get("terraform_backend_s3_access_key", "") + os.environ["AWS_SECRET_ACCESS_KEY"] = tfvars_data.get("terraform_backend_s3_secret_key", "") + + Logger.info(f"S3 backend credentials loaded") + def cluster_deploy(self): Logger.info("Deploying the cluster") @@ -1261,10 +1285,6 @@ def challenges_destroy(self): class CLI: def run(self): Logger.info("Starting CTF-Pilot CLI") - Logger.info("Checking availability of requried tools") - self.platform_check() - self.tool_check() - Logger.success("Required Tools are available") args = Args() if args.parser is None: @@ -1294,6 +1314,11 @@ def run(self): args.print_help() exit(1) + Logger.info("Checking availability of requried tools") + self.platform_check() + self.tool_check() + Logger.success("Required Tools are available") + # Run the subcommand try: namespace.func(namespace) From 49afd8bca475789efcf2b7216dc5c947441848e1 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 16:18:32 +0100 Subject: [PATCH 087/148] refactor: update template.automated.tfvars with S3 backend credentials --- template.automated.tfvars | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/template.automated.tfvars b/template.automated.tfvars index 1f355ac..6065edf 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -2,6 +2,14 @@ # Clone this file to `automated.tfvars` and fill in the values. # This file (`template.automated.tfvars`) is git tracked, and MUST NOT be changed in the repository to include sensitive information. +# ------------------------ +# CLI Tool configuration +# ------------------------ +# The following variables are used by the CLI tool to configure the backend connection. +# Specifically setting the credentials to access the Terraform S3 backend. +terraform_backend_s3_access_key = "" # Access key for the S3 backend +terraform_backend_s3_secret_key = "" # Secret key for the S3 backend + # ------------------------ # Cluster configuration # ------------------------ From b9cb418620b391168cb126414e1351451cdc48ec Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 16:26:57 +0100 Subject: [PATCH 088/148] refactor: add error handling for missing S3 backend credentials in automated.tfvars --- ctfp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ctfp.py b/ctfp.py index bfa7590..70aa2cd 100755 --- a/ctfp.py +++ b/ctfp.py @@ -1068,6 +1068,10 @@ def load_backend_credentials(self): os.environ["AWS_ACCESS_KEY_ID"] = tfvars_data.get("terraform_backend_s3_access_key", "") os.environ["AWS_SECRET_ACCESS_KEY"] = tfvars_data.get("terraform_backend_s3_secret_key", "") + if os.environ["AWS_ACCESS_KEY_ID"] == "" or os.environ["AWS_SECRET_ACCESS_KEY"] == "": + Logger.error("S3 backend credentials not found in automated.tfvars. Please fill out terraform_backend_s3_access_key and terraform_backend_s3_secret_key as they are required to run the Terraform components.") + exit(1) + Logger.info(f"S3 backend credentials loaded") def cluster_deploy(self): From 97f6ae67710ab0a242399bf69a872672243c2757 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 16:27:17 +0100 Subject: [PATCH 089/148] refactor: correct typo in log message for tool availability check --- ctfp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctfp.py b/ctfp.py index 70aa2cd..ef2c5b4 100755 --- a/ctfp.py +++ b/ctfp.py @@ -1318,7 +1318,7 @@ def run(self): args.print_help() exit(1) - Logger.info("Checking availability of requried tools") + Logger.info("Checking availability of required tools") self.platform_check() self.tool_check() Logger.success("Required Tools are available") From 0e5f22f830e0f5e76de058f1b9b0e93d35888b49 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 16:28:15 +0100 Subject: [PATCH 090/148] refactor: streamline loading of S3 backend credentials from automated.tfvars --- ctfp.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ctfp.py b/ctfp.py index ef2c5b4..a0dd480 100755 --- a/ctfp.py +++ b/ctfp.py @@ -1055,14 +1055,8 @@ def load_backend_credentials(self): Load S3 backend credentials from automated.tfvars, to set Terraform S3 connection credentials ''' - # Check if automated.tfvars exists - tfvars_path = self.get_path_tfvars() - if not os.path.exists(tfvars_path): - Logger.error(f"{self.get_filename_tfvars()} not found. Please create the file and try again") - exit(1) - # Load tfvars file - tfvars_data = TFVARS.safe_load_tfvars(tfvars_path) + tfvars_data = TFVARS.safe_load_tfvars(self.get_path_tfvars()) # Set environment variables for S3 backend os.environ["AWS_ACCESS_KEY_ID"] = tfvars_data.get("terraform_backend_s3_access_key", "") From f6b043465f295a5f960e6e1ccc8607e9b27b7a5d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 16:30:37 +0100 Subject: [PATCH 091/148] fix: update placeholder check to allow GitHub URLs --- ctfp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ctfp.py b/ctfp.py index a0dd480..b5f1137 100755 --- a/ctfp.py +++ b/ctfp.py @@ -1031,7 +1031,7 @@ def check_values(self): # Check if fields include "<" or ">" def check_placeholders(value): - if isinstance(value, str) and value.startswith("<") and value.endswith(">"): + if isinstance(value, str) and (value.startswith("<") or value.startswith("https://github.com/<")) and value.endswith(">"): return True elif isinstance(value, dict): for v in value.values(): From bb08350ecf03209c8ba05c8e9e4060d053559421 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:03:09 +0100 Subject: [PATCH 092/148] feat: add dedicated challenges node type --- cluster/kube.tf | 2 +- cluster/tfvars/template.tfvars | 7 +++++++ cluster/variables.tf | 5 +++++ template.automated.tfvars | 7 +++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/cluster/kube.tf b/cluster/kube.tf index a24f974..1b88ec3 100644 --- a/cluster/kube.tf +++ b/cluster/kube.tf @@ -227,7 +227,7 @@ module "kube-hetzner" { }, { name = "challs-1", - server_type = var.scale_type, + server_type = var.challs_type, location = var.region_1, labels = [ "ressource-type=node", diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index 0f9813c..5594fd7 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -51,19 +51,26 @@ network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central # Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically. # Server types. See https://www.hetzner.com/cloud +# Control plane nodes - Nodes that run the Kubernetes control plane components. control_plane_type_1 = "cx23" # Control plane group 1 control_plane_type_2 = "cx23" # Control plane group 2 control_plane_type_3 = "cx23" # Control plane group 3 +# Agent nodes - Nodes that run general workloads, excluding CTF challenges. agent_type_1 = "cx33" # Agent group 1 agent_type_2 = "cx33" # Agent group 2 agent_type_3 = "cx33" # Agent group 3 +# Challenge nodes - Nodes dedicated to running CTF challenges. These nodes are tainted to only run challenge workloads. +challs_type = "cx33" # CTF challenge nodes +# Scale nodes - Nodes that are automatically scaled by the cluster autoscaler. These nodes are used to scale the cluster up or down dynamically. scale_type = "cx33" # Scale group # Server count +# Control plane nodes - Nodes that run the Kubernetes control plane components. # Minimum of 1 control plane across all groups. 1 in each group is recommended for HA. control_plane_count_1 = 1 # Number of control plane nodes in group 1 control_plane_count_2 = 1 # Number of control plane nodes in group 2 control_plane_count_3 = 1 # Number of control plane nodes in group 3 +# Agent nodes - Nodes that run general workloads, excluding CTF challenges. # Minimum of 1 agent across all groups. 1 in each group is recommended for HA. agent_count_1 = 1 # Number of agent nodes in group 1 agent_count_2 = 1 # Number of agent nodes in group 2 diff --git a/cluster/variables.tf b/cluster/variables.tf index d90bc6e..4f326af 100644 --- a/cluster/variables.tf +++ b/cluster/variables.tf @@ -138,6 +138,11 @@ variable "agent_type_3" { default = "cx32" } +variable "challs_type" { + type = string + description = "CTF challenge nodes server type" + default = "cx32" +} variable "scale_type" { type = string description = "Scale group server type" diff --git a/template.automated.tfvars b/template.automated.tfvars index 1f355ac..5080401 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -22,19 +22,26 @@ network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central # Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically. # Server types. See https://www.hetzner.com/cloud +# Control plane nodes - Nodes that run the Kubernetes control plane components. control_plane_type_1 = "cx23" # Control plane group 1 control_plane_type_2 = "cx23" # Control plane group 2 control_plane_type_3 = "cx23" # Control plane group 3 +# Agent nodes - Nodes that run general workloads, excluding CTF challenges. agent_type_1 = "cx33" # Agent group 1 agent_type_2 = "cx33" # Agent group 2 agent_type_3 = "cx33" # Agent group 3 +# Challenge nodes - Nodes dedicated to running CTF challenges. These nodes are tainted to only run challenge workloads. +challs_type = "cx33" # CTF challenge nodes +# Scale nodes - Nodes that are automatically scaled by the cluster autoscaler. These nodes are used to scale the cluster up or down dynamically. scale_type = "cx33" # Scale group # Server count +# Control plane nodes - Nodes that run the Kubernetes control plane components. # Minimum of 1 control plane across all groups. 1 in each group is recommended for HA. control_plane_count_1 = 1 # Number of control plane nodes in group 1 control_plane_count_2 = 1 # Number of control plane nodes in group 2 control_plane_count_3 = 1 # Number of control plane nodes in group 3 +# Agent nodes - Nodes that run general workloads, excluding CTF challenges. # Minimum of 1 agent across all groups. 1 in each group is recommended for HA. agent_count_1 = 1 # Number of agent nodes in group 1 agent_count_2 = 1 # Number of agent nodes in group 2 From 456c3952f09a804a3eef3ccd8588605cbf930079 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:04:54 +0100 Subject: [PATCH 093/148] refactor: remove example environment file to enhance security --- .env.example | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 5fe1f9d..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= \ No newline at end of file From c6a0f429581fff0cca478f3715ec43da9c40cef1 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:17:19 +0100 Subject: [PATCH 094/148] Continued work on documentation --- README.md | 135 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 47bea05..490f0a5 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,25 @@ This platform deploys real world infrastructure, and will incur costs when deplo ## Table of Contents -- [Features](#features) -- [Quick start](#quick-start) -- [How to run](#how-to-run) - - [Pre-requisites](#pre-requisites) -- [Architecture](#architecture) - - [Directory structure](#directory-structure) -- [Contributing](#contributing) -- [Background](#background) -- [License](#license) -- [Code of Conduct](#code-of-conduct) +- [CTFp - CTF Pilot's CTF Platform](#ctfp---ctf-pilots-ctf-platform) + - [Table of Contents](#table-of-contents) + - [Features](#features) + - [Quick start](#quick-start) + - [How to run](#how-to-run) + - [Pre-requisites](#pre-requisites) + - [Environments](#environments) + - [Configuring the platform](#configuring-the-platform) + - [Commands](#commands) + - [Guides](#guides) + - [Updating sizes of nodes in an existing cluster](#updating-sizes-of-nodes-in-an-existing-cluster) + - [Architecture](#architecture) + - [Directory structure](#directory-structure) + - [CTFp](#ctfp) + - [CLI Tool](#cli-tool) + - [Contributing](#contributing) + - [Background](#background) + - [License](#license) + - [Code of Conduct](#code-of-conduct) ## Features @@ -144,18 +153,120 @@ In order to even deploy the platform, the following software needs to be install - [OpenTofu](https://opentofu.org) (Alternative version of [Terraform](https://www.terraform.io/downloads.html)) - [Packer](https://developer.hashicorp.com/packer/tutorials/docker-get-started/get-started-install-cli#installing-packer) - For initial generation of server images - [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - For interacting with the Kubernetes cluster -- [hcloud cli tool](https://github.com/hetznercloud/cli) - For interacting with the Hetzner Cloud API (Recommended, otherwise use the web interface) +- [hcloud cli tool](https://github.com/hetznercloud/cli) - For interacting with the Hetzner Cloud API (Otherwise use the Hetzner web interface) - SSH client - For connecting to the servers And the following is required in order to deploy the platform: -- [Hetzner Cloud](https://www.hetzner.com/cloud) account with a Hetzner Cloud project +- [Hetzner Cloud](https://www.hetzner.com/cloud) account with one or more Hetzner Cloud projects - [Hetzner Cloud API Token](https://console.hetzner.cloud/projects) - For authenticating with the Hetzner Cloud API - [Hetzner S3 buckets](https://console.hetzner.cloud/projects) - For storing the Terraform state files, backups and challenge data. We recommend using 3 separate buckets with seperate access keys for security reasons - [Cloudflare](https://www.cloudflare.com/) account - [Cloudflare API Token](https://dash.cloudflare.com/profile/api-tokens) - For authenticating with the Cloudflare API - [3 Cloudflare controlled domains](https://dash.cloudflare.com/) - For allowing the system to allocate a domain for the Kubernetes cluster. Used to allocate management, platform and challenge domains. +- SMTP mail server - To allow CTFd to send emails to users (Password resets, notifications, etc.). The system is set up to allow outbound connections to [Brevo](https://brevo.com) SMTP on port 587. +- [Discord](https://discord.com) channels to receive notifications. One for monitoring alerts and one for first-blood notifications. +- GitHub repository following [CTF Pilot's Challenges template](https://github.com/ctfpilot/challenges-template) for CTF challenges and CTFd pages - A Git repository containing the CTF challenges to be deployed. This should be your own private repository using the CTF Pilot Challenges Template as a base. This may also contain the pages to be used in CTFd. +- GitHub repository containing the CTFd configuration - We recommend forking [CTF Pilot's CTFd configuration repository](https://github.com/ctfpilot/ctfd). +- Access tokens to access the GitHub repositories and container registry - Fine-grained personal access token and Personal Access Tokens (PAT) with read access to the repositories containing the CTF challenges and CTFd configuration and GitHub container registry. We recommend setting up a bot account for this purpose. +- [Elasticsearch endpoint](https://www.elastic.co/) - Elasticsearch instance with an endpoint and user credentials for log aggregation. Used to connect Filebeat to Elasticsearch. +### Environments + +CTFp supports three different environments for deployment: + +- **Test**: Intended for testing and experimentation. This environment is suitable for trying out new features, configurations, and updates without affecting the production environment. It is recommended to use smaller server sizes and fewer nodes to minimize costs. +- **Dev**: Intended for development and staging purposes. This environment is suitable for testing new challenges, configurations, and updates before deploying them to production. It should closely resemble the production environment in terms of server sizes and configurations, but can still be scaled down to save costs. +- **Prod**: Intended for hosting live CTF competitions. This environment should be configured for high availability, performance, and security. It is recommended to use larger server sizes, more nodes, and robust configurations to ensure a smooth experience for participants. + +The environments are configured through separate `automated..tfvars` files, allowing for isolated configurations and deployments. + +In the CLI tool, you can specify the environment using the `--test`, `--dev`, or `--prod` flags in the commands. If no flag is provided, the default environment is `test`. + +### Configuring the platform + +> [!TIP] +> To understand the full configuration options and their implications, please refer to the documentation in the `automated..tfvars` or [`template.automated.tfvars`](./template.automated.tfvars) file. + +To configure the platform, you need to configure the `automated..tfvars` file located in the root of the repository. + +It contains a number of configuration options for the platform. +Each configuration option is within the file, explaining and listed with its possible values. + +An automated check, checks if all values are filled out correctly when running the CLI tool. +Therefore, be sure to fill out all required values before attempting to deploy the platform. +Non-required values are per default commented out, and can be left as is if the default value is acceptable. + +The configuration file is the single source of truth for the platform's configuration, and is used by the CLI tool to deploy and manage the platform. +If configuration in the configuration file is changed, the changes will be applied to the platform during the next deployment. +If the platform is manually changed outside of the CLI tool, the changes will be reverted during the next deployment. + +> [!IMPORTANT] +> The `template.automated.tfvars` file is git tracked, and **MUST NOT** be changed in the repository to include sensitive information. +> Instead, copy the file to `automated..tfvars` and fill out the values there. +> The `automated..tfvars` files are git ignored, and will not be tracked by git. +> +> The file can be initialized using the `./ctfp.py init` command. + +Each component is not fully configurable, and may in certain situation required advanced configuration. These configurations are not included in the main configuration file. +These options are either intended to be static, or require manual configuration through the individual Terraform components. +Changing these options may lead to instability or data loss, and should be done with caution. + +### Commands + +The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. Below is an overview of the available commands: + +- `init`: Initializes the platform configuration for the specified environment. +- `generate-keys`: Generates SSH keys for accessing the servers and optionally inserts them into the configuration file. +- `generate-images`: Generates custom server images using Packer for the Kubernetes cluster nodes. +- `deploy`: Deploys the specified component or all components of the platform. +- `destroy`: Destroys the specified component or all components of the platform. + +### Guides + +#### Updating sizes of nodes in an existing cluster + +> [!TIP] +> When upgrading existing clusters, it is recommended to drain node pools before changing their sizes, to avoid disruption of running workloads. +> Along with updating one node pool at a time, to minimize the impact on the cluster. + +When updating the sizes of nodes in an existing cluster, it is important to follow a specific procedure to ensure a smooth transition and avoid downtime or data loss. +Below are the steps to update the sizes of nodes in an existing cluster: + +1. **Drain the Node Pool**: Before making any changes, drain the node pool that you intend to update. This will safely evict all workloads from the nodes in the pool, allowing them to be rescheduled on other nodes in the cluster. + ```bash + kubectl drain --ignore-daemonsets --delete-local-data + ``` + *You will need to repeat this for each node in the node pool. You can use tools such as [`draino`](https://github.com/planetlabs/draino) to automate this process.* + +2. **Update the Configuration**: Modify the `automated..tfvars` file to reflect the new sizes for the nodes in the node pool. Ensure that you only change the sizes for the specific node pool you are updating. +3. **Deploy the Changes**: Use the CTFp CLI tool to deploy the changes to the cluster. This will apply the updated configuration and resize the nodes in the specified node pool. + ```bash + ./ctfp.py deploy cluster -- + ``` + *Replace `` with the appropriate environment flag (`--test`, `--dev`, or `--prod`).* +4. **Monitor the Deployment**: Keep an eye on the deployment process to ensure that the nodes are resized correctly and that there are no issues. You can use `kubectl get nodes` to check the status of the nodes in the cluster. +5. **Uncordon the Node Pool**: Once the nodes have been resized and are ready, uncordon the node pool to allow workloads to be scheduled on the nodes again. + ```bash + kubectl uncordon + ``` + *Repeat this for each node in the node pool.* +6. **Verify the Changes**: Finally, verify that the workloads are running correctly on the resized nodes and that there are no issues in the cluster. +7. **Repeat for Other Node Pools**: If you have multiple node pools to update, repeat the above steps for each node pool, one at a time. + +> [!WARNING] +> Changing node sizes can lead to temporary disruption of workloads. +> Always ensure that you have backups of critical data before making changes to the cluster configuration. + +Changes to the `scale_type` will only affect new nodes being created, and will not resize existing nodes, as the deployment of these nodes are done as ressources are needed. + +You may need to manually intervene to resize existing nodes if required, or delete them, forcing the system to create new nodes with the updated sizes. However, this may lead to downtime for workloads running on the nodes being deleted. + +> [!NOTE] +> Downscaling nodes may not be possible, depending on the initial size of the nodes and the new size. + +Hetzner does not support downsizing nodes, if they were initially created with a larger size. +In such cases, the nodes will need to be deleted, forcing the system to create new nodes with the desired size. ## Architecture From 3c040169486d81df546b34def7780e55dcf01b5a Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:31:28 +0100 Subject: [PATCH 095/148] Continued work on documentation --- README.md | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 273 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 490f0a5..0c6a678 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,13 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Environments](#environments) - [Configuring the platform](#configuring-the-platform) - [Commands](#commands) + - [`init` - Initialize Platform Configuration](#init---initialize-platform-configuration) + - [`generate-keys` - Generate SSH Keys](#generate-keys---generate-ssh-keys) + - [`insert-keys` - Insert SSH Keys into Configuration](#insert-keys---insert-ssh-keys-into-configuration) + - [`generate-images` - Generate Custom Server Images](#generate-images---generate-custom-server-images) + - [`deploy` - Deploy Platform Components](#deploy---deploy-platform-components) + - [`destroy` - Destroy Platform Components](#destroy---destroy-platform-components) + - [Workflow Diagram](#workflow-diagram) - [Guides](#guides) - [Updating sizes of nodes in an existing cluster](#updating-sizes-of-nodes-in-an-existing-cluster) - [Architecture](#architecture) @@ -214,13 +221,273 @@ Changing these options may lead to instability or data loss, and should be done ### Commands -The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. Below is an overview of the available commands: +The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. Below is a detailed overview of each available command: -- `init`: Initializes the platform configuration for the specified environment. -- `generate-keys`: Generates SSH keys for accessing the servers and optionally inserts them into the configuration file. -- `generate-images`: Generates custom server images using Packer for the Kubernetes cluster nodes. -- `deploy`: Deploys the specified component or all components of the platform. -- `destroy`: Destroys the specified component or all components of the platform. +#### `init` - Initialize Platform Configuration + +Initializes the platform configuration for a specified environment by creating an `automated..tfvars` file based on the template. + +**Syntax:** + +```bash +./ctfp.py init [--force] [--test|--dev|--prod] +``` + +**Options:** + +- `--force`: Force overwrite the configuration file if it already exists (by default, the tool prompts before overwriting) +- `--test`: Initialize TEST environment (default) +- `--dev`: Initialize DEV environment +- `--prod`: Initialize PROD environment + +**Example:** + +```bash +./ctfp.py init --test +./ctfp.py init --prod --force +``` + +**Output:** Creates `automated.test.tfvars`, `automated.dev.tfvars`, or `automated.prod.tfvars` in the repository root. + +--- + +#### `generate-keys` - Generate SSH Keys + +Generates SSH keys (ed25519) required for accessing the cluster nodes. Optionally inserts the base64-encoded keys directly into the configuration file. + +**Syntax:** + +```bash +./ctfp.py generate-keys [--insert] [--test|--dev|--prod] +``` + +**Options:** + +- `--insert`: Automatically insert the generated keys into the `automated..tfvars` file +- `--test`: Generate keys for TEST environment (default) +- `--dev`: Generate keys for DEV environment +- `--prod`: Generate keys for PROD environment + +**Example:** + +```bash +./ctfp.py generate-keys --insert --test +./ctfp.py generate-keys --dev +``` + +**Output:** Creates `keys/k8s-.pub` (public key) and `keys/k8s-` (private key) in the `keys/` directory. + +--- + +#### `insert-keys` - Insert SSH Keys into Configuration + +Manually inserts previously generated SSH keys into the configuration file. Useful if keys were generated separately or if you need to update existing keys. + +**Syntax:** + +```bash +./ctfp.py insert-keys [--test|--dev|--prod] +``` + +**Options:** + +- `--test`: Insert keys for TEST environment (default) +- `--dev`: Insert keys for DEV environment +- `--prod`: Insert keys for PROD environment + +**Example:** +```bash +./ctfp.py insert-keys --test +./ctfp.py insert-keys --prod +``` + +**Prerequisite:** Keys must already exist in the `keys/` directory. + +--- + +#### `generate-images` - Generate Custom Server Images + +Generates custom Packer images for Kubernetes cluster nodes. These images are used when provisioning the cluster infrastructure on Hetzner Cloud. + +**Syntax:** + +```bash +./ctfp.py generate-images +``` + +> [!NOTE] +> The `generate-images` command does not use environment flags. It requires you to select the Hetzner Cloud project interactively during execution. + +**Output:** Packer creates and uploads custom images to your Hetzner Cloud project. + +**Time:** This is typically the longest-running operation, taking 5-15 minutes. + +--- + +#### `deploy` - Deploy Platform Components + +Deploys one or more components of the platform to the specified environment. Can deploy individual components or the entire platform at once. + +**Syntax:** + +```bash +./ctfp.py deploy [--auto-apply] [--test|--dev|--prod] +``` + +**Arguments:** + +- ``: Component to deploy: `cluster`, `ops`, `platform`, `challenges`, or `all` + - `cluster`: Provisions Kubernetes infrastructure on Hetzner Cloud + - `ops`: Deploys operational tools (ArgoCD, monitoring, logging, ingress) + - `platform`: Deploys CTFd scoreboard and associated services + - `challenges`: Deploys CTF challenges infrastructure + - `all`: Deploys all components in sequence + +**Options:** + +- `--auto-apply`: Automatically apply Terraform changes without interactive prompts (use with extreme caution) +- `--test`: Deploy to TEST environment (default) +- `--dev`: Deploy to DEV environment +- `--prod`: Deploy to PROD environment + +**Example:** + +```bash +./ctfp.py deploy all --test +./ctfp.py deploy cluster --prod +./ctfp.py deploy platform --dev --auto-apply +``` + +**Deployment Order:** When deploying `all`, components are deployed in this order: `cluster` → `ops` → `platform` → `challenges`. Each component must be successfully deployed before the next begins. + +**Output:** Creates Terraform state files in the `terraform/` directory and outputs deployment status and timing information. + +--- + +#### `destroy` - Destroy Platform Components + +> [!WARNING] +> Destroying the platform will **delete all data** associated with the environment, including databases, user data, and challenge instances. This action cannot be undone. Always ensure you have backups before destroying production environments. + +Destroys one or more components of the platform. This is the reverse of `deploy` and tears down infrastructure, databases, and services. + +**Syntax:** + +```bash +./ctfp.py destroy [--auto-apply] [--test|--dev|--prod] +``` + +**Arguments:** + +- ``: Component to destroy: `cluster`, `ops`, `platform`, `challenges`, or `all` + +**Options:** + +- `--auto-apply`: Automatically confirm destruction without interactive prompts (use with extreme caution) +- `--test`: Destroy TEST environment (default) +- `--dev`: Destroy DEV environment +- `--prod`: Destroy PROD environment + +**Example:** + +```bash +./ctfp.py destroy all --prod +./ctfp.py destroy challenges --test --auto-apply +``` + +**Destruction Order:** When destroying `all`, components are destroyed in reverse order: `challenges` → `platform` → `ops` → `cluster`. This ensures dependencies are properly cleaned up. + + +--- + +### Workflow Diagram + +The following Mermaid diagram shows the typical workflow for deploying and managing CTFp, with the sequence of commands and their relationships: + +```mermaid +graph TD + A["🚀 Clone Repository
git clone https://github.com/ctfpilot/ctfp"] --> B["⚙️ Initialize Config
./ctfp.py init"] + B --> C["✏️ Edit Configuration
Fill in tfvars file"] + C --> D["🔑 Generate SSH Keys
./ctfp.py generate-keys --insert"] + + D --> E{{"🖼️ Generate Images
./ctfp.py generate-images

[One-time, 5-15 min]"}} + + E -->|Image Ready| F["📦 Deploy Cluster
./ctfp.py deploy cluster"] + F -->|Cluster Running| G["🛠️ Deploy Ops
./ctfp.py deploy ops"] + G -->|Ops Services Ready| H["🎯 Deploy Platform
./ctfp.py deploy platform"] + H -->|CTFd Ready| I["🎮 Deploy Challenges
./ctfp.py deploy challenges"] + + I -->|All Services Deployed| J["🔌 Configure kubectl
source kubectl.sh"] + J -->|Connected to Cluster| K{{"✅ LIVE CTF ENVIRONMENT

Monitor via:
- ArgoCD
- Grafana
- Kubernetes"}} + + K -->|Config Changes| L["🔄 Update & Redeploy
Edit tfvars + deploy component"] + L -->|Back to Live| K + + K -->|CTF Complete| M["🧹 Destroy Challenges
./ctfp.py destroy challenges"] + M --> N["🧹 Destroy Platform
./ctfp.py destroy platform"] + N --> O["🧹 Destroy Ops
./ctfp.py destroy ops"] + O --> P["🧹 Destroy Cluster
./ctfp.py destroy cluster"] + P --> Q["✨ Clean Environment
All resources destroyed"] + + style A fill:#e1f5ff + style B fill:#e1f5ff + style C fill:#e1f5ff + style D fill:#e1f5ff + style E fill:#fff3e0 + style F fill:#c8e6c9 + style G fill:#c8e6c9 + style H fill:#c8e6c9 + style I fill:#c8e6c9 + style J fill:#c8e6c9 + style K fill:#a5d6a7 + style L fill:#fff9c4 + style M fill:#ffccbc + style N fill:#ffccbc + style O fill:#ffccbc + style P fill:#ffccbc + style Q fill:#f8bbd0 +``` + +**Deployment Sequence Notes:** + +The diagram illustrates the critical dependencies between components: + +1. **Initial Setup Phase** (Blue) - One-time configuration + - Clone, initialize config, generate keys + +2. **Image Generation** (Orange) - One-time per Hetzner project + - Must complete before first cluster deployment + +3. **Deployment Chain** (Light Green) - Strict sequential order + - Each component depends on the previous one + - `cluster` → `ops` → `platform` → `challenges` + - Alternatively use `deploy all` for full deployment + +4. **Live Operations** (Green) - Stable state + - Monitor and manage the running CTF + - Optional: Update config and redeploy specific components, such as deploying new challenges + +5. **Teardown Phase** (Red/Orange) - Reverse deployment order + - Destroys resources in reverse sequence to maintain dependencies + - Alternatively use `destroy all` for full teardown + +**Quick Reference:** + +| Phase | Time | Command | Repeat | +| -------- | -------- | ----------------------- | --------------- | +| Setup | ~5 min | `init`, `generate-keys` | Per environment | +| Images | 5-15 min | `generate-images` | One-time only | +| Deploy | ~20 min | `deploy all` | Per environment | +| Manage | Ongoing | `kubectl`, ArgoCD, etc. | As needed | +| Teardown | ~15 min | `destroy all` | When done | + +**Key Points:** +- ⚠️ `generate-images` is a one-time operation per Hetzner Cloud project, not per environment +- 🔄 Configuration changes are applied automatically on the next deployment +- 🛡️ Always review Terraform plans before applying in production; use `--auto-apply` with caution +- 📊 Monitor deployments using the timing information displayed by the CLI +- 🔑 SSH keys must be generated before the first cluster deployment +- 🔗 Use `source kubectl.sh` (not `./`) to properly set environment variables ### Guides From 10f42e102aa8af3b1d5a661f3fbbcafb607fbfda Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:32:19 +0100 Subject: [PATCH 096/148] Continued work on documentation --- README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.md b/README.md index 0c6a678..9264039 100644 --- a/README.md +++ b/README.md @@ -222,7 +222,6 @@ Changing these options may lead to instability or data loss, and should be done ### Commands The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. Below is a detailed overview of each available command: - #### `init` - Initialize Platform Configuration Initializes the platform configuration for a specified environment by creating an `automated..tfvars` file based on the template. @@ -249,8 +248,6 @@ Initializes the platform configuration for a specified environment by creating a **Output:** Creates `automated.test.tfvars`, `automated.dev.tfvars`, or `automated.prod.tfvars` in the repository root. ---- - #### `generate-keys` - Generate SSH Keys Generates SSH keys (ed25519) required for accessing the cluster nodes. Optionally inserts the base64-encoded keys directly into the configuration file. @@ -277,8 +274,6 @@ Generates SSH keys (ed25519) required for accessing the cluster nodes. Optionall **Output:** Creates `keys/k8s-.pub` (public key) and `keys/k8s-` (private key) in the `keys/` directory. ---- - #### `insert-keys` - Insert SSH Keys into Configuration Manually inserts previously generated SSH keys into the configuration file. Useful if keys were generated separately or if you need to update existing keys. @@ -302,9 +297,6 @@ Manually inserts previously generated SSH keys into the configuration file. Usef ``` **Prerequisite:** Keys must already exist in the `keys/` directory. - ---- - #### `generate-images` - Generate Custom Server Images Generates custom Packer images for Kubernetes cluster nodes. These images are used when provisioning the cluster infrastructure on Hetzner Cloud. @@ -322,8 +314,6 @@ Generates custom Packer images for Kubernetes cluster nodes. These images are us **Time:** This is typically the longest-running operation, taking 5-15 minutes. ---- - #### `deploy` - Deploy Platform Components Deploys one or more components of the platform to the specified environment. Can deploy individual components or the entire platform at once. @@ -362,8 +352,6 @@ Deploys one or more components of the platform to the specified environment. Can **Output:** Creates Terraform state files in the `terraform/` directory and outputs deployment status and timing information. ---- - #### `destroy` - Destroy Platform Components > [!WARNING] @@ -398,7 +386,6 @@ Destroys one or more components of the platform. This is the reverse of `deploy` **Destruction Order:** When destroying `all`, components are destroyed in reverse order: `challenges` → `platform` → `ops` → `cluster`. This ensures dependencies are properly cleaned up. ---- ### Workflow Diagram @@ -490,7 +477,6 @@ The diagram illustrates the critical dependencies between components: - 🔗 Use `source kubectl.sh` (not `./`) to properly set environment variables ### Guides - #### Updating sizes of nodes in an existing cluster > [!TIP] From 436b28d4e2da019d8ddbca17166482f9609a59b7 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:36:44 +0100 Subject: [PATCH 097/148] Add generate-backend command --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.md b/README.md index 9264039..f2fdd2d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [`generate-keys` - Generate SSH Keys](#generate-keys---generate-ssh-keys) - [`insert-keys` - Insert SSH Keys into Configuration](#insert-keys---insert-ssh-keys-into-configuration) - [`generate-images` - Generate Custom Server Images](#generate-images---generate-custom-server-images) + - [`generate-backend` - Generate Terraform Backend Configuration](#generate-backend---generate-terraform-backend-configuration) - [`deploy` - Deploy Platform Components](#deploy---deploy-platform-components) - [`destroy` - Destroy Platform Components](#destroy---destroy-platform-components) - [Workflow Diagram](#workflow-diagram) @@ -221,7 +222,22 @@ Changing these options may lead to instability or data loss, and should be done ### Commands +> [!TIP] +> You can run any command with the `--help` flag to get more information about the command and its options. +> For example: `./ctfp.py deploy --help` +> +> Available commands: +> +> - `init` - Initialize Platform Configuration +> - `generate-keys` - Generate SSH Keys +> - `insert-keys` - Insert SSH Keys into Configuration +> - `generate-images` - Generate Custom Server Images +> - `generate-backend` - Generate Terraform Backend Configuration +> - `deploy` - Deploy Platform Components +> - `destroy` - Destroy Platform Components + The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. Below is a detailed overview of each available command: + #### `init` - Initialize Platform Configuration Initializes the platform configuration for a specified environment by creating an `automated..tfvars` file based on the template. @@ -297,6 +313,7 @@ Manually inserts previously generated SSH keys into the configuration file. Usef ``` **Prerequisite:** Keys must already exist in the `keys/` directory. + #### `generate-images` - Generate Custom Server Images Generates custom Packer images for Kubernetes cluster nodes. These images are used when provisioning the cluster infrastructure on Hetzner Cloud. @@ -314,6 +331,34 @@ Generates custom Packer images for Kubernetes cluster nodes. These images are us **Time:** This is typically the longest-running operation, taking 5-15 minutes. +#### `generate-backend` - Generate Terraform Backend Configuration + +Generates the Terraform backend configuration file (`backend.tf`) for the specified environment. This file configures the S3 backend for storing Terraform state files. + +**Syntax:** + +```bash +./ctfp.py generate-backend +``` + +**Arguments:** + +- ``: Component for which to generate the backend configuration: `cluster`, `ops`, `platform`, or `challenges` +- ``: Name of the S3 bucket to use for storing the Terraform state +- ``: Region where the S3 bucket is located +- ``: Endpoint URL for the S3-compatible storage. For exampel `nbg1.your-objectstorage.com` for Hetzner Cloud Object Storage in `nbg1` region. + +**Example:** + +```bash +./ctfp.py generate-backend cluster ctfp-cluster-state nbg1 nbg1.your-objectstorage.com +./ctfp.py generate-backend platform ctfp-platform-state fsn1 fsn1.your-objectstorage.com +``` + +**Output:** Creates a HCL configuration for the specified component's Terraform backend in the `backend/generated/` directory. + +See more about this command in the [backend directory](./backend). + #### `deploy` - Deploy Platform Components Deploys one or more components of the platform to the specified environment. Can deploy individual components or the entire platform at once. From a7e2e4fe1881f34c25e69389cdb240ed6492c2ef Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:49:08 +0100 Subject: [PATCH 098/148] Continued work on documentation --- README.md | 186 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 121 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index f2fdd2d..4fa0a1d 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,8 @@ Changing these options may lead to instability or data loss, and should be done > - `deploy` - Deploy Platform Components > - `destroy` - Destroy Platform Components -The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. Below is a detailed overview of each available command: +The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. +Below is a detailed overview of each available command: #### `init` - Initialize Platform Configuration @@ -443,85 +444,140 @@ graph TD C --> D["🔑 Generate SSH Keys
./ctfp.py generate-keys --insert"] D --> E{{"🖼️ Generate Images
./ctfp.py generate-images

[One-time, 5-15 min]"}} + E -->|Images Ready| F["📋 Generate Backends
./ctfp.py generate-backend
(for each component)"] - E -->|Image Ready| F["📦 Deploy Cluster
./ctfp.py deploy cluster"] - F -->|Cluster Running| G["🛠️ Deploy Ops
./ctfp.py deploy ops"] - G -->|Ops Services Ready| H["🎯 Deploy Platform
./ctfp.py deploy platform"] - H -->|CTFd Ready| I["🎮 Deploy Challenges
./ctfp.py deploy challenges"] + F --> G{{"Deploy Components

Option A: Deploy All
./ctfp.py deploy all"}} + G -->|All| G1["📦 Deploy Cluster"] + G1 --> G2["🛠️ Deploy Ops"] + G2 --> G3["🎯 Deploy Platform"] + G3 --> G4["🎮 Deploy Challenges"] - I -->|All Services Deployed| J["🔌 Configure kubectl
source kubectl.sh"] - J -->|Connected to Cluster| K{{"✅ LIVE CTF ENVIRONMENT

Monitor via:
- ArgoCD
- Grafana
- Kubernetes"}} + G -->|Individual| I1["📦 Deploy Cluster
./ctfp.py deploy cluster"] + I1 -->|Review Plan| I1a{"Apply?"} + I1a -->|Yes| I1b["✓ Cluster Ready"] + I1a -->|No| I1c["❌ Abort"] + I1c -->|Fix Config| C + I1b --> I2["🛠️ Deploy Ops
./ctfp.py deploy ops"] + I2 -->|Review Plan| I2a{"Apply?"} + I2a -->|Yes| I2b["✓ Ops Ready"] + I2a -->|No| I2c["❌ Abort"] + I2c -->|Fix Config| C + I2b --> I3["🎯 Deploy Platform
./ctfp.py deploy platform"] + I3 -->|Review Plan| I3a{"Apply?"} + I3a -->|Yes| I3b["✓ Platform Ready"] + I3a -->|No| I3c["❌ Abort"] + I3c -->|Fix Config| C + I3b --> I4["🎮 Deploy Challenges
./ctfp.py deploy challenges"] + I4 -->|Review Plan| I4a{"Apply?"} + I4a -->|Yes| I4b["✓ Challenges Ready"] + I4a -->|No| I4c["❌ Abort"] + I4c -->|Fix Config| C - K -->|Config Changes| L["🔄 Update & Redeploy
Edit tfvars + deploy component"] - L -->|Back to Live| K + G4 --> J["🔌 Configure kubectl
source kubectl.sh"] + I4b --> J + J --> K{{"✅ LIVE CTF

Monitor:
ArgoCD · Grafana · Prometheus
kubectl · Elasticsearch"}} - K -->|CTF Complete| M["🧹 Destroy Challenges
./ctfp.py destroy challenges"] - M --> N["🧹 Destroy Platform
./ctfp.py destroy platform"] - N --> O["🧹 Destroy Ops
./ctfp.py destroy ops"] - O --> P["🧹 Destroy Cluster
./ctfp.py destroy cluster"] + K -->|Health OK| L["🟢 Monitor & Operate
Manage challenges & users"] + L -->|Config Updates| M["🔄 Redeploy Component
Edit tfvars + deploy [component]"] + M -->|Back to Live| K + + L -->|Issues| N["🔍 Troubleshoot
Check logs, metrics, events"] + N -->|Resolved| K + N -->|Rollback| O["↩️ Revert Config
Edit tfvars + redeploy"] + O -->|Back to Previous State| K + + K -->|CTF Complete| P["🧹 Destroy in Reverse
./ctfp.py destroy all

or individually:
challenges → platform → ops → cluster"] P --> Q["✨ Clean Environment
All resources destroyed"] - style A fill:#e1f5ff - style B fill:#e1f5ff - style C fill:#e1f5ff - style D fill:#e1f5ff - style E fill:#fff3e0 - style F fill:#c8e6c9 - style G fill:#c8e6c9 - style H fill:#c8e6c9 - style I fill:#c8e6c9 - style J fill:#c8e6c9 - style K fill:#a5d6a7 - style L fill:#fff9c4 - style M fill:#ffccbc - style N fill:#ffccbc - style O fill:#ffccbc - style P fill:#ffccbc - style Q fill:#f8bbd0 + style A fill:#e1f5ff,text:#000 + style B fill:#e1f5ff,text:#000 + style C fill:#e1f5ff,text:#000 + style D fill:#e1f5ff,text:#000 + style E fill:#fff3e0,text:#000 + style F fill:#fff3e0,text:#000 + style G fill:#f3e5f5,text:#000 + style G1 fill:#c8e6c9,text:#000 + style G2 fill:#c8e6c9,text:#000 + style G3 fill:#c8e6c9,text:#000 + style G4 fill:#c8e6c9,text:#000 + style I1 fill:#c8e6c9,text:#000 + style I1a fill:#ffe0b2,text:#000 + style I1b fill:#a5d6a7,text:#000 + style I1c fill:#ef9a9a,text:#000 + style I2 fill:#c8e6c9,text:#000 + style I2a fill:#ffe0b2,text:#000 + style I2b fill:#a5d6a7,text:#000 + style I2c fill:#ef9a9a,text:#000 + style I3 fill:#c8e6c9,text:#000 + style I3a fill:#ffe0b2,text:#000 + style I3b fill:#a5d6a7,text:#000 + style I3c fill:#ef9a9a,text:#000 + style I4 fill:#c8e6c9,text:#000 + style I4a fill:#ffe0b2,text:#000 + style I4b fill:#a5d6a7,text:#000 + style I4c fill:#ef9a9a,text:#000 + style J fill:#c8e6c9,text:#000 + style K fill:#a5d6a7,text:#000 + style L fill:#a5d6a7,text:#000 + style M fill:#fff9c4,text:#000 + style N fill:#ffe0b2,text:#000 + style O fill:#fff9c4,text:#000 + style P fill:#ffccbc,text:#000 + style Q fill:#f8bbd0,text:#000 ``` -**Deployment Sequence Notes:** - -The diagram illustrates the critical dependencies between components: - -1. **Initial Setup Phase** (Blue) - One-time configuration - - Clone, initialize config, generate keys - -2. **Image Generation** (Orange) - One-time per Hetzner project - - Must complete before first cluster deployment - -3. **Deployment Chain** (Light Green) - Strict sequential order - - Each component depends on the previous one - - `cluster` → `ops` → `platform` → `challenges` - - Alternatively use `deploy all` for full deployment - -4. **Live Operations** (Green) - Stable state - - Monitor and manage the running CTF - - Optional: Update config and redeploy specific components, such as deploying new challenges - -5. **Teardown Phase** (Red/Orange) - Reverse deployment order - - Destroys resources in reverse sequence to maintain dependencies - - Alternatively use `destroy all` for full teardown +**Workflow Phases:** + +1. **Setup Phase** (Blue) - One-time configuration + - Clone, initialize config, fill configuration values +2. **Preparation Phase** (Orange) - One-time per Hetzner project + - Generate custom images (5-15 min) + - Generate Terraform backend configurations for each component +3. **Deployment Phase** (Purple/Green) - Sequential component deployment + - **Option A**: Use `deploy all` for automated full deployment + - **Option B**: Deploy components individually for fine-grained control and plan review + - Each component must deploy successfully before the next begins +4. **Live Operations** (Green) - Stable running state + - Monitor infrastructure and platform health + - Deploy updates and new challenges + - Handle troubleshooting as needed +5. **Teardown Phase** (Red/Orange) - Cleanup after CTF + - Destroy components in reverse order to maintain dependencies + - Use `destroy all` for automated teardown or individual commands + +**Key Decision Points:** + +- **Deploy all vs. individual**: + - `deploy all` is faster (automatic), but `deploy [component]` lets you review Terraform plans before applying + - If a component fails, you can abort and fix the configuration before continuing +- **Live operations**: + - Configuration changes are applied automatically on the next deployment + - You can roll back by reverting configuration and redeploying + - Monitor health before and after changes **Quick Reference:** -| Phase | Time | Command | Repeat | -| -------- | -------- | ----------------------- | --------------- | -| Setup | ~5 min | `init`, `generate-keys` | Per environment | -| Images | 5-15 min | `generate-images` | One-time only | -| Deploy | ~20 min | `deploy all` | Per environment | -| Manage | Ongoing | `kubectl`, ArgoCD, etc. | As needed | -| Teardown | ~15 min | `destroy all` | When done | +| Phase | Time | Command | Repeat | +| -------- | -------- | ---------------------------- | ---------------------------- | +| Setup | ~5 min | `init`, edit config | Per environment | +| Prep | 5-15 min | `generate-images` | One-time per Hetzner project | +| Backends | ~1 min | `generate-backend` (4x) | Per environment | +| Deploy | ~20 min | `deploy all` OR `deploy [x]` | Per environment | +| Manage | Ongoing | `kubectl`, ArgoCD, Grafana | As needed | +| Teardown | ~15 min | `destroy all` | When done | **Key Points:** -- ⚠️ `generate-images` is a one-time operation per Hetzner Cloud project, not per environment -- 🔄 Configuration changes are applied automatically on the next deployment -- 🛡️ Always review Terraform plans before applying in production; use `--auto-apply` with caution -- 📊 Monitor deployments using the timing information displayed by the CLI -- 🔑 SSH keys must be generated before the first cluster deployment -- 🔗 Use `source kubectl.sh` (not `./`) to properly set environment variables + +- ⚠️ `generate-backend` must run for each component (cluster, ops, platform, challenges) before deployment +- ⚠️ `generate-images` is one-time per Hetzner Cloud project, not per environment +- 🛡️ Review Terraform plans with individual `deploy [component]` commands; use `deploy all` only when confident +- 🔄 Configuration changes apply automatically on next deployment; use the same process to rollback +- 🔑 SSH keys must be generated before cluster deployment +- 🔗 Use `source kubectl.sh` (with `source`, not `./`) to properly set environment variables +- 🔴 If deployment fails, abort, fix configuration, and redeploy—do not force apply ### Guides + #### Updating sizes of nodes in an existing cluster > [!TIP] From ed74bcd4d6db2d74edfcc1243eb2bb6cfe10b7c3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 17:54:53 +0100 Subject: [PATCH 099/148] Continued work on documentation --- README.md | 178 ++++++++++++++++++++++-------------------------------- 1 file changed, 73 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 4fa0a1d..f232974 100644 --- a/README.md +++ b/README.md @@ -441,140 +441,108 @@ The following Mermaid diagram shows the typical workflow for deploying and manag graph TD A["🚀 Clone Repository
git clone https://github.com/ctfpilot/ctfp"] --> B["⚙️ Initialize Config
./ctfp.py init"] B --> C["✏️ Edit Configuration
Fill in tfvars file"] - C --> D["🔑 Generate SSH Keys
./ctfp.py generate-keys --insert"] + C --> D["🔑 Generate SSH Keys
./ctfp.py generate-keys --insert

[One-time per environment]"] - D --> E{{"🖼️ Generate Images
./ctfp.py generate-images

[One-time, 5-15 min]"}} - E -->|Images Ready| F["📋 Generate Backends
./ctfp.py generate-backend
(for each component)"] + D --> E["🖼️ Generate Images
./ctfp.py generate-images

[One-time per Hetzner project, 5-15 min]"] + E --> F["📋 Generate Backends
./ctfp.py generate-backend
(for each component)

[One-time for tool setup]"] - F --> G{{"Deploy Components

Option A: Deploy All
./ctfp.py deploy all"}} - G -->|All| G1["📦 Deploy Cluster"] - G1 --> G2["🛠️ Deploy Ops"] - G2 --> G3["🎯 Deploy Platform"] - G3 --> G4["🎮 Deploy Challenges"] + F --> G{{"Choose Deployment Method"}} + G -->|All Components| H["📦 Deploy All
./ctfp.py deploy all

Reviews plans for each component
then deploys in sequence"] + H --> H1["Cluster → Ops → Platform → Challenges"] + H1 --> J G -->|Individual| I1["📦 Deploy Cluster
./ctfp.py deploy cluster"] - I1 -->|Review Plan| I1a{"Apply?"} - I1a -->|Yes| I1b["✓ Cluster Ready"] - I1a -->|No| I1c["❌ Abort"] - I1c -->|Fix Config| C - I1b --> I2["🛠️ Deploy Ops
./ctfp.py deploy ops"] - I2 -->|Review Plan| I2a{"Apply?"} - I2a -->|Yes| I2b["✓ Ops Ready"] - I2a -->|No| I2c["❌ Abort"] - I2c -->|Fix Config| C - I2b --> I3["🎯 Deploy Platform
./ctfp.py deploy platform"] - I3 -->|Review Plan| I3a{"Apply?"} - I3a -->|Yes| I3b["✓ Platform Ready"] - I3a -->|No| I3c["❌ Abort"] - I3c -->|Fix Config| C - I3b --> I4["🎮 Deploy Challenges
./ctfp.py deploy challenges"] - I4 -->|Review Plan| I4a{"Apply?"} - I4a -->|Yes| I4b["✓ Challenges Ready"] - I4a -->|No| I4c["❌ Abort"] - I4c -->|Fix Config| C + I1 -->|Review & Apply| I2["🛠️ Deploy Ops
./ctfp.py deploy ops"] + I2 -->|Review & Apply| I3["🎯 Deploy Platform
./ctfp.py deploy platform"] + I3 -->|Review & Apply| I4["🎮 Deploy Challenges
./ctfp.py deploy challenges"] + I4 --> J - G4 --> J["🔌 Configure kubectl
source kubectl.sh"] - I4b --> J - J --> K{{"✅ LIVE CTF

Monitor:
ArgoCD · Grafana · Prometheus
kubectl · Elasticsearch"}} + J["🔌 Configure kubectl
source kubectl.sh"] --> K["✅ LIVE CTF ENVIRONMENT

Monitor via:
ArgoCD · Grafana · Prometheus
kubectl · Elasticsearch"] - K -->|Health OK| L["🟢 Monitor & Operate
Manage challenges & users"] - L -->|Config Updates| M["🔄 Redeploy Component
Edit tfvars + deploy [component]"] - M -->|Back to Live| K + K -->|Ongoing| L["🟢 Monitor & Operate
Manage challenges & users"] + L -->|Updates| M["🔄 Redeploy Component
Edit tfvars + deploy [component]"] + M --> K L -->|Issues| N["🔍 Troubleshoot
Check logs, metrics, events"] N -->|Resolved| K N -->|Rollback| O["↩️ Revert Config
Edit tfvars + redeploy"] - O -->|Back to Previous State| K + O --> K - K -->|CTF Complete| P["🧹 Destroy in Reverse
./ctfp.py destroy all

or individually:
challenges → platform → ops → cluster"] + K -->|CTF Complete| P["🧹 Destroy All
./ctfp.py destroy all

or individually in reverse order:
challenges → platform → ops → cluster"] P --> Q["✨ Clean Environment
All resources destroyed"] - style A fill:#e1f5ff,text:#000 - style B fill:#e1f5ff,text:#000 - style C fill:#e1f5ff,text:#000 - style D fill:#e1f5ff,text:#000 - style E fill:#fff3e0,text:#000 - style F fill:#fff3e0,text:#000 - style G fill:#f3e5f5,text:#000 - style G1 fill:#c8e6c9,text:#000 - style G2 fill:#c8e6c9,text:#000 - style G3 fill:#c8e6c9,text:#000 - style G4 fill:#c8e6c9,text:#000 - style I1 fill:#c8e6c9,text:#000 - style I1a fill:#ffe0b2,text:#000 - style I1b fill:#a5d6a7,text:#000 - style I1c fill:#ef9a9a,text:#000 - style I2 fill:#c8e6c9,text:#000 - style I2a fill:#ffe0b2,text:#000 - style I2b fill:#a5d6a7,text:#000 - style I2c fill:#ef9a9a,text:#000 - style I3 fill:#c8e6c9,text:#000 - style I3a fill:#ffe0b2,text:#000 - style I3b fill:#a5d6a7,text:#000 - style I3c fill:#ef9a9a,text:#000 - style I4 fill:#c8e6c9,text:#000 - style I4a fill:#ffe0b2,text:#000 - style I4b fill:#a5d6a7,text:#000 - style I4c fill:#ef9a9a,text:#000 - style J fill:#c8e6c9,text:#000 - style K fill:#a5d6a7,text:#000 - style L fill:#a5d6a7,text:#000 - style M fill:#fff9c4,text:#000 - style N fill:#ffe0b2,text:#000 - style O fill:#fff9c4,text:#000 - style P fill:#ffccbc,text:#000 - style Q fill:#f8bbd0,text:#000 + style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px,color:#000 + style B fill:#e1f5ff,stroke:#01579b,stroke-width:2px,color:#000 + style C fill:#e1f5ff,stroke:#01579b,stroke-width:2px,color:#000 + style D fill:#bbdefb,stroke:#01579b,stroke-width:2px,color:#000 + style E fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000 + style F fill:#ffe0b2,stroke:#e65100,stroke-width:2px,color:#000 + style G fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000 + style H fill:#d1c4e9,stroke:#4a148c,stroke-width:2px,color:#000 + style H1 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 + style I1 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 + style I2 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 + style I3 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 + style I4 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 + style J fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 + style K fill:#a5d6a7,stroke:#1b5e20,stroke-width:3px,color:#000 + style L fill:#a5d6a7,stroke:#1b5e20,stroke-width:2px,color:#000 + style M fill:#fff9c4,stroke:#f57f17,stroke-width:2px,color:#000 + style N fill:#ffcc80,stroke:#e65100,stroke-width:2px,color:#000 + style O fill:#fff9c4,stroke:#f57f17,stroke-width:2px,color:#000 + style P fill:#ffccbc,stroke:#bf360c,stroke-width:2px,color:#000 + style Q fill:#f8bbd0,stroke:#880e4f,stroke-width:2px,color:#000 ``` **Workflow Phases:** -1. **Setup Phase** (Blue) - One-time configuration - - Clone, initialize config, fill configuration values -2. **Preparation Phase** (Orange) - One-time per Hetzner project - - Generate custom images (5-15 min) - - Generate Terraform backend configurations for each component -3. **Deployment Phase** (Purple/Green) - Sequential component deployment - - **Option A**: Use `deploy all` for automated full deployment - - **Option B**: Deploy components individually for fine-grained control and plan review - - Each component must deploy successfully before the next begins -4. **Live Operations** (Green) - Stable running state +1. **Setup Phase** (Light Blue) - One-time per environment + - Clone repository and initialize configuration + - Generate SSH keys for cluster access +2. **Preparation Phase** (Orange) - One-time setup + - **Generate Images**: One-time per Hetzner Cloud project (5-15 min) + - **Generate Backends**: One-time tool setup for Terraform state management +3. **Deployment Phase** (Purple/Green) - Per environment deployment + - **Option A - Deploy All**: Reviews plans for all components, then deploys automatically in sequence + - **Option B - Individual**: Deploy components one-by-one with manual review between each + - Deployment order: Cluster → Ops → Platform → Challenges +4. **Live Operations** (Green) - Ongoing CTF management - Monitor infrastructure and platform health - - Deploy updates and new challenges - - Handle troubleshooting as needed -5. **Teardown Phase** (Red/Orange) - Cleanup after CTF - - Destroy components in reverse order to maintain dependencies - - Use `destroy all` for automated teardown or individual commands + - Deploy configuration updates and new challenges + - Troubleshoot issues and rollback when needed +5. **Teardown Phase** (Red) - Cleanup after CTF + - Destroy all components in reverse order + - Use `destroy all` or destroy individual components **Key Decision Points:** - **Deploy all vs. individual**: - - `deploy all` is faster (automatic), but `deploy [component]` lets you review Terraform plans before applying - - If a component fails, you can abort and fix the configuration before continuing + - `deploy all`: Reviews all plans upfront, then deploys everything automatically in sequence (faster, recommended) + - Individual deploys: Full control with manual intervention between each component (slower, for careful deployments) - **Live operations**: - - Configuration changes are applied automatically on the next deployment - - You can roll back by reverting configuration and redeploying - - Monitor health before and after changes + - Configuration changes apply on next deployment + - Rollback by reverting configuration files and redeploying **Quick Reference:** -| Phase | Time | Command | Repeat | -| -------- | -------- | ---------------------------- | ---------------------------- | -| Setup | ~5 min | `init`, edit config | Per environment | -| Prep | 5-15 min | `generate-images` | One-time per Hetzner project | -| Backends | ~1 min | `generate-backend` (4x) | Per environment | -| Deploy | ~20 min | `deploy all` OR `deploy [x]` | Per environment | -| Manage | Ongoing | `kubectl`, ArgoCD, Grafana | As needed | -| Teardown | ~15 min | `destroy all` | When done | +| Phase | Time | Command | Frequency | +| ---------- | -------- | ---------------------------- | ------------------------------- | +| Setup | ~5 min | `init`, `generate-keys` | Once per environment | +| Images | 5-15 min | `generate-images` | Once per Hetzner Cloud project | +| Backends | ~1 min | `generate-backend` (4x) | Once for tool setup | +| Deploy | ~20 min | `deploy all` OR `deploy [x]` | Per environment (Test/Dev/Prod) | +| Management | Ongoing | `kubectl`, ArgoCD, Grafana | As needed during CTF | +| Teardown | ~15 min | `destroy all` | After CTF completion | **Key Points:** -- ⚠️ `generate-backend` must run for each component (cluster, ops, platform, challenges) before deployment -- ⚠️ `generate-images` is one-time per Hetzner Cloud project, not per environment -- 🛡️ Review Terraform plans with individual `deploy [component]` commands; use `deploy all` only when confident -- 🔄 Configuration changes apply automatically on next deployment; use the same process to rollback -- 🔑 SSH keys must be generated before cluster deployment -- 🔗 Use `source kubectl.sh` (with `source`, not `./`) to properly set environment variables -- 🔴 If deployment fails, abort, fix configuration, and redeploy—do not force apply +- 🔑 **SSH keys** generated once per environment (test/dev/prod) +- 🖼️ **Custom images** generated once per Hetzner Cloud project (shared across all environments) +- 📋 **Backend configuration** generated once for the tool (stores Terraform state) +- 🚀 **`deploy all`** still allows plan review before applying, but proceeds automatically after approval +- 🔄 Configuration changes require editing tfvars and redeploying the affected component +- ⚠️ Use `source kubectl.sh [env]` with `source` to properly set environment variables +- 🛡️ Always review Terraform plans before applying—never use `--auto-apply` in production ### Guides From 2570a6dc9c2690cdb52cad901580961486c22f08 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 18:19:45 +0100 Subject: [PATCH 100/148] Continued work on documentation --- README.md | 132 +++++++++--------------------------------------------- 1 file changed, 22 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index f232974..14a3421 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [`generate-backend` - Generate Terraform Backend Configuration](#generate-backend---generate-terraform-backend-configuration) - [`deploy` - Deploy Platform Components](#deploy---deploy-platform-components) - [`destroy` - Destroy Platform Components](#destroy---destroy-platform-components) - - [Workflow Diagram](#workflow-diagram) + - [Workflow Overview](#workflow-overview) - [Guides](#guides) - [Updating sizes of nodes in an existing cluster](#updating-sizes-of-nodes-in-an-existing-cluster) - [Architecture](#architecture) @@ -431,118 +431,30 @@ Destroys one or more components of the platform. This is the reverse of `deploy` **Destruction Order:** When destroying `all`, components are destroyed in reverse order: `challenges` → `platform` → `ops` → `cluster`. This ensures dependencies are properly cleaned up. +### Workflow Overview +The workflow for deploying and managing CTFp can be summarized in the following key phases: -### Workflow Diagram - -The following Mermaid diagram shows the typical workflow for deploying and managing CTFp, with the sequence of commands and their relationships: - -```mermaid -graph TD - A["🚀 Clone Repository
git clone https://github.com/ctfpilot/ctfp"] --> B["⚙️ Initialize Config
./ctfp.py init"] - B --> C["✏️ Edit Configuration
Fill in tfvars file"] - C --> D["🔑 Generate SSH Keys
./ctfp.py generate-keys --insert

[One-time per environment]"] - - D --> E["🖼️ Generate Images
./ctfp.py generate-images

[One-time per Hetzner project, 5-15 min]"] - E --> F["📋 Generate Backends
./ctfp.py generate-backend
(for each component)

[One-time for tool setup]"] - - F --> G{{"Choose Deployment Method"}} - G -->|All Components| H["📦 Deploy All
./ctfp.py deploy all

Reviews plans for each component
then deploys in sequence"] - H --> H1["Cluster → Ops → Platform → Challenges"] - H1 --> J - - G -->|Individual| I1["📦 Deploy Cluster
./ctfp.py deploy cluster"] - I1 -->|Review & Apply| I2["🛠️ Deploy Ops
./ctfp.py deploy ops"] - I2 -->|Review & Apply| I3["🎯 Deploy Platform
./ctfp.py deploy platform"] - I3 -->|Review & Apply| I4["🎮 Deploy Challenges
./ctfp.py deploy challenges"] - I4 --> J - - J["🔌 Configure kubectl
source kubectl.sh"] --> K["✅ LIVE CTF ENVIRONMENT

Monitor via:
ArgoCD · Grafana · Prometheus
kubectl · Elasticsearch"] - - K -->|Ongoing| L["🟢 Monitor & Operate
Manage challenges & users"] - L -->|Updates| M["🔄 Redeploy Component
Edit tfvars + deploy [component]"] - M --> K - - L -->|Issues| N["🔍 Troubleshoot
Check logs, metrics, events"] - N -->|Resolved| K - N -->|Rollback| O["↩️ Revert Config
Edit tfvars + redeploy"] - O --> K - - K -->|CTF Complete| P["🧹 Destroy All
./ctfp.py destroy all

or individually in reverse order:
challenges → platform → ops → cluster"] - P --> Q["✨ Clean Environment
All resources destroyed"] - - style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px,color:#000 - style B fill:#e1f5ff,stroke:#01579b,stroke-width:2px,color:#000 - style C fill:#e1f5ff,stroke:#01579b,stroke-width:2px,color:#000 - style D fill:#bbdefb,stroke:#01579b,stroke-width:2px,color:#000 - style E fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000 - style F fill:#ffe0b2,stroke:#e65100,stroke-width:2px,color:#000 - style G fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000 - style H fill:#d1c4e9,stroke:#4a148c,stroke-width:2px,color:#000 - style H1 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 - style I1 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 - style I2 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 - style I3 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 - style I4 fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 - style J fill:#c8e6c9,stroke:#1b5e20,stroke-width:2px,color:#000 - style K fill:#a5d6a7,stroke:#1b5e20,stroke-width:3px,color:#000 - style L fill:#a5d6a7,stroke:#1b5e20,stroke-width:2px,color:#000 - style M fill:#fff9c4,stroke:#f57f17,stroke-width:2px,color:#000 - style N fill:#ffcc80,stroke:#e65100,stroke-width:2px,color:#000 - style O fill:#fff9c4,stroke:#f57f17,stroke-width:2px,color:#000 - style P fill:#ffccbc,stroke:#bf360c,stroke-width:2px,color:#000 - style Q fill:#f8bbd0,stroke:#880e4f,stroke-width:2px,color:#000 -``` +1. **Setup Phase**: + - Clone the repository and generate backend configurations. + +2. **Preparation Phase**: + - Generate custom server images (one-time setup per Hetzner project). + - Generate SSH keys. + - Create needed pre-requisites. + - Configure the platform using the `automated..tfvars` file. + +3. **Deployment Phase**: + - Deploy components in sequence: `Cluster → Ops → Platform → Challenges`. + - Use `deploy all` for automated deployment or deploy components individually. + +4. **Live Operations**: + - Monitor the platform using tools like ArgoCD, Grafana, and Prometheus. + - Manage challenges, and apply updates as needed. -**Workflow Phases:** - -1. **Setup Phase** (Light Blue) - One-time per environment - - Clone repository and initialize configuration - - Generate SSH keys for cluster access -2. **Preparation Phase** (Orange) - One-time setup - - **Generate Images**: One-time per Hetzner Cloud project (5-15 min) - - **Generate Backends**: One-time tool setup for Terraform state management -3. **Deployment Phase** (Purple/Green) - Per environment deployment - - **Option A - Deploy All**: Reviews plans for all components, then deploys automatically in sequence - - **Option B - Individual**: Deploy components one-by-one with manual review between each - - Deployment order: Cluster → Ops → Platform → Challenges -4. **Live Operations** (Green) - Ongoing CTF management - - Monitor infrastructure and platform health - - Deploy configuration updates and new challenges - - Troubleshoot issues and rollback when needed -5. **Teardown Phase** (Red) - Cleanup after CTF - - Destroy all components in reverse order - - Use `destroy all` or destroy individual components - -**Key Decision Points:** - -- **Deploy all vs. individual**: - - `deploy all`: Reviews all plans upfront, then deploys everything automatically in sequence (faster, recommended) - - Individual deploys: Full control with manual intervention between each component (slower, for careful deployments) -- **Live operations**: - - Configuration changes apply on next deployment - - Rollback by reverting configuration files and redeploying - -**Quick Reference:** - -| Phase | Time | Command | Frequency | -| ---------- | -------- | ---------------------------- | ------------------------------- | -| Setup | ~5 min | `init`, `generate-keys` | Once per environment | -| Images | 5-15 min | `generate-images` | Once per Hetzner Cloud project | -| Backends | ~1 min | `generate-backend` (4x) | Once for tool setup | -| Deploy | ~20 min | `deploy all` OR `deploy [x]` | Per environment (Test/Dev/Prod) | -| Management | Ongoing | `kubectl`, ArgoCD, Grafana | As needed during CTF | -| Teardown | ~15 min | `destroy all` | After CTF completion | - -**Key Points:** - -- 🔑 **SSH keys** generated once per environment (test/dev/prod) -- 🖼️ **Custom images** generated once per Hetzner Cloud project (shared across all environments) -- 📋 **Backend configuration** generated once for the tool (stores Terraform state) -- 🚀 **`deploy all`** still allows plan review before applying, but proceeds automatically after approval -- 🔄 Configuration changes require editing tfvars and redeploying the affected component -- ⚠️ Use `source kubectl.sh [env]` with `source` to properly set environment variables -- 🛡️ Always review Terraform plans before applying—never use `--auto-apply` in production +5. **Teardown Phase**: + - Destroy components in reverse order: `Challenges → Platform → Ops → Cluster`. + - Use `destroy all` for automated teardown or destroy components individually. ### Guides From 118b4e2a42049812c7087b3c60af39d22345ff41 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 18:21:58 +0100 Subject: [PATCH 101/148] Continued work on documentation --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14a3421..d6f2dcb 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ Changing these options may lead to instability or data loss, and should be done > For example: `./ctfp.py deploy --help` > > Available commands: -> +> > - `init` - Initialize Platform Configuration > - `generate-keys` - Generate SSH Keys > - `insert-keys` - Insert SSH Keys into Configuration @@ -308,6 +308,7 @@ Manually inserts previously generated SSH keys into the configuration file. Usef - `--prod`: Insert keys for PROD environment **Example:** + ```bash ./ctfp.py insert-keys --test ./ctfp.py insert-keys --prod @@ -468,22 +469,32 @@ When updating the sizes of nodes in an existing cluster, it is important to foll Below are the steps to update the sizes of nodes in an existing cluster: 1. **Drain the Node Pool**: Before making any changes, drain the node pool that you intend to update. This will safely evict all workloads from the nodes in the pool, allowing them to be rescheduled on other nodes in the cluster. + ```bash + # List nodes + kubectl get nodes + + # Drain each node in the node pool kubectl drain --ignore-daemonsets --delete-local-data ``` + *You will need to repeat this for each node in the node pool. You can use tools such as [`draino`](https://github.com/planetlabs/draino) to automate this process.* 2. **Update the Configuration**: Modify the `automated..tfvars` file to reflect the new sizes for the nodes in the node pool. Ensure that you only change the sizes for the specific node pool you are updating. 3. **Deploy the Changes**: Use the CTFp CLI tool to deploy the changes to the cluster. This will apply the updated configuration and resize the nodes in the specified node pool. + ```bash ./ctfp.py deploy cluster -- ``` + *Replace `` with the appropriate environment flag (`--test`, `--dev`, or `--prod`).* 4. **Monitor the Deployment**: Keep an eye on the deployment process to ensure that the nodes are resized correctly and that there are no issues. You can use `kubectl get nodes` to check the status of the nodes in the cluster. 5. **Uncordon the Node Pool**: Once the nodes have been resized and are ready, uncordon the node pool to allow workloads to be scheduled on the nodes again. + ```bash kubectl uncordon ``` + *Repeat this for each node in the node pool.* 6. **Verify the Changes**: Finally, verify that the workloads are running correctly on the resized nodes and that there are no issues in the cluster. 7. **Repeat for Other Node Pools**: If you have multiple node pools to update, repeat the above steps for each node pool, one at a time. From 168eacb80e2959ce984692c4afc88f2b63260c5e Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 18:37:34 +0100 Subject: [PATCH 102/148] Restructure commands list for CLI Tool --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d6f2dcb..c1b7bf1 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Pre-requisites](#pre-requisites) - [Environments](#environments) - [Configuring the platform](#configuring-the-platform) + - [CLI Tool](#cli-tool) - [Commands](#commands) - [`init` - Initialize Platform Configuration](#init---initialize-platform-configuration) - [`generate-keys` - Generate SSH Keys](#generate-keys---generate-ssh-keys) @@ -44,7 +45,7 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Architecture](#architecture) - [Directory structure](#directory-structure) - [CTFp](#ctfp) - - [CLI Tool](#cli-tool) + - [CLI Tool](#cli-tool-1) - [Contributing](#contributing) - [Background](#background) - [License](#license) @@ -163,6 +164,8 @@ In order to even deploy the platform, the following software needs to be install - [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - For interacting with the Kubernetes cluster - [hcloud cli tool](https://github.com/hetznercloud/cli) - For interacting with the Hetzner Cloud API (Otherwise use the Hetzner web interface) - SSH client - For connecting to the servers +- Python 3 - For running the CTFp CLI tool +- Python package [`python-hcl2`](https://github.com/amplify-education/python-hcl2) - Required by the CTFp CLI tool for parsing Terraform configuration files And the following is required in order to deploy the platform: @@ -220,6 +223,42 @@ Each component is not fully configurable, and may in certain situation required These options are either intended to be static, or require manual configuration through the individual Terraform components. Changing these options may lead to instability or data loss, and should be done with caution. +### CLI Tool + +The CTFp CLI tool is a Python script that can be executed directly from the command line, and manages the deployment and lifecycle of the CTFp platform. + +**Prerequisites:** + +1. Install required Python dependencies: + + ```bash + pip install -r requirements.txt + ``` + + This installs `python-hcl2`, which is required for parsing Terraform configuration files. + +2. Ensure the script has executable permissions: + + ```bash + chmod +x ctfp.py + ``` + +**Running commands:** + +You can now run commands directly: + +```bash +./ctfp.py [options] +``` + +Alternatively, you can always run it explicitly with Python: + +```bash +python3 ctfp.py [options] +``` + +Both methods are functionally equivalent. The direct execution method (first example) is more convenient for regular use. + ### Commands > [!TIP] @@ -236,7 +275,6 @@ Changing these options may lead to instability or data loss, and should be done > - `deploy` - Deploy Platform Components > - `destroy` - Destroy Platform Components -The CTFp CLI tool provides a variety of commands for managing the deployment and lifecycle of the platform. Below is a detailed overview of each available command: #### `init` - Initialize Platform Configuration From bbc6ff000a41011e41040c69dc79258692b12d2a Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 18:38:31 +0100 Subject: [PATCH 103/148] Restructure of CLI tool wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c1b7bf1..106144a 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ The CTFp CLI tool is a Python script that can be executed directly from the comm chmod +x ctfp.py ``` -**Running commands:** +**Running the CLI tool:** You can now run commands directly: From 4b00c0b2367ac2f0b2402278c183af89307b79b1 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 18:39:41 +0100 Subject: [PATCH 104/148] Restructure the Commands section to better align with overall hirecki --- README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 106144a..e09e5e7 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,14 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Environments](#environments) - [Configuring the platform](#configuring-the-platform) - [CLI Tool](#cli-tool) - - [Commands](#commands) - - [`init` - Initialize Platform Configuration](#init---initialize-platform-configuration) - - [`generate-keys` - Generate SSH Keys](#generate-keys---generate-ssh-keys) - - [`insert-keys` - Insert SSH Keys into Configuration](#insert-keys---insert-ssh-keys-into-configuration) - - [`generate-images` - Generate Custom Server Images](#generate-images---generate-custom-server-images) - - [`generate-backend` - Generate Terraform Backend Configuration](#generate-backend---generate-terraform-backend-configuration) - - [`deploy` - Deploy Platform Components](#deploy---deploy-platform-components) - - [`destroy` - Destroy Platform Components](#destroy---destroy-platform-components) + - [Commands](#commands) + - [`init` - Initialize Platform Configuration](#init---initialize-platform-configuration) + - [`generate-keys` - Generate SSH Keys](#generate-keys---generate-ssh-keys) + - [`insert-keys` - Insert SSH Keys into Configuration](#insert-keys---insert-ssh-keys-into-configuration) + - [`generate-images` - Generate Custom Server Images](#generate-images---generate-custom-server-images) + - [`generate-backend` - Generate Terraform Backend Configuration](#generate-backend---generate-terraform-backend-configuration) + - [`deploy` - Deploy Platform Components](#deploy---deploy-platform-components) + - [`destroy` - Destroy Platform Components](#destroy---destroy-platform-components) - [Workflow Overview](#workflow-overview) - [Guides](#guides) - [Updating sizes of nodes in an existing cluster](#updating-sizes-of-nodes-in-an-existing-cluster) @@ -259,7 +259,7 @@ python3 ctfp.py [options] Both methods are functionally equivalent. The direct execution method (first example) is more convenient for regular use. -### Commands +#### Commands > [!TIP] > You can run any command with the `--help` flag to get more information about the command and its options. @@ -277,7 +277,7 @@ Both methods are functionally equivalent. The direct execution method (first exa Below is a detailed overview of each available command: -#### `init` - Initialize Platform Configuration +##### `init` - Initialize Platform Configuration Initializes the platform configuration for a specified environment by creating an `automated..tfvars` file based on the template. @@ -303,7 +303,7 @@ Initializes the platform configuration for a specified environment by creating a **Output:** Creates `automated.test.tfvars`, `automated.dev.tfvars`, or `automated.prod.tfvars` in the repository root. -#### `generate-keys` - Generate SSH Keys +##### `generate-keys` - Generate SSH Keys Generates SSH keys (ed25519) required for accessing the cluster nodes. Optionally inserts the base64-encoded keys directly into the configuration file. @@ -329,7 +329,7 @@ Generates SSH keys (ed25519) required for accessing the cluster nodes. Optionall **Output:** Creates `keys/k8s-.pub` (public key) and `keys/k8s-` (private key) in the `keys/` directory. -#### `insert-keys` - Insert SSH Keys into Configuration +##### `insert-keys` - Insert SSH Keys into Configuration Manually inserts previously generated SSH keys into the configuration file. Useful if keys were generated separately or if you need to update existing keys. @@ -354,7 +354,7 @@ Manually inserts previously generated SSH keys into the configuration file. Usef **Prerequisite:** Keys must already exist in the `keys/` directory. -#### `generate-images` - Generate Custom Server Images +##### `generate-images` - Generate Custom Server Images Generates custom Packer images for Kubernetes cluster nodes. These images are used when provisioning the cluster infrastructure on Hetzner Cloud. @@ -371,7 +371,7 @@ Generates custom Packer images for Kubernetes cluster nodes. These images are us **Time:** This is typically the longest-running operation, taking 5-15 minutes. -#### `generate-backend` - Generate Terraform Backend Configuration +##### `generate-backend` - Generate Terraform Backend Configuration Generates the Terraform backend configuration file (`backend.tf`) for the specified environment. This file configures the S3 backend for storing Terraform state files. @@ -399,7 +399,7 @@ Generates the Terraform backend configuration file (`backend.tf`) for the specif See more about this command in the [backend directory](./backend). -#### `deploy` - Deploy Platform Components +##### `deploy` - Deploy Platform Components Deploys one or more components of the platform to the specified environment. Can deploy individual components or the entire platform at once. @@ -437,7 +437,7 @@ Deploys one or more components of the platform to the specified environment. Can **Output:** Creates Terraform state files in the `terraform/` directory and outputs deployment status and timing information. -#### `destroy` - Destroy Platform Components +##### `destroy` - Destroy Platform Components > [!WARNING] > Destroying the platform will **delete all data** associated with the environment, including databases, user data, and challenge instances. This action cannot be undone. Always ensure you have backups before destroying production environments. From cb3d4e80dde0b3fd7092d8f17879edcdb46f3383 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 19:04:13 +0100 Subject: [PATCH 105/148] Fix typos and improve clarity in README documentation --- README.md | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e09e5e7..2ae844c 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ > [!TIP] > If you are looking for **how to build challenges for CTFp**, please check out the **[CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template)** and **[CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit)** repositories. -CTFp (CTF Pilot's CTF Platform) is a CTF plaform designed to host large-scale Capture The Flag (CTF) competitions, with focus on scalability, resilience and ease of use. +CTFp (CTF Pilot's CTF Platform) is a CTF platform designed to host large-scale Capture The Flag (CTF) competitions, with focus on scalability, resilience and ease of use. The platform uses Kubernetes as the underlying orchestration system, where both the management, scoreboard and challenge infrastructure are deployed as Kubernetes resources. It then leverages GitOps through [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) for managing the platform's configuration and deployments, including the CTF challenges. -CTFp acts as the orchestration layer for deploying and managing the platform, while utilizing a varirety of CTF Pilots components for providing the full functionality of the platform. +CTFp acts as the orchestration layer for deploying and managing the platform, while utilizing a variety of CTF Pilots components for providing the full functionality of the platform. CTFp provides a CLI tool for managing the deployment of the platform, but it is possible to use the individual Terraform components directly if desired. To further work with the platform after initial deployment, you will primarily interact with the Kubernetes cluster using `kubectl`, ArgoCD and the other monitoring systems deployed. @@ -41,11 +41,10 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [`destroy` - Destroy Platform Components](#destroy---destroy-platform-components) - [Workflow Overview](#workflow-overview) - [Guides](#guides) - - [Updating sizes of nodes in an existing cluster](#updating-sizes-of-nodes-in-an-existing-cluster) + - [Updating sizes of nodes in a running platform](#updating-sizes-of-nodes-in-a-running-platform) - [Architecture](#architecture) - [Directory structure](#directory-structure) - - [CTFp](#ctfp) - - [CLI Tool](#cli-tool-1) + - [Getting help](#getting-help) - [Contributing](#contributing) - [Background](#background) - [License](#license) @@ -171,7 +170,7 @@ And the following is required in order to deploy the platform: - [Hetzner Cloud](https://www.hetzner.com/cloud) account with one or more Hetzner Cloud projects - [Hetzner Cloud API Token](https://console.hetzner.cloud/projects) - For authenticating with the Hetzner Cloud API -- [Hetzner S3 buckets](https://console.hetzner.cloud/projects) - For storing the Terraform state files, backups and challenge data. We recommend using 3 separate buckets with seperate access keys for security reasons +- [Hetzner S3 buckets](https://console.hetzner.cloud/projects) - For storing the Terraform state files, backups and challenge data. We recommend using 3 separate buckets with separate access keys for security reasons - [Cloudflare](https://www.cloudflare.com/) account - [Cloudflare API Token](https://dash.cloudflare.com/profile/api-tokens) - For authenticating with the Cloudflare API - [3 Cloudflare controlled domains](https://dash.cloudflare.com/) - For allowing the system to allocate a domain for the Kubernetes cluster. Used to allocate management, platform and challenge domains. @@ -386,7 +385,7 @@ Generates the Terraform backend configuration file (`backend.tf`) for the specif - ``: Component for which to generate the backend configuration: `cluster`, `ops`, `platform`, or `challenges` - ``: Name of the S3 bucket to use for storing the Terraform state - ``: Region where the S3 bucket is located -- ``: Endpoint URL for the S3-compatible storage. For exampel `nbg1.your-objectstorage.com` for Hetzner Cloud Object Storage in `nbg1` region. +- ``: Endpoint URL for the S3-compatible storage. For example `nbg1.your-objectstorage.com` for Hetzner Cloud Object Storage in `nbg1` region. **Example:** @@ -497,7 +496,7 @@ The workflow for deploying and managing CTFp can be summarized in the following ### Guides -#### Updating sizes of nodes in an existing cluster +#### Updating sizes of nodes in a running platform > [!TIP] > When upgrading existing clusters, it is recommended to drain node pools before changing their sizes, to avoid disruption of running workloads. @@ -541,7 +540,7 @@ Below are the steps to update the sizes of nodes in an existing cluster: > Changing node sizes can lead to temporary disruption of workloads. > Always ensure that you have backups of critical data before making changes to the cluster configuration. -Changes to the `scale_type` will only affect new nodes being created, and will not resize existing nodes, as the deployment of these nodes are done as ressources are needed. +Changes to the `scale_type` will only affect new nodes being created, and will not resize existing nodes, as the deployment of these nodes are done as resources are needed. You may need to manually intervene to resize existing nodes if required, or delete them, forcing the system to create new nodes with the updated sizes. However, this may lead to downtime for workloads running on the nodes being deleted. @@ -588,9 +587,14 @@ ctfp/ └── ... # Other files and directories, such as license, contributing guidelines, etc. ``` -### CTFp +## Getting help -### CLI Tool +The project is built and maintained by the CTF Pilot team, which is a community-driven effort. + +If you need help or have questions regarding CTFp, you can reach out through the following channels: + +- **GitHub Issues**: You can open an issue in the [CTFp GitHub repository](https://github.com/ctfpilot/ctfp/issues) for bug reports, feature requests, or general questions. +- **Discord**: Join the [CTF Pilot Discord server](https://discord.ctfpilot.com) to engage with the community, ask questions, and get support from other users and contributors. ## Contributing @@ -610,12 +614,23 @@ To administrate the CLA signing process, we are using **[CLA assistant lite](htt CTF Pilot started as a CTF Platform project, originating in **[Brunnerne](https://github.com/brunnerne)**. +The goal of the project, is to provide a scalable, resilient and easy to use CTF platform for hosting large scale Capture The Flag competitions, starting with BrunnerCTF 2025. + +The project is still in active development, and we welcome contributions from the community to help improve and expand the platform's capabilities. + ## License CTFp is licensed under a dual license, the **PolyForm Noncommercial License 1.0.0** for non-commercial use, and a **Commercial License** for commercial use. You can find the full license for non-commercial use in the **[LICENSE.md](LICENSE.md)** file. For commercial licensing, please contact **[The0Mikkel](https://github.com/The0Mikkel)**. +Without commercial licensing, the platform **MUST NOT** be used for commercial purposes, including but not limited to: + +- Hosting CTF competitions for profit +- Hosting a CTF as a commercial organization +- Offering CTF hosting as a paid service +- Using the platform in any commercial product or service + We encourage all modifications and contributions to be shared back with the community, for example through pull requests to this repository. We also encourage all derivative works to be publicly available under **PolyForm Noncommercial License 1.0.0**. At all times must the license terms be followed. From f12b1fe4181d24e1957930b531a45de0c42aefd2 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 19:05:19 +0100 Subject: [PATCH 106/148] Refactor formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ae844c..55c60bb 100644 --- a/README.md +++ b/README.md @@ -620,7 +620,7 @@ The project is still in active development, and we welcome contributions from th ## License -CTFp is licensed under a dual license, the **PolyForm Noncommercial License 1.0.0** for non-commercial use, and a **Commercial License** for commercial use. +CTFp is licensed under a dual license, the **PolyForm Noncommercial License 1.0.0** for non-commercial use, and a **Commercial License** for commercial use. You can find the full license for non-commercial use in the **[LICENSE.md](LICENSE.md)** file. For commercial licensing, please contact **[The0Mikkel](https://github.com/The0Mikkel)**. From 7b0089fbce0e6bab08dbbd6b988a1eff14aa65c1 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 19:06:41 +0100 Subject: [PATCH 107/148] Clarify restrictions on commercial use in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55c60bb..1655ac6 100644 --- a/README.md +++ b/README.md @@ -627,7 +627,7 @@ For commercial licensing, please contact **[The0Mikkel](https://github.com/The0M Without commercial licensing, the platform **MUST NOT** be used for commercial purposes, including but not limited to: - Hosting CTF competitions for profit -- Hosting a CTF as a commercial organization +- Hosting a CTF as a commercial organization, even if the CTF itself is free or only provided to internal users - Offering CTF hosting as a paid service - Using the platform in any commercial product or service From d11d7d53f073710e3c7bf4110790ae0819a48152 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Fri, 26 Dec 2025 20:35:34 +0100 Subject: [PATCH 108/148] Add more guides to how-to-run --- README.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/README.md b/README.md index 1655ac6..1790d1a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Workflow Overview](#workflow-overview) - [Guides](#guides) - [Updating sizes of nodes in a running platform](#updating-sizes-of-nodes-in-a-running-platform) + - [Deploying a new challenge](#deploying-a-new-challenge) + - [Updating a challenge](#updating-a-challenge) + - [Deplyoing a page](#deplyoing-a-page) - [Architecture](#architecture) - [Directory structure](#directory-structure) - [Getting help](#getting-help) @@ -550,6 +553,113 @@ You may need to manually intervene to resize existing nodes if required, or dele Hetzner does not support downsizing nodes, if they were initially created with a larger size. In such cases, the nodes will need to be deleted, forcing the system to create new nodes with the desired size. +#### Deploying a new challenge + +To deploy a new challenge, you will need to add the challenge to the configuration file, and then deploy the changes to the platform. + +Challenges are split into three types: + +- `static` - Static challenge, often with a handout (files, puzzles, etc.). +- `shared` - Challenge with a single instance for all teams to connect to. +- `instanced` - Challenge with individual instances for each team. + +The challenge should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and build using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenge Schema](https://github.com/ctfpilot/challenge-schema). + +In the configuration file, you will need to add the challenge under the `Challenges configuration` section. + +For static files, add the challenge under the `challenges_static` list: + +```hcl +challenges_static = { + = [ + "" + ] +} +``` + +For shared challenges, add the challenge under the `challenges_shared` list: + +```hcl +challenges_shared = { + = [ + "" + ] +} +``` + +For instanced challenges, add the challenge under the `challenges_instanced` list: + +```hcl +challenges_instanced = { + = [ + "" + ] +} +``` + +An example of this, using the [`CTF Pilot's Challenges example repository`](https://github.com/ctfpilot/challenges-example), would look like this: + +```hcl +challenges_static = { + forensics = ["oh-look-a-flag"], +} +challenges_shared = { + web = ["the-shared-site"], +} +challenges_instanced = { + web = ["where-robots-cannot-search"], + misc = ["a-true-connection"], +} +``` + +In order to deploy the new challenge, you need to deploy the `challenges` component using the CLI tool: + +```bash +./ctfp.py deploy challenges -- +``` + +Removing a challenge required you to remove it from the configuration file, and then deploy the `challenges` component again. + +Challenge changes are automatically and continuously deployed through ArgoCD, so no manual intervention is required after the initial deployment. + +#### Updating a challenge + +Challenge updates are handled through the Git repository containing the challenges. + +If a challenges slug has been changed, you need to remove the old slug from the configuration file, and add the new slug. +For this, follow the [Deploying a new challenge](#deploying-a-new-challenge) guide. + +#### Deplyoing a page + +To deploy a new page to CTFd, you will need to add the page to a Git repository that should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and build using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit). +You can see the page schema in the [CTF Pilot's Page Schema](https://github.com/ctfpilot/page-schema). + +In the configuration file, you will need to add the page under the `Pages configuration` section. + +For pages, add the page under the `pages` list: + +```hcl +pages = [ + "" +] +``` + +An example of this, using the [`CTF Pilot's Challenges example repository`](https://github.com/ctfpilot/challenges-example), would look like this: + +```hcl +pages = ["index"] +``` + +In order to deploy the new page, you need to deploy the `platform` component using the CLI tool: + +```bash +./ctfp.py deploy platform -- +``` + +To remove a page, you need to remove it from the configuration file, and then deploy the `platform` component again. + +Page changes are automatically and continuously deployed through ArgoCD, so no manual intervention is required after the initial deployment. + ## Architecture The CTFp platform is composed of four main components, each responsible for different aspects of the platform's functionality: From 4b230e68f23d8ec66b81bb546d7dcca8656218e1 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 10:59:36 +0100 Subject: [PATCH 109/148] Correct formatting --- README.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1790d1a..7301cca 100644 --- a/README.md +++ b/README.md @@ -597,7 +597,7 @@ challenges_instanced = { } ``` -An example of this, using the [`CTF Pilot's Challenges example repository`](https://github.com/ctfpilot/challenges-example), would look like this: +An example of this, using [CTF Pilot's Challenges example repository](https://github.com/ctfpilot/challenges-example), would look like this: ```hcl challenges_static = { @@ -631,8 +631,7 @@ For this, follow the [Deploying a new challenge](#deploying-a-new-challenge) gui #### Deplyoing a page -To deploy a new page to CTFd, you will need to add the page to a Git repository that should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and build using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit). -You can see the page schema in the [CTF Pilot's Page Schema](https://github.com/ctfpilot/page-schema). +To deploy a new page to CTFd, you will need to add the page to a Git repository that should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and build using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Page Schema](https://github.com/ctfpilot/page-schema). In the configuration file, you will need to add the page under the `Pages configuration` section. @@ -644,7 +643,7 @@ pages = [ ] ``` -An example of this, using the [`CTF Pilot's Challenges example repository`](https://github.com/ctfpilot/challenges-example), would look like this: +An example of this, using the [CTF Pilot's Challenges example repository](https://github.com/ctfpilot/challenges-example), would look like this: ```hcl pages = ["index"] @@ -662,15 +661,19 @@ Page changes are automatically and continuously deployed through ArgoCD, so no m ## Architecture -The CTFp platform is composed of four main components, each responsible for different aspects of the platform's functionality: +CTFp is composed of four main components, each responsible for different aspects of the platform's functionality: -1. **Cluster**: Responsible for provisioning and managing the underlying Kubernetes cluster infrastructure on Hetzner Cloud. This includes setting up the necessary servers, networking, and storage resources required for the cluster to operate. +1. **Cluster**: Responsible for provisioning and managing the underlying Kubernetes cluster infrastructure on Hetzner Cloud. + This includes setting up the necessary servers, networking, and storage resources required for the cluster to operate. This can be found in the [`cluster`](./cluster) directory, and as the `cluster` component in the CLI tool. -2. **Ops** (Operations): Focuses on deploying and managing the operational tools and monitoring systems for the platform. This includes setting up ArgoCD, monitoring, logging, ingress controllers, and other essential services that ensure the smooth operation of the platform. +2. **Ops** (Operations): Focuses on deploying and managing the operational tools and monitoring systems for the platform. + This includes setting up ArgoCD, monitoring, logging, ingress controllers, and other essential services that ensure the smooth operation of the platform. This can be found in the [`ops`](./ops) directory, and as the `ops` component in the CLI tool. -3. **Platform**: Handles the deployment and configuration of the CTFd scoreboard and its associated services. This includes setting up the database, caching, and storage solutions required for the scoreboard to function effectively. +3. **Platform**: Handles the deployment and configuration of the CTFd scoreboard and its associated services. + This includes setting up the database, caching, and storage solutions required for the scoreboard to function effectively. This can be found in the [`platform`](./platform) directory, and as the `platform` component in the CLI tool. -4. **Challenges**: Manages the deployment and configuration of the CTF challenges. This includes setting up the necessary resources and configurations to host and manage the challenges securely and efficiently. +4. **Challenges**: Manages the deployment and configuration of the CTF challenges. + This includes setting up the necessary resources and configurations to host and manage the challenges securely and efficiently. This can be found in the [`challenges`](./challenges) directory, and as the `challenges` component in the CLI tool. Each component is designed to be modular and can be deployed independently or together, allowing for flexibility in managing the platform's infrastructure and services. From b1530d201d905418069ed0fe023aad538268e767 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 10:59:53 +0100 Subject: [PATCH 110/148] Add CLI bypass guide --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 7301cca..f731da7 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Deploying a new challenge](#deploying-a-new-challenge) - [Updating a challenge](#updating-a-challenge) - [Deplyoing a page](#deplyoing-a-page) + - [The CLI tool does not seem to support my setup](#the-cli-tool-does-not-seem-to-support-my-setup) - [Architecture](#architecture) - [Directory structure](#directory-structure) - [Getting help](#getting-help) @@ -659,6 +660,17 @@ To remove a page, you need to remove it from the configuration file, and then de Page changes are automatically and continuously deployed through ArgoCD, so no manual intervention is required after the initial deployment. +#### The CLI tool does not seem to support my setup + +The CLI tool is designed to cover a wide range of deployment scenarios, but it may be that your specific setup require some custom setup in each Terraform component. + +Each component is located in its own directory, and can be deployed manually using OpenTofu/terraform commands. + +However, be aware that the CLI tool also manages the Terraform backend configuration, and you will need to set this up manually if you choose to deploy the components manually. + +Documentation is located within each component directory, explaining the configuration options and how to deploy the component manually. +A template tfvars file is also located in each component directory in `tfvars/template.tfvars`, explaining the configuration options available for that component. + ## Architecture CTFp is composed of four main components, each responsible for different aspects of the platform's functionality: From 9b33cc65c8d2a4b180b55823f6feca2744dc817e Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 11:00:04 +0100 Subject: [PATCH 111/148] Add initial architecture diagrams --- README.md | 10 + .../architecture/challenge-deployment.drawio | 166 ++++++++++ .../architecture/challenge-deployment.svg | 1 + docs/attachments/architecture/overview.drawio | 294 ++++++++++++++++++ docs/attachments/architecture/overview.svg | 1 + 5 files changed, 472 insertions(+) create mode 100644 docs/attachments/architecture/challenge-deployment.drawio create mode 100644 docs/attachments/architecture/challenge-deployment.svg create mode 100644 docs/attachments/architecture/overview.drawio create mode 100644 docs/attachments/architecture/overview.svg diff --git a/README.md b/README.md index f731da7..5a2d05c 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [The CLI tool does not seem to support my setup](#the-cli-tool-does-not-seem-to-support-my-setup) - [Architecture](#architecture) - [Directory structure](#directory-structure) + - [Overview](#overview) + - [Challenge deployment](#challenge-deployment) - [Getting help](#getting-help) - [Contributing](#contributing) - [Background](#background) @@ -712,6 +714,14 @@ ctfp/ └── ... # Other files and directories, such as license, contributing guidelines, etc. ``` +### Overview + +![CTFp Architecture](./docs/attachments/architecture/overview.svg) + +### Challenge deployment + +![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.svg) + ## Getting help The project is built and maintained by the CTF Pilot team, which is a community-driven effort. diff --git a/docs/attachments/architecture/challenge-deployment.drawio b/docs/attachments/architecture/challenge-deployment.drawio new file mode 100644 index 0000000..43c9d4a --- /dev/null +++ b/docs/attachments/architecture/challenge-deployment.drawio @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/attachments/architecture/challenge-deployment.svg b/docs/attachments/architecture/challenge-deployment.svg new file mode 100644 index 0000000..8373da6 --- /dev/null +++ b/docs/attachments/architecture/challenge-deployment.svg @@ -0,0 +1 @@ +
ArgoCD
ArgoCD
Shared challenge
Shared challenge
Instanced challenge
Instanced challenge
Deploys
Deploys
KubeCTF
KubeCTF
Deploys
instanced challenge template
Deploys...
Deploys
Deploys
Master
Master
Container registry
Container registry
Generate
deployment files
Github actions
Generate...
Update deployment
Update dep...
Challenge updated
Challenge...
Push docker images
Github actions
Push docker images...
Pulls
Deployment templates
Pulls...
Pulls
Docker image
Pulls...
Chall dev
Chall...
Commit
Commit
Kubernetes
cluster
Kuberne...
Github
Github
Service / Deployment
Service / Deployment
Cluster
Cluster
Github
Github
Action
Action
Background
operation
Background...
Github branch
Github branch
Challenge deployment
Challenge deployment
CTFd
CTFd
CTFd manager
CTFd manager
Updates CTFd
Updates CTFd
Deploys
Chall information
Deploys...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/attachments/architecture/overview.drawio b/docs/attachments/architecture/overview.drawio new file mode 100644 index 0000000..626989f --- /dev/null +++ b/docs/attachments/architecture/overview.drawio @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/attachments/architecture/overview.svg b/docs/attachments/architecture/overview.svg new file mode 100644 index 0000000..073c178 --- /dev/null +++ b/docs/attachments/architecture/overview.svg @@ -0,0 +1 @@ +
CTFd
CTFd
Redis
Redis
Redis
Redis
DB
DB
DB
DB
CTFd
CTFd
CTFd
CTFd
Chall
Chall
Chall
Chall
ArgoCD
ArgoCD
DB cluster
DB cluster
Redis
Redis
KubeCTF
KubeCTF
Challenges
Challenges
Prometheus
Grafana
Prometheus...
Logging
Logging
Chall
Chall
Chall
Chall
Challenges
Challenges
Kubernetes
cluster
Kuberne...
Uses
Uses
Deploys
Deploys
Deploys
Instanced challenges
templates
Deploys...
Ops
Ops
Deploys
Deploys
Platform
Platform
Deploys
Deploys
Challenges
Challenges
Cluster
Cluster
Deploys
Deploys
Deploys
Deploys
Configures
Configures
CTFd
CTFd
Pulls
deployment config
Pulls...
Orders instanced deployment
Orders instanced...
Deploys
Deploys
Deploys
Deploys
Challenges
Challenges
CTFp
CTFp
Service / Deployment
Service / Deployment
Repository
Repository
Terraform project
Terraform project
Cluster
Cluster
Github
Github
Action
Action
CTFd-manager
CTFd-manager
Deploys
Challs
Deploys...
Configures
Configures
Git
Git
Text is not SVG - cannot display
\ No newline at end of file From 84b83c660f83be2d855a4a39dc47c840613766af Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 11:02:58 +0100 Subject: [PATCH 112/148] Update architecture overview --- docs/attachments/architecture/overview.drawio | 11 +++++++---- docs/attachments/architecture/overview.svg | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/attachments/architecture/overview.drawio b/docs/attachments/architecture/overview.drawio index 626989f..d65521c 100644 --- a/docs/attachments/architecture/overview.drawio +++ b/docs/attachments/architecture/overview.drawio @@ -1,11 +1,11 @@ - + - + @@ -105,7 +105,7 @@ - + @@ -120,7 +120,7 @@ - + @@ -288,6 +288,9 @@ + + + diff --git a/docs/attachments/architecture/overview.svg b/docs/attachments/architecture/overview.svg index 073c178..c488b15 100644 --- a/docs/attachments/architecture/overview.svg +++ b/docs/attachments/architecture/overview.svg @@ -1 +1 @@ -
CTFd
CTFd
Redis
Redis
Redis
Redis
DB
DB
DB
DB
CTFd
CTFd
CTFd
CTFd
Chall
Chall
Chall
Chall
ArgoCD
ArgoCD
DB cluster
DB cluster
Redis
Redis
KubeCTF
KubeCTF
Challenges
Challenges
Prometheus
Grafana
Prometheus...
Logging
Logging
Chall
Chall
Chall
Chall
Challenges
Challenges
Kubernetes
cluster
Kuberne...
Uses
Uses
Deploys
Deploys
Deploys
Instanced challenges
templates
Deploys...
Ops
Ops
Deploys
Deploys
Platform
Platform
Deploys
Deploys
Challenges
Challenges
Cluster
Cluster
Deploys
Deploys
Deploys
Deploys
Configures
Configures
CTFd
CTFd
Pulls
deployment config
Pulls...
Orders instanced deployment
Orders instanced...
Deploys
Deploys
Deploys
Deploys
Challenges
Challenges
CTFp
CTFp
Service / Deployment
Service / Deployment
Repository
Repository
Terraform project
Terraform project
Cluster
Cluster
Github
Github
Action
Action
CTFd-manager
CTFd-manager
Deploys
Challs
Deploys...
Configures
Configures
Git
Git
Text is not SVG - cannot display
\ No newline at end of file +
CTFd
CTFd
Redis
Redis
Redis
Redis
DB
DB
DB
DB
CTFd
CTFd
CTFd
CTFd
Chall
Chall
Chall
Chall
ArgoCD
ArgoCD
DB cluster
DB cluster
Redis
Redis
KubeCTF
KubeCTF
Instanced
Challenges
Instanced...
Prometheus
Grafana
Prometheus...
Logging
Logging
Chall
Chall
Chall
Chall
Shared
Challenges
Shared...
Kubernetes
cluster
Kuberne...
Uses
Uses
Deploys
Deploys
Deploys
Instanced challenges
templates
Deploys...
Ops
Ops
Deploys
Deploys
Platform
Platform
Deploys
Deploys
Challenges
Challenges
Cluster
Cluster
Deploys
Deploys
Deploys
Deploys
Configures
Configures
CTFd
CTFd
Pulls
deployment config
Pulls...
Orders instanced deployment
Orders instanced...
Deploys
Deploys
Deploys
Deploys
Challenges
Challenges
CTFp
CTFp
Service / Deployment
Service / Deployment
Repository
Repository
Terraform project
Terraform project
Cluster
Cluster
Github
Github
Action
Action
CTFd-manager
CTFd-manager
Deploys
Challs
Deploys...
Configures
Configures
Git
Git
Architecture overview
Architecture overview
Text is not SVG - cannot display
\ No newline at end of file From cf199fbea1cce78065fa71b951854f2e1c078dfe Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 11:30:48 +0100 Subject: [PATCH 113/148] Update cluster configuration documentation for improved clarity and redundancy options --- cluster/tfvars/template.tfvars | 20 +++++++++++--------- template.automated.tfvars | 20 +++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index 5594fd7..cb7edab 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -35,20 +35,22 @@ cluster_dns_ctf = "" # The domain name to use for # Cluster configuration # ------------------------ # WARNING: Changing region while the cluster is running will cause all servers in the group to be destroyed and recreated. -# For optimal performance, it is recommended to use the same region for all servers. -# Region 1 is used for scale nodes and loadbalancer. -# Possible values: fsn1, hel1, nbg1 -region_1 = "fsn1" # Region for subgroup 1 -region_2 = "fsn1" # Region for subgroup 2 -region_3 = "fsn1" # Region for subgroup 3 +# For optimal performance, it is recommended to use the same region for all servers. If you want redundancy, use different regions for each group. +# Region 1 is used for challs nodes, scale nodes and loadbalancer. +# Possible values: fsn1, hel1, nbg1, ash, hil, sin - See https://docs.hetzner.com/cloud/general/locations/ +region_1 = "nbg1" # Region for group 1, challs nodes, scale nodes and loadbalancer +region_2 = "nbg1" # Region for group 2 +region_3 = "nbg1" # Region for group 3 network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central", "us-east", "us-west", "ap-southeast". Regions must be within the network zone. # Servers -# Server definitions are split into three groups: Control Plane, Agents, and Scale. Control plane and agents has three groups each, and scale has one group. +# Server definitions are split into four groups: Control Plane, Agents, Challs and Scale. Control plane and agents has three groups each, while challs and scale is one group each. # Each group can be scaled and defined independently, to allow for smooth transitions between different server types and sizes. # Control planes are the servers that run the Kubernetes control plane, and are responsible for managing the cluster. # Agents are the servers that run the workloads, and scale is used to scale the cluster up or down dynamically. -# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically. +# Challs are the servers that run the CTF challenges. +# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically if there is not enough ressources in the cluster. +# Challs and scale nodes are placed in region_1, and are tainted to make normal ressources prefer agent nodes, but allow scheduling on challs and scale nodes if needed. # Server types. See https://www.hetzner.com/cloud # Control plane nodes - Nodes that run the Kubernetes control plane components. @@ -59,7 +61,7 @@ control_plane_type_3 = "cx23" # Control plane group 3 agent_type_1 = "cx33" # Agent group 1 agent_type_2 = "cx33" # Agent group 2 agent_type_3 = "cx33" # Agent group 3 -# Challenge nodes - Nodes dedicated to running CTF challenges. These nodes are tainted to only run challenge workloads. +# Challenge nodes - Nodes dedicated to running CTF challenges. challs_type = "cx33" # CTF challenge nodes # Scale nodes - Nodes that are automatically scaled by the cluster autoscaler. These nodes are used to scale the cluster up or down dynamically. scale_type = "cx33" # Scale group diff --git a/template.automated.tfvars b/template.automated.tfvars index d2bb212..ce3ce5c 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -14,20 +14,22 @@ terraform_backend_s3_secret_key = "" # Secret key for the S3 backend # Cluster configuration # ------------------------ # WARNING: Changing region while the cluster is running will cause all servers in the group to be destroyed and recreated. -# For optimal performance, it is recommended to use the same region for all servers. -# Region 1 is used for scale nodes and loadbalancer. -# Possible values: fsn1, hel1, nbg1 -region_1 = "nbg1" # Region for subgroup 1 -region_2 = "nbg1" # Region for subgroup 2 -region_3 = "nbg1" # Region for subgroup 3 +# For optimal performance, it is recommended to use the same region for all servers. If you want redundancy, use different regions for each group. +# Region 1 is used for challs nodes, scale nodes and loadbalancer. +# Possible values: fsn1, hel1, nbg1, ash, hil, sin - See https://docs.hetzner.com/cloud/general/locations/ +region_1 = "nbg1" # Region for group 1, challs nodes, scale nodes and loadbalancer +region_2 = "nbg1" # Region for group 2 +region_3 = "nbg1" # Region for group 3 network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central", "us-east", "us-west", "ap-southeast". Regions must be within the network zone. # Servers -# Server definitions are split into three groups: Control Plane, Agents, and Scale. Control plane and agents has three groups each, and scale has one group. +# Server definitions are split into four groups: Control Plane, Agents, Challs and Scale. Control plane and agents has three groups each, while challs and scale is one group each. # Each group can be scaled and defined independently, to allow for smooth transitions between different server types and sizes. # Control planes are the servers that run the Kubernetes control plane, and are responsible for managing the cluster. # Agents are the servers that run the workloads, and scale is used to scale the cluster up or down dynamically. -# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically. +# Challs are the servers that run the CTF challenges. +# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically if there is not enough ressources in the cluster. +# Challs and scale nodes are placed in region_1, and are tainted to make normal ressources prefer agent nodes, but allow scheduling on challs and scale nodes if needed. # Server types. See https://www.hetzner.com/cloud # Control plane nodes - Nodes that run the Kubernetes control plane components. @@ -38,7 +40,7 @@ control_plane_type_3 = "cx23" # Control plane group 3 agent_type_1 = "cx33" # Agent group 1 agent_type_2 = "cx33" # Agent group 2 agent_type_3 = "cx33" # Agent group 3 -# Challenge nodes - Nodes dedicated to running CTF challenges. These nodes are tainted to only run challenge workloads. +# Challenge nodes - Nodes dedicated to running CTF challenges. challs_type = "cx33" # CTF challenge nodes # Scale nodes - Nodes that are automatically scaled by the cluster autoscaler. These nodes are used to scale the cluster up or down dynamically. scale_type = "cx33" # Scale group From aedee8c0ec9120537c26c16c9e58326d48a87fc9 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 11:43:58 +0100 Subject: [PATCH 114/148] Add networking diagrams --- README.md | 13 ++ .../challenge-network-architecture.drawio | 104 +++++++++++ .../challenge-network-architecture.svg | 1 + .../cluster-network-architecture.drawio | 163 ++++++++++++++++++ .../cluster-network-architecture.svg | 1 + 5 files changed, 282 insertions(+) create mode 100644 docs/attachments/architecture/challenge-network-architecture.drawio create mode 100644 docs/attachments/architecture/challenge-network-architecture.svg create mode 100644 docs/attachments/architecture/cluster-network-architecture.drawio create mode 100644 docs/attachments/architecture/cluster-network-architecture.svg diff --git a/README.md b/README.md index 5a2d05c..24e2b58 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Directory structure](#directory-structure) - [Overview](#overview) - [Challenge deployment](#challenge-deployment) + - [Network](#network) + - [Cluster networking](#cluster-networking) + - [Challenge networking](#challenge-networking) - [Getting help](#getting-help) - [Contributing](#contributing) - [Background](#background) @@ -722,6 +725,16 @@ ctfp/ ![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.svg) +### Network + +### Cluster networking + +![CTFp Cluster Networking Overview](./docs/attachments/architecture/cluster-network-architecture.svg) + +#### Challenge networking + +![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.svg) + ## Getting help The project is built and maintained by the CTF Pilot team, which is a community-driven effort. diff --git a/docs/attachments/architecture/challenge-network-architecture.drawio b/docs/attachments/architecture/challenge-network-architecture.drawio new file mode 100644 index 0000000..98c2242 --- /dev/null +++ b/docs/attachments/architecture/challenge-network-architecture.drawio @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/attachments/architecture/challenge-network-architecture.svg b/docs/attachments/architecture/challenge-network-architecture.svg new file mode 100644 index 0000000..b4369f2 --- /dev/null +++ b/docs/attachments/architecture/challenge-network-architecture.svg @@ -0,0 +1 @@ +
User
User
Kubernetes
cluster
Kuberne...
Service / Deployment
Service / Deployment
Cluster
Cluster
Hetzner Cloud
Hetzner Cloud
Request
Request
Challenge Network architecture
Challenge Network architecture
Hetzner
Load balancer
Load balancer
Branching
Branching
Challenge
Challenge
Yes
Yes
No
No
TCP?
TCP?
Yes
Yes
No
No
Available?
Available?
Fallback
Fallback
Traefik
Traefik
Traefik
Traefik
Traefik
Traefik
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/attachments/architecture/cluster-network-architecture.drawio b/docs/attachments/architecture/cluster-network-architecture.drawio new file mode 100644 index 0000000..6b721b4 --- /dev/null +++ b/docs/attachments/architecture/cluster-network-architecture.drawio @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/attachments/architecture/cluster-network-architecture.svg b/docs/attachments/architecture/cluster-network-architecture.svg new file mode 100644 index 0000000..1393aed --- /dev/null +++ b/docs/attachments/architecture/cluster-network-architecture.svg @@ -0,0 +1 @@ +
User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Hetzner
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
Text is not SVG - cannot display
\ No newline at end of file From 0306576e036be6916efe5d02a0819064a7ed712c Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 11:45:49 +0100 Subject: [PATCH 115/148] Change svg diagrams to png --- README.md | 8 ++++---- .../architecture/challenge-deployment.png | Bin 0 -> 105644 bytes .../challenge-network-architecture.png | Bin 0 -> 65472 bytes .../cluster-network-architecture.png | Bin 0 -> 83443 bytes docs/attachments/architecture/overview.png | Bin 0 -> 154619 bytes 5 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 docs/attachments/architecture/challenge-deployment.png create mode 100644 docs/attachments/architecture/challenge-network-architecture.png create mode 100644 docs/attachments/architecture/cluster-network-architecture.png create mode 100644 docs/attachments/architecture/overview.png diff --git a/README.md b/README.md index 24e2b58..36c649f 100644 --- a/README.md +++ b/README.md @@ -719,21 +719,21 @@ ctfp/ ### Overview -![CTFp Architecture](./docs/attachments/architecture/overview.svg) +![CTFp Architecture](./docs/attachments/architecture/overview.png) ### Challenge deployment -![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.svg) +![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.png) ### Network ### Cluster networking -![CTFp Cluster Networking Overview](./docs/attachments/architecture/cluster-network-architecture.svg) +![CTFp Cluster Networking Overview](./docs/attachments/architecture/cluster-network-architecture.png) #### Challenge networking -![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.svg) +![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.png) ## Getting help diff --git a/docs/attachments/architecture/challenge-deployment.png b/docs/attachments/architecture/challenge-deployment.png new file mode 100644 index 0000000000000000000000000000000000000000..f5caef675dec124b99f182e4a1091206712c9809 GIT binary patch literal 105644 zcmeEv2S8I>wzeQDSg2kE8wes;&=jhL-lBveMFABk1Vt41*E&fw>b>*sotbyC z8LLr9_Ab++`@b?FqL4ko@O!jvtGD1UxBELUvQIJ1>(ZpIP zq+nPV?;`$$c))?l@E6Fzn-h=D@$qDP^Ul?y@K=Gyt-O3#-h!FQI{LgT0vLjm`2&Y} zGdzPLh35KMkpgvw1_i-*LRRDbApHN)xTaqIsO=y0s7A5)-%ORM-#M2`IOmNJb9pDe&$neiF zc4&~FmuZlHP$(P;3<`wPOa!L+CNqB-5niEeFFpo;1GB=q6sR3g>=hK?#SZ0w=={qy zDO5e)JbVOelF3xwfyi%o&=;KKErbOduj|8B3QB0KToX1S}$CDjl*m z&wRqBqUiH&`q!9>PZ@uTsTOJDf6;_NfovfYQi=ckstI-QKYx}9DI~r&1pY?l6OoV& zb+v!RhEyHCqy8l}gsBLkTX0Gx%i8W`wf5_~g!qJIV+n!8!jdO@?~;GuS>6 z!lO_~e~AV7*8gWM@Xu+F9Jko;c=UH&qKvPlPTMku82 z6cW>$;qUJi2togE4`V9%`&|A%1jqj>ZGX)JC+mLA{eDmE^b+trFXZwWA4@g-;fVS4)O_Phv71Q|0v&D0(N*cjD{%-j6waO#3bwS z-R*xa=Y^&G&GEH#_zLIK_y6|zR0@B-f0z8E@7?yF9G_|m^6(7#dyGb$F4VsH$Jg-B zCfaBJ)0sbY+4$-bVpm#V7@HBu{7)xXExzIY)_DRyml4vwnOCrX5QlK@@A5Ke079)R zTx0)g4^g<0#cwMz{C!w~;O$JPW4xf0owft{P_MI&(sqG?otzzqeuXn~D`~eDE`2 z;n>A@-~Xmkg}+K6pO_Wy;brP*!PAkDhQG(=|IqLKO%4AWq5DIv@*hD#{0vV>-Crq4 z#~&%jBUFu|!@__0{CZ>{vAqI

6ha7XE*Sw7!<0f;@*xfy|YtN`wQD4nMmR<}Gdh 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 hQD=wqffZfvw+?WHYeF0LSjTK9XKF=Xv^eBE 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

W+*Z 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 aG*rd-`ZBgOA1?R}mAa~yO73CvTmJ(TqI%Z= literal 0 HcmV?d00001 diff --git a/docs/attachments/architecture/cluster-network-architecture.png b/docs/attachments/architecture/cluster-network-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..63fe1eae3b2008ae05bbd28590d39f39b55bbab2 GIT binary patch literal 83443 zcmeEv2UwHYwl*M+U_~7}Dvlx|(vpB67LXbsp*Inb8VCu&5L%FCR}>WmM2e^srKog8 z0UawK7J9LbiU@*?N)ZtF*Zz`dEOTbgx%W(Y?(dW&U)g)@wchovvcJSV#$??|fY08@j2zZiM+SV+3fSU)+RX`H2$$2M<*OpY!ki;pV zfAN|w9*z{IBON|?xJcr)Byl)nuK=>5P9WRW)X3(bWNn0c_d z^C|L<`g*vy`fzXNT*zWDXeFkn1Bt8q^7{~sAw)0Kv_{goaiaXSD0y`QwfboJscg)8=dG@KF`6xr0z zk><-)B3 zow>xL1fTvKOM}?RGCW$ub3XFfc_lI%x`f}(N(x+d`Ah7q%q2ZOCw6rKw&eOWgUNDd zP#9j0H0{qv`JMPTHS5FhWnzvGSKwjIXxS`BCdHLYMao>rquIdy;e!9v)0nO_N0x^l z|0pJ$)=es}ySRS2VF}_Ly7``l;hZkf4yi^a7I%jHc$Ny?zR$R#?Xs#2k zuJYx7$qcY)JQ+;Eme=7(3Dtsy^p@04?3*Zp6QC#3!0JoCsU0;}h zYZm^Hi6i`#A(Ozx4WGZ5xibAcAlh+!6{n!(>P=$=(ox=xEs)QP{yFARRQ)n@#i?@9 z$7dczUS#1n&tK~z+|a;h9b=}4pCb!d$IF%F&tQ7~3#0sjO!h0E!DD~GXZVv^l!_{$ zf8hAH$*VH@182U-i8(_3mq|~Fs~w)leb=Pil<@CkQm#OJCe?FgvAs}?QqZI^d|m#B z2K~)yL`y?kMF+B=->XKPRa~9i{t|Uiku5Al!r<;dG&(1@$k|i~o66$48Ay-$@>L$NnoN z3q9cLGJ~Kh_hn5=;58q3*$byi+{VP%rGe zy_GK$i7!IQZ@I>w5@L8sGdIK#l(@MXpKI_E1pWxayN!p;KQ*sb;<^c+xix9NKFBIK z1u`#bPyjkXElp>ZFB7r}=Eg}zyALIa_*d*NzNH6 zbDWL?^wzN!9W=FcSnjTL*pb2MaIb-P*f{8rn!_@e%yA_IGwV-*K zDW5r&zE|6S3g>@`JrxzcC1eid_yX}~-}yHU`sGfP4)%vr`*Gf*ohbe^?#t^HpaRhb zu0^$?f`XzVPVu`tS8z&ey(WzGMaK5$0|3X{bKtTmkEHkjKv3qUMg0Cu;BDsoB>+(2 zh5$Y)MZLFw$)JA#1%C#ELcsj8pY|!;;ZynFub-yEP2c$(43){e+>x8E@YUtNsZ0JH zLEb(J|Bj#v*SG)YRrnt+uTjAIukNkz*2#xgNt`kb6^t$(e(;up-trEiB>jt`LV2?< zdTM07Gv^@*^uP}v=`bly%U}UOIItDYd6DM4qBA&D)Sz?xZ=?|?!F1elc>=)p3p|X0 z9z5}egam3Ej)*Ax!$kU0OgQ{^68@W9>NkY{Dq;T|CQ#b^H%zgJMV;CIo8QQ-!(pA6H6?J!{(f)x7$1DC5s6~~VYx5zT^Vsq~ z4&VeFmtg+h0RJ|5`KRdntH?>Amy+}TJI^$HJwA@tEdBS!$DUvKTRuOUdXQ{;X5@IPf{*8mTeH9WzNdRFMY4ZK%?O;OG#E%eO})JH_8v`{+(efUGI zaNZ+X|DVmd-;Vi)ZF#xvzsI)!huNIF4bR8s{}^)lip{_Km;nzD{M(X$X=h3i?Z150 zgZf|CB;#cn{LH{(i$5{VLyq)c-6T^~;6}s$l$-w$&wf{%+)VmU)aKtNX;$QYp_|)$ z{|~<;_?tBQ_4xl`QvUZ%niUnm<^P4(|8B7dy=!J5$|8$RPo5kX;yDsr|F^6W6&1K-$(L4qdPw>!H~74_{s*>r`5#&4 zd%6K0qxw@H!TQZ0^1u0O5MS*yfBz96u9yC8(i_6J`J{oblPeRxO@|&D<~(lU#N<6p z;>>$;{wq-Uc)GHj(T}Yly5oCb==+a4V~97caFdLT7?h%1c`?i#T@)W#4|tz>4`idq`FLN8{rVyAf8%*<7x>{4cpRJacs5SQ$B!b3 zTOR;FhJw>H)+6IK1gQ6qyKPS45uTvnA;)0`j zy693=iBxBQk_Ul6*0N!f%t;Dlwv8g0N>n6ztY>VnqIo&$nZPwR1dth(BQ5PfxCZBFWr;VTs}hXOhyf%`fp+#c%_icHGD~08vaJ+ zBsLtwlgup?VV*#u)^E(M^&9h133MXf-QCFFQ^gr?s^p|=L2=Qe`EM|X-^S6kB)Dj* zQl0T64`UA-s;jQ{N=-8*wu{#Kl^f_ZA15samBc3EY;YEeMtB9@IKIw!lK@xT25%=_ ze`O+W1APOPrK;~iCfJz!=o=%w>d|~QnDfo6tE1qkxt{8ztHVZif&2YI51z(m`eYK@ zf?!DXB#;9&FkO)8Bo^6&5D0qpC9$>HhUVIVhE$S2$xI`_kV;V`Ybpeg0|`nbEgK41 zlb}dq>yV7hZG6dW5}v4~%?6#b$y)kU&V7^sxDRJY)ebPyR0sr{1(3{bK)24mWGY3; z(444fNF{=uG|+ttB(}Scp}CDe_gEm<)R#oHgt3UenC-xRaL%6`sNhRBCn^xN(ES>L zWKA>&;6PJ@O*X@1atCaH{1z16D@jwspQxol0U2;)BtswpXQbus1M(<=e1T*$4OlbS zoJt9>VN+l}8v?8c)(poz(0>GkHID3BCKM7I)@)7+fNSuER1>z5u74nztqu2UD;b&* z{K-^(D#_dgeFm8|;5s5)2RIE>K(Z>4JwR3uB&#+H(}$J@&PhufXN28LV!?f&2T}kD zM*{m$z;_ftmw;6&1!Mrdn0w+m`ZK}7x?v4ShYD=CAJ#$R>XC#en>fJ{Mn#;u$KuN)~aAgwNN4B~uJK%r^#c zjgXI1;a)1TH5Y4IHY{Wpur16(3@|cNKw~32A#4(m4Z$`DC!j+@0Pp|>y}~>cgmJJB z1>_1Kv7G@^Fb>xz;9MYy<4b`s9>N>wAI1UM$y6KUBU~AYAfpAc8GP1OLi2(jfo%}h zH9*%Ea2(c()`V~zNC0jk`+!{FUnUg5P5|H+(+Qc4bmANcc0|631AL%)f%A|(0jFBX zHvx;lmta@8))UsG0l2aOTr1##199O0fuJWClf*JIhq=uW*I8ipMm`VM27tZc6}Zwv z0gjs}AiN>p0o`%^7=3oe)&OgQYe|ZLTXYWD1h5BOgXw{Shjjp6Oc2*7lC>-lZWKU1 zup#K*9Q@fF;ok&!Ndas_0RDxw0B;~3fHi<`Yrs4tbghCRk{37(xC73c1Q?ooVmjuG z!LhB8mS-T$i3gpsFq@c@SYRsx3FZdd0sa-hPc4*St*{0S%jA0Ho19_0|lVSaUaj+lQ0P}NCG$-K56F7hX@eQ+k0O%886g~rA zfuCYH20tgUDTuQJKnK`yzzxEc5^y&<4!nqPins;Y3+B@DgmnOJ*(QJ=(6N>VU_u+@ z_f$fD3F}AP9f;_Sp4r$sfOAP~PlQ>(CdlLg909Tb-Z0Kba~o-K zWaD5O<8>wQBR0qfHZx&ie9P7VIVcEw{(w2a9fl#08F7^-;$VcQ0N^n0`gKqo0y}7< zSOM#yV3+|c!1{paAa6jJL7Y#-;)My=3t|(tez=|uSOIJqnIk;_=BNl;$Zrv*z_y5= z1CjlKTiBiu&%pNPCV(St9G9Osx<@=g1)QN*EVdF624T&ZY;Ye5?n8L+^v7_dWrA#u z)`(;b%u9Va4v z5IENWreGe#tr#v4hP6C_hlw~CAFcxqgL?q;U=QSP*tLi!5cfi!hWRWT;vKd&jt`Ph zj6nR0=@`2XcpI>R1AHUjhWrrW8s!|o0m$Bnvmwrb?m%Y13i5xdHj+b;V?1?k7@&`L%aTQx5S(k)jAL361m|Fq)9QYsj3c?(+9TARe!1_774LJtb z3iM3($1s3$5x^{)%!WLN>IwM)n#TnB6KDQFbUg*-Comrg`3%SkTnPCu=2L-iFUXGk z1Z)871Y9G3z+yZRVjo}!&O!X*VGj8^=#t1n@*-b>dm%@rpfNCAlQ5sbVj$$ap2(NL zUVs-YN5K3V563b7M6n6RR|0=Vy2eAyK{EQ2VSIES%!BD3@ipdSoLC1O>P!IcK$yXy zTnN^TI3)md0UTliw$N6>xCM(z&Ma)**m&Hyjow3^0X9N<;`#(Bz!`Ew$lDbV#z>$q z5{5xifDMYDoHYPH!f{R<<=6n_b0`)d9U(o!8bH7P!1pLF;mMph4m?Cea=>+bvN5ppj$hw(GQ5$G6rg$_Bm z7Q{t_eRIgYA-BNtVJt7^@GbZr@&_DX6YPQI#DG1UK(ZAHbZh}Wz+F4|Ek}MLL zJsZ#wI*#QK2q#Dm@M%~h*cgj>NFJ2aqxiy!eaMywmuUTP4@C)HVJz@N#A6VD0G}WS z@GqMJdLRNGF;0hTh&)`PoDj{2X zfCJzfl>dTnq1=&*<;@UhA@AY%8pIbA15lm}oMDdakL8JAcP!69yp8-HcpJ;rAwK}W zqoP=Z#zr|6Y=`177XN@dKp$EtU(^O40FI>+5r2Z* z+5{MzfH(yCHPSuU0CFs3UyL81*4F0u68em|415t}2CQOyg2hjyE8r*Yn!qP9k|~UbfEz;49;Wxi*ho+JH!?6eIz5&7wF!Z zt0SxqfH{#aP~J(w1ID37M!JGL0sI51bA7S|!#B8u3+z&AuYB3}S z!W3EqC!Yj9gnR*N6|Rj@?t$f<1e8+%mqVTda)JF&PKkV9J$>;ZO$nkf+DV~$-QX9E1eajY)j;sN6( zuqEUmSk6I2^%WWu#YeP8#7hXL$dAxHC}Ufq6nq`U2;gEg7xcxjctwF+0$~zyF{;x5_ux-P<|qas z-8zF#QQWcsyrZ5F_W6 zINI8q1;=3h!0o87!L^AKR_9_pHauL1)#IFNP`uedg?S)80GA`aM{y2u1j@@XUk6#C zo7OZAM$$`7y3#Z&cOUz3FY6AW3j=n zVGhK#+JHTD4y%EXy;1JY;dB&Z*pLGO9kz z5Xg9MP7MmNny9rN`m%s`sxvnRdBXj`sZfKV7zgrzP9P748XDwyLoplW%b*L81-Ksc0yQ^q3c?`POF;Q1 z@+XvcgU$iR;EU)!kQsOr)x8!de@1-=@HyaSP7F|Rr32Rk7ht$YHGviCom!xJ5pfLg z2a5m5?>$k@2OI-y2mE1i7uA?p9)qxfa(QfR$nT-9M)@biVbrI_;t!S^pd1D6;ly`X z56A?1#cCwv_gsBLTm*Z;IIupsIiI85wzme~7d|Cm`$vKbB!FGfk3h4*(SFDCb zwIKKc=$ji?(P!juDE~1>aSf{pknCt4EN;N{fXh!kF%n z17<)!h`&)U2kj-GF*OipK~D|k9LVk<2kLubF&yQt5XZ25nX}gbaSM0>aWu;9c)W{x z!*C4saS)c-sAmh<$MP%;52*i-;sW%Fp|6Je;HV}>yr`w4?1XGih5jYh=K^K+781u_>41*VM8#|1%HCM z!IzM&kpCz^EQJ41ZVd4T!#nD2B7LE{9I%XH3{~F;vmcTj*$T;q?b(1W=*UM*6p`M@ zP!Gd8QU3vOkMeSq8(?@udlAY`SPv7c_t}6QB@~lkJd{f#KS92XxD#?Tip^dQ0S zKwij$QI5drD`Dpme{ticIT2w9^`!uNCa9kVG9V6tG10yk^o^kg;j9zITB@ZFa5G0g z7)ODxSb;A=jltngPsEi7S&Xgn!%pPcekz zr=JevJrIXi;{E8=-~3BQ+;6-5r;ksfXV(8akG6B4WZ{2+^%HphFFtem^`q_o;X`7) zN5#JR@zt-#|EGRY347@CH~z*X?A#ZBITimSwb)tSUx&px!My=~xeIDlzmu=0xa7uov@Rn&O9RsNQboLBxByLTXziD?; ze(Tby3VR;RcK)$^LBr)&23u~1jn3YGQ{%yfu=R!;rwW^ESqhre5A>hgIhW05yRpxl zEwvamzc$4 zUQD!nDD!a4xBs+W9CveEB&|aJmxaRmh624}VFJPu0)isy0z&BRdWo>b^Mtg_)sv41 z-=Fm9FuH=UG;DFp(Q#H{Vd^-M`xAI~NC<=rPrq`A^y|}VC3I|Hs`^}PD0I*DZ32(d}wx?wSxh?~^lRF{Mb zS5|av$svK@tpdVPdIGx2drs*e$FAcn1&!3P{>o+{mE6cFVlOqgN0A@`LT@F)q907X za%g{~lI)jfV0q3$W9@#qJ#+hh{cnVmgV9=lGP?B52{90D>vi*MAH^i)YFPFM(r-1K#sj$VbKL|K3 z;J*o^3=rFWS6{s8m37UuFV6^yY*&{siKL0FHWvI^wnz}bcT3n$3I~?L5C=uF9LgR? zt{rsud{P>Gigj1D&4sQp5S4xQY~d`2EsB|j(m2=jX8U56hji`AOfz~$nf}Vh0nXD^ z6~!h*8x-zQUofX5o^|?Mpm~HL;ZCWJTTkE4cLjFNIo+j0gWaMf>u|SToY=J>m)3u+ z@1;I-nQpvfyNEtgj5$*I_yt>v?Gc`m6$OK*cGqXeuNio8c>e@btFqxP25Is!@vvG~ zo6+)}C4J46Zg&%GEY(U!hr46%1jZh&KIgE`_Hw{xn<2gD+0*Y|+i6BtP4&GhCRy>o zZTHJl<1u@E*0-2o_;%$*x7ux=3|dB%w48U`mgv{)k=Ar(=2!#k({blz&Rwo{F%XR} zCl=5ybQQKRqpEcEw#Z-V%-xb9Ve2GR!tTv2dd#j=rZ>ABSkrgZx1+Y4VbVQs&5O74 zKe45Coqt6x_-)@aPPF6oQ5!kc_NYnAV@dj}tW1N;UqlgPGUqr|{n+-C{3GgOy|}_XiGwMn zadBmLY%5C&SgJXq6;h;QweIpo6+$+N)+1>{7Pi%rGutPh`BB=am}T!?J)AZ~?yUB8$!E9k=g9Ed7@91+~;Qw&*q^^>| zj?CqK{W9s}N{+1wDlB<>b6!YXf}QHJVv&TYs`?uC_bKgGYEcucU$4e{6xutVIzqR! zkBoOQH@ee(u`Ow4qIJ2)WtRg9PE~yXlhL{&+QypJ0)Hi+kFL6Das{cIjW5MXZ#rbq zqG6Z4(YWO4hR)iw#@mW@NJq3A)~->9wxr3f8)~{YSKOyu&vJFGQMruK7F((r^G3?K z6uPwW!rG+fX!3m5!-FSAmYpjIvd1mKeAPV;VsPb_WjlcO@2?OH{zcB;<>3|S!-vA1 zlba+|8H4W_}ko;Wi2_^uet6>LEdvz%%#B9}f9Uw;51a+6R9Ur%%)tq|M zG?I8wO4o>-CwyaY*Se1m>KIGRy!s)tjCt8S?d`hJkMBu=5vEhtY+akVLa+U~{J_-3 zIv=#3EiWC>PLe-%qQS5vWB5FtJPtZ*2yY6{ir8MyZ`cnm}O;A(}Q<`viLI*^*GU2mF!Jh6RvKO zPwMMxHwZDVuT~76lPxaNB{Qi2Ls;%mz5=}6LS(asb15#n9#bZUir4cx|#Tqc6Rl>nuT|F`+Cc# z^^=~T+FZRxF5RXVPk%5mYr!k`P7C{tBdVpj27PDN?9c3@j|;v(ja3qB9`o49?y$tc z#e&zti@G477j1irG7Jd^p~0sq8$rWOCb=$@cNe!K$*w(OH7v--G&uUw{Nlo~frhviW&q-YPZIKkURmZ9- z>(BSitZ2HgXd9Rlcuc8gT1`)Ic=+8d;*0AZm>PDq9xlC;;a8vv zt8Jq;Di(UAncC9Xwe1<1XWzx%%$Q$A z_DnNrla*8*nd?w~TchDa!~qp)c~N5bnL9(q4YF0!F4Zy~X>N#-jmIB$+h>OyDGJ3&b+EpkHYh+ zHm#lull!iBOvn=#Xq8TV{lM3)+SF85+9ISDZ+EP&eytub&km3H=J9U#TLq{yttTfZ zSXWK<+;dvIeGcC3L)z{=%?&$TmSnzX6&72DEmOU+YD&ycuU9vu3^`XEk)G*&(5^Uk zLx#NWO7H8f7djS&-7HI8RTX{aKu2Z;{+7cOhm6cjQh!dxDLwXivHY6ml6m{$U-XXL zAJS^m-#|^gH!Y~nq@uRw*rbfZN!8tW*`Rvs)L5U%SYGyvn0MWwN$LLX;m9wXga&__ zFRW#1##AQ^%}wc9vzZx?Cp*TnM56c2qn0V+DW0m!uf}eYT^ap8^`k_)LD2bpLQRw2 z=)1%>vxR+Y4h%k<-?=b)`ivDOqy~rGng<{27_^O_DM?uoZ`fGa?zo<-)hYZh3Cu*L}%dJj)v&HR_(UKZd(}gvO zuX^4#+$l&1dUT69(YAWJrOn{AMI{bGQXB4U>$lhxaZ}bh>1_Y5L3y>F(y5w{?rm^7 zS6so6BOX}ba(d$w$sX_ZZ5ior>jT$HU$}Rj+`sG1^ty&^D|#2bAI`vWa@QBSkV~(# zk2G+GEZ2ClP*s;%RlfG*bEOKDm+21aYKB*1y@TIQN{C?Ds7ka?^=YN)SxO&_-FCY5 zQijyPCLP@U`6{QQ4rRUQxnZ}edfV&UHttn^b+Sv!&n7Qy>S%Me9qc?QKCf5fZpc%; z6g48%V0CTj-7#&NZA^Wo37I~U<*%eR_b9Ayh^lf~@^Z)Bz540RA}a1%Qj7+gqHg}= zeOXe5Lh4ItD5oeJ_n&v$S!Y@JwxQ(2X5&C>>ERRmJ7b2K#&s)8tZF00uRblE>C@^l zQOMR;_L9r-yJOZoTO?O;IBPKdeN6Mj05?xF-}gkSQC9}pdtpYXQ|9X4)T2S#tFWA9 zc_PG=)F*;HPbU5#CK4XjcR;ylor!~c!9yztFI!LjyLYxtS=)Ht!So$7BY)I{%}Pw_ zakZ}0-zNXA-8JgozUGYKUYqmBcEGhP(broBj6cd*W+H?Q5o#*;W`zRrvsxV-XC8>g~;w>Zq= z==(TcFJN)}Liv3u)QM`;$NF1ZDZ;%LO&j8nzchoq+;G0TYy8 zPO{yyG)jDFt91F&rnD`pI!>t{?;P>aO$#Z{Jj>FvDty;d=yIS#nAngZWxHh73yRtg zxlvY8bQtn#LC2Zn#g#!OSkThgqH?)g&F)vt+m(x5~=C*sz$Km>x@1wmWw=RWsPwVM%6bxn(P5X}Q7D zHPZ5Ck)Bwlar-!+V&u+gRLdnB3)NyZDyOa!35Qc6VkVq2FLg4W48|oRAVW#*c8YD-_DlG+75igIeB*46*+X#A*dVo zi>ppr|3$qIRlOpFxrh_GcAqB30^Ta2d}00w$afc1x>1c%;!R5;m!lp7PY5V@Bo1y8 znyQj3nx^|nG;|ajkO`6QAR+AkWXTzP(RIu#>;qhXTq77CPW;IAziO zxq=mZ)wqP13=EeuS8g9#Y%BLJe*)6>7u^LhA(%)^;-nC#6TshH zfbvzm>E`MSy!CwUE-XQ2*g7uu?5B1Jue%_z05tbpk!&scc|F|j!cS1~&r@a2{cHw~ zfZ zOMoeRHE8FT94)g8!uNmp>J(Sm+toLUZd%E2ywA!O2(K40<^(uQPJU%eJe@4K0!2H{ zl>EvT5Q>IIh$a^)UnyH4+;o~ccfPNcEg)1bp)QX`{8rh5A`n?TAOri3vIPXKL^sV6 z{&$rvAvQm}OZJ!FP&Od0Z1eihtvX)63Vg2c&KTo4RW$}*+2I%HmrXh#c}zk~PB8xy zN-(wCfKA#YT$UmQ?egZU#ry4AB#caVBPVJ1fheCR$B0giw1c8wfwJ)h96E&>y2hV35zS!y1- z!*9Z=dGDWcnpa#!VCV*F1ZG^Yv3O>vQ0C*1mXVKL4BM`L#(96Yk5w;62dDHk4vf;D zuQ_KjcXCMajagIaA(@ zoxg4MeBb#??8Zsou2H;QW742e?R#+R>OXdPvwcU<%d(IUGC}XH%8KGAtJc2xSS{W6 zkl|It9R1~XOpvhqLLnQGEgw2;mD_@f8V{^hx^G|FMUEfpitpNE($?~%tWVZbcd|bCl6MZ(UhK{m1u{;vH<`Tj$X|^1NLPDra?x z-Hy95@4)H~wX^s3#k(J<-*Ur#;4VS!)v2WVp=*=t9~HmI8mIBuEYQFsC6^TB-7Sn5 zEy=$&6i>UlCN#HA?x@SCmCxQso1YQ=s@?>pxKTr$O@<|}9ooNlW|w-hmT*W9V^?Eh zdIFU`T5#ua@q59c7u%vGsiFm+{0mr47pLE+Wm}4ceC(0wyVJO* zuXCWG@%iD>*h^c5mTY#J5T+Bocd3|un^nnh;hLTTgRZ7?p|_sc-uH@_wb%X#mz^-6 zB_{|MZuz*UWFTUwGumPFfL)%gQBD72)5dL!ZO3Q4+O9r9IDb}Ob4h<;QFqX7%aRZ0 zW-d8wuI0ezuFLnR_XgQ!R}L!sbxTv43Q6&r-K!#Luk=hr!e5Hgntsh$GyG^_lVD+N zS8*x*{njJu>cX669Ts+O3bRA1%Zo4PSKANvRTTz4(ml~9tgCFEuxy+9d*Q;6k9&rl zE9muo?M1`88C3?RLV_YcYKDCZh8MJ-M$ZeShmOANr{9QwuZf?it4uhuY%AQGzw+ro z_s82k&ec^0CUBd2GELZhHpgd;O@$aE&+LcF%ce}A-&+w$Bkr2eDt21x*ZAk;^PwXh zp&jx87%PQu7fYUpS@57tcwhb9zQ)?75N6{q7cD(ZChHbzul{j;a)8j31pgEC07_if zWk1ii75BMZwp&fG6Ph2(W3HT?*W7f)e%FTCf<~KNWcrj(J~x?qnq1=8Z}UVc&T5MvT7GOHHWJzFsH;D zVdb+Mohy^JoJjWim2^phX1)~`ur}ajYRuSY8~1j7;dpzl+;R9=n=dQx;0sO9X_^a! zvc-(^pYNB!&S z`W}C5c$d9%HS_?F2&nRAS(0$&?!vkznJMIZmts|N0~enX65g=%JOEmDg7J6+zZ79! zhO~Q1_=5TylV^X7yFBUUp{jXvW=Pz}g-6aWwx+bCx-P=@O4P5&3R#bV`vins7bU;C z(e=9d#UtfN+T+`&1;S%QC$4@ryJlXlt2F0g*oKo*pMs>{uA%-Q;(gt=#qsirFBaV& zA1=Evw0f`b4W68^2U84*nv2+l{&Cas%nfDN5785+P7}Kwapht2uJ=*8To*x#Kv{-V z+?7G4%y(}Ve46?)a1~C1H!m9ZTc4muY6ND+$HgP)4GCwvs-qZtk;gZ*0a>^v71Hf>*QnaWEU3o79@6>N!!eMS4z*- z%sS;DH?WIdP;L|by!dD37J0hErTW3z3uBB_#u>WgXck|K-BW&BDr$-ilR=)ZU zF$wDC9TN$USn=xO;x<)L)R%a@WAnR?GS`mDm7z!2Q76#%m3T0jO4o{ScFcdIuA^I( zOD*hMmRZF3(9qYX|1EIQ*DTOux7) zN6jX>yXS`?*M=0g;8d%@)Lp^=`bFw9IXXxd5)mu7AoN3=T(Qy9`$yGs>+0*}462Ti z^HP;>J|>KHsV;a|&|_5@9_rQ|zg4+?BTi#{g01IEBQj%(C#9{)HZ8SsoXt2hd;BvO zl!q_dEf9@aRANBbdPBlT#k5^w?po>wfzj3Y0@{b5`i2)-8#;^{$_j@K4MKfueV4aP zE{F_yJhpI`9X)8EElM=1M_<`4wnNWSFeyb7*Z!`;EDjo!p3{W2o*urd)}nT8O8(hh z3ur+}TlV#S96Y1^`tqUu)jQv&H)~iO@0Lm&*Sbn7H0A1vk&tjrvUtG4x1)PTM?I%K zmcQiSyJJoNezne(vy6(KY-Ghg^0{h6Yx>~xF=*rQNga!i&uSYwK2>m6##>5bY1?s+ zpd?m)_UK13+qdndeU9g-!Q#46A*0hz&eRn;rXz8G8iFUaJfuRx*h{Ex%(xE@X^&_3 zFI4_8udOlk&S2uCp|1UY*AJA@S3cR|bE>DVJG5o;Nc})ab)fNCpLYypmD(rr-eKF` zgjatYy)wGa>2<|u5ygRdN_I}bMZ1zWT0=uGOX!KdIllJmY@6H;KDy+(Z#e6gRo3er zZ3Ck#nd=gs?7mk1q^#nHM}!AEhwmNeOwKIzntyjVDtn;9lhH?|g9Sca@5-O#n0Z~k8Tp~+ zUgvp-;CqjUJ8tVyJZ$IvAl_6jV#2A2B^M{RUNg8mm>|2V7w`8ppJ7`jf84#xuWMs& zAMA{vNy`^Wq$_!EdcWCF(pIm8d0573sOwS}HCXrfrd`K_ca8>@xb%oj80zkMyFzwy zy!4>N!Ij~f&I>xFyC<6o3XPrCcqXX8_hWN;;pp4N^orhf=e-_Xnss^)6>}Y9qVS6= zRJY#ZCkzGE)E#|xzGHS-u8KYT*z$D7-qZdcC_9^6twL(njut46ggj65o6_xbX}d{; zj1%qE65;iWyq87NmWi80(S#n242#VRsL@huzcjrwB0V9Y^KP{orZbTpVF^`N!mDny zuYUXM1MRF(x4}g|5~gQQwv?Pmo?bh7;)z5}!uiTAQ<2DoauI*nmo0+x#a@M{vDL{_|Dx znK7H)<4@%;F1y!$t7*{rhn6V(x}wE-SMVoi&Ju~FdQIJZNl7?p?~sU7sI=(ik-DjF zLtU=A!L9pDmA7%4W_kjGsxiz>jM3LpV|(4&D`eBm^;}h}!ZmI6hV{?9PgQg%3^_iB zCS$bb4D;-S%mPTd9~f^n?Vf zE>lOTiot=5I!m+VI=jR-G@0&;T+&cy`h05ozTCrQ%6E$=4V$Ofxq56)xd5GGG4BY0 zdM)iuZT6R%uDma!Julr%G$&P0#^~z$(JKxcKFmqnSe#mtqcJyklw5zwHhyD6sv5!SYd60L#i=MmO zYhB=(Ikttfd>YB>L2j`Ln^W3-LW3;tjypNCZQpnekt2~%`nEToR810|naH{ztFoym zVD7?`b*HiRSolLxm-w-3x-Z;ay370)i*#)Bqy#HygQgzOG@N|aH0@f(>h>5z_WVSd zUk^v7)QgHUYuji8dv+Dc&EBeIA+n11Ov`Qkr`PQ33Ui82C3-i9y2(mShcqr3GM3Zo zuzf%besyt@a$D8@K40y{WYq$uPHQbSP3a0@v82!AY?-d>#LBHJMmGj7uSv2|&17y+ zQj=MvnzJg_t9+R}UWsCswlURVA;IM!z3b7MZX27EGpz;_HAfRA)Xz&v?G#)rzUX}A z?yG%uKDShzCb_96n~K~w61g6)?la`vy1imi;>Q*xzh~zsk91}wZ0NNlzdC5}7$4of zF%a^QABZdsk9V?_d4_67L#${#r(X;#aj6N{*2xtQGKwT%4+ez$K@4X^e zcuR3$%^_0eT~6G-9;PmBczO8T@%#+S%560}s$<8ew^5QZlU#z*$L^n796Dr}v$SD_ ztk>3*8~fIjdZ%QsbJL5!weQf9C8u9i6|T^FdjHt-idtONdPrZpC$39=s_@aPH|2~{ zjT156QcFHfU4k@!Pw`^j~{2-2a34(}R_x zyJCl(G;2m#c;<_JKSZ968IvwQqn_y)ofz;zZc+g&dqP*LMfJrfx6Kw-gZY=O1FIxu zW#6d1TR2i8{`2VjL+x>cZP|WLYFawG8MM6PBVHHR^nPr9Sv;FM!fv6y?{7brNB$bS zYlVM&;qj*4_pRcE83waTEtq(+9kthUisQq&$}L4%3tyT{yCtEHk<&eQ#@4+T@ul6eo+D zww5od+m{&$+X$EYey(t=D@d?Xy`mA^nyOY>eD_%26M4VUdqy9RXZyV|Dr$US25D;0 z+S~`zXSO7r8F%hp$wgVuS=TB$;i)O#uH4M_A$99jRdYR^JT~*%{?KVE?8z#;dVG*2 za;zfr5Oce-zhIo&+BhU>Fjv?D9fjv5r>uN&}vC4*ya?DKUkr|$PP-<+&T_DZ%IXfjRjOZ6SeEtuo> zc+jhaZqYr4M(Viw`fUTfU~^4jt^?#wkK+dhoab`riPHx8%Z1Ub_M5;rqc}W+2_2OauelPyT^o+ z)mq=Ikh8m(dcxqCYuz21rcQ@e!?8dMn~v(y^wpy-Lk7i>T{cwXH^0X9HZ0%63DQY`C#VvM^M1=Y8(I$?3e_y3X3kq4cAyZJzDA zAch4q>GCb6f&%oLPpb&26%f~~rlmHtuS@lLyg70BNmSiP!xfE58U1fcokerS8zdas z#(zi&&R?akYg{kWB^9*w@*QV6Ww-jxBddC^_}Tf&UmD&q*{^ktZP)((j`jF~#H9H? z)!Mbs5)>8*hgVONXf*Q}P|i#gN*7sOQ`md@FnLY_?qz|0m0Nm}-nl2;W^QMsYR{|~ z!MfP$+l5yZEXF2*cOmzRw(%Tzu>u#LB0dH+z=&Eyr*w^S?Dni!)O9_H(p zx30~}Z)|;VRT&HF%Txus_LTJ8bnUz1lP&Mj;x%y`=eS__%b2d^5{JbtRg;-otzj8~ zvSUYDk2eggyS8{2L2vnlsCQf6Kb@^|rEE*UElsQTBa5??Yb^`e8BY@SFE=oxK6$L% zv+l}BPtlZ!$9T01nqId1cW+2IdTCeW^*Dj6NzYcZ8yCzBpYGQ+JtiaS^+5H-bB$(< z0+~QH0;gFaJH7EjLA!XoxV~!aF^%Xk%O_1)Q*`ynxj8k{CwC`j?PNXs-jH)DY=Ki#&FH6PCG9e^eln|w3l8lc(@8n-W08ZO9z=Aj7sHzA&xx?Fk@S3L z-7nK#)t!AiE5f^AS2}6k%KL#&9(V4UG)4N@&r8GBikORq_s)x7mD^M-KhEc7UT@Cq zd~p?T9pBo=)^!giW+qg#Y-9~$_Eq?vEnXFy6?riwdS3I(pJHam9bZ#i*W<&I^IKMQ z>4tXY{2Y(U7}-ect#hLFHf_uK5!e3w?(A5v?XOLz#Hqb`R9SBk`zBpbpR(o{><`C? z7|%2kGH58wi(c6Ja@h~~(;a8}@+-pEI1WMdwTjL(B(_!5Ty%MHojN58o=s#X7RA)l zY0kMhN*9+z%WNG@J^VOV>UgEByP4aQHk$RJxai(vr*GKgY>$_|)mzx&m3Jy~RfXRp zU#k|^Si6s>V2F3#wpz9iMYQI0wjU_0kju9Ba_%IV3e9n+EzD7xd8=&K+KE!lj?zOT z>9Y!(dP0wQ5Z~^+QUA7UUe>L3IySL3+wLsr)Mtii^ww*CA1t~K$M{?xN7 z4+bWzWGz?uwI*Vo<;&}7X1*g=XT}|>5~Y{i-!}(O8y)U1vdMF6T3Pybl5M{GA^PLX zP3t-~3@ne`7(B^d=8*c4NZPo}-15UxwsutN9`jh0$cIM8M5-u`k>(&4P%(=%-i z&D{$|FRXakmD9P)?ZD)Gp954z=!dETXWlNaOk96T?DR$^oCedHd4~S9!gFTfcnhVuX6OCHO>=5?9r| zxzxQ@?U(J-vxy zFKBxZU`X#et{U%CsHQqJi4njZ)Y zyV>4Rd8>)2G}OGd+kTox1Oc7}5Ejn2@@w7su&;7?*kN%C)jX#0jLXC}jp}{bQR4RV zCC~ksUsr8H&g#2axWr7<6~)DL`|Qd~nOd@U=h;NecqjMqjenWv%7@1jOxIHFRm;v-&1FT&()Y; z=P5UEmlb(6{aDSRl#gXq1C2d%I`oepJw$h1@;GFO9(eUA49;-!|XgtDAJBU~7O_P3^onWCO<33j? zny4O`6*G5bLr_%~RW!%VG5&D*o!Gq0RT=R%GdFt9w9L!7s-&BzdQZN{;o*>m{W4(T zS!ca>%(z0Fx@W?J40eWto?XZF*efTLZgnXn;YG}s3C5n9DQ{g^_4t6d=f#uLk27yJ zt&oZw2-3LQsi<2gR;BXt`R;+YOOn+MWL5Kyc|d*gaNER@clTZ!`V#Q*`*$~I)Qa3_ z3J?x_-Qru=OnDz64<$gD{hY|XhL@j&49%#`h1F_ApZU=ib^>oqnLqcL(UsQIH;P6E z6ZHiB8F|;qAJ>yo=+)L9A{)IQTYDA_wpTCHmBF4)RgI|v5a?`c{kdxS?PVezE-ouK z;-WY?DEuY_nw8Bh_gE)cWyxqc_SEG*b7Czi^cw0@zU+4IaDiF0rbOlvwqQ>^cfi$>n~-`X;P}3WdoF(RhgwxAN+V(rae=0 zM|E{ts_`-gVnpl7S*+M}tG6D$ZxUMX&aSL}X1R0y!VQac_MKli`r+1R%uPFs zWy!E*HhIp&QwdcyenKLbd$wd(1)ZpWQ#QO|Ua(8h>%lt`Ej|NNY`f4#1#}AXcUj!c zikY|Tgh_FJipoSgVuRxIk+Ffkhfd!J{AIku2el^yk))#vSCfMKni7r}kl$s*nvLB) zO*?Fp(Vn^FRb~ziemyM5fM7p30uR*jBB3n|Ikq?Dd6Oqp8J}z%grvn zSaOanJKpT5p3ts$@74B*Tdj&-h^A{BBB+)nEwAq|dmllf>{l)s+ZablYFEqmtj~Ju`FhznONciKFNW9K zV|u$HEi@Xid52lwCPSRa?YNT8lbij#k1x**8Gh!%+H0?#TrG0{iKXa^d6ccEZ)E?p zUq>CE%_tp6lMbt?TKR;wSMOmOY2N5*&(_S1%m>z5GI9W?!x5*pl1!$1t5P@Du5drc zgmDwuH9x+$yql3gt}zpNp^;qmFe1AvPb!W+aQmD^Z+&+Co6#<>ihlgDz_&4Lo>jIt z?$ayJb#+#4mbKZXpL{_wT1(bz5Ba()y```qLcX-|fXl~Ub}ei!q_-IAYA>@atyrwQ z=cV{!_;yZX*e0?K>LRab^r#gSV!9nr&DiOFbMn5iOD~Yr?>fdelu}yyRzE77TwFzT zE}7D16TgzdtS)5EX|2vk%V27Vq}%0lH)DbIt4ESPuFzd2CoHGJ$SSZMW^KH4gACs{ zx-#T_wjj8Db>{1}4+}%6Lq^uYxp$h^hPYqr%yL`bGMsumP?~~MI8SY#80u!3cY(Qm z_+wY^kyyEfwTz(;5@sQFm->fvt40QM&%|vi|FFtbp7hR)-EwDvXJN0ir`k~F%gVHO z&Hsk(z|`(82EoMVnLR+{DBTKaf+e8N}mR9?l49Y1yuh(xgJ0n6?~ zFusI&L3*lm#BuvFe>Z#gZcbWS(@?&i-o5tXZh>*@3WF1Lb^qvrw6SV2B zBhQ;yZ2J^TQW%7y?=l)ieVD4>APjltzknHN}3!q7f%p{lRZro0GUC_dbHXJHT zc8_0cUu1F9*Ft|gK6d&#X;l1n?#2XY!ky1`bWdO0+KuZDHCkDdhJCdQ<#sm+8gv+& z?(Xw7=De%De?V$VuK~-PqTL!pUMBqjuzAqLrv-rQ-b9-1d@viHTbIFi1q)@nxG2DVJrW&YNFes%mOW>2v;kU2;6X zJ|xABYF9qK&w?t#>_7a*!0HcLR?TmRBs^3}@)_5ic)|9}s4lL2xgz~~%O|emLu(>> zIeGmai>^C)Dz3jjAuV*-m=%YZGX3r7YW=oO?CgZBmiSk@%j>z8m*2u&yN0k-OHW@d z=Duwj<1Z>NjdS?Y%Ju4OZGT~0uQp(jdpvwyk#sN&OH0t9cC?GXd{}1aTdInzN~@5T z`YRgw`1S6m$oPmuMP_H+^ZjSt?4{YqR2UUWOZ)z02PI+>;CuA@vE-MgnzD~ayUVa@PmrK+2_F@E&zwZ}1*sL{VaFxKWhZc)RIN4CIG9K6E)IOj$*1I8tG1e!E?#XJQq}1;IvdqqQJ`iPwN^jQ`GQ8B zM99Y#X-DU&k@hxVRjSSb6$u6)#Tf5R=D{x!2MO${A2@=R(Hg)@my4f!K89^k~=WV0{!w$2iS29H_pfNOMAu3%JaJK6!Aw~6ufCPdk6o5IiT{$+=29P zH)6GITlp!q{E7Z}TAh*odIZQhnmMG`a$S-oSxUuKqoSE}$C2{mWhaF+uF;Arq#LiT zVh9N|igMGqNG->$FMf5G9T(?)4vek4aHvKdozT|OjGk~@#49uDVL4rBJeT=>;2h=# zP#kE_2&$q5FSgwLjU(`9W~QW7QO;mEPsg?Sg;#{Gb0)KTLBsr)RZVtCxyA{rxBC

UOHs$)_D|tpcO;E{D_gZi0TXhOG?p;`^^UqdDt-WLqdHqnh|=PJo31I}(uX z%K9LKF%O5c7fW$wdh7LD#~ZQ;s97}QSR>A+D|x)fKEcU|9B}eywor!2UfhuWbhc1UdU)VqhP15~KPbyOMEn^(+N=iPN>!+5ZtAya*~zL4 z0@aA&C8mcEGre&xrbDW6KtWgiAym zF1Ill5l|dreo*QADD zv{A|#ZwH4JC6TM)ZPe+2D3<$HPfbeRCO27cYFC{grmdW0KlsQ+Ls_oW^0z|@3*%H} zx}?}qFpmB5*l?JgqpJzgzDasQLPG@6AxlfP8YnMr-~WzF$KoMu;m??lLQ$SKD%q)I zjx3CAu-_jO&PuI{l1n40QOQ^DMRMSLYWKwt;qs+Fl)*eJrBH}W+<)c^j3{C`#;1!N2mefiUM&^c~4 z2ra-v9mk~V3{ojCiE#M!-RnGFS%;BcJtX|t+)>FtCsyXhE2XG|hD~s=IRb^>YEdFp z981mb_w=(!*FCva(VJ0%HZ zT@;D3t-3hF85e>c-UZvmh%B|H&o$?ycXw5dXxTOCP=?iNl_G3&_pN<~Y-z5yR^^Z~ zF6WK+%8%@FL(pKbV&zxShEr`ClFM>ruJC){1^7@RR5?`|Nofv*$urSZM@*}`rV7JS!)hR9E9=~t8 zVXI=3s+cUMQNRR?l(mvfuj`T4?Mk0drPUFzX`@a~xL38Bl`X?w9&I2SAuWh=oIax* zzjSo=!aXeENi*^p#)qnUglHQca^twjK^r@9(`W;0SPF$YH*qGtu#Jlf#?6E{h)g#B zWE#s}c#AM)4cF!qyv~;9rqgnK$UamN$Ikb80m8sb8*C3;jGB8&1G8j9UH&Cc4m?BI z`KAapD@iqBR(5~(L^S}CbGz5aZSEf|Ah6XSls!@-ZAQw@vQ$K)=(FnEVI8NHfdEw| zD@kctud=#!yuS(af|g^xz77vBq8 zoIWQE8{oO43iTjM^11SaY3g^3)Mn_u(H%}YzeZ-8<+3MUnpjuEA^ z&27}N*l}^xH2cR_D+7+c`kHcbF+4P;y)%A(|EGee>9M1-jFe7hr6To}1}cFZq9;Sa z&aKPo$t6zOE-em|K)Wh`FVVC+4NW!mIMvn- zmYNADV=Hl?&v9Y9&+o0{e zD>;?joNr;k)lx_!H$UZ2g(shicednd)McE0)JH+nrVfl|wL1CpHEr5oWhb1!?`L0D zxpk?d`wC*pdsl`%C-@Mdr5x-wdNDqkpZ_*YS{%yHLB6P2J?4J6mYHXt;Rly1;)QZ% zJ@-WR)NpqsM3EsYn5AM6S3TMLENPohEb_h<%5d&7H>2 zzozDE$d9*XG5r~bIn-sB7G_(r@bx)j6~Smi8(N1Zzt)NdYH z{dVyo$IanR-h<-K<|ie&*XyjT&SAYys;`Uq7SPmcUA?WwckRdD4z#<4WjS38@0}F) zsg&==msKtzv%z6y!S0e{1>1{AoA%CKJ0CrEEVur>rrvyBcC@U<)LM_FHQhU*E37vd zs#UC!npLzIjOp4{w!c0T8VLpN@p*h>J^T2vr0dpv19qgo@|5^nPuykj|lz2289EsF=|%%;@p z`0g5}3*^Wv(MwllO;N|GeN@NmPO%k_PxhwawmM{&JNk`Y9q&Fh?YSvk=2h+wIOmg)>*V>5|5)^ zvIO^k$qC+ix>cg3NuTb857yw<`HcHosCH7J^8-z}cMbT8)JAnX8k#m}B+AQ$x*2KA zw?Au`p2R;Iy<=H6F4>jkCdDuk{Pp$1jiTdBI_7Ovq;pi#LyJXK=}_wIgl|>82~GI7 z0Pk%E)k9y*;b{8plCq0dX~5u#jV|TdDle%SC=83`r?_7kF<9xU=$*H+S%7`DnCm{N z$Cg9gw12}-Z_Sa^SkqLy)c#{@tJTT6r zom^*{+u(GwX8Br!*do+wzFCfR2p%k5Gd-W~;JHY+0Tq`C+(h;S}LJfa8%o`W~N6UoTg?gY88oENiQC+<(ETAN^ za(8jmss2P~dh-W1U(VM$663IJ|5djozUl1h~v2aV{*zdQTkZo2+DvUF`Gvb9#07>i3Tj# z4z)BuLr-bJY}Gd_R+F?;MDcneoc)`;YleSPIs=bi+n!5!)ezEjb*i>HQ~M}buh`UY z#ffEKlxOl~$H7UIrdZ{hZNq-WeRdNL&n$ADBECP9GO(G?rwq;N4lh6sUCGP2PursA zKKN>1)T>C@bpV#h>X8~#F79F(yYW+{PCg{==|f5>iED$G6e+;&R#4+y7&BwHQ|X)! z!9t4;*FE4V@Fh}&{HDFu7&p`4|m460j`Z*9ZtS>|m=%6l(BXKDU4%Q`KV zL~inKyjs5d?9X*Y1I~~wh5Oo3rbVg0l5 z|Ht$t@KuCBgr-VPl3~gF{J15gITJV@wSniP^jJDFAf@IYGAu@KYz3Asw z#T<6O+p}1WUUDC}(tG|^`mpQwnbNWRJ?^F(1*WTD+HnlJGgo8n)F0MvMYU07v%x;v z@u~4LsUwu zwSQNhS1rEng}h|Z=#Gl`kw;t~D5;IWwExo?0BIjJ{wJM**%X}Mx^?q#1wqE7I)75W zA$xx0j zEfxBlJRYrpmk3RJY&zsl(%zL8l)(#N>qcCCb~2pA*{%h-QF+u&!AA9#7Ae76as}!0 z{>=RCad6Fn*$GC9f7G7vBnU6=_%S+BH?r6f%--%cy0}aw_~=TpsznPQm>nW2WqSUX zMBTvucYQM+dYc+j_f6EPhnYxg8g{siS{k=Nl3)d0Xi?F#_@I@^MO?KfQoy)rE7km> z?{?6$e9hbzHCSYP(9R`J7j=Z@BDGl2mwgHzf=5tSLKw6%%z#r4Bq9Ni^}<-IhY|fn zapBn3*n4$fQBA{~V=&fu%Com@jqh@&*k@b=gEobd+x?DG7Dvo!-iA;O_1HSG`=DYh zVDc+IoCu8}_?>iiw7TX*2Xo|B-H7Qxq`>bHmT?l&9f+SHB2T`0#!t*8HZ88ngoVN% zn30)D?`Kycl=v8*2GL@9fs`C~IS{DCyfiOCNdUjb;T9^rj@>n`tIH{`G^QvH&l zxai0)RI6{yE+tpUTEmMI+6GC~U}DXXOB2xoDI9mTl$AqX$F08~eZA1JDy*F z33!^J9~4t?8lim8=>6@oJLOA@PfGf`NX9GPWvk=%2wM}iS?16MLX$i%2L_|Em_eQG zPhb`pLv`^d1f@SC&x$$&r6hnvMCfL)^*so7C;FpQT-1J8-5DQU+wk#DHf!#+w!oRb z=co_UXHGoG?D@}btpQfXgh8*>YbGZq3xSBciBLGQWw))3{~T^!U# zyo|iOCvqsIanubX;ihJ7W%aZR9Z$v&xwMqHCvHB{H9~wLvp7CgWIK@))g;PC#KuaC^E!V=aI3zbl*a^9r2KEPmD!sR(YyxK4Y2Zqk zaf3am-cATsS69Eh(7kD=3##|6o9BtYA)(ndR4lZ%vaHN+?@4J@_~c+$;{9O^`={qI z*e)`Ur>8@V(2wu%^pyzFiQE1pp-Eh5b=Kb~@QH+!*NNm{n?1oG4kHKBd_Z4r<}omN z305tSdmS;?q8@E4=j;Uc<>v8ueZPPd+P`#Ath^-_3MYRE`oz69X$0laFI;SIAN!SQ zrA-F8i<w~@wXxr4=jfloT%Th|J0kjZuG#FPDk|8?qH8kzZHk3UV;}}kPlzbOQ z=VI#8Ycdv8P1UzcaZV3iEay0^M}U@TUJ6=(!9v)mtW?mPJ(&GAe~SVva@3nu!g_1G z_DPc*VH%^T%P?h=i3(e;mwi--^>4TspACtCO#Av)ZlLit$KsK z`<6u3-JLpS>2a{Kr#A)|7(F0}c!QmI&1mKZuq+TF-_*D|@F%utAZZ;S0MuYql+>sD z{L0iAT{xLEmG9Km7ydXH8J@`%MDYU#7J7rvp!^{Zg8U}5&@p`AQ9wabN+!;Ay*(E8 zI_K4NU7C?|?hc#N=ekRC@K}_{$l$v7s-T$3Z>+%`G67T;TR>^=*oyfa@T7sb~2XxZ!bef)`5+Hbdk z=-dAC2oNiKS1nsyO3LTBS1+@iu?F`uwdT)1_Z55US?zY_oSm6j2-2lv2h)y7yic?T z63tMUr~vbO#-Gb31uL;QmMK{9mMN#co)!~VGM2BTriMt_VGG>NB2w>cIrjVxzUrFC zEpqUT4{(ZN`xx^ozC$FN3+WI3Q2S$=5|#%{zlxxi$ogpU#+XHZseQUbPvUd8xV!St zdLx#oqtHHs7$!r$mvx!F+W(kBGz6(?X=zp6>>(jSVV2+QOV%^q=*5M;DFa%b=CjBW z+Vnk8ii&IB(S+jsX+6b(XuE z!}$r!I#neejX!LOqturIlRD(R;=ArwK$sM+gQ(fQVnrrnp4QaAcz zNzNaSi`sV8P1sisEWl|5;>hdJ2LDBFzd11ql4|CW!>lxd4p1@b769~lEWve9cX;ob zd~_Jdz|-q<{-niN??ez;gsUEnaWj&pv{3+QJj_T|J_gD#7Dw;t^nJ~DAm#0~j(!-U zF`85zD{><}mUZahPlt-+`a{T}178M;4NS~k6}=_)e}J3g4nmthU*PyRXz{<47dT3( z-%=?qoIcxhvRdx>dX8_%%vyJTyynV$mqz9(EZ^=UXE8YEjM{c@Bd=7Rw3qu-f&AZ2 zHm!_Xt6=vd0!UtNlXe9Gj!vl;G$hN^mAXN1x zcnI;QUGxaY(Yo@{;HqCVOUq#Q%ieGx=pv>*o~wGXC4PHi>EOnKyT*podeP*D7F zTf(lApxCtx3iQjNV||+GJ%MNpQ6)ybno7=qcQa1LZr-s{uL<-z?zmKdU-sG51y`EBpuD< z|2dUHNDK-NK2?@I=6oVT`{bKz0B`|mK%&Yf6C${gXVEI7w>4XG&A)zm49=Qs;Jmm2 zPnMd6aza~SDeu22^dg`re6};LJpB7-kYyTh{QZC1$6$zg{0Pg0$mPKS)%H(x6TxPT zyR6N?0Rd$n$X`MaP#35bwpzl6Z)$qOE5W9(n4pB8T$KhMzb{LJ)W%-{EDr&vQwUQ5 z2lf9b1ElUf2gxv+i`^@kVCx{?GfMM9i2CvOgUq04qB&J^(<2G;N&)(3ObT4jj7Cff zS?<5j+>2-PQsu6+Z!)oy=G8NzLkqGQg#;P>jt7J!;Ni&zMpSKQx&QFzUBFbD^^U|| zgoD?B!G%)nFnW}sbDaP>_y~A^+w;W(M)DlYTAlO>_$}}DyH>avBB*h`0`#+OxN?YS@w&#A z3XQW;kSGh96dWu`GzCQ>KB#P&1ZGW_PYh2~LG@GREM?HqL<9-y;@&K|<;`4^vX(PF{((IUKW{&)=i5W}GV-qlAnD1wi6wAY_S5E6<%2h87F zZ6&{W!Mjxp2#s7VW89X6zbpIVk76>Q>*xW5R`IebqARe!d6_}r%Yy;(d1T0|-U>;N z1dnS}xu9V^_|xxlRoJ})@2P%I0{0Up(Bwtl35_wOX<|qsq&1~m0VgMz2RL9lT~!fJ znPM;B0MyF!h*Iejmz`n~;Ohb+{5v=jT1+*kFG*3Up%Dz81s4=RYxw4MFQ3250v{>^ zeNd9gKsFETRzhg1W})e`C55e9`(0ar{uM*i)mLe^Iz7HJD)fNfN7>`gik%65;xPCk zA?Q;nu1j*02l@dvc99}QejoTzMW0ygIVco<-Gx)4%|dUz{YcnZ;W=0qjAjKZ=EWdl zPcS{ab@fKT0a{1tZ!st7{cr)7j@bjNEVl$Jv34Ep!Q5|e=%oI2i0$|H(N8hwHNgV@ zw-ifojG?;3xzewYTN2YavC#GBkUGf)_=>g+D|JiK>AG*ur$(U=v10vO9u@k77lQmP zj{-J##WePVi8YBtK3@^!IZ-hKG$%9<>Am4K7$Pezjs*-c&yYofgM&f%E&8W_wzsQ} z`g5qwZ6AL5($61$*0bL0;kc=|v4K-xIkZ@-(0(!;bhrh`2*-1_7aPN4%ar3*x-l?_+X=MO~9n6#S0EQJp4b1xZRDG#*fMWD-jn% zGyM`3*n~>N6Qd7anAEtUE2}iGDLLLYg=Sue07D{vgE8n_9qTT0YQ`MxKUl~+f zj7xw__iq$6!=;TzIP?LjR0>@xvQcLMbfUq_g9i13&^78je7ffI_`{3y>nA^IovJ>g zRclg^BOF&{l{>Ux*#wf8qfZqBo-~BNZZGdUCW+>2ZLhnDC%8`;dCYB5f6?=9(<5R# z?Penba$`r0{Ie*cZ9AIrc0pMDZ@~@hT+-{KwVn8U+qXIS*raSewwe780o3eaG6D3c z*x;eE(iWcr4K|Ez{?aT6{uooFwjOcC>h$?Qv?aG-RcmU(ou?D)#Lob2EQwoSx6+zW zt)Qb7;esegNN21<0)DjhV5gLkurTKhy8lRm3bJrZD2OzX7wlJ{>;wD5NmW89aUHDv zb^){pGuOZ`@IA$d#N|b~?9jDjC6F2Gm|_)y^;n4DvTwswktin*#6?xiH#f&5miE<3 z623A9pz?nQwmCj}xe)JO-!8;8*R1|eez{;y;8edXh6pe+2S>?(;b9Y+FFEc*l=u8z zAMr(5X~57tz#&U3mx9UWUza+b&NvQTkh&qtRNh?9Bh5^x8+b}ik_Xp; zn*vE&_g2ZBPko=E4EXZ{Q{KwBcd=vHERPVhgX0O+St(E3>Js-qF+y#<)+ko zDWOlom3V*>ad1d=$4b;myldU$zF)ED$qNmDOQ><&=EVU9RT0p<^OfD$|031;7{n@L zKiXRQFW`r_=frZ~-2ESxGQpO=K0h?1fl$by3e1qahl1eqq+DC?(O3TfWq=fx(RQET z{aql1%E|9pL4Z>KOU5h^BjCsVbOYrJa4Z5b?1vduNW+aKzf#BS1Ub{CWk6+D=!Ghp z-XHQyA#xxK#UwGP3grfKwdD8Z7Utu`%T6V?<7HL|)-t!08f@zrx9a0E!$Pq*MGh zNCiZts*Z=olS_Vv-Il0LVNbvSnu;(lK6wWxLj%mG^^NlBu4FN!DCVVXbrVX#s#&qa zjJ#s_U004YV-AIKC*4m7hW4YLd$YYY`v(gsTiSV4^^!^TvI0N>zZh|ifng4K z#&hY;d6j}*F6-enw2tG$tu0#qz&OX0LhuD>NOR#zN#fm!ckgjGAoI*>@R6<2(OH{yWU%rO<1;DKwl*ADWjgq#;5l?i?&yM$tft#s1{dtPo@t|DQ zIqdb~NaAx(CU!v`n%%eHuYtxvnN5p*e6H#;#>E9$;5*AZf?oR`8B?kiBEhw6(&h(N zs^xd&B#=UsXL8H3zFJ^fvVvkA00$>5#=&CmXsL2?*9RO2*BSX}j@C4ps9?q5^>%`? z(Y#`(b(IsIQPNZ=+)t74j1w2t6q2CfynqVyU)<^5&w*3eRXbrvI-N27h6RK)dCPMZtJbOqOXfOA75Y92#!+M`Wr2? zJXlF|?e?>7KvMJzsKcZJ3rY{Q!QSpTFcvYkk;Kt zecn0$Mz?meqXqul;iS_zRBdXEb{s~usnjd={oRy#p__g3 zV4P8*04m_=pGy5Wk#-oAz}|w4eAW{@A<9}wX`fYz!zmesv5RzaJXJ;+#tj9FoH_}- zRrPecpY6)T2)I_|YL$2GIrWO(kE#oe|Jz8^)X|CSjOQPZ2d#Sd%%r-Rc)K=w$4<`S zKPFlI+RnS3+*TW7rFF6M`G9?=S6)YQv6l|aO{QGBS@`w`lsAU7URkAxm9-L zS@w-zn}gXAMH1tfFJuM`VMPg5LzcVVroLN#xx(2RBi(No+0TVDA>?9fazl89FU~nK z`LMyZnH1sdp2hS=ei~3B&rcNDoG;ccP{I%{`Tx)`o%L3=w-RQ~QV;h5vva+E>qR|& zu)sJV7i(kcqy}83Z6B(#G9_r3m;q6I@CSKd47Te`JAGe7JiwHqNi;iqhK7cCH%1nR zs@IjG!_JB}OgB1;J5`sy2xqgz+M#k{$MHI_!J;3c!%BHqZ)Z6VR6D%_9K9Gh*QH{M z;k@$*brWspmDDtgI7Hg!WOYniSUyWFNOf2{AH=4^U8kK&fkQj(4L zbO$f4S9KleD!aXw#PQs7>o0^wQ7x(=CD#cqulRt{U`bMqV--6-& z`!(yJvo-`?eWp)v)%Wnb*X^@iOP0Q0$ap>ZL+zf6#r2M~1St^}ldER2q`Ofi6!^0;!4lgLiU1X0|=-(^@lE&7{vx^|z&DF}K!SOX@ z=OdHqB~HCfWV}yr^n&mA>ia)x^5+tzxA>`RyLD^m_HkF7Oo=0Z4b_Ujf5!-7^m?0w zzBd2_dl;e%!-)a%BH2ErE%CpYK@Uf^VtJEg->Bh04Y+Bbjr-}UjVnLk;M{lLz!g)Czts$88ToWq>(J#^{A;?}C*^t!TeV+#v5ApY<|vZw~1cEH2^^GM5@*ed1*_BwY?xooMc z%n5?hOu*vqvE2#OYG$8{rXD1F;kpfgE(9gg6>sQ{9~;iI-|+0bJg$WmO}+Sq(^YV3 zz*gqLI=x;q5Y;rl&;#~{mO%&YH-|_4J*-CC`gTh?)TM_!7eYaA!6fL|F!$_zk;)F} z=qJUR=rpso+78xI(yDf?50<4*`?YUaX&J=iM%9a$c-Peh81`s@kOOxw?30FDf1|nuP%NF}|cub^>qO662rHgH_X^fVEPR@sQl=*38?z8?Ar7ii^ zm9z7P>o}29M)6BgQ{z=YU=t&47hK8=2ePa+aJ!b8joDH-C`<(_ zjgH%Ahk(U42xq#yLXWDvbxi zjZ@?uOuXBuk`Iit!#P)hgNQOeb*pK+8tDSMqf~z0Ts((#b_#!`!CorhkFGfxAMy)2 zVp~3Ks_Nd4Z>tG#a9=x1whNOK+w$#pKi^FYyJyFnN*SmeH4oU;gZ(FxTZ}quu6JvO z?Vlh?`yCkN_hkqqaOuL|nZ68DH`Qj6Toq2l^XO6Ip_OB7Oy+RJB!p zDv}+`(nh7#;&y2p(&&qL;0@SLdZC;rAwA<_?Yp3JemaN;$h*b)l^s(9mdnQE8MYQcrO2S8&am!Z&`@ zXvM*fEK}*QK7`I>F`+Mc;B$EUmy>vvN1Z&$W+&7@6`%MMN?Q#ZGQ8oDpm_L^;*(c_ zuU>`pEKURBve8k0{RdE;Q-qt?D@B953PQm1qLK=UcQ2rxP9PQP^&HPy^GCFq(z3_1 z06C>;3IfUE2Wfcw#eVU#U*@93Xgjolfe(S{NDI6V3&WTa*R&wr^0?I#PLH{6YGvhR zMPKFf`Lm`r2lG8pVL<*%jq5?oM7sBcfu&ue7_+scAbSyW@7Fu1Hz2E(YY$aG)6%ub z;DIu$qVJD)UfAFr?9t|1!sYm)DHLp>(gv8`t#622Mm17(by=k6?K;x~+gpR(6bfGyGM`%uLQ>PS#$8g7D1 zy*|My{4Kv!d1vCb_<)M=_4f}@e*~(hRKXCEWdHiE4!~PQ)W{ni?=KPbL~Mn>w?!$? zKz+iyZ=${6_W;%gfgyCUa1Kr%O%g1G<`wD=GHkFxo&;c*Jtv3m54(x`gfSRC3xUaW zeg8mdMi5@}ctm-w@Yj_GExsHCM`4Ov?crQLY%sLTS|7?gU-u9L-!y=Me*a%U=pEE$ z=zRhofTxk`Q0NPi0!F}En8`Aav>+({$^`3!bmI4tT1QVJdgL8L8{%p}2}x~atuT0s zclib1FZ+(aoxNzlxc6bm2)srx@_zk6^wGA`9lTCN(04hEdvH*){%@!n9a9FBBoe*s z7%`Qe<*H^hc%?j-_Ma=j$uB&fmAL{!=J=X20A~So?Ht8#A_}R0AB?}!0{lE93&D>N zEJ7C!$IFiXY*i)c8cxXlTnM@k2bAqm@PQK-E=w7<&bHMUG6_Hjd!d@X!VE0{h!G{{ ziPuW+2PYSTdbGG)vM_1rn?5$JagSL?IX3&>=`xi5r{g*@7Vrz2^&|+M$nfcpmwy-W z9fGQx{4@b10MMMd%A_-!n4@`_e#8BSL6QULoZW;>vo3@*e8s!uBz!C` zsi5W!eGabg833P2LW_HJ9*^ysyVm35*#E-RgtVShlj9X=ySgdF5qi-<{>6bd&;$Ru z)?eF30{{r2rUYI|XmXk;tMuD#T&y>R_%=EU{ccx$zH!-db!7Y7a(xHy0?RO#odJzP z!40$u79M$iE^PSa4}3X{Z2lLwfLXcoFSnq<$92|+mZStX1R6*YY4RNvS!Vly)JEop zuNbJd|IKkzU}?bJy(1X#6D_l&i}Z8ag2k^~33-GPJ-4UrM}&WYpto1IPh^t*I4^Ka1wV5KjT5;owI{0lz-_9Swi|7=yM4PXank*|`FS zLbT-vXxboX8QvGa5d*O>NJdQ?0(BIjxxKJ)xgyoH)Ia8orF{B&QDzOaNnbk}fJoHy z;TjPVA8MTdr3=ZkpO+{y`P3!mFJ~uu@{4P?H7acs$e_FCME`Bc*4$~f9V&I|Q;&-_GZ{dFW1mR)VCjaS6w&hr&?8RoX zfjSss*|Q%&ZS`v&mqdYwu5uJJyeK3Z!{QhxNf2W9osMOMZ6ZFVx<`)hwWYzW3>8&W zK zcd^4`0~Ipy&qn7j`7kzyaYSSg9fkxjCJe=IqOy|*-uC$HWc=FWgUl?{)@Y%SgB7>C z=)KHFmn|2cUIub17eBwsnTNa20_HKU!}68WWVX?rfqV(gX-5*GoKVI6g{}6g{U9fv zLh^S(P|M3*KD$=#|3~}Zw;Iiv#GuTeu=>D&2+dFgtR&WlOfp{ZI*6DAjATP3$df?L zC^BrG01;OJM@TD7xWN@#mZ{=`hgh=jy?BZwABY~~3WHGN|0~hJZUHo1awiJYm-qmj z`3Bv$pqtB*NtJS6hawRjfACDH*emZr=nFpQw?meVIVlJJZqLatTO?VZOO`kwA`BFo z*&%H8-uv%YQB|BmB~$s12mV0Zc7*0hMwUD#M{*IS2bOfSF_a<9U+w_tLNi0C%U(#t zaEF-!0s^r&KOg+CS`1;=(ADPs8Dut8G>d+I_(ZF6>4U@=qZ*}L$+(mK*%v0zfG72T z-34nND}OajwNbrQS7yJ`r(lUW#&f)!Yz1z_bDZ&*8>Zew~JX1G|7ZRl(&e8 z1D*AMk6~>;b^BphtU)^jxY-3f*Hn6+=*L(j? zuTdZvV5IcuCWG}&=~Q-3Z6QsFax)6jJX;!l-Iv^&7v3E5XHP}BzHyhHJ)iO>b8a~p zMLdFvtj@GH`aw(cr=HnwJ9BP&5_HsLY1;)0uB3!m0(~r(<;c^JI%otFg0NQ6#0Rm) zFm`wdA-veCMFgUkKRtD0>waB*azq;7HkGyqX#cYtU$(G^_1w~KI==lO-E$F(ae zxBeWRbiTYdQ#rlEFi$k`r%y&B_02EN;7Uh;BFjWCe`W-$N&Kl@Cd*>`L2YOF02Vh) z9Pr-O+$;O(`5`J7%U2_;#=GN46%PH=7etP5vIP@P0)Z5freDO*n zV-hh?UpT}yNSESNTR%Q{o;VLx2X;UGh$UQMdi{v^%!0;M%tTKo*HZ?m#2@7F8|LLo z_yNry@pO?%t@Fn5F_FfGunI0+XP=u}yvr65vzIkmR$Go0?Jgn==Z{!&aE0FKIzn)_ z8rZA?wJ00&qXYJ`dNhxu7ilSQrE%XPGLZ2J#ymB6FBrl|%ae^86$DAUs3Kod^`bpq zC-JxZQ1G%%0|YKdN3*AQP{X9*>;=(Coo)Nt{;Vhs>J4#O{yTQq9nuk2+?j2wq$7e+ zaRonR4y;)uXP>iYjIm_by@VBp6I_6M|5!W6vSb9OUxPT2RWZ01mjAZ#d^^DXk6>$m z)>EJh1ZSzTiX0&&a3g*}g~E&FL3O!1@sIjw!J!WKi05d}BvMECO;|@BYz|o!O4;f! z*gVm$=&5p8GZiPX{U8{VW%A8Vx3swwAl7ag^=@aDfO-bI=Y9#CX`2Kz95mZ(Yl`XWSp;kn`A4p>HfXeF`g#d)?- zo!W0-o#D@-b%3?-f}Hk-{Vi*ZxV3UU;090w7i<4W?E++MoS!*zQhzci_f@WDfQyqN z^1@is)lj|y1Rqx;5k#1pZPJ%A>=V6>fS&LiCLeblhEGh?1!AG$};{O+m{5?cPLYGlDUv z-~K+THJJZG;<p@!3Y$rMAms4rr{{uRI_gE+2ztci1DXFkLk)0Is;Djm)Aa*qbhOv2hT@p`c z$Y#hso$xEHyYCEeU#ZZ@XYQcfa8dech`94TP!;oEA?$?uuv3Jq%DT^#S6b2%(2tL%Gd6xai$0h79Ae>KDXPA#|G!xaSEXL&ykbKo=u{ zfAuLEZkS9cjum_Wem@qI9K%BbI_s1s{yDF0g28vhkq~|X9GhVIcVvVRgyj{S{P!AT zpdz~DZx&V`VunLs`-v8%IoI)}d%#@A)2mvOya&nziGR&pvFwAzzy}-!&~aWyxNE4F zWlM@5-5GSW{ZrOd_wzGd9d#IgDMfhL&t$J&>j+Bn(oZ=G64@|&F7MXzg1T-76B$=Y zAS<*+BJbQY1@L3#T{sx<7N}3ZD<*d1Dg&W~5{M81D`=2yysw2*`Tk9{a$J0;ph0;D zXM~2S|8pj|&j$LV3BFC)@ISExTDRY!!|o|v(%*^0eTCX9#_aedYzeuDr0SN&_OzQa zbZ~Isay_=M23MnQtm$1i3j;0rxiN5zISA?UWQz;AaPWQqBb-^w67(R-3>!b&O+79l zg*af|X!w^Tb1*>O03pbtCzSSGcv}%)kl$rzppbX?YiFU|m3BWwUZC)sC;B+x44OgK z+`_zzkw&L>uw3Aa2;_;>aUu1iQjG?nYg1-BCw2nxa}!AEa?$Wr`|$P#O$^kR50;Id zx*Ao80mJi>Qp)`nHLhgsqqAl4I%O)vi!LA~os^*uZhA@e3e}s`F;d6AS2WEV=h55W z9^3cjq0Dfp!hrMYxFVMUf5rx^90t(RR)^&y;v7`pAIF{MEkE~c0=A@G=XP`RhdfAr zDs$VNrcg5Q&6{|UY$l(BX1g0|`LdC|0F}Ato7&i!oPvhhd3U7DD#nkDe=K3rLiaO% z1m-fu2h+X;x8s%BPLb!te2YiJJ51A*dDa$#i|s&#NR4cw@F#;F@L5yb%!)jo&oV-X#VBi#Fu+Z&r! z)|*g;=o;wgjtt#UIs5`{Htw-L>b(c?jnB_c5cg@g?NA@yruJ+9PgANV{uAM`#3Hz0 zH@y1+K4Sg%jDB`k;oitj(4OFoKLu*uM(_eA6)uS4LrO2CWQx_*V z>o|u~qdXzmBe?%!YY@k<0$&CjFSyA#@JebUb9dIe<^lE8-)TaL_xDR&hl(KXPbhnr zDQp{?s_qeYc-LI0_=@9TFTBu+6-56bKP5_;7q9(&9M z2-Y>)-J$@(#Eip3`@dFid5aWFPd#pT5nF7i3T_m5T?q!DV^&+UuF*VQ0B~o?L1m1N zo_ZtXlV;8Ju4vI>8AY2bu}*Z@WQC)xc4zaY`FRjTkDG6K`P3cNdT=IPbK)URPUbVP zAf{LsE!3C{HMX%y3I(%w45x4KLWpFd{v}_hRUsVkPQ8mp@IL`GWdz@U%H}=4X!o;B z=Bko<+RQH@V=VuT^poi+TMrJ69B4ri+>V06Vl-{wW(yaKsLwwQG;#X>(@;&i_Z}|< zKKz6n%}5{C0o`LSU_$2tlOi`>s5S#^hp*~%{C$T#!Z=<@@>1Yddy?DA3HD7C020Jb z{@{mq_frWBWbdD2njX_6LRp<_{C|)@x*GAcgkD1rmUNkU05urvft}&SoZ$J7dw5PD zGNugxV+1}>>Im7I@;R(tUI`D~&W=ndymxCfj0aQV3v#d{{p0S%iDxZ1>93_ zeryGp3bG?CJfl=$p$rft@^Vq-D2oam3Fy~c?&og>^>{;B#O;BF&_2S=VW4z}`cVh) z?#^c7mvjU67^UDM=yb$uY!7u%mHQ1BfI-onBd97R$WZuC&JsCF6?7v(B0V@-xJ{J8 zNnlCle-b^h*#Ag)4m&2v1E`;SxOkEgZ5-I&b>{9t+^^CW}3gZRc-)AG5*y#|+XY zEC~zsQks_p#)s7phEJrHmYIQpV?(_w*+SHvhX)HwHUxH1g zE(kz!9J>gw&r;Q0yax^$>PG~50gU$1fp@%Q&a4*+j+K6CZ4Nn(w&o-pI5}=Seloc z*aTcHQG0>uS9Y_{$gynqg~`>>t-)M1eRy^ZQirS>rF7?#5_kzOF5JI6O(zi%Wam|l za1R0w%IvG_Kooo{Kx;ic9)HLCt_^H0nO!yoZU09#S*M>>OIkzy)Zdn*=*zvvEYbD? z58C&ML}73I`y{ZhORXcx8@ zNJP7lDi`OYJ~mY*!-ok6g;&57+30_(%!2Ci-87HA3Q#D^u0TM?xCcr>vZ(v~G6-h* zuYREA;_7ZHz8tFo_1zlofY)*Bb*p(OsIwb6S!po5^}vnbA=SY)U{7gm^aM0Kt#u7lumJuew%UA6Xp6dWBhUjWL=UlPw1YfLQJFw4R|I?wg5ic85(+r!roXrp1Tp2?*1eZM*D<-e$E zPHveLMj&LYUxFRq*`HL?+kIK5lqXxGcfF-XnWyB=I$vaH65Yw=yrZsX1EiTbHe{PsVH{*}!#XO-CK*vIhU3?l;8ClpT8fVRI zjpc*_r8wa{l+B+hwx{!|KKRU`JRKkCG4#_W^{Pemv~yM* zZmk84*Qi4Qup&SL5nWR^e&jghkf57P7!2!L^stF1e~cBznW4{P<3x+Fr>p7G8u>&Z z#Pz3<_*K1j=H%5|A1k|%wC`hM zDZU~By`w1+)OHIP=f+FYU876v<=L7FYsa_EM7irntn}QEkK}JJrrSKJIHN3oiq~er zt3Kf(lLBx*0(RAatiZpWxe4FXy70@FH@jRupbC*l{$INVt`Bc;EXqV1 z_%2(Y^Kt?qdfNCIASFmecxBvfA6C;mJ=%9jN~OvyD4E_rOq#|fHRu~~!EK`TlXjkU zrI5T=MRw~UWAoDXcv!iK^;+=-ZUN4TSY^R@@on}#*oc+=5~vp`DlbN*{dsfXXs^$U zPAjmQ`FDcDh&K&X66PO9&7%O^`EiRnuK^p=9tt$0y$9VofFP^T_O&v8DAtm zenCnV^a9gAe&3?a%A~b)?a`J?V1QTt9AI~9bA6z(rSA`(kS-$CE{d2NTTtifOmbRl zw#YqAzrD(9d!=q|$$qkUv%BbNs@%Bx?Lnii`z8exENP)og14Nf=eDa^ysR1)Dg`|; zKdHKY1jW49UsbV++xzpo@Bhwy7Xp-@=1%AlyYj?kcgkOP?f{BWbz>tpKU9e5ogsA1 zkDZ>@)v3r;)k~lv%(|k8Ivt9Wgpw#ffc*uO@>B}}Y@*~Ia+W&r(gm;T^@NN&6f%Y= zmKn!`^uX{olCuKJPlZ|I`NIuSRRu0PjfbQ|6Nd)7dU5pj4 ztY}t-D@AjvXsyj&vnKB(T8e>51ZApHABZfCxU{akj30}Uie8~XP`3W)dDwrtDd#g6 zo7b33KUHbSRh!HkpL*!X(5rfBwDv_()%@y|w_^-w<|WU@h0|Cv6ln9S#O>yVBo_&{ zzJv#4QmY2*+B_?3K~WXyiDgu;VeG59ozcsr7EcwyV(kJ)z*)V7Szu_SK3Vf=QsAE6 z1T~(Y0@8u1JzxMW_zYKGBo!p!+thcYcb_!p8W${!-mX5i%$+*oX^Yrm$H?is6YHj-M9Yove99=g{&++c9Q8#7JLSi5zL01JlQ7g$= z>3D~WnCivw;pTWHYB8oaL^))Ao#V=~4pq{2H80>=Xc#Gh z>wM~dKE3;TcZX+tb9CTlAfCb^$qq1?SNpuTG^>V*lj&PIYEerF20=tT!wOe)eceHr zbu>4p`SMp6<~3bO&kRu6yPBO-lH;9(v@Df$=m~GY;#dIhtt*`5t@gbvcFhq@!>63kx=G>Hm8^PY?=o>Pq;5lAq3%QC1er(jm&dGekt;*EWe`e_UaI-tRzh`(}) zW$~!lHRmlZ(J|e>W6A!k^r_9v#Qx3sx!Ahb-PCz}K>cMk0w_*$IYWxLiR>Z z%F$U(fZYDjTdjqSw-A$j@ zxxYajyxyu${?F&(fyfWuBZ?l&^V?JNqf80Hg{I*Crz7lxBfe>+XzU>N@rCz zxz=Kp9L>kdoU`R{FP0x~JT!_;Uwy)PCb4Ija}usEw?=5kRV}88bD4ZnU1ZJ3|K0jh z%VG0fkuCG};)cP+6tCs*u`$Qcgw1ph+(c{Bte;vUAybxJYnIxD^w&xQ`d^dl@{LIV zOmQxX$c;xXiXghMXgB}_{IWspaUGZM#?h0eaR&FY_v@@HGU47;Hr0@`XvHWrtO~Ue zcQ)7QOy?x2wT!Md)0_*q`GHhH6m?j8*d@-Yirq*c7FT~dpSQiN{iYCSNcG7 z$=1Yp%dRn0*xb)Faj3)fF66Wj%?8UdSGo6Rw29^T;A*1t?%d>PG{bQHgQ`mV_-p?O zDNA=H(i{HGD4j|<=Lfr+jfx3Y=lZS^@PJ<7ied;fNvFy;)_H5aGg(-;%0r^vudZf> zk;8FWdXAH@Idit6{9gc$Kr>L<$Wmnq!=gE>1PvL^Z0W7fTNYtqA_W_|XCjq99zi zHTt0v#gb3atQkUEl8iEW_?@KXWJ;?R#EkiUrq*8c6R`)jBvnf@LC+2aGp1Y(_onsamB0yN~+V z!U(E5@?&svUJ-prK*&h%um}oi`ET8W@kom~}(=F>)?LA<0%0|`&C7vN0T?&UXX+I!Ah*PWAAmVv$> z{=RbFChuC_0=25o;-C8Re*+QOc+r; z*xA#!Lt{0tJd?R5mM7=bxcv{DswYK_c|A^#`H#p`boq63os=0q0_{N%E4_g-0fa>9 z`fjE18(RgHrd&FT|=A5V%g*OWUL9@XzKI3nrWpqYZpf5^Oxve5HmFJEKXrt;PV9utc$L00=bsx`f;_p>3h!)Ld{;ENBAcKRVm496IP~K z{sfHv7h|7}2#TIP@CzZ0QWsby zrVjZ>epboj-{of^E)|d=nf=snp$@j}$eagrjEL{w%+6i;6#)KKpfRcKd>OYB1DTJR z4}R^B5Mx8{?E;J-nEX$g64Nd8>vNDo+$RFr$HSg#ZC-l-3pG6b!T>N?N|<7Q-)(px ztIuTG8Is!Qc;O1*MR??AAXf1PsnHcSPz49c%HPoopJKK<20<{&kB$yj4(K1lb{&NS z5P~y+Yv~S^+ks{s0*Kvy-flMpCYHpPxWJe51?04OJ?D zl@I&KK5dmeHAZz(9_(_Nx8@j6eh;6;2L$FoAo0WRmprLWbwM&=|)y$vdP5lW!vq%u~J3JeP~!P zrky>6>pwnzDj@j7-peJB8K;DDwdxr8XZhWL#62j?8=1Am#P!*(vxG!I4KJxXph&HNt_txKCp5_}BUE;#MCmEZk8~80d#|~u`OgP%YM&6?i zfqws$x+<4JSCqU3^k4?YmMO(E`!Z1OET0+&hU)ZpHo+X57v;>MaPE9G%=oaZ=I#oK zq|e8=6h9D+v|ETy5kJwrn}rNNXa?oqKqbERk8;vM=7i1!Jf7I2)SrJIkrNtVfF8-* z|KOndl1KFZHLhhd?z^6kW`E^fzHPm^W5WVY0x2|3>C*opZIhzW<@5jshfxd|SW5RF z0}~+egHWk0wR!G%7swIzt zhyuyoDfT7_zgx#t(UNTprSa(a~N4+Rc0jYfcw-0d`$m?U| z{BaPoA1|?ffIN9u`GDZu%e7|7cmB^eHcde->xY3+0mDz(c3;oTQdxDm8C@=XMm>!% z_inoQJSMp>7pEI%3KtL(QEkUQXK4m z&z2m^d&2+$M6P1sJ!_fm^Bo6-Yb2RGg5wcL|6@hgp zQTV=(h$GVp>M!nrMp7VaMH~No+->sdmZ@mnrZ;rn|)zq7%1Q%P!mHiWZ>pJPCEJu_KF6MtUw1SIy@$R-B zn_ozzX0bl9%eRgo=Lrs7^BLTTj5YWLF(j zT6+}dX=FALFhlVV7LZ{C&*}||Q*(b+Bi=kYKlQndufL9a9OY0Re7b9`lyd64t+UT% zMnKv90kUc2<>`Mp2Dkow7>DG^enz+isUYu95K2E zv-v?SlAaXPhRABWU2D~vb<9yGXNDspdVXt37}jenX^+oWg{}#_x9@E&Da__IPnS6Y? zP8XT60p?OH|~B2N)Lj**z>yVey%ZdonNDYS^l%ZIgU^ z%nyFB6(Bfh?FPun*=i&zLNka8G5=$Zq(V>9y+OSUA%Up0eUiXpi_037vo=u%td&;N zr_#Sy&75zGeV9H`vL<@dZ3! z(?BMFL=osoin!tTmhVf}#*l>R4zUPJ_2XxLb7DARGk7vsP_08Z#!!VfP=EX{ijRrw zZg!HvwN6W~*_<^kv~Sk?Woq5ewBs??T`kD~k(@k0pj(Vyw&%szLlEgag{|??QIvMt z5`{{m9>dA~Z+d+4Dq=v5*ia2tfgKV*w_Vror^8TOEB=SNIY4wH2<`+wM{Y>$yfTHs zs&hi@FHG6UF96G0mA)qkeuElXP-Nn`XJ5Ge5N>}DPrZwJkL8zac+oYmAjuIAxP9IT zUwnWUmVn=Q2#-u54_HJG--+^qzi7Z)@GWv^5K6k*0%JOW$@RE9EWY6Ry^8-+HWl35 zy)lvTsR<4!>i7z%LYMr03z9oV1vtpWkYf2P?DhzIKMa4wie`dHC{IyTBXU8-j+lJJ z`^SHS7qGhE!?@5+Cp}|@15ymoB=1GYun&Tt4M%qS6Epa^-mwB%Du1;mXqPWY-~%qt zYPtRs_*6*JjEKrtAO7y(6(A9VPS3|r4A7}#eJ{b&>}sq{ky+R^_oM-3Bs|9AzcE{^ zKjh*Bu|9%dFGf5A3cAL2#`scy_%lE(1&pizE})9XraB@jI@5hzQ@?Bra}OL2)l3lI zk+nALu2&)03G-mP?VO(J>%c;6G?;+IulIqr<2}18W*88*Q-%F~2AJRADQB*kJ_~`B z`CFLytcc3Ou1~-)3sv~;5GU#Xt6oCBXpZ%b1H^gWQ+bgYMungdx4d%`>m$?%!|IA77-nvoW&8umfpKR3}U!Zy% zdwr3KdgIi}7%8)9blX_t&^nnKPi0Xt-Z|dI35XVVMQ7;DFcZMi?o^Ie49TgSfg1t( zg4{(#MOC2Oc^;D9ccAb@!Zfc)x6j!Y4PT zG0uNyCL{uku)ye0#mSeb*HvV9fXD{@8}7*xs3JYq(Hm04NR=C&k_CC%^pZHk!pb;W zHGsp9dzzjV>0KA{ULAC{I?x3vuiL+0tG*M+fckH-wA!nWa&Ob=F?{*88Y8C()L#Nv zO0FbjOU3AKhWxBO^G?#{WWobC7a4R+#pE;>sUfL@+I51-g>2!fM(&$+)43#q<=)$h zZe5Ma$!xii-OY4s)5Ys+D(4Bdlg2!6>x2h2p!@34k%M*JCxb335cofJ{V)s+{ciy* zbdmiiFaYF<@<{5hY(Kw6y6YMikqweB#pX*B14wZJZWb1Ss$NgI?6V1-XI{59mo|Zz z8L)%etfXKmHLDjR!VL9 zokcN$I#+3uWDuH}r6tFN#x8%|%%bjsfsdPc=x%Z{N<0~z2EDCn>xhml;0n4qCeMlf z3Do}t{9SjIM}^C%Rk)XAqmbMOe^H@^8dsvGO>s_#-!l*-9uj^7z}q~;h5(Wgzd`|% zeQN0~k?WFQ?I_>nne3inB(Dh^0xIif+|qJ)s2A7hk>~m;=zDOyrd$i%OpmuxmE}lK zfhdgA$yPZ1fy&6bO51f8>wKYc=VnJHv&!CV%K%YZZA{zSvu|1RmxIZ1R1M{X7MtXP z?Z%LQv`fOQy~8H&fvRTUD`LdM%Gg#kfj}kqgYSs(_y{8wpmW)^h6-1M;250^2{A!{ z`l_xxUP2GyM*YheqiE0K3+v9n+lWUK_@i2m8S(^aO;`nQTgvd-fD7W4!sax2>v9v9QWnshldIrFQ(|}h{ zff-9#VKzlKxI|Zj&VE7(-U8G$1~|?STjZDcN{bPFn@V%D zmJ-%|Q#oI%qIy(N+qy!FpniT)khbQsYcU5`WSL_%_%=t*36|@=t0dZ4w!57;Fwaiv z-Ez>d^riZnzhzlKCF~FP>TMGZ;?}BLTAQc3hc4)oCG^llCqQ2|O|6aYEZMTO0^|*u zEC)4pi+QV)*WzO6o)HqeF2V4NgFK=$kR?clIyZyfZD4OrRW32rSsnZ?w9qJzCK$&j zI_oNJ_#GokIJPhi)d9K4F3#S%rEw(K91XH%UF5BET%H$}QS=BtS*_!rnF5-E7sum# zg^XNWHJZ_M_?{xh?w>ROMh9Kp2D*Kn|Hf+Xpc~|~34$^*Q&lPNfG3<4%vw@`Y@Nv( zxj7wxb9`cIO_BGcPcpqUfLw!~DjpT|KV#GaiM!C%v&kMw=(U(QeXeB!#IWre_0^e z>l%(T6)1M^k96kW5OfKYSLDk>KRw+J?(T8F=E`TdTfG8K&U&f0nJuGc19T)%U*v<% zjr>GFx(lKcRWrYT5+r_41svnje@_*r2J;sz{LosClyN`;1+Ouvf8V727ZpKE|+xt?Z1=eGSxy;6sE*h#U&$5C>`E#wj?| zQmy_LQz!ge>gc(*)cujZm$;>A*mdZ>4Nk}=5 z%0_$>9bSk`KH{2JQ}&xn9gW9c(%hE|zf80L)dtrLFXv9UF9KrZ=)wiO04fK-G_H~w zbb%IRX{5a2B^XU&?>|kh@lEZq@PU9&-U4fPb6k~yAxj`+C~qLFM(63f`dQx=NSqF! zx|RX}6ftam^8e38wlB7@7;y$A@M3{i>X_857Iwui!+(JlmW9t;SRDT=H^?;`7&ZS- zliKQeQ3FD71N6gy#*FkM6Au>LI1Lou(}|3sgVaR_fhHLUnEmYkNDjWV=M06~li6rD zahCoxyzAiI2RUT>RaVg2s$P`D4Gf)(pp+MUsKa45M1+oZ!H?|JR7H2ENdN&d?4gL zAqG^I*pIIdO^|f3=FXJsYngg@UH4W<>C*DwN%L1hKiK`*p?e1sgq*@ZBGmyKG6dSX z%V#1wDWL8KGp_{$-Apyd#3Zizx7*yX(oXlf#qL?hZt?9)91g9ZJ8HSa-|9@N9}$^o zG7zYTgux63wF?CiRo}@`nW#wqjs1>swD81|?*tGi$)Rx*WfkRp1h})?gusLftZm!1 zvk{D3tNkfVsX==wEE137ZOZ+rY)vlcn+Yb&t5QLuZe8cEoGYhVJn9*1Kwc704o~5;t*0>b zw5@;f;t%SzYLPTrw^L0f+fyQAMB%aV?VEWgRZ5Y&s&k@Wh-iSt2Zdl7Gm*XNa5F6V zzLjTa!uAu|8Hniu_8_&?E-J@PC6kO)LU#KP7SILC&3Cj5p>p$aPEli}+ZN|qskW&_ z?lKKLm6&QJQrMu~>7Eg;(in4TsR?)sDM%QY!Z&1K2K|dm0%*|J+oSsk3UQ)np_7rT zty*24jkXAvmrJ;@wED^Kn+O7~v3()XT*KS2Ai+U)@Ab6LI>~mM2c6APZ1>e5`4__@ zn#-U4FXw+CR1ecFA$V`7`6c;|Ij|}6)F_5c|xJiXvJ8}@XNjW&efrT z=VMHASnm`3e0wBTa&n>GY=oK8H;)$H>1|mpULKC*YVgiiKOZ|aFGhqdsMu;=MVY5x z>q}#A)_EL~ugL(m4+D%S{?PQCN%cmUj*J$RSwv*{rJ)L9^in3zg7 zJOaeiG_7fJLeAF_fj$FXOn(g3XgG27^T_?o`V4 zwb705B?Eku&PiS;Z}(0I$b<)N_vR_az$pKPVWy1}s{2UW^cXgWMMRYi>^AQNf(2q4AhZfT}z zVu#I#EMJ5lgaSII;8r^UG=6yRHae9o#Jv-=gQ7x0wY%3+arK$m2qG6+jF;jHG&;W9s z3#7U+k^+lbu>c0N`d?d~s%CKS#Il>Nib$;96bD<7Bo`be+N*%VqmMjWyJcVt=iXyL(Sp|p&wzmEYebz z(`9;#{>lZR-^LjKQN|sl?~FDHKka3>(HZ!_OKQ%W#g9&n54LXt3NMtz0rqYQ?+%0@ zXB%gUtyL^DhvOj?B9lQ3m5ghbGM)59hW^+8H0wsua)#RMQRHT!I=&j&VG^6HCmf!E zkrhcgSQasXqeR~+pnk6$C}0Pz0s+6ppYKomA8fcZw(V}vOs^vI6T|-$empw?WzR>1 zGeBv-@e>Y+_J56-YUC9{=|eNni}7oo z3}jARynbFP4+Cc%MJtWeb$sGthg49|Qy;$7}@c0=X#AmBWUSRnv11^(*$c`S;yP)IY5?Q88xY+O0FO#ARR^i{4A6!RM^d>Y7f!gJ z;M)0%P%<$A?-3?%tz*0so1ii~PKAocYvi$wxvrvWKyjjOWQ$F}i*)9DFAMwf9*s)$ zJ*236NKd5%QfJN0{E8lj;MgH9e;Gh7dAV+Cja8%GI!9K$Ct`XkU7ABUcX05AlK}O7 z3>Q#WG@Dza&GR_j5fl{kvv*vx0C@)Fct*X5nHhBg9#?r@sdJ}k9V)qmoX#LL+V5=_ zlV!>co;q4(x-BF{?S<{RXwQ;m5NsyOyY1p8)n=C&&4oEg=CV(?sya+PqaTrCr>|ea2JTrLMibRxdmu#(OZdWzx1+*$eTp;d ziOhJO`vWiT#?MkgVUFDzRW^;se1O%f8%bbc2QSHjz5a?he-d%yz(c>iad1%PMslP$ zM0m?0xAnVv>depK6f$P!u^P>_oR6Ii(X`CVR|6rXh15qR*4<3lxV-&^Hn>dVZ4=gI zI~orZG+K12>;v^8MRn&*rYUI44qYfmipcNBqLJ&{z^c|pD1?^qcvOFc7YD=3kc6!b z)}$j}-R2#^kwQgBP|eIZW6C`bof zq6>?U%ggCK`*g7m)%JW-K+l_lh6Wjs?m7YAPo<&wV4$$YQm@b-ja&d0yaa{Y+w;5C zUi=8z!4g-$`xE&RJE^%W3Q1D{F2zRbza)(6OpMQC}hISq-D zy?rJ3EzJqgzEm|%&iw(Xirs*L7yv46i>xb`H$WG7 z3mj@$OorqIvnB5*K7)26#tSwWQF`{1<5)w2Q0VD>;r#=BKdpO^_esy&#k=V1P^qP= z1S=NXdd1N%V+=_od}xl9Xz2@XCL)&{$OZ)wcH%NJ?^ zMjPcDj`YH!BAjYbR(M?s9nz0-9Cu@}mOt>X#0u`FBo*VQWJ)cZ4I9W&XZFl@h#RZD?Wkh(i5x6Ncwf6qzV} zn_6a{idbNwA0v*uY+5314fQNQ_@@6MH4Q_@hbyJEp}Ps%5gbv`3L42`!?OEG1F2S<4WYO_A^OqJXJ0-|cBA1{+ROdWM^!T& zW?`v*3-2ih&AwM;H1ACEJp?&uDyE=$7&%Ybb~NaABb;e!Il}TO5fb1ti{fg^m{gn( zFEQZQB?mJf0eqxKZ)6Erux66>T<}poGW}c~nRxXN^n6K5Wyef$U7IzDY599m_b`Kr+G#BcIz9%0U1V7F>X-g*7 zkou+i8+cZ;b%x}Qn}D~XK?W4mPAz-4aQY&!)eXq7wGoWL7}21n=$? z+|lq*vwb%M20YR!1vJRhkM|?)j7z*d1paBwOn}DajXaDdtM!s#=zxd@zB=qQ=lbDA ztqiOsyayGS8!oVcPPq)6{}`J9Y%Z1lHlXPziyY88!}C(~!YnLT(&W z7Fp)*Bm~dLh?Hoq7PDE0y@)m4A@$A5h8;G=IXh*ToOo8a1`NrB92%0zs&LAmy(ykS z3LB=W{TMfX-rv^{fedHfAhYPDcpb8Yf#~h=8_LSi#OGCTKcHm+V`68Q~)LT*(Mf zgY+&8s9Gta($XSSOEk}GF)hHelfYnNLQf=zDs#l^cc$KobiiwT3N|1Gu&ac#Th|Lk zK2G~n!grn|k8yBJ_3jaHL8EgOZ=X^KZK{hrM91m=vq-Qqzy~l6`(m?T_~=w0E5bO$ zQ$)qazD-QD*rTc3$v5YOKB!H)w67zR;NHq?dQs9Hrs=tstU8*GnfAqX zkBG^wpzKxhK8v)P{@Ho=S=DB%S!Qx_@)`WM>0+aMU?zq{z;6w+5m)z&LJK_FK1+xM z58VQq1kfyl`_hz{OkvcFb@J62UYZO&g_{1^JCEyYVJu}xw6j8=U{w+n{<9w6o2c;Q>}kDA;KoJg7-j--Ewbn=8FQi!&sH zCcF+&8vyqqs=S}^RG!&YxUcbyMp`E>d}!fh<$Hh$Oc6DAc2!+G!xOR-RGd5(kL+C` zqxfUtNItuI+f_4gYl&+o9!D3KlT&kuj*7ZxXJFfUZt^9VZTR>LI{1Om$NCe?hg4xX zzy!dH5?V?u>9FynVRJs*^Y54y&q{CuBR~2f^p5)GnoCo@CK=f-)~K3*C*pkL+wt)U z5+7uL?IN%+vy+Y$+q6V7ZKQ+Xm>Wt;xbJ5>@{eP7rqmBP;*|}mC2>c#Z=@uxG9-OL zrzayJA%T<#J1CHx^N;VRhK7+tomfEzBPB{-yYk9zh zg#Ae)Q`0PPyGGY)nRae?&|1j|eW&N156`X}e(eh?m~#PA28NN;p06}uZZHn~eV;qC zT^srI$&KcEX|Bu|)?3G_EAB}QO{S14~>6R0|M!MzIKUe@qF-Nw`50Z=Uv2}~s?;ts6HG}^A`EyQ(pJ9@? zxVYkylAkY5dWPa?wSETp-$VFe2%Qfr+{*MOpCu+f4b_yZg7#B} z0krx|_*2RP0tAy{Vkja<8&9(d`SQlX5`L~M%wHyOS7EWT>DYM|x72B|Sx!B|xPLTn z+os~6q^$f_FyKL4=}aScee|@N1E3risLC4OR~g6&ug0P%Yd%CiR?&SP!`io z!`DFy#>%#vld@lcEN{5f?x!7xB`zI^e&91zVBw9cB3#RrlN8wuWlkFJt5t+5CdOY>q!3C@1#b5qLrC@_9**V=!z zf5TxoT!@up>@GSdwHW=XY$Jhtv&kx*l-7xqta8j`xgH_=eLn(x*W)<3ZBSpIUCj?_ zY|LgWb2LkITHM)0!|`$mBtLi@_ESAGu1rozN(#QZ3Wa&C+=)y?Ge(8F3vOE<5)$&p zKr!lv`1n99TGb4o)IL*hN9KOKq2Q;A9ph^XISdwEN$z3_QV-eg)cfAraVmsI9pbPc zu=DdgFw2(Rx;qW3(4k0a+?F2o)r5f^x2cR5K@o?Q$ky-sTa6N~7j|o$H$Rean5@pq ze&wA@sf?+KiPRddm)5sx6-VFh`RJ{x!PhlSqov51mwehx}ri}mnHKq>>M(AyY>IvDJK^u=+ zydy5|QRhm!(A1}ZxL~!W5gUcjZo8fNI@`^VA}bDj4jY0!AQ<|-ibi(#yUSvUYfpD~ zfy#7oF&$`&EF9O}quZQIDV^xs*C(!``c>HYLVNygP|yk}Va{9GLVo8q4<7hDt@6w7 z^z9(}0)#jF@4p%soz1<1r(vex#?h-uG4rNzrpTu+neVvowzAU zr%-9UuVICWpw&ZDD>1*o@olwms=OI&)26f!ag&o98)h|ITgb`;8ncJAmHqwwU+M^f zCJo-n)}#ZL6RlnvpIxdgP=pgF9!szibC$w=C!T=^KSexREK1An=b*x#_g%=d(Tm^U zvPZDCAUfn6sgGw#m9(tJS&OUM={xN~2cVF8A(Io51k71;!*nsbNfIr9=;ENXQB4JWS5oXzg*}7H zNM4AuhOraM1B&_g0Ldg*iP~qOqia5FRElFjYh0lfiQB|%$<$K~<_Ag32dNnP(tOa# zsH^Y9T_rZh@XADj|9As4=u$Kyu=9sJ(iIa5jGZpwh?|7CxG!ggy=@+*W%4ExsW9gg zj2AdCEV~xW*HZ?1ApN^;Prbdt?YzUNV?8-PNb|GH4o$$HA?`bJ@nGz4IxX*Rx2YH_ zGzPhmnjIXMg@8j<72~2xa<$pYk@Dg%TSddA_N605oZpu{o2D*lZ0qv7jk#J6K0h1~ zbmAn4YM0Y#a_)xkghbE#vaepfwSRF!-wlq#+(Jy;X_iW0>T0IiC%avw%1M0SJh~@G zN>u>7quD}pgHe)ryDAiTL#^wy8F4Akf= zDp3=2Q*sh&p4^3gepCVvJw?GC#4B*>Y4^1a-dp+v!1TzcuqXvnuhl_HXevI&Vdlp5A?6$1mMR>aunFmMkbBu24V zQRD(N&DEQe0~#97tSRbataKvYaF2#*79afTAOWY36}otyuogP+eJ&l$53@{<-~8Ll72W-Y$yG)uI`yuW)AUlDc4Pd^5?;V5v4PGUhzID;(wng(pT)Pyp&s9Z;Cf&Suj_kg|90;!S zx$WMZ*t_Xg8U@qf@E-jf4rq=)3^`U&VYn9e$qWdFZ>G@yncw|;VXh~)4nzc>mIo}R`c!o# zN{748rLK%jH&0({7*E#SG}_Qk4X(G{Fl1{dd`<6uj+MkQZKWJhcKpn^^jbnUA)&eN zeHM*%c1be><0iBFd>LvgwUfaKcS{S;wtc4BmPlUlz?O4cqZM1WtWl1v(P~X~jA_C2 zcJ@NP5!fT5c3DK=wquC+?dFKGhZ;F5o>&ec-_Y5T6DFwB?|mXn;C7DC-E|1b@3YWV zl&;eb_H?VumFv)n@w@XMZBqH=rJ2a%FDtQdN9^xmzte}lQzoW3exN~p*4`>scIz38 z-eYy!<^1kpGAU0stG10v1JUKO(D;iSM(1TM*lj+?B_r`_+n1l5p`UpX(w@Zf-bP`b z%sNbS=P?6vjKCVax)hi;4_FgWr}HKo@duTI7g~oHFG0~Hnnb_EH{Bhw)*D6I`4F|X zI)VoJ+_NI7Q(@>ntNYjtUE$#^M{}?mcbJvc1=cKE1g~gBLZh@m`<_Cm%!~VzOn%nl zBxZ=Lh;Sps1GRgupEXG<*OiAf+Ep~RWl7)s_R4zf)H$w71ZxdKpmJQln-;vKk@^dw z@J2Cpvbt5Zk(DWfIspaha`+vh8qV_L*(M*PnK}*@F?yTXOmp_!20<(>N zKx0u68HBsyXhg+3`{2h3@5}WS!zv!7eeJkm^E&i&o5jePRy%OT|6L49?1cn=7Ko*D z?hNupW=&H)=z8ER3C+ahvp18Qq)!O#8Q&;59joPqgD~_GFu0rqD#Goc6NfV%GEI42 zSfrH{gM(4&TgLLk4z7>0WY##ORoTQ{T#X=}5PzaAu@q^J`2L8-;c%7Mq|j}k^lPMS zvAIc5Ah>|nOOcS15!Q9-u9dnL5M*H|Wa?8sMSNjl_hKk>-7t#7;n%NUTj90qC9|I` z;D-o6SvFU7LLeUYZ{r`1CJxDRqTat>S&3T){xCFM>iETyVU8E$@yK(eK&y=N+jeR~ zUGykPn;uOaW&l5UB2_Z<=!Az{@eYQzqa(cooVYdW)Gt2VLsCx`C{iss076ZIsk7j~ z=B|!U8!imzaF{UoelcS?k~xz0{X)u;|0Us^|Hkva&E)q(5y9={o|G{!IGISWdQ*=u zw7&+IT(1jlgWEL^*_+l0C} zmvTR^^Zur}hsSQUapc{+tKU)e)ok(Gar&nFOX=FXP<;?1;|C16y!!heyrVzjw`(kL zpY6kuMx?$evR3e0C-zRcX><8~s$heXCPco(03$Q*=}jp)J`wYWh(S-AbtxCK zm6esrTEmvq(J4xK#_N2F2PEk)e9lB)hPC7bQtae>0M7xvmL9%Uajg7s?80IQ- zyiQEoRk<9%OIi3}47iuMqsdNwVCO0-!sfO!sW&=4|J>Or#i-jaCL#h!0@B2(zA!Y0 zedmF?q#VzdI(xv~j#(G9RJ)yeTf9bGm(xG)`e!d-9Hg~RHf)})Ow`sp33w_0HvR5V z6qU5UpmzG`R>!~h`_uhCcPm)@nQ#yY&Rh2|67K`ro`SM+cB+P*{&a~}(9Vuk-}&qy z#rePhJThZy_u#{y8YYOlnXJdbG(zg`cmk$V%txZae_)C*syA|r=vTiA%y`%ugVhG= z^1Oq;q^{U(B1RruaYk`X1rTQu;3UyKcmnUPPCt zyH!K_d12Q62>a$?2aobtEOCp>sqau5kbZd8elEC|c!oYyU-y8hxyo-^QtnB*e>3rc zrow$>>HR)t3%mO)rauhevaq!h{dr~dT0I-)_=d35J%)U)hqQOjEfR7byfn37CD?cF zAr^ZpM9QW);ED&F<=w(elFPS|JbcWb{iDKf{aI6NVL@*7?1yXIjXJ8fV<(lQxTE@- zdK%GU4j2eZI3|ZL_2EJor0?=u+22pw_G!e|2@$^jUNU{{WTAvFhl13x*faSmJw9s-#=`O-6(5D#gA zT1?~XL73QB*KbpgP;6RdI~S=$GL-^i@|#>ZK3B7tya;875CsU-a)BR772L&as4w$D zRONXbGAv~fb?9xn@7P$~`sS3Na?0BA`7fENW3ABr;QghP4@#Eph{ZBMO3V6LnyVOM{1y8Xm)}T&G68LVHhV{m%a=0goiXV@FiqV^ zOO0gq`vUPEsfH(`!H^y7-jE@Fg`MJk2?AckdwyM`F%{mEmoOW%^$cs5!mG;%_wFH8 zNxTwNHu0#PJP7*4R&HWXZ4!puIXa*E3Ssw!E|Ch{=r<)e%m-<3&O7o29XH<-_vm7j z!|ii~9Tm{r4lfEm-Ng6#5xDXfFI<-o7dSRE@jW5t#SEE@5vuq(>1^@vSJg@~-fy`v zk?{+kQqk~Y?Z^Ngw)Yl4GrL=gf!%^^8X5g{zDL%l*i9s(H3G4q4aD(z2{>T)VPxo4 z&JSyIttyn^R|-XM&+;kNw>??Pj{-(qJ>Q`4c)6eq5#a~MEPl^WoWkLKF|0uuyvFwA z;i3bHkFM=iQ;Y}tK7TV>(}Jx_$-e(3!drcTJoMu|Z3K?fQZ&6At83Ex;nUW?>O<$k zd48h-8=rcknOI2+$4_9l|MERgZqEa6iWm86w2o$*=kK=E$YycDT7H6GEw(xyfs1NK z3#7?q2Hs#FNgmuMp)Ew>u;%l{;{-KQ&CtOPHo_&|)#}~2%yE6V9zJWa9zxwoa7XQ} z@mYF0+Z8;c}imKNTX zALb{l!~r85f82L_h`*Cm^(19M%bI)s4@MQ`JBm~uKtl0i|NraiTAv>tE-bf3JPceti4vz1I1Enof!qCcKQDBh=}ad z-^Ru@w*{W~+YQt|U7c&cKPWsu1cUi^4IQhxsh{+^bH%!)ARK^G$qjnRt-6lTBbDM; z_mLqS4>RW zJO(zY!(fkvhnw|ow7CcT?V^+z(Dw(IGS%`5$vU2iydSF7H#@42TW=GOj$;+c@IWgh zFXlxLO6&I8N!t6zVI{mL&GYt=4l36tv+3;E#-TIpI0iul|K( zB}OyX?7s=9gd=g@_=z{8-;Q{KBW(cnB0!ANB&NXC3z?-$Zja~6<%Yby=z9mWb8jn> z^g#OtFZgCQ{tH^lQL(k`^MmpY_0hPLw!+g0vYj9DkhSKU`S}kPd1b?I;4v$aZ@vSq9Q~7`i)PN(~ z+95_Xk(t=;G-LDRFz($SqNxXDh#PA|-M>5;BEQ`NZSeKw0stFuM!FpHZ$CV5hC^!# z^U!_hC8{x#JN!}D!d#dI;>41WNnnrnaKM*_!R!j zs*$vjim0W#mcg;q7%zo>wOR<)9u3;W{y0YMVf%U1CAvLYOHcW1Zd171F{p8m2667r zFCV)e{Kyjy1O?wj2igJ-QIhybs_|Ci!DmUyh@b7>9;J0j2w$9$9%QlSu15QdVfWY1 zh<$rn@@BJ(ek)5)WY_L>;nX`8DI+F8#yH3+z}u+c=Ky2*>#F#gpxDXEs0HCbpbi`k zn8VhnBnM=?RhVF4Dzs7Rfvp$a8XzdSf_?QQPta~TJubUn$_f94iSo&8*osDWrrVfvZ44;o zSaUy*lBuQgk0{TmacWnnaRK#QEvQFXbK4fZLT^`D^xeh-#Z&Cd-)YG|4xef!5^#_a zrjyaW!kzV~)(nC~npIL9m+`Kmg$oUx=vE7W<^hXt;J+w}qsXT%Pb-rcuxnBl4qoS# zU3!&}TM=3txVI(oADAA3N6KLQn937v1&GRLfc`))ga*H0g4*X!@4Qxy>mbQ0YpjXm ze5Obn0l(!#&-%+ptw>5hbKm}zS@6Z{3=*&Wnhf%tAP?cfkySi(^WyGSH`=akZL#KH zi21&Gz|$@PUW)hXYF&IkW2-yUXp~25Hyt)Rn&Wo1e|?DN!b!soV?)~2ORgGPL2U5?%oKs?5$V(DqWxvd)s9MFN`Ka!w=Vjb-;};_J7R$!HyW32?RDo}MSqoy#w| za9DRe_(hJ_byaPMrTgnchCeSHj0xEh3Lk3!l!=P3KW$$~*BziwVksdSciVmVP=iK{ z8cSIL2DY~oNiMQn(s@yRL*;=Kyn4}birl=KL3IYmY}y9Y#QJm4i>nQeR6u2^)(EqO zA9J4le?k(RpkFj-+^^=s*;9&PZI|3tQ=lg5%TBA{#R1JUy~t0DVoTk?N7Qirc>Khv zlD~hw4JbS60^~@<@#j)~3)cUl{Q$okp7dNc$v(Kv@XS`Jc;_pihH8n$PsBKQrerWc z{h#zJI1=F0spnF)Zq)!JUCpsUJ6orCq2$pq9Q~Cm{#TW2=TgBcz*gG)R6bX(0RqY8 kzy<2-%CqPE+pIYN^|zqT8ZPMbxcs+pW~SEjMXlFeDAKB{Ilbi53dU zkY%K3FO_A|CZV#V<^R0ro|9Xc?K39d_cun}bI*B~=lMOqXL;Xq&W|QWdXf?oB!q;7 zBn|X+mI?_C93><)V4?UxI3lI-qFqQRWg*?r(v;nBp@^^9NIGK6dGtue(046x*!eM*BCo=r%?B&VP_VV!Z z!Q@9JDWU)1xG%@Y%iW3i#?#9aF4rPd_F*o>aGZQNPB=C{gJnS(6Y2nTJ9)94I6i?O zZTxZtGD%gH-w_UN1!XEOGXD=iAuA~<@y~J*K#3}Grag{MR!{rL68?=gR+t`7(_37LLW@*IUFY+PvAa~3`zAX z*$f4-1R@GTgB0qgG>GF<&|)ZR|E9%IzO=>k(g(y=g8b&Jg#H7K^zl$s><=98?V%9w zVSnHodmknq@2Gqwje?89;}$}tpCUmrNg3C(pici?79``8AYhf+=8GKp`24#;#aCfM z8m$ix;%-Qg1(C}~hSp2o__2S51j#r|1jN16$U`nSY!yA+VP4xT!ETZHnB0Q_UG{B4MvNZ@+-3{aC^&x$P=}KvRFd#|2VR*FsAxC^xN??3M33?$suucWjXvSO>!312&j$oO^novj zPo!?XUR)onasp@Iis#t-u$=Hx4sLpAK+qDL^LL9MK29F?99KWVRw$%@B|;$zaG#O4 zmk-Cqi{<5M@1gra3xWlu58{XH{?E!EYGXcu0!l+4k=9V%1N-iS+R#@z|Eb<^5B1-q zHzhp${Y!dtUZn7mJ}%+^bYbXckinVOEM1Ldzdi1mE>&=!0enRbtl2 zw@pl!JqGzjG>fM*Us1EaUnUeRds4OOK0fxS)=5~lx0kCY2MhlHd@CP{rzYrXXbnja zxJQAk+&5mt`RR}Ao`S>tD;+5Rtsbb_Fe1Jz{>;3w(*i zrij%)!RYlH5ja$EdlgN|s^5aZsW`#~u#9{Y;dK9eIECD2kJIf5t^UC2_QX$or^~jypmxcCWc|4ZWkTj4B8 zQ3ZJAgLRP=NhB)%`LCH577723bs_sTaq@A66>lfB-u|aE&rhhF$mPGH$^CvXB^W{e z!+|cDpJw;0pOMLUtnZLQ2MesS?yaW$l@3h5V(P-FUQ`K z2^;Mx${w(=ujK%2uFOKeVT%z?o-DXgrr+~Bh1Rz~{!QqEsND!C#KZ^P6yoB<1*!Md z2;2_kZ0~8`|J9-J5&P2!chIlhElOQb{3xIgLoXJ~)syuN$o!)a=Npmvx4R6gf)AYt zalVCBXN2!Q5dUUYo%@KJV9)+dDfm*F0Hv6@zzvb#y2DlKqYR>m-@lddaXy72P9INL zl=_6f{akwM>+Zy1qMLU6Y!X9TEL`C(U%H1Y3*A@6@$!Z^%ER8l$-~6U*OlYyg^n_z zP=uDiw20FU`<&q5#o>6d;RJiUot`Uv;Z{~~M&_tp9R+k(mq76d?mWalHLqBMZSLE z9JK=Ardo=&iM|oVCQys!U=_e+@<^`s`b$Yn9WOsasw34gP>IS2RPtl8nSKm6x<5l( zjpxW_x*F)aa2)iMc*dUAY6ff6_sZwlvF(->wnB_lUx07J*X z-^iR!HP+D$gmWnNR+dy#wi?aE%picFP4j2y==w9?rAiKZ7A!}74}TkTxW`)0lIEzb z=EkHlTuof9-JJAvRkY2Nc#b++DmH8nUk4p8HwMpuVok9i8&gR{J6tB!G{A{slt>U;Rwm<#l)w}@n~t>xyRw}^-I0@wS49NbOJ78@~m z7BoXQcbZWk9g~F-o53-1r3HdqxeT5z&(K^q(9n(H&oHA07`m~@M%ttRqd=MxL&utB zq)j6;c#9au=GI&z9)oJ2qss%C^Ne&ByYa7M1;BL_LpR+3V{K9(=q!L?ZVj?!a*fZz7eE<%$={zGdY)meI4H&-#i}=aVru!S{&{;4BiV+$^AdOaQG!u7gJ zhGsN>Be%tF40BWT8OBV9^96XL=+^?1fg#Kd&C7`7&jZe65!m@c@T_ zd0iIZ64@t?OKa4>zcI`W=EP&@xBw1VV3SOcBk+eA)+Yt}V!>EI_RuHl3-JKR5pV)O z=`7GK&luUb8(iy#bd6(8$C`um0=k8M3<8YJNT_Y3CxlHJ(jn*u;RIwz3jiKqp`XwX z3t=4e!-8=IFnCPB6tsie1bi3B;M-Clw1@Bp@`rX{>_%?Z$VTun8o(GWkj~(ji0_+3;eleLC@sLcIfuKiZn-stY>K8Z< z=@W3OgKQJ92z&{8g>&6uPISPPHQ<^=1rDTu{Re`apiKtH*c|#cM_gxt=^NQRoErf8 zhM&Ndt|T~aNgS=alv^k;2caAV9$UbbC4Iz&sYa>6Vfvp8>4&e*}f6XA21I3107&??vDBd9JvDr(7?Z8dJh13 zB8c1V9VGX$pJ9uhOmX~ z7GVl>i}*PZ=^wa-=MMf1bZ>47IMSuy{KS_%;t4mv8TyI&Rs)1Vm@_suxQ+qWAw0PI zV>r?=MLI`wL}Nz&1KBC27la+e8DJ}j2N8Y{1`wtYpTiiz|G3d9FfPo#@p&UZ4&&4% zgWm9%hi_{zM`WAO7Wir^%tsse3*iQ{Lkq-LnEyfgMLdD&hu=Pof!`+B0Qw!;Ab1q% z19%%dZh+)L9|U4So(}2V(}TAp3XIMdKjz z?b2Ea@gb5ga69y63VK1G-6_Ua{-7t!uVQmF(qkashxn5OeUp&Qf&GE4Aj~1%8NhKm z%%9KO5MzL@K+bG`3LkuyCa`~e3uf|9>^48d9Xu_Kd69V(5ERGY#e?v zF@6E;;C_b=@c_ap%nkWE9Y5+fo`#z*ggiADkxQ3+!KcOwK zL&Rg?e*m8_4&YxN3*=w`c*Hmz&M_cxiDE+39~uY93&j^GmPfimJ_C5x99;)EwgOv5 zdPeaJW($s3&H@|&=b-o(YzxJXZdlw5eiq^$zO8|OK|TP*$-o)rNdH)z2ztlj48+^W z{(-l#SRLX6usb*8i%{Drrh-@si%lRO0DOXMAdiI@0{1z9SBQ}I$+uYVG+rW2J=TTVj%cI4wjSY7=Z4OAIAJ2a0kdk2gQrJ zU<1IhZU%@yVcfbjXq$#O1lcu`J?H>pETmtIA0XG(<=YbajJOPJ5ylKy#rOpCpGa1~ zPxzd`Hjz)(F@P8u#k8PjjLQ)YF}oxg^XE@R_K(`a>;c&|;sazen12VJgm@T@88Cxn zM}v3+#$^Hai(*nNMnW|M=nLgbFm4n}xj~=iEc6-0WGLQ&>)k;Xbns#DJ;uK%CIh^K zUjf@kV?^=-*)wq&VR-=biDZG|P8Jm~4mmQC6~qZ(9}ovpfyZ;jB=8}`3y`beI!3Vv7I)H6OaWXDaSn_N^oL?%#G_cQ3ghHs z3itrSp8<-CkKnzGiz7=E%xiZQ-5zeuE4&nnOZyt)%5mvGJ zV>|p-TpRfSw^|3dH!BuNR1! z0Do{C%L{NkVB7?{gct;iISf#Kh1x{^5zP_t62d96BXkXl0iiGWjKwM-MK~XmJ+c{o zE&;j%n}?hZFoom;@c`I6io+rPLf-+75$9lfv<4pvwvK!Ra53r&>SCC`VnHl{Fp0Pr zuqR`4L@_q9AuLZsJOc4L;sBJVU}Hz$!~EfVlyia1`SBUj4XOuXafy+e2XHZp zM^Squ=}HU6(9Qyh)-Y~G{l`) z9Dr;AaTnrHEDyutG4R(0NRQwnkzZw@91Y?vWNS!9{G6E2{X{JQ#jIdADCTg(;x6Dl zEcSu=57+`0)1e#z#aGxEKyFOv18QIxzF0KG@rZ8_wt7wtzKVavepZxk9KPJa{5#kE)tzaK8K4kaMF4UFy zoPpW55{kbe#^QlpLm!B1bpdMI0xp6xB&4o;1kR5 zpbZPKV`RTrJpk};O*5i;^K($})do6RP?rU~yD@Pefd>o7iWCC$8pLint#>gKxlaN5~N5fpSQ|hLbzgVtH7ekN6Gz9`GL9A94>Y4q$}t47P}_gE0edqP*7v#m}hj05%8Q%=ZB#CpK_BZ~=yU zloME?+NlM~7ZJw*e<1&l?A{&4e84dQ7q0g6%J8oqyr`M{V!u2_zQ>>igl_(jkcv;*_Q^oDBUzp_%SSA=C3t? z-vVAh9F1Z-f_G7E7>=Pj4#F}I)ocO#Se%970oDJJUw~RM)YVWO9OcA_7j+gXJ0P9A zLH!b|^MLL1<$&7>a6OiXq5Pe1qevL6!oLiiuW#^7%-yrbGCk{8O$0n5n8xGna@^oPcdbcM!- zt=WJs*vLjq$w+QSkPpK=QT+jMkK%F^8(?@uYZ1y0SPc`)_j!OFCFGN#JrqkLJ3+RL zxD#Sv6c=JXgs7WgF&CVR=^nKWc!6`lrlB?nm_U9JFo(4bKP|m5o3`>tbwd<;vmoXM znIOG_-ShK)s6m3=!FVAKMlk}vu7rJ$_zU-&<^~8us4fNAGez|@7z5%EXcMh_LERW~ z5dJ)ouXVHZ1#afc2jeKP6)UhM$T9fb>5jM(VH|$;a3{hi_$R~<$X>i)J=BQAWUKk0 z^+5-=8rR0m0M0ZDdcnFS!lA1n4`1Ws`TH@cHl8LdSklG*_I}(a zf6K=|`%nM{CE_8~#C8}Zsw(lgR)JjsG<*Y!i@>9ciF=laM}vITCl?csO%~XpVd49U zO^@)9XS6L+;Au;}pTdbhjY;rPp7^1U*71pze`2Em+ttw#J;C~u&qVJ1_=jGP4gD&5 zrS-WX6dqEIZ?+WBD)ICcL9G&k{dctb@jfRVCvOk0z>jyc{;w*QxXT~6S2Dg+sb6dL z6E~s|TJ5`Ame@b*N^EEr*y~JUq6dHAo16Py$Z=wOd)N!?2!i|BdK&p?Bk2Dqder)C zxc=mRYy6!4LkjqS{?d(u0)+orz7@O+lepnl@a7F-tKxst-5bPHpZ~5fd+wLSA6AGS z9$^i^OV90-AMRlP8&5~Wclrq`f!N*s6_wC$9vh)RBklqvqBAPt#sBzjT_iZ-Z~wh< zo3J~9IL;W}=kob`R}j$t;3X7-!5lng-Pzem`7@8%S5b3-8{B^9X_{Z~`CAHVv_3bH zQmG$@WMl&Gg7izoY5z`4_VEp0{W~W6EZ=`gWvZy*fme@ZenOdq-UM~|ug2Fz%J#R} zAD#z<9(szgeb2MKzQ98esQ6tNg3OOw%U2|RWPAVULlBhkTa~`7{QvB;)kyfGPrt0s z{)Fc6Ebc36t{=H<6y_Mm!bblqA233=5rJsZ1d0OAUOw=W zLv#b3fYLuy=>CgBN(%A59)WsL{|hOJYO;Wu{y(Ht_$Z|OlrsBYNQrZcfMp@SZ-(E_ z_Q#KlMbQZVLHblR^!6)^53NyB9_qQCW6OX3Aif??|abg-j_ z&z=Z&$TU1H`a;2o0d{ZkJJ`7iQ_Wa8bd{o5nr zZ&~|~SyX`p4+F;1b0W(lDUzX9@dq@DPX2q0h?lT>G2(w==1x4SM$nS}@HuM#MP~GE zJeGv^|Fz0byr24`!17bd?011B@s3~ti~Ij1;S)&$2@hce{9FIiB_0(D=u+Fu)7h29 z_35{Jc0OBK`J&4Rs(s$VOC}bH`n5Vgej>{syixE^c*3tDZ?(Qpe^U9_df-ju#11&Y zg&S2o()m{w;gmjp3$F>+!vhvd`|g7wj`Zw(am2RTupzBR&wj_GotuTX*jdct1~`H@};`xUU)ey3LA9Vr6l`lZTkU&9Y7s1eZ+ZcN0h-~?n&R>6Ix zpe_BYp%F14L7|&3^6sbcOfP~&A582y@L)moh(V}bkW`^ihn^1jsi*&a9SG1UMDR;6 zDvhWj3&Mcd)F23he>EH;Hbn@);3EipSn@>+?|-z5@ZVGeu}Oi@!DsHuA>KJBs0FeL zjnLq~lBy6JMg$aL=H%n&3NIz+@2{ee_`Z&xlJp5lzZn(v+2x}|{?u(#J(-1|f(WXKtA~exJLxC8p~-z8Mf}aE>=UKTFR~ES+ucyHKPVdLqdp~P_z#ZvzKs#K zxhrFT;G13{#@7fMKGNmq$Ph1L^&c|C^L+tVrRU0V;W~U#i*vMhQgvp2Qe{k4CkN+$ zLuC}=&aa*T<1_xN->K~5%=sf_Ie9wLeSEz9A*N<}*!%jrBH;_%5cWAo^&jlE{Li;K z3-~Ewm$;yv5XXF+U_Ya)A9@Nn%o}&+pNb^@%8uxd-X}{Xq1(GKw?hDgMj{i3i0=4X zOz~IIqR8Q|ZOZw?-ewx9CwBdve))S1{zkvpMjduQ&l2hH%LqSP+sVUY`X8Oi4!}-> z$}2pE9chMG*Abu=Ium=Ad|zkYKZ>xuoW|!2gpY`S_K5^k1S15d9sp2#GExB>A;LC6 z8~IoBPa>ZA+dFtZxswrAB4FnZeDz_2IQoEO)3bM+zj+cnv1iW@x(a>xa2fWAxb8n- zI>CMAL}5U1^AElQ<3A_Fe}w74FW93hBSB%_Uap=TSTTXusq0QB(J>AK{QZf)pI|Qq zC88=qU>BAW`C$HQ%*gNmPxU|{aQB6J=o=UO&G!58mPp)Z{%t!&hV(Z~?z7w}Xi&uc z3xWoP`{A!>P=Cge|3}VI4X60Oq=P@^EP)X3)BD1IpG-gJ^)MpkYn)HVf0kwcoXTb9bDeS>nF@f6*)z$i#k$p3sKa5c7ph z=c68QKmFI5@6VQlbB{nqh&IhBu;JyPtZ(9V+~hvbo>Cwa8J9p}*8lD{B$GbgJlC_^ z;t#jaVGH<>A@ENGH1_u$bGy>yE{e0ui)?gk;a`ImPS^xO{kJJ9HNl^inpW2S~>fsf*zUN4mN;&MD( zJ)K}zyC?sZjHo=*C!?THDT^S`rfRW#>>XVptss`=KJgGrSkS>vhX;f9S?a-0?tud8 z2w5|FhvjEqN=ckRJiC(rlFN@CaVd~N34~Lhd`IO+@1Dd;DNa7RelRg#s86Brjd)=t zI{>Ea?V@P!?@Lo;xpG$ed{}e-T2Eh6#TR>eph&z8UVx^E4NrnqMH)#F5B&Zeo+36U z3-A=(hw0+V0i40@6Hu)30`2;_I{ANu(Ed?c?qdZ#Z!;A@802ewYa#LoJox)(OU(*2 zO7D%|3d+Re)dVm>`4lL=hU>y&SsyPicz*3iwMo4E0`Hdhc42rqI`v+mz`k{KwFjev zH_~Dk;}^hcGAtdTBZ+EKON4}^gy5;h<^jUbDu=Au#qI2VnP(^+c1vbj_*qHQ`P27l zA6hE2c+L-&`#O&r-ZNzEUfg--zRa1cYn?Z2bakE)*Kzgi_hDK~oF0yF`oVIN$$Ckr zRiq71rpdi;?!4|^IdR^Dis!$)$#}kd_~p!`_d&ti@)`#|YJX7OoT?Wb+Ra^RYQ zZ->4Z|B!lx6`Ch>P7Xg_SoML6Q99(5HX?GH$o!yqvQO0S`U*RB4wygR;|RE&<|bT zry(Jcdt+B^FdZqkN9yRQTg!V7N=G7pj6lDW2TK{g6%QW^jmvEoxigp;Z_h-2o_l5J z2>l;m^yae)1_@jVv&ax)2{it~E_7A+o86m)r-X)fi$q9$x~ZEU!r_DW26ygVAkfy{ zsaFg~^mHku8CM`8I!c|PrzfUDp=Tg69|xF7`jU9#>H7%KL|5)vPgtTuUY6H0y#)K6pR*0 z7Ll1MpgZg$q(B&zoYZKTqUnY0cLwxaEH_>x<;M>#qu=u7G)aeq!&aJ(S>^fJQOQLi zaorjdAN)c4uf=AC^=|56s!+CQ(p=+!fj9I8#fXVPu3h8~rVBY=z?S3lRMu0xyQn~s zJL7tW$d_4|5dfl2+BRd(y}1Gk;$NY&GIabDY}OACLXX{vgQsfYmz^0xTH7;3yv->y zV1_>oSuBXRz72(Hc7UoMr~f<+@9pjMD=q@+K&mviHEEc)~m#0)`Ybz?QR}zcT`tW1ENiNXwn*D<3=M>84;#8De%ZG zLuIcj595XJ9Cys0T-DlKUA;@e*0~~$eQxG~fUCT=hMEmWi-f7HyPO**+JA~%Xr`2Y zrq#E*^G#-+7Mq;GW2{dnakxWL0m5+rBQ`BHLw9%k#V5+`J*Ib(=RW1QHKF{zFfHlxxFvir=Oy&Hx9ClB< zkfWH@;Zss^^@kw|7cV}Lz0s9l)mnboE~v;oX7Y4d`RfalH0hg5Ye{u$mgK2zV%t`~ zwz?p9qN%hcnLUtw!tmMaMJf-x*98sK4r2beU~$OcIwzllr*gj-w3$I~Gt+^4mCVsX2T>S>y+VCUEVr}M& zYAlRvEi3~UCkNMGyghs6?AN}Bm^DA)sFXV-LfarD!WNe@bQ!SGjF6*{$gQ#BX{rtP z4sVnkN4jFPpv}A>nK$fi{rRIu5`NgpG2)tA-N?AN@8H4e-_(ZQwhDc`hZ&h%8k2F` zYU#XWdYGKCN(A>o*^!uRJ(<zOY$ji zhuY(fXWWYBSS`}93u;ojbiR6W`=!TKFFw$>P?$8>fc^+trCg|{jEFQH+3!tN;mTRX zl1#>Ol_>61Yl9oN3)G_Tt#&sM9<<;WxzcpInxxC^ZdS`}XINNTW&}6oX>_mIeJmy; z*Q+&Sl%z&@nzyaoXJ2T<)I4t)K-r2UYv3)TaJm#$arn zcZ@P8NobMxs1?oJ)AMEBFYdE?wv1!sRNi35&D`#Afl@(gf$uCMjZRKUL}IjM;&~NGjaC`S%2ieq>2h*uO{K}{&JXeppCnybcDLP- zbEG<502r(!EU2%8W5q zlxxN8rezg0vFd9jV~qC2++N#gTTN*@#@woxl##urf}LKUzd&Wm`SFR#E{!wAROU<+ znaaEo$#vS^Hq~!`!{MW9v$C-Nr7JdgmQM!SNZj)@-u;M5hI59WdNE^duGNj?g$k91 z+t~B`o;7VXJ}4GEqfjqO{l1sU%-2engoN~X=a|BwSp z9puD$Fu$gz#=0fuWOnYdqHUEDi&V~}@(!iOyi`anx|kJtd3esxyo}YSJ6}fo`1(#v ze?QoBOk+%a`-Y6$!zR#fo;`PtTesa(?R(p_(`3?P%8nO@FYL{zyD4LS`Ka`A?bIaq ziMH`)&g6?3?>x3D%=2dB)1Vg#ovXNu-Bk+a&0EMOA6Z-I71g@1YVA7p82gS@M-JdSNn20QFEkk)dLU8l)g<8}D;y?+gM24|jy7oxQh9`DLVm zlf}#fuYmg2%g4*DzkIK$iq?@ohO%zZ@HAV`3r8PShJ;LSnKf=pyZGg;iBEQIPgA=8 zdXaibM@*}4?cLP<`(+yIBqW1>UD0A0v-nlimdGaz*}dBC?13tGiu_xyL>(9%S2XD4 z#Gu2=tIJEyP1&izy1PGzeaPY0y5&=Hj`@X4du7qzm6oI>+Iw6mWwNgyjTS$9Z$8(E zF1mHS9VK@Z75(7N=)$jhxIa zxts@fn}at!FS1(6&d%j5cwnXC*e3Mw-eJAWjpb=2mR5O<6*bxG)*mz6dw<+e6K~B5 zwb^3Xk_+C`hf7%=%vR-|mfL)JZ(>YtdDrY&NxllxQxDf_)a`E&RcWq~8(!l-jq5lp zA+^G0Vls34j>Hun6U*=FtCX+1xa)RGvEPy$`n;w~IR#!z*f;I6hsCA)0S`=*n4~7JJmw zyKzyv54Xia#hxn@v-6nmZ8Phld#i&Cs_QOyzOGn7uFTKP%TuYmW+6r2o1xZOaImYe z@N(76DPq-%R<;d#o%Qh{&z8{!i4EW=p0|F`@$_wJGE?FFp~Qz9Z$EwdRC%rZ%Is*b z;6~eq9Ggb7nd@G2Wg_&3y)v=WTdG%^zqG>b$)k;vmCJ6ZUyC#-F|2WDRLQzGX$?~& zEu!Ig;wy)WVMF&#JheP%_JOsw?%E}KB?h&&85?Zahscc=w=dk7UAw&5>4^KOCy5c- zZ3e{5Ai|lp6vELdN{1he@$E@kM^20LDtE7#qI7KHvG&)gk+zG~k5Sb9^A^0JB~tR= zr)~GVZ3SLzP-)Jt0!Ed8Rp5=tdC$+6K2w~0YC)xHBzsnA@$uB;SuXkaTSh+5>RwxL z@oM@?q83oqoy@LZCO`2&z~ditnE|FP`*VQex}tc}w+pNxA#Q&NTCtTy8Aql0#mj@AuuM zwbMmwnHse=N8{we@196l75!sXx^hS?2I9e^;=bdDpf)4q_T)IZo^<$+=Rl zD7Tnhk+#=Uv2lj4`;%g|Xw$NX@!lQ>JTInOT2*&h$`(m(Z@1{C$EKAptvGqC%fg83 zP~lOv%hTo5nkX2^PKU-+j-8W(SxcJvv7<5hxxZe2U3OnBSG&bDDA@_zb*$Om(qz#a zva#AGN$=eAcek6lJukg@@o9q3;(cZQ!z%Q*IyGoRh1ksw~E=}iwqbQ9NsI~%8JLcCRgR#bQ0?n(a$B6A4h$!)% zNXkZ{iJp4KTJdSrJr8dT9WO>OU{5|X%`7wx&m`mqKwFMibe%Wz$C9FO#kz0TQkyPo zmN@H^4!N%_6in>VY~_?hTEmIHkC+0iC%mkmfPHgLbdSPEUF)orJ4v0kN2*Bv*Ngb@ z30bL6)s*e&I&2J#De@~iKarw$J;=UC$HBJ?=AT&LkE0l$pivN@uC`s zGcdzY4Uq`Z-seL1n3lw$L(!2Jd#8T)My*;TprmO!(4E#eSJZ_5E}^F0C&P*S0z?<2 zkuSO@khOXS0q4J15&BYq)TLGfsgKCIVhBTjJStB5lhd0Ls#%N5QB{%q1V$kjWh2b% z;SJ>OX(%=xHNSF7)LxN0nDhyuVlw8V5UNX}qkays*gHy~uN5QW6a_v)Q3O?Cs8Ur^ zq{M{rES^v=f)=W4p?S`MVI5f-b6G%#Zj5M|8yzYq+4{{h+e%QDE8Dc z^r31ct^eWDd!tu5fB1y`wgTKkzLVAj{WPJbp3Imyhp765hE~H2J1#Gb7)GdAe2Wl~ zlM;tEodH7>U-=-CvGqRDLeC6|S`^H%76#9`yVZs`N0TR7H=1aKA8mo)Av4n_tMluC zJ*Eej^o&VJ#1tjnsq(ki;Ifh%A(GOkd<5bki;R$*aqUi&wjgG(ZlG{kKooh*k)oq% zop@JQo{Rh@*ef;<%~_%u(uU&KhmI#IIv-Bq7=+#?CIt1Pg#-8>dQ|b@Ol%6<)wRP1 z8`KlKjKR9yDQ&+z~j%;G)dnr zw%Tz4&FM$!IE9{G2qkX1EJW3jGXgtYtz|ltIEpp*!ZegDV60IRC;~Hy9}aEAafR;+ ziit0CtnhefENHrY$<1Cp$-NV4MR3Qj(R79>-h9@W_iV&X8DVDuM}nWjx3DN0^k{JS zsJ&oYb;pHjP}KBV)Xm+UeRcg{<-k{tg7tQ}r_{J^jkGu2_Y(Kd+><|gK?5*|uW%JI z8Kbzzxx+o~-?5SJcztq)LEtI3g&e21JwW6vlAAg6M!fMsPhOK@a06&YOVkB;ZvLqJ z?y+8(O=(9WH?PuK8t5v62`cQ)NE4|?_vaPwy}w^K#y{rbtGoNp=fql-RZ2ubo3ls8 zO@xulJrJ>p^=>P&s<`&!D1&hy76rmKI2 z?@@@l`g-uHi%o;QCkb3FDz#}dSbx-tTN-x`u(pip{-w$~Ro!vw@EY&&Gz^aNA`(+v zGJg(OCqBSOqj>R>%(J8QO*H9EC)nW<&t5VD7k^_u~))x|o=> zVrFo{iKKw{n@+8k&|MaKLI8OPIn!*RhqFJ7%D*f!c(Gf<{&P8d?$ZYhBPA>iaC|-A zY2!RH4?{__7-wD9i;7c*NwSx+tF!VB#25Z#aB_It(JNx~A0ls>jv*xRVp-@RtdI5J zKErEw8_N%u);D<(*D*KiT+EV?hub5gkNM7d@{Trc5+;;QVkwch6b;;iYKZgnAHTS}*? zheX54h`lfQVT0yIF=?IrVr|*Pab|}OpBm|KQhAcw(8XKF%X~1hlo^@}`37VFA!L+v zaiQ|b=)n>)G-HYHUF44xMsBFWM)31AlNBxY$KrjTzL@k}ZLsf*H`E1dF1*OU#K4U9jnec!b6msTl-AJ`ffNB3yCUGV;tV%E(n zo3m#O+Oyt!`pb`>w6}B5<|zJ@>vmpwJjU~(yeg@_WB-_SBDbxC=t&Z@j(g-Mi%9Ga zR;=LI8Pu-x%|3PeRO0P{f!XpEGGaY3M(9|Pq5Rbi$b+C%`c7=}!e=`aXT@n=+3@A&A*w->Iwm=w^muCk$iQI?2s*2N4jrY4;( zlrZ1qa-@ZU?X=EUbGhmqgXW_6s^byi0~f0gdu20(Qg<&d+TOcb<5t?J-ee%yG8ZFwW&bH@0E!uP+J1 zoHF%_4i2Czqk#1eEZ>y2;ZPj?-O9DBZvX10cnk5`{Yp8~Iwnx=Ih4@8pC9_fQAI|3 zrFKP#R6esZ(QKL{7gr)D<=uQyy&(Bj`Qg8>h@Utz49L zD*x)Xw46e%)n^4ki#aSgx(IvFsF8((bEhrR3>zRe3XDSYJ95H(2|oA~hJ5e&C~$D@ zlDmPAvcP1dXU%s^%1PL^Z#TQ^a$s3$+P>YARX3>dao?j#1fuhNVd2d^F=7*{;ttwW z!2fPNU2Z({vol8Fn|``mAN*oM(CMbA)-__$7Hd5!OJAv% z%y%`Uzk6C6AW$L4JceeW*nKsP@WJ(k$8bkFfApr>4V|Tr7Cfl4u&fv*mPa=^qnT%d zEFe~G&15LDa!zZm6B#_`@O-gB=2q;w&eCn=g=GH9W0<%EX*yxhaw!l{X$94fQ^k0o zL5y?iuO^<=v1A2vb?or)fwv2j!EBh*ql9MQt~MMz}4;+gZYwbQRvz1Su>|G~VZyVBD()fAjux7pct z#_UaTXXzIr=P$ePb8NC0`xWLfqq0Q|dscQeM;a|&hlzH@=(x4*V*9E)ns07$ z7%;$1uVUQnfVd}du?^gL;o5J?J1+g|QKNSzcB%ZaTRrKF`g9XpTlY~j4A0!!F>}Wh zhq%VF6T`Jd&&`gd@{F<#6%wfO0-JRHx6hf*p44S?t%GK&(*_vwW z)bd36cR!8}elGtybxC5{yyttJZ@XJYq+`<FCJ+Ei+1f$wObQj-}~C8d5(613Hj%EHzU$(IxPY2?jYx034%j>XtbRZ1Qj8!XLr||tftfwJKr0;q!ezWWyQt7b4%1M4{VRTRqc7g z+hp?OH7~*zK($dh==HOdU?ZD`x9en`m0}v2uB#MRR97#r_P%(Ie97!wLDlk>Lrc^X z%TgNm!?mur`~v(tje;644QC{UEIa?;>HN%}k1iQkKRc;-oS=f0CGG z61QeL2qlnyU#({L+qj2XQIjkDqDneae=bWi$jxgZxJ9ZiLKBv!Rs^>dDX)I4v-GAU zM|NJz_&T>(m8yVWP?-dlxMGIL4V|cbCui$K7Hj|NMNIYIqO>Pu-dpW$bR&DP><+J8 z-w#SKeEZ^;;R#T?ZEasSx#J3_Nu--|@v@zTdsbJAyXA`ahY~8z7YwXz@;aViB=n(n6aSjFoX8 zGUez)=dAD{bCRTY-cws(MNd*lAemGwZdek0a%-IDi`%}VLU~_9xNY#^uR8p zll%T%UWg|Yke#$<4UB!X?oMuQYJssvW#YwGizm`tqK-CZ)H}DX2wCPZX!tW$g~pv1 zTSjiLZ>~6(Dck+yn)i=q>)Co5c~vA+rwwR+Od33cGlDdXK z|DoE>X@_&M@{DNr*_}RBi!%M~V(Zl;=nGO0opqzCF7(wIp8l9FKaQiQd}8^tph&!q zfe2hbJ@j%}V$A(^<=~7PTHUgvOib)=M9Rh&Chub`@qb!*mv=5iVWk)Apn74#q&mZi zg$4&h!zCuBJxghe@!OZ^)V^r@z5|129LZ5*Uz#;5ZnSvrjx^=_ad#Is4!uM(U>#J- zxn#A%{|ZV1gu)_5y42g>%*dYnpu_5lW_D;$_tM(s(k|YE$Qn7rn(w_$glOBaZC%7w z?bi~+DjP!|Y_rbaP;UO7zLK{!p>Yna!^Jg1vaACob{SJNSMV8ET_G;PXW{#@ybN}r zdZqa20>u|qLCs}(YhRz+8Obad)aG~RQK0C^r6&)(y?e6j+;z|QmD{hWJhR>+I%etYEoGK--{}_5cbzx2t?l{ABA)AZo7?V7Lsi5BW?zYe;LKD+AwkrnLPFwNqIf=| z>)|C!1(>SO;%te5u~wRz!XmPRY8`$(nfQup$8J#7Z=kA125+ssAdy_7*29(1jI~I4 zh=y#ni*fOq4lk8=Qw!(p`0dTvQyVi9+A@c|9nT9J?!WKSg|o`7EiUfB&E@xhym{ei znCK;q_lHLh_J2Ulb+D3ixgNefev+sR#}LbIZ_0!%(b&ySR(YSWwOe{xzN?sAIFDBx z_G)K%i+b_N0hi{SxM6yJ6p_Gg(w`UtO;&F>ihI zunfz^5$D`X+$(cpUpH!GIjdB?318B^Cg>8o(yn<@>ABWeo1k;wU8s)exET<}Z7kl~ zwfg5)Sd84e%ec|jxFDW!?pMXk(+2l_9yC>?$J{@abh$hr@$kE>3n3?ipTE77(7m>) zTZt||sI^0M#VoePk(dsUUsdCNi04xpG*1oZoW7#%AxCFl+hwu0_`ZF`v5Ma&FMqUa zvU}p<6??t(f@ftl`feOBtmATH`@!^%g^%Bi$l9jf{r-4r*377Fu&T35amBt!&mC2) zc)vK7UBviv?n7QUPW(QIrF}Ltu2Wh7fv+rJFXH~r~PuBa*I?wJ>lLmJlo2{+xUU<*1$jaEwaKNBv zUKz>9X+EWg2LGD(ysUDkjeGZI`NWjwjy!I0Qnj#UYKLUS#no2RTA$j+mzQ-nYpKNR zM9$m;73A+iN?0Nr?|7RCnT9B@dAaj2<3vziFxx5D$hPa?aQ4fIjZMbJbN$1vzmv?) zQK)EO*Bbd|XXkec-wK^KO-ghaqQ^H6lb|MDmet+q<$rqO)3UO%*V%(6?|Ic--Tgj9 zncGk}qaH@%9VQwL_Tw_YWN*h1zxOV2OadDbqmg9%s?A#--&bqg& z(o*$Kp#3?tj(kd`e4qil?))XrOIBt@utru+ZUVJs`_`pa!G#+}Ha*K2ynfy8VL9AK z882#oOYAm&EXCOPy6f(hK+&bMP0Y-g7cN{l@of9$?X#k8yIY&37rkyu^&iR7E;u2b z->_bGM2G}6d;PGrKesZvv*u@AwM?jZeBk~i-Y-TKKc%Ki-mRz>6ZV-9KFBt0Z`0fS z2kXotRnH#`YFec-TfVC-?|^;5q02>&9Tid z58gaC_W-4^bFYty_L-NlZSxXNKGlBzOL&dv=wBiNlWvTuRTU8_XSUtbo6N0FDH)Ju znPk@_GSDWB1X~5>b+#5XMNseRztb>LoKRWGX<0bQL2N(@<0labdSJrRJ=6;aXInf_ zyRtK!HT!tP?Kx3W&70RVhgTKQW)w{qNQ@tDxdO{vl|N*idQTaW^V6`fEjg2z8FQl| zFV|D8MXYbRm2U9^xAc%QzjJXaqoZxe>jznu6_1ZLvYT>v1=>r8)m2nPx`dgM|um$fqJnDeoU%^tt{ozqQ9^mBSULQ$A) zdn93V%5BY4k`hCPH04b6N?H9Rr)==d{SR{;VMYg7(X}a*OTjS1`mX-7iN@W`=33ZUBA(2 z=}gkgDzOud-%BnS0ee7JoRVsNuud|@--g>{)EIp!>3#f*A6T-J+aJ;=7Ti5p`%rw10LeEL6_(f8N?2 zW0%SS+){2jxDHTB@{=Zs+L%6|05apx|{@EXBdTd*P+Z zVm%v!qqXw0X6<{n)-@|QtoC3?Rm#Xm)!kj`LBqnEPf6Lt09=_Hnu^L@8>?By>GxON z9U^J^)^dr`fZR3LR*3LtwbKrsK0vKi6+IV>jo<|d!^oflU5qk+)R64TIKh!>(acqNwGgmOmfi; zcWyZfcyZ5Wk-;S^ zLoUHyLe-JB6H+(wg0_oDNK9Ma9X+Xuo7ZD@a$+JJqIjDuGJdRK!1I--*pHJ|o*C_) zR$AP2ullui#hBUG?KrT}r((kF08;bIcdui+FWmLK>~wO;Z8J@}lld+f!#%0|IX_%I z{OZ{YdZ$P2q@c3M4FPU6=UrXaF1Ckd=lPvyP2In?y}o|3iNvC~n69zGX_@s(KUL0- zic^Rjd4I;%o-*BcW8c27?)FquQ%ftc3%Wq#wYpTCU(M8VKI&Ik_27cRlPZ_RvJxa=D#g&RxT8-MzJN7p0Q+U0*)~fXFTp6zc zlvPSs>++sw9BOMhSm{x+VxE2av|Dizk5hH#4D8u~XNt2zM*kccH*W}Ef_~_iRSo`wJ`tn1i$8BQVF3>w&Ej+01s6lzw zN{u&bbKNH8Jd=AHrl}*=YVvftSBcH7;JG@S4Yv*!`{pfRrC0Gzxmsww3TipG(XR5` zvmJw)%W61vq1&vJo?EXq+HTt8I9;Wdz1+{SBx(VvZqaQ}DY%sz*z~09b%j^O{i91N zH>F6#y*v5Cs_S8%4UG?=a?g;L+L^RiVkyd!t9XqHyOWf6CanrhduA?sso~Nqi-j{; zB@LTO*TimN%&E@w_bS-!;n2d*7dAzN>Mq<97o0uz;&Qe-lni#U?^PGidVTnW#lZ5* zF6{%(8QPA&KBMN;vr!?z%jYugj|!=XR%zYCjQOo)AS~(3JQH#-xM|b6_ajW|t{t_I zv%kAv&F}hXE6&f#7DNFr%zQ}x%2HA3q4|5ZsXi7jkPMbZ8_VKXh1*5@7g<=94Vim! zRkq5LHoKagk!nxJrSE(6>espH<>lo$Lf3W}FLc%3I4pI5!{z12HGe6|>f&a?&XS3Z z54?Yoj2FtjP9B}DEl zci-3oYg(Sd3-s(uO#}Vzp3tkwa{c5Pv#@%5jKQKj0lP*fHd)f&>s)zQ5p{rG1BwctqAw4%go)N<-mtLPAW+`#} zz-@W)7jK_zt0-d~OfcJOATKNJEiN(+tTmxBq`TA7)b!D9(RViE5BYGpTt$tyhx|qt zs2pVsjf0O?sZCi3y#)epQW@&A>&+#O0!V!X_=qsK{W<8$9dUcS)Mz z2Y&J{cRhC9v^;N0yWhUoIYk=nw5t`KnYpdQ4Qm>M-Ce6L1}5&evz6a*dd*g{Z08L3 zg(+K5;3Ty|*m;BI&te>=Mu2npfrD|;A~xi>wPO^QyJY;7098)WUw6+>wPwnpxGTbm0$M{8bU$Wg`$BeST+E~_`2foHJ zEyg9crZ3r5Y8=vDz~LHa5w2O&Mie%Sk1^Zaw45Fa;LKxd9O-Du)2L=Ikrthpvi&>1 z_fw)x?{5yXvJ1REX0F|X7tP&=GAmmrlU9YESP{)WuqaZdKFw63-Gro7@%{B(1rh@n z$f%pG8$5mb^kf;CsZ!}z3az(1R$P(|1;kY|MW>86Jl>u+#y0Cj)xrn2cFDe6GvjV| zdqC#9T_@r-!$l-s79JW3gliVZ+Vls@m~1tEag!><8yp$Dh4jFwVk?J&?I2 zw?r-4B4!$t1qf&+U%J z=&TCsW0Pv{UVoYyzPjsemBrYyOHPkEw90;4VjSta>(TdPF08s*Sgn}M>b_9!_iSjb zn0+DOL^DGyvRmiCR#o-9*X2RYze!GCuV5UboExdp6@XlymTG(D#?v9vYis0JuAaHJ zV++Cp%vo@X{MG_$Vk2&s+rH#F#8Rphol1X_C@7P5fXO6r^n`^F5~nD2RYhc-S) ze`;>U98zwv5>pOZZiC8S`|IxJknW&)H9O4=ZHLZwon8;S!=G4=sq2872<)dWitw%- z`~9+2!xz4dYkOAan|@-Tt*oSsYJN(_p+I4=QIpobwSVJ(K5laA(X975PmT9duD9CN ztW!?m#;2*?-#tHkzFp_Y{OTJ?SzWveAT@?9bN1{rN2EoiJ0_41zhr2tyq>xIwMEcx zNps8Z7tLGV-k`g?V*RgK&JyEV>&kw0?s_?UUQJ4J=J444`h%a1I9m4Lb>?(Q`g-0( zKXbdC@z>GKTwyzh44qxaN{#U=FK#;hV&vrmJG^tov;L2?_l~E!{r|utdxVfOQjtV< zD6&#UW_Gq@cZ}?Dj#U)5QIZvo>~S1h*-{kYWE~tc*)z_u$M1TpdwjmX-+#YEGtEIx)RoAYojJSUK(RT?c6FiBlJXBEydF7q2zudnPpdpWn3;aK7a_osf-aqdKdyD0r3>5+74MD_U?fcS^yr{-^MvM# z9+bBV)-IK`x*=x7(n~XAWQPxTv5gWl^YVp^6(I_N=mCFt#uXz^WMfL;;YPZ5AsfD6 zix16weOFRJTho_IIof0TA+E<4E}}BqS3KN}ziu_+x=LBGJ0aP-Qkvhm9|eCn=N2_} z(DT8fdTShAS)Cx@Nkj@@_NmqL6+a>y9;1);n5DPe(@QH?QC`y)hkKD;-;nUp0y0vb z*ZKN11(}vl$M`MyPkR(q`1RKs==qcsGNz#2v+$I6coLEE6xOn0pBjAHF?fd_F>p>y zB*-TymD|)lnD#eW`VhFhn;w|JO6@xh8eF5*v3f<>$eb1|?coI5KJ;dONeX=bXm|1kl|i<02! zmmXE5&?#ibd(ZGBq3Z6tDkz-n|K3hk&-XkF`Kh{rAkdqDew}-w zCDnV(p>ek#txiNHp3D5jPhV$Eb?4V+q13#JaplVh1UPrE6r^NTZgeN`o8pu?b?3}O zIXPFckC63{{NdCY5O`!;jXB2%|?Q4C@|#7u(!#G#HYh+yA;!LukuE z#g4p?KXy%LH2Lc-`Mxmp1RW$AND$JV(= zMNO@A3$U>b{-sWDN3j8U92;=O_`ghPY`B?Yv$$&;>)ha^mH|$%_sh{#R8%7xz9<8q z0|M4Nr8X@IseYiSP)LENBd^SLY0B@KPOxy^<#`*eFPN4!5wpX-ab&{mJROa1J~Orr0lX z+w@pXe>qLL4R)41ZOm;GJ{gHEx@G;z04kD84C=fVIxsc~Y;I7bAhh?-kX(9O>C(O8 zwg26BbHW;}?DGbtwY)Q%F1dWMW*o6sY0mr84&Qk}Mogg>~@^~d!{c?#6|E;1JpK+ooN^;EJ zPx#lDmt3RcSb|ITo8i7jYWy><_`PO2gyrxSSudQD3(YHz_L`(A@4*Nx+W9D2A~QnZF$E;uQV)e02SbN~8Q-D5ZOv znUzpO)v(uTHI<{>7Yv{dag#=$?dxuMGOWF4smz^=oynT{mSmlq>Sdf_^U-G=P^zUL z+-Ga_DOekw2c1mbUC=V2c8I(r&#-~t%CMqUCHrY zgzRm5vvNoC4ZO^_oHP?8Qwnzfh6Kg;TrEyrUzmFqEC_+m3JpWIKM-g<$&OtQ~x`N)9@UKV2np3;R{b?aKwm!vIz(_FkBeZ-xU_-+Mw5!yzyc8MtjGwJ_x*&Qh z`t!&5D=TbX8n8C5xAR9$<1sdHRo=lF6~Ye{re7)-25s#>+E~EWi$arnSE^XjEe5k+ z9=d;h=)d3PiZqZG>9`!@hpA7j%{XtSI&LWg?6rX z9(p+5xKb-IgV$y4ZK0FvyYa=u<--sVNMEi;=Rd&uq(RDCq)afa$pVD3yP%C8bp>uu zhF7`U*^+8sa{rY`K}?bLAZX>7xEp57D=w}A9TpC><^9|a@wWb+I4Sh94QRb>9UD!oyNHq(xmVawUpoa*r3y@0BC820GQjDj3ex zZ}J(-5||Xcarb!|t{AK51f{LUf7@jIUgbP94yx;!hT$>Mzp;SFGI;PnhfeMB#?rsl zrB{HP1s)KJ-&S3z*w>-{BiMf%p3c&Odw zYi9K=uiN>@Bv8h&x-n_>k7qd3MMu{R)4Hxs(@JHDSWJ63Z^i{mf{iLyZ*6RCa#?cJ z*^l$*V*RkT1(hwY9F4<|dg_nD-!x-HDw>}@6EXy7!7W||!^5vvj%?Y+(d90tPm-w5 z@Vab2*ZP1AF(;VwGgLg8OnOFb8k^zQ&sV#j!h%b25$tz3t^f&5S6jjd-x*>PtCAuM zZ>vyuCW^ES6q~X2d>4BREJ92~Gsk*9dI2IBoqG$dus`p+CCa>2uS~*|*kC14Sa!k}g|6B89x*H0N)< zcl#W=Q1Dn-qViltc05C8w?p)Vwi8^|KdChb+Urwp^({VsRAzjdJ>B?Mnq zukHIsIs1{%bs<037>FeL`}>oe*DXBz^>di1n@=JLvWY31wShn#GO}Z_|AVDCz*Q4n zfQ~<9d_Zr~2&52%Qjhs!Ar{%4pK%foKT#|WmFE^DZT1{b1_Y*&8$X_NsT0Tn4!ofI z?D{DfEK{GEFBj35-c?T+F!9=*&#mo0({AX>EiH|Mx_UM#k35-;Z(;+Dfq~3X>OJKH z!-+Jx^!ECe3q^NPX;UP`vQ$q+6HB%$`@QzBtOwxLafxI8E>W((PK$; z2LeU8RV-Bya=2}U5Y~;7bMRWV@tk#6`bIUWjBorxgBUH|XI)rtFh* z29(gA6a56N&Nf(6_b7dR{ULyzj0EAcX+~dTBf{RjyQJ&u{UejJ&;TWRP;W3BvP*sD znya0suEi{2`V=WY(8DNA)Tob&ynQ{fK`}+0ReAdR(|;^EVuT)!1zG> zQ;(RI+{4Nz6O8HGPfe82mK}Az8(wT_4Q@Gsu7VHBK839Bk{44gyj{4O&yi}#2F-%- zMUA5q1~Qi`8B8>7^k<=2Je7J_ulC<5&XKt0Vk6>Ux7EQR&~NlD*6VhSNF(s4Ry!&H z4n-b~x{NXgHsMCKWq`5^s=*xYDwIem5K()-n^g8+mSA#g>cf6qjMSsJ>xY z1kU;M!JexwrL~;CH*cMccF!UZ2vxwQcSFriNd#`P{7Ox*G$e%Bd&+_+$O&CPKuYt2 zL0ed!pT0|a^_D;AP0ixN&-;C8{p*~y<03Nj)DyLJx*HQGwwjtPK%?P2RKMAy-`EWL z;3D&*!TQ-We^a*o=V>Rz{s{7k#71MW-4^)1PHrMM;c=JZd)A1Ys zS7OFiTCFE^*JI=#lHO)kAN*xi4;y}C6vSKcCI#2F(y(Xf%xAItV^+h2T_f2il*4`L z%5wXULQ}+2OkMDfASSknT)0i58!#-7f`SafIr`Amop9&7S+@R*D1draI&q2T7HcXU z-7qVC_{{%o`r*s`X#&(%u^utt%=Zd1<)0E$gmWU^Q{UrN!PJe(9sZ(YDLO0Z{MLjh zV|_uAf`@kwS(S{P`X@jH z@BO{v8(e<@lsTon&@{HGEi_wR!&kJ} zG@|j-p>FMB%2vM3dSms<-jel^{{~88^;a%y9GDz4u>;D@dy`zU)9w2!bTPvj;itYI z9~&5}U@PV62i@nt`_XTtxHLHID^j#!HPyhn@G<@lSY=uRst0_8a) zPzJP6?cTNh0X*<^sjeR>UnXh2<9TNpWN_xzuTP9CDK0=N(lDJf%l2v0ZO|%w2;giQ zN5TMvZdRQ0D6%_W_p6o7dG8YG>4%G?ZRz_zuV@s~ormM^V`B~(IyV@IIUqmhF~kFk zLus)AmC2dYY=; z>NkKgK;TPnVS8S5m!!+$;ofSsC4VOJF~%!)ye@iJCxs6!;&AGolmX{`Re)RGPommT zDTQ!Loe{3lix}}>3<)D3MIFQGBDypRi_Ih)A%Qc3;_;Y)i%AuBPb5(=`Zk@}fLu<^ zp!&~(MNWj@OmhS?|41hSrkFL$%I?2ZsxiY#a?;~4TyEcLii&P|-oHbj`u!}I{m$Of zQ2BD3++pdSav?E&^T|YJ5AB;Umn>w+ZVH&}<77YL!%F{A`ZiG6Kg+99>uC>xtfdX7 z#Pp`98dMky6vPH4NJC(C!`EI1#es6qh@sNh<`&-RiG#Cher!9PZ29p;+e|KBVyx>U zR>%$Uj!%1>UU?g+N-v4X$lMV$I^=jkdD-<&_i zZ?YoY!;Q>-td~;gR>QQ+>)0sC{R4O_Q3^jGxg^*`H9KcE@#Dvj*Xn#%YW?r+X2kVT zs@HidEJ&#-YGqLH>~DURTsqKLy7(rno7@a~0n2|>ND1T4H`A&6u-KGW-PY=}gLD6% zIW%K5^>jSf{5(BdXwR3!{ZHpRtv-ALiAPn!gAM~^kuIwq9;qd=34&&hz#gt?HHg7tbXZ7oZV?1T=DQND;9qu$L0ipF-E+TgOXGKnsp6+IY-bSbxHZ_FEr zJ7hme>B6uYO6g1fa}Wq|sL{e)&<_xmw;bIwfz3>z!@Y8r+*pUz+M0fEN0s^$GXJd0 zb9@tU;mJ^rbAC3763~n+8eBbwqW%2bgq6En&0h7{t>4hf?^)uxG7`{RX%Am3SV{&{ zP8nTnWV=T!*3+v8_p8ykj~*juJkwYub(iUPA`s>Qk&I+B>tSF%GgT^#E0y3}yDVYt z049YK@ZU~@$t*?dm7kJ7J|mmw%BLh9(kcy)ZNhBvVOo}r^e;K(J2`fc%p(^ z@sFVV_d=?;vQ`=cFRH)vmhNR!jpyPuet!PTTw_k%uRY>^YTC=v`vmqH;JCy&Q;{97 z8~r%cV_UoY4!Dm|m2E-H(bUZbx|e^OHvjffQFuI%FMJyaa01T{9IR`K$f~LV*@Jnl z+(*JG)d7}bwvDe}HOz7E5SvsXEYoFc0@tPQS<{)5Gj1+_sOB)h2u0oUfKrII1txmu zssfE%q`h{pzuRupF)%n2Xm#(Omvwv#BE;~$%}M|t4n_(!oQ>_)@prj!E0ZpxGu&5k`_NY75f zHb3RyENi>}Q!ZGYK3JPV_=^w8*!XsE|3;hFExp(O3>IJ!1wW^>xB0mJYf3{hlS`uV zuf#4BmwKDw0!N@eQr`*K+gx#;lJkB_7;9U^Rb2I^R104%8k(po7)H;YKSP+kJeTZq za1}mZ+0V*#Y)-g9LcF&*jB{M53bV|rr7L~^-e*f4%DW&U`FN%2`JWY^*>v82$!fI5 zvwUkjaNM>t*``JTwm)0n^y72nC_zV91uM*Pa;$vv6wDcYZO`a{eDmkK>>Ref*1$39 zYi6abX?||`se!wbIZks2L5#vpVKI)x#;Q7uS^~u$I9u*~-(*n1x=GGzZj(0b^-oTw zlZgF?nl%ar0TC&;d_}JklR;rB^L{zIEpOxFr-`ZdRvQ~@6%H_pbPo&+-mry*P(zr+ z)%igiiG57z`fHDo*lEPPk?%bzuOwRF(YSS)L-NrFi(@sunQaRl5OS!t(LPv70QRgb z>x7{-#kO|c9yi4|d)e00|NnnXsLF!f-&nx!A1Po`^$lj1HPOwW%x)#O^Hob{^DT*F z;u*x8t%qie%u-Xs-g~yb4K`<;K>T;N=4W@0imqo5%7;SNH-&s~e7!#nrCRRXsM{MO zTP!tvLzu4&=kT^KKA3yO;N@ekiShVpQGbf32#s^x+gv)o6WK%p)k26_2Sv^krPqjt~3ia=#NWdIR#~;n+IyImOCWeM~0E(-wJoBuQ6c#g7l-Km=7N63UnRIAHvmSWUD$)EpLe=)O{H*kN?HZH(tK8Qr;#Z z?J8#$d2KO0#diw@?u3aWVSK?wP!n9V9cf;;{5pwO*!5&>@V;*`8UNiF0x{P!c}>k| zjkh;mGF=SK*Ue2Xc9N0{qPq6TW*P96)}-fNLnTG>6u<9IZOwDok#72KU-7rF^!#sH z*np||>fnzZ7Qp*vxw}2L?VipjoIR<`fqV?EO|I)co2}OSFWC<@jz+GOwxOgx+J=mngw=#-fSE193)ImTp{ixc~Vv@*)a*l z5v;vDA@uun_(!;uab`^_QClyO({V(Y&YXyZl&&>!=XKbLkM@Odbi3bboQ>px{VNRz z6)hoCOE6z%(FkKI&EH!#)UKq<9Wd?f&u1=KM5cQMbCaIK-+X`{2@wt)IVVkQU|l`dyrt%9Z zJ&z;_9T>)b;%Ot&>d#Pw&;UrFBA4Y`brrP9;tBS!@Jve6mR@s;|XXH+~$2=^6>MMUaF_`nxPkSAIS5cIdhHITj3e!brNRh zL|3-jpPT44d_I!2YRke0J|TMXR+z&puyo$KH2_q^x?<8K$?iQrF^J{;;Q zBi9}BKQ#TAWik3*qPUbYe%3|IR^17ayhY)miUf}wrTu>U9rbdEP)t>=v%s>Bzuu`B zc}=gaHJUds!xVx<{yDbIDdv*g?tux`yKskKbE}72=$eN{dq+q8_S@^6pEhflb1JOn zJxuntRJmhlJTxQ>_jW!w9NzpPLo7?lEs}^KIGkag@vdhu9@MbhY!O@9!v?g>bf>%Q zp4o9G{UQ%Mir{^ekxp-*emo=fk|cUYlHa2)O48om{_6WR*guzX?h^f|1CTWbVrf7y zl~NR|4FId2uRKze5;Kbf7Ppk*m&NgPz1kZk1Fp5R!SnVFwCF40K5B_<^$ zmDp%LxA|rD`~@U^m)c;)eXZ2WJjLm&OD|5e{Ci9eYRHd!VQC#xfkkI$eOdxGe7%>} zsfXKz-9rO!qnce*bgj2h!E@PTwCANn0#G4mwzA8BZm7g{LnVuhjH&v6y+hoM-jQNiFUh+FuzjavaNlBl#nNwmDR-2q$~8 zria~A_W}l%ez;doDB4yrrKLb(DXE60}pdY z;#O-M5%F&1=ucI~4Xo(>hy&dqr#JH20io{LVSGpJhAw2`OKh0QSV=OS%ye~Ci~me1!JgW#|%78 zr2Vr)A0bcZc6ksjH=gE%F;z2G)LydXwy}0Q9C7*d( z^tc_Iu0CsuIe#)WD9!!5f77`tuU}DPQBgz}CqkiP^ZqkZ*|X35Xd1VD^UYkFeYP~# zw6&f#E8m04O@tD2Qq{QVA$6gA?1wBJrk(Lm>c&_NeKuOahr%W2q|N`G6JUJi0@O2nF(rb()W8{Bwm{16m|+y#NoojZ zEpd|i*+p_vw=bywy^iCuy{+q0^eM;87|HM!L(|8uj)$60w^7i{pKAl{CjIjo z=(pufUGYcg$MgFb+~)s6#U1b~(xL*0fg$6l8r0!K5;sAlFhKVba@~PdpyBGgk?L&U z65!oYIXO9{rb{bkj^xJ^-uM+Mv-&*Us*zG+- z3~s%3@4D$sxV;n+-c`XukjjkEtcqZv;JI@kwI|zHRwg04F5jL2j!fuAfB$j8 z40@GH%xAJXF5?dzBQx)3M%8ahV)$y!IFvW})*KZE<_@?@nyH4I_ z6M6X5q!Nj6em@!3b)B(ZO5mGHdN0d^But3W2UZPF~`4u$M4?;4qXh`Om}pe z7AO6$jKi21S@2M^bvGB~799z!bs?=&KJA}|(j@Ecs`6k464 z_W>2RqeJL<<|A7cU%+wIkYM(cC`#tDdzm>lQpYRm>o=2hi2CPFy0aAoiw&w! z^3Jj3Q;MAX@9lgcA)0|E>?}R#ZhT3{4OYJ8PNv4k8vr+jMooFQEbh!TqY|7W5lT?o zvHS`96~e8sni?s?C;bKbRfq#W?{6`0Nb$b<^hML^&>F(YdanJOh_^bjG>?aOE0GBTH^n7rx)nCnxOD(V1fGG53iDajftk5YrVE* zi<4OFFH8U*=)5t|2FV&XR}XNYsjsQ1M4+?qw5rhR3DE<^3P!dNPDf!UMX7r zyh_T?+s0;wSa_p?`ZX}Uz-ef}J-xaj-|>GMJ2MujT`Y)o!Ftb}UbLzrC4EWx$s_DS zF$i;k;Ay~ROC*5I8xtDt5uoYIVZ!C5#UN`Oxg_LnbOE#b`uY>Bv&nSGdeUJ*H-Ga} z`v59v;YGa={%`v6ze(mkf?#hoyGw^1TXZn$(7|8MJ_f>a{_7(8vsQVjL4%lRQF^I; z6VVy4Q_D%ByYOe10Ldu2>Ih zIeRWaQt=ABS}w(-S0~($!O;KiK@MmS_7J`LDeC~?kk2ciE>opD=Iej7KJ>XKK7&0Q z{DtBh$dSeeV1P|=)va?L@Xt_!<>z?)QK%XML;I*d;;9L=Ebm((-g}-q9>;(I9CVAf zMKsBh?h)<&O=SI9(c1Ku!xau*gjExXzp(&74-W&C{E!uW)yY-iBrG#nfPe8_n`1v; zd0aTa9Rc9;_>TT**U$0MzjRo{HS7+of|_hosye=+L15~)TZ{TJIHecEwQ$h6gS*|q zDqQqA%l$=K;AejHgYM=lHHWXhBi^;uy7k)fOU>#mE*ZHrIE9({5I)_ZFDAMLP`5GZoFJaM0M;x@6(tyM_ zOBS_hcMg2Rdm&?8TwHt;=sDg$0qF;;sOqsYNW^}ylPduMtW`><|FX*mD?Urh2Jtx} zYfFLr_tpY^V(7oK6ZD;co(mX2tb1>)fV2mvC7DgOre=-0iIfJ$E1Dku$%|}_%{TS+ zgm3%a+Z{l+^IJ%+h4j<=_4`j3()Alg9C(2=gk|3Oy7j)aVS#JcJPP@)pQ<-(=qelq zhfR1&$j9emq~q;$27)BxU0A2FlI~h&vfY8h7WYHj(p7z}@h@}>4wpj=xbwyyuprvb z`)qgT`0?e|Ax&d*C+BhdZEvNzP?p*zen-n}2*;ha7*Ah6bU;7tuwngRR#pEDi_mUy zVP;Lg(?HFo0#00mXngMG>%OY>>q?awRd{o>6gn#))v)2!0a*AlGLqTDl8}C>@nKureXUw3(Wr?!2GE(tU8*Dol-&^L*P0pOEmkevs{?>+9(vs8^mNJF3tU zbMT~0V+U*IYt?s5t|8uZM4%sp3T7d1W3fNhHZ{loj9?0`^dRlzI zP&lJf{*LvwvEm;i(#oc z2t>nt>plpD9j)Fa&|1QX-m$svl9f0wT=k(x<`+4Cnt$8ytB@@Sh(4Z-@Av!n_ezF% za=8L8gM#>0fow(nOaI<>Is!8;eAh7c67P8und@Y*XYU%AE3_vrZdbW!zrhZW;%O11 zriK^fvz6d}i5!7TFLPiK_y!l}=-LOx*4cww!>9Asvp)gyi zoTM{W%@GaxS_D)X)gxYuwtG}$c^acxavs`gW`wAX*Q1VmQc2zuL|)Aaa%e@v!|A|A z2Z2g-(pE#uuri$W(D0S5G9Rrow=!Bce8ZW|}Y?N}S~;DX$;| z?FKMA`3HZxaJ(Dd2_PFUTz(k#-(*98fZ|c6qdr(#>Cr6C8?H#4)~zZT8H`>q+2zr1 zy+Cap;3_8PgT@YS_{mu|oxr&?9u(Pir)^Gz(hUQ1_i_~v>n)!(XF7fToePy7y=dRS z(pArm`|gAmU#U02 z*RBfh3{sp_4BsZCFi*nR05Oji2wX4} z@WPSIiw{1@i*ob=^Zlp;j5JGXc%jYa`kvd**qm`5?7e3MwlNoj0w)*G@(6dwjr#=j0HFe*lM`q_MrTaj@GLWU#YzUe0CT1m56?2rBGrgj{VX5Hid( zrbTI+n>&M+FdSH?iQn$l7jGGJWXt(8?e}fkbLzVbNuf`YYB6U4)$??`GF3PRZ_+C>95$u^`Dg7KcsVI>!@^V+2_AExKvm1Q@>a2 zOoi0MfWNJAN&J1aY3Vpa!iuBGK8SlOs1k5Q!BKZGH^j|5^=(20(oCnrI5V}RnrGx{ z-~al+;FnisU}?WpfVShAGlI=ob#qiY&V^VAQ1IsE2SF<>ZJ;wyhUM-o!LF52>(*q* zE2<}aS~i5Y%=Os9bv-4Uc#k4?5E8xas2gZN3cqSG4YX_7z1%U--u#9(nq*-99gq0D zK;F&GO)t0gPvWjh`Ux4XAo&Cm4_%FJUW~yV5WJJ~))}+NR_~Vv#8U0lQj}ev5zyxojs!H^90{0op8FC?H1dOgl3UK-t$ugH% zMrIntZ%41z%u7|nW2*4yyjroSpVH&kW#}^Mhy9d-4M^;zkqp*tIJyS-ODAQ?Ve7?( zBx1Tz_c({z3-SFo3HjJN+1AnQ4<*L}VjNBlN90v*R{pph97dFWtHn5_u+<-{RqQ_7 zpO&^NyttAw;(l7sqLA%6U1i3s(3g9KLdV@jlj6IXJJJ*vEN6}(<5y0{#CTP zXu!MlGLNMWfBN)u0>IjAtbxQDQH(al9N3nC%=c>Fcn@|H14*EKW%7@+09;$dL5|B~ z#D;7idU&94VP1c=KX;6u)VEnOsy_{g#rat(l5fw3-ZD2iljyehNSCV1aW8#cZUpxp z72}+^_}2Lqzs0PHA6lPw`0&ag+1xsHS1B?;%~N0VDsQHQcr>zcX0;!&bTV3Bb6UUH zw|#!JBh42tD;ZujYzX3y+<5afrp2{b8@u=&qFL3nbFz08d{e30e+@%lfqHHTwfS{% z$hQ{6xJ84(;fT|P$?mTNQsV8<;|X`yG4fa?RjkAE54M9{dXZ!d6&L~OmIu$>Ahu$_ zgm$RNjD~V$E=+XTi4-(2yfw@Xl-pi^V9gG&CZLYZh0NU5F}uL2g0k0$9?H5SkK-V) z`6Vwp_2q_tFhqc%krNtRBB(YFv>!6LQ5V7I$xJqJ3brX&<8cLx>bgnqp~-X$b?7>H zT((D7q=p8P!5+;U5h0R#U}v}tmcws&-&@$qRM>*S-bE1 zZ|7@r=D6-i0+k~%5q!gf|5P%XMnwTD;KlHlvUQJJ&3EL{}0dw!jE@p~?&W?Ae)D9J~0 zeX|tjJ$&_fI2CGjB9e_*u2Fc;GE`rV(C@18^t<0>$gYlVZL$14FQ_D*p#9He7CQ+& z1=0QCr;fW++=J1um9PqDop8rBngIyNY9yR@1v-Q`j9-I7Qn=+_32FmV;5-~d^naNS z6lE;P`TP1ei?VL3*+I!@RJ$E+%PB!}T51216|6_wkA#V00X}iIoWlfFvmb(|2{C6- z35E7Lt6#R-aREI;^5s7R-USpfFhM#jkI!}isuF5uAGZz4o;Ip|6hYu@KN%qRgW@2P zmh1O*p84O{!AC(SbnjfKA&$GERdw>zjV1~{VE+U^b4k|p zNdTjf$^D1*$rUVdfa!%5Y2eR8g)d^Dgtq=J< zuE<9cgDPAt?ldr%WS6F@oskp^PK0+K_ibCIQ7*(d$!kvU=L3pdS|av+bVQoR$Pv-~ zl=(q9bVATnTX&W`^gq)yn71F2-T+i&AN9}KSsWz916**F!ZTo>F4xJJ)B%(IS$!jn z@Eyd?wG!XXUfSlTx6{MF3`$+app*k~4W$KfmHBV-iGekyy)R$P1w2&bWuo=p@Ba7~ zfdo)c7ayO#3L}Fglb|u|Fq0-_2AMaV_nwi_f^L*{YARtCTsW%2S%%C`5*?1=h6|m<9?+w{&TIt4nI=i}DYe#Zr+~TCKzp;Sndtfk% z7|azooClAN7oGQ4AkS+W)QOYINPc3A{dO#Det|@>(YF}PqVa=0|w^?{i4z&;;ECq@B|I!SeH`dK_5(XK{2A3+H#DnDvI@)T{$7Rxj^0XwPiW3vf zijOT1FZjC<_*ic6Hq3QM5E2?iQ06x##jYHCMr$U8qJWdezU>s-cb}CvSF!xHB3IXt zyM~CZ@F*KS3sR*tzc9#5CgpKpfA`b(Hm2ck4&Jz+;GN=q$anWlOLnHKHe`Rz09jP@ z4Oc26{GUf8#Up?GeYD^%s<{W0U`+qEz5JK)#s-9xl^`qK;-Ga>!M_Gs0nd&8#5R*A z1mMO38wVs{MZG3$u9j)=`4M!jb*lftxDal5%b<2^x71dnayccgH|tw_`}F2!S@!1q z%S_nR9-FDeU!-j!8bWs8$Z~$zqXs9R#jFpCtRM2WbSuZ?Z0@j0L1sb z`PzM_#On}T7N%>k{#GZHGFMMb#Z;$T`Ob~La12$}%`S$4*?0h-jL|gCB?%-uyK0*U z6PDspEob5kB}&tIlp#4cj+0c5b3_%DiL zBZ}#v{d7`0z0x`2&h8$#&dX8VE|{CI(uqHR{){;F*Zs@?eoo;;@F)pBN)P5%ilY>z zw^u}(bO%?#I|?N3xl%!`f7l=T!r=9B^r#mLrkwuJ`^VAX8e=3dwT(^~HcdRU>oA-2 z??5=-eMT-R+ah0hrsD2PO-Ik*w0w;B(MkNkWzbZ%Kb4c+NBSQ%!M$r{MMw%ySM-19 zwAFC$R-vd8-36}kq+K^=R>V@ROLTr}Sj%7A;-KmIEAdL)pD(n5i&2S}cig*}7;C)x zXsgPyIpp_j8f!Kw4`t4p| z)3sE~uqK~<&$efL4p}-!76=(npLR4Xw2yc$#vB`l!a2qJDxSy_2q&ghRMX65f znG^J8KHISj_W2)nqwZ;6BxR14^-3f)VZXXM%;sD+u3p~X&!{_1Mu4BcP_<@fCS~gJMYqodg@wz5)-mEy4xqfRDD}O)>3$kZ zBmmO9s-Y!nU~ay$ynWWFjijRe4z|L&Qp&u>1WnvaI~R@33X37eRqL5lFA;Nwwy8j82hykfBWsF86d*w)8<`rWyT5J(ZTPA0 zBG;iB=uWbWpSO-=5t4vC$g2R~K}!*LdQ+R9ev|4OQ!t(avnUu0&`1mjXKz@fl~WWF zhL~t^jm+n_?A61T;N?twvxbF3E#P3ff4QuEv5%AEfu>AVUnu0I>3*pZf0IyE)7! z+dJBaD_>pwqT3K1xZT28QwA}t{j@bn0MobG+1bg7Cnu(Ex3yVzR9wF50>T+K0X~HT z^w%xH59|DQj{l9ZI5<{P1!^@Hlvjv^$RXbG|UeW1rb>TCC`}A6|&6 z#r2%EEmfB10EC_MDb9kf)qx0g7DGjXx-<9fj(&PPd2I&}tc=RUvHva`QdieNY}+7! z9AJ=JN1zDg{6e82;4sQ@2hj#;P^8wv!otW7Kf11`exVJ76hJ;Nw^;~l#{oiYzI~o& z5=hLDmvYu?lF<6}j>^mVW~i_mGl|xw7LY>TY-n)vo|@XIBPHqOF4?`u{!k`@V+3R* zx^Ri&!uyC$L|zv#=?=apo&sTlC6>23Bi1Ee5v%jj9E&_4nZ{ueWsA?bsTJZHFF=n~ z1I@t-LR9r8Jp5pApuv+9$8q_qBy*=@VZlPe6~L(T?Pk)EeB_1RJ%EFSlSOTz+YOF| zbOUXY5APkfs@(eW%^_ATVdAo43T@t_o(Ukhs0OjOxr{q&gmHWZ;L$ONN3ZuIl&vDR zYI9PQ3k}g;Hf?{Mn_6dnu5CFHLM`aJ%6vQPVdI^GZD?>qqJGHT;F_^tVx#bU8+pLQLPj-tcmy8iV_it}v?kMDO^x|ydM zwJGPiH}BIPxg^R3HZY5m`09%Lz~kyy-5623>2jBlv3z)v%}n{*{nQB$#u6LL_$~$) zXE;YRS3|c8R6%8*72=Nld=t4%Pu*G$*w1Dj_r5@Dw_(R)|G5eDlS4N)6++39s$_Lq zL3(CELTeVX8L_vF-3CrV*6|8A*MSFiA10m+o0V7wi@QkX4IBFdBbZ>*OR&@`@MhQUf2x5moxoDHvL?I{2I!uo-YsRog#`QR1dhR z=U6q8uYdKZ*Wpm1$QjrxQ7c+?i(@0ElB-%Uya(HRqu+ehT!tAV-XnIp;Am$mO#Wls zz*JMWpOjA6v)_}|{)LB3$I$*SXmX3Gwl|rE(oex(0d-`j%Zk(8W`GOfP%I|$&E$Y+ z#9UJ0FuRO&5q3(+Mjiqu8>^wtr%h(IEX+OXcwG*Z55`Na)CLM^k|O~*=dt(slkv3P zK*7P*-;UJ)Qk;}N*A3ZGRV?_dRUrfZ>w|`X57d1NH#qXQtXNFO2A>U}y1yG#&fPPS zIHDFT(LPvzWKxSae?>>;GKO>2K=ir7i|thmlb7S1LfiDkNlt63wkh-U?marZ_=jHP z6x9q{EN^<-C{R&E)WX`^>D_71Ufji;>Yfi9srVowG#0ub?zBAk-J&wW&^)r5Y8sS? zyU=tX30KUoUjgm@F#ku;K0jBKr+e3b@(F=e`Ijck_v-haz4COW%N|^MH1$CMros29 z<+!9Th`9m#Q%#C8IlHU8se3OK5iHOHvf`}ojw~p&bL^M`g2LI@fyVI;o!@|lGAtu& zi#0v18My^&Li>|$;~OBkgN>6I^=&4YVfq=D$UP`%X1q6!)j9F_dk7C*d!=^E>Dp*K zWq7pG9`9}#VcWy+dv^#5FV~`xx5tvYjbcI@`(nE!I##sSn~{criz0K81K!)6g3mEq z+gD`+9-lo)oI!s~g_c}kdQQ_5NJL8ARlDzJvOgGV(CAp&U;hC!BSR?qOvbdIPRe!X zLNl?dy)&res`ix|8Vj~lAeGD>09($c3eUQ>?r4BRK09c$4I4Eyxa^`cBnRKQh;-SK z!($Wy0u8zfC6IDBCq9<79v>g?oWey)cJJOj^>qHqgt)k=s!O}T@bcZ!ol51_`WG&* zG2ht_5}=$x2fG*k7Fne<@O(;Yif6gSut|0?(R^)hFZ11)Imge)t#}(KJ4l&d;{~>% zb1Ty2=VI$khgb7{ykDlKuBa%GW{-tOSC1*Snv75Jyu2lXAz<7E1NKL_ZPc$v$cI4q z6}Nr#^|(o5W0>orj2MI-Rea1)z7b_E<3EUEyfRoMC+lqdU;>;ekB93%SKL-;4iBqd zFzAsZ~bMzl!#^O5Af|V~Vc5_Tpk+&)tib5!cd)lAXS*1&6?@zVWRP~cn z2J=z^i7UZT3X-n5PRQ?pPslww_BTm-dYtSjR1iH&B6>%DoZT=;h@Zb>)FbL(vpTxG z%xiivET}!d&K|F^3aQm3TOmrPhK&q-wXGo6C2rAV%r8B@vXFMVuvkZp5U;haB`-Nd z*e9y5x-JDQlj)OGJfySnxu}rWA?{LW>r~yFmDP{Fp6;bZ)TfJBD@~n@2`wu?Gd*@= z`sp%U@l{i5o;wnzwVWqYS-6QWcv65i1pM2pU7ZHvGvH%zx%2hw{W0tDeO0@<2&WeT ze>cbAU=KK6QH>$w>^o~0DfiBf85uaZ7&bToC^F-#9+Qy~K>mvY8yhs%P5d*F^3&4& z&ibJS5|4cXGxL`pc#l77W54<7%;7IXK_jmqGE~@jh9bR8H zV7c`irdP<`o?ciY6eH7b6SIpKmOdQhg7qgKz2Aawsx93x&D{TTHrU&+xa3aO-EM)B z;w)u^bc?YX`BK0r=)lGt?|l+on#l0>(`SX$=lTx&%55O0Wem#6lVm0+BD z$jcwa_$W4(yGw19bHTn{O)p(iRIWiq5BrUMgm_o*Wj)gFqfv9Sni0>GlnG>*)Ag@% zz-Tn$#q{98SO5N&vX%V@`5^nCyDOI_&;M{JQ-DErz)V$0f`rRh?dY&gsD7{MZ!CZs z8X63AFUb}U)9FjH5DNNwfQcbw1FEla#nsNEj^rF3vA9fQlZ)avzk<&Jsk{#|V4ur) zuFyio3gDR9YVdX50)N=NvI&SBpPiYhIheKH@L+d=$Nwkj_vxJ=E3jCG{}4{Ov|-Vi zT&{x?0n#^)f+An-?l#oW4CZuMW31 z_fVC3n;i}kFyqCm4E&50Kji%j7WMOw?G|;2Ztz(NEHK~oG8ti71PzN#9YDX_UIi3Z zTkUk5!^RY$E;8YyKPdt*rA+zmx_7N*_d^5!KeqF(1_LoD>*A!W2 zzlE239NE&K~0`6uCL2(df&EnRNh92m5!#lHjYM7{9L>)Z>W?IcRWH7YxP zo56_HG4MY&=f5|vHI^&Z8LT-5(U5_U znLv$wm^cYVS(z6s1`E8ly2A2wIs+b+_a>nn9T|2RpEDr_zFFRm0Ms!5^A zB@6!_*4{EIsxRytmT~9~2|*ZA5D^LK7`j71P$?0R7L<^Nk(L&ek`!qqB&16LQE5pj zMd?uKeD|5bzufope0V;*Yq4gD%sFSDy|0en)c}x061(0~J~*1UJrt??RdI78ZI&|2jm8N&K}u*1BG`F#>H^bhc&Os{x>%Ta;L78hV=YNF)eGIo8ha9!JiNI zEnSH*F+YnhW&p}TGzw`6LKw|bo=biQ02|I-+x8zQn;yuVP1U9Qa)Y4i3c>FiO0EIh zmtIoE4~SO2`a-zUe;wSFf+ZtGeXrfQbmF>QBxr!Hc6$Ou{k*^b{Ygkk+^y~y@cLor zD(m6l@x2C`x?N^@p)jh;mXKdkOXkOoX<&dmyB-~``Hf2-?`jPUDTw7(>1jFe0w4fT z-B zXo1;5U39lsW2UIcc=vW+27LzsNyUgZVUlt2lx$p`Wo#@X3d&$^Fhoti$-2&&;R2vP zM&Mcc+R<6wK=Q-OOjI%wIk*YB}35?s9kn70(FyMk9MJA+qp#quFEn!*_t z{`l;!!8z+^sNatT&D$eL9=KHchKRj%>H?WGKM*Sw0`0hj`Wf(4?h&ee(Y`7}LYgar zKCd?ph>MMSL9Q{U5g_@DLojy*FK^(3a!yqK>&GyjXM}s&&4sUDci-ztTY=FKD0AAV zy?!4~Q};~JR;%Z@_hx`6W-Mj)4gYR*lN-fJZKyPsZZW6{uP^bG7_h1_NkZ$aXyS)JjhCpI!3kRrPac z%rU@Od!N9Hg?eqll=+e|0u@7xg_~G7*yjQNVPB{yj{TjIyVX@m%^6B;{lazV*5{NT zP*GY89qbbY^dwMn4}5qQo}O9AZ^eV!k0Q~P*Ga6+>g0F<{$L`<;u#0mZ2IG0QKIgR zkE=L!rezGtEA@mYAC|dRUVB=lLr|q{%QzT%8e{ql%g7?UevrBKKnngIfS;B5xebyamI5xtlY=kfNUtpuEMu zQeoxrlb~KjMlToIA)A9RtS4)p49X&2!V|%XxZFVYNcR3A9n{nxdhHOtVIRo-`fV!! zZ7rk$b4;+Y@Ep7Zfi;cB<+7C%KK{;CZ8MNFa#tft2{__oIx6P#!zJke9L_@@G{D`) z?#o6vo<5o~umU)*F&%n}D6$X+f@X%29Ve7Q!9ym&Ed#Kije)NpX*@5}RfY;eoXLDh zz+&XP`y&#C`$gBV;OlKsY_fH5AMSWy%9jDk)=3LW&R=Y&Qwu~Q9>5xLD8CXZouhsWWedRu2FgLWE;F>8 zFaL!k*A@I9oYW8^l_8BE_$~%$fADQ@HjpaYe93CUujHSE{-2ZYfxiJ4cmw+rCg~lE zKkP4x{(JVVjJIz^KC@I zT0@5t_(%Wb`aqYV;OI_vRB@kP(U}gkI75FNpBt&LiHp5DEzoS`-*YA%mFJCfrC~#- zu%zTseqqV+VUy*zXvmH6t*tjX zl(2yqQV7~zxSFYjUH}m#Xg=7tZm&Q6zJFi(AldBx{nw^vZ7l@>*MQ3H3Xr`(^CD*v2p^O%Q_xZ@hmK#v>0(`Sa}d{Auh>8cb2Ew@9E zQxgv)0BHB?|0K7)g1<3xJW%Cftl;%tgh>{)VyJ(sI_<2S$iv_WWCu1FVjyl3b{_Na z|2bYX8<_!+@Ua_Q8VE6(!Z@9ohs^--aVc8+;T+?yp>u4DeQVjFJ^YIUYKI4amD2V! z7s&t0p%aOOilDyp8!ubYU%Vy%L6kjjLx_YUY=TGR^LBN+PG)bb9@vP$bPuFmI3d0Ku3(f%LTu4vS z76jZxR0mYYjI2ctV3wAa|Fi2Cy(Z;?Gm)$@mr-B8ezmxk+Uo}LNuW$tdEUb2@-Ns# zIbqRSnY4wg>^GZ%vAdhx(p8NeM(IO*y&fXir;Fr4Q}vywlQYAR1~zn5#)&jHbUAy? zr|GH=1CDQk>s8H4T%KJ<70_G}-oGilipmmceUGexY)Mu+&!;V2M;SL z%66`+_uyhYpoin@LaiS9*47-&hJSerB9{3Wq=tq@NLOy*I^)8kNSg0Q(CEpfNJk2< zn7Dp?U7)x_Cifbg>UAlIc}+M|f%L-q9T%d^YgpK1E|Eixn|6;TJQ+mB@YpJH6nrJn zJw~~-mj8<+Pb%V(N>KNXiwl3eR*Anp=f;IC^N;lVV1k+W;LsJn=AlN(Hk0E&SD)tM z+z5YNQTdTcaQxyhPPE~NA;pD59;&cOfTXE(mr)%C*bq2Cm$}2^pPiEoAo1SP(Gle4 z_H4g(>k}6Q7m&^*_RmBQ)q)@q8XwzS`GFyr&k`}5odFaW_I5|UBSY`Uw{w8Kuh%PZK_S2j?yHtvRD}fv z<-1J$$@dw zC@?kwr-VbqaZ;iohDDU8O?wT9DYAQzB+w?B0u^lsPtIi`En?85 zIztVr;7PXOw34tHnh^(_+M01tLz{36FfL2QT2$6q>0sVNPcv0h{t@;C;H>5^DF3Q> zwh2|pUr!RE6<#{>dM z+3nsWWb;puK0XbVTeniA{iM<>Ur+1MpnCDHYGZDGK*q_%^~)Tw648n|;}xRx0nDkn z7z2g+b}6z~b`g7??7h!g_*5<@dPZq*{&Zj~x~{j>&OQ|b?LXN~FE zjdLr*9qm2IJwlKdAu&GqAz4oV?|+FbVAbxynA0)=ago*CTkaihbP3?uUqQV2Y*)ON zQDQi`Ubnp7!^`1eonaRTI$0m3+S_T!O{%{{B!pd*hsq#j$1=(P?n|=)Ry!Im z4bl*7w==S{+4?j%UP*#Ag9axy%YMOR1{AuX_tEja0IehkAJX06&3Rqxi7mRA7v0vC zA7QmZ6I%<8pe^+FR11wT+}GD^s|Bafcd^j;ehLoAd1kzaB6Zb?S{x_4_RszZ)IsP4 zyw1c8*Q!LS?7(YR2BthoMF0xNN9GK&#*cHKLoY_q zuYSHs3*bM|w+g)FU-qx?X9vpMw+LTuA1H4gdoL}xU&*e>{I*jA{HHoo29Z#x^S5+*MYD*Z8)QF{lwM zJ{%CmT<1%0_0nH50L*@y2JoxRVYZSL#{PwhK5bO){7KHGe~cf*lgh$73!CuVs9vEj zl$8pH6Q|!}L3h~iit@{xx(%RMlp9rKVL zmP=y;qil7qnf4sCCZrpt%1+8Q*SYoC2cTLO9A?%_Hle>oF#wg>_2M4*2JDFIhIJpVpth+%2p z0zR-8>x$Firn|K~0Dbfo`$t=cFI}ojw#b&04}3B4_)yzf5w92|>|1%8iB5ASA{GWF zsfF2Jv2g%R;-CyHrXp$gikuoy%s*VBQfNuQ+2hceXIXSKVQee|${}vi4{{LKP z1oid=e~PyOp=TF(B0h%qFoFwEyaW`a_?C3W675a&sv}X4_x3!;kNvS$KjEjJw*eC* zYaK#BNzVU!mC@yqZ=I>uJHJ4cQO!}s=RODF1VO2$%o)8|zaJpqd%%zc?E~5)fIcwj zV<1982t-}*T{o~EPqTfc5!L!ZGI%&<>( zulRN^wSrjw7i`ZQ*{MU)Uc-b63K?$7qn-7D)x)#f6ANh_WJRjI`bvI@*FH+=UVR1m(b!8Wo1Ms z&cG1`%5nVmCIYDKxm=c~86KINa-S8vGcnDZvgMWHG-_a+RCoWd3}mugkJeuXC+m~s zHR{u-?|n$^y03mI_jf@qKd)3>bI#-+zxy{H6L94@ZIrcveN*-ro^*;kyf-HKTH2w+ zKh5?(w=9QY^@P0AX*Hj@%bQL+5lozUUtQeo~zR$JwZdOZML4Mi|Wm zGZ7&3UUYF|uL!>A3N;1begb#X)G4t1uxaT|pBs4g2pnWtW%(@7%KB)Z?2ERwn`_LV z(ivceNo)?+xPd~sk!!=xgz&Eh-h&ZUgh4^7xb6B=Op3^d2|@ro;BFC+&-F>%j})Vd z_qaPc0bdM4Q*h4V-45$b3d{55cr9VwV+-1fSI*5yz+%bnP~?;66%~Ol_WD5=MP51{ z?Hf1VTk*V<#!`oRPETYmBjk7fQqQ2Y%tN$MHW{ z{3QAw*!Ma_nvd|ZHT}~tHvnZ9kZaw(@Yl5_!UXufqTS~-q=iz?yVK$r@*qPSk@xT4 z=j2GLzatCFkuE4jU={abMhh0eAyHcJdL9>oyF*S;sMAV^NR<0(MThh z^HEi=qVGaV)p~SOIn)iR^FGV@N@Zk<&{7OYi;zRkQPlc!;(6E%)!XIrkIQ>YCDjMs zRSiUQ=ZJdpc8h}MZu98`KpOyUJQH@z?KMj4n)=dkFl#K1qK*v=Qy|- zJmtSztb*=}M-s<1@2>!~h*na^#<~Zat)PO!X>L=lkoIOD0$(SG5CS(Cz#mJL2I$}j z6D$Wuy&E?yg}*w<)!*O@LFNKK%JoU)DdXIMKz=(LXr%da0fBTllqasF7aA+gR1%`o zFG-WUe4KH#wO^wp^iz6gziGs<=kNoy^j5~iwUtLX#jzCynV_ev(91bfjec@!{x?1M z1AinA1ai=mG0G3zC*=zNs+7L!ch7uxKbmsXdW^|sTt}u?s_`}q?i5{n>O{uf{NGT( zn7dFsjB0zNW)&PF)w%Mb2Iqulx5Fqz9(J~9B-h?OK)1_PKQvy*B=98oOawblb!vtpUKAOK@j~SLr!k z>g~7EFtq-4%Wm`g;C*@x*O7p5UsH*<;~IcPxXOO$r^p?9 zult1!!DXR?l3>#l0oSX7k`%iYZHp(bI9=MZ`+T4-fyej97Y^tCH&jp-)W>5vKXSL^ ziZRgAnuhc>!owe2__ud1sbg{9$JZABYrYAl8Ab_dmnJn@k%V*zD)gBacXZO-p zco#qMlbX``m89ar1X`lsfL_^up-(KSv)(v6MDnu&>tIyzD5xG=O>1(7YP1de1D)y7)JaRmAh#557wSXtQWYPP*fK zRK^H^c_^3wkjflVAN8wT935`lx;F5}9l+Yo>G#>bhZR!s?*z0ALf<9D1~#t*!Mr;+gj_?n^x5HqHTiH{_&Z3ow;M8BPbxRkA zYphs}M30OHE@6M^tI;B+U;KR|Ef8sd-Ky^gK{?R#fB)Cz-${lg$^8Qo004Y$xI&d# zF6Xl#kf&@gB1+U%JdiTQJBu)nh(gZvvf2-nTYYs?014~yi1CYSLZS{H+0Lp!&-gzk zCJ^J)QZZ!YN)H4HTZqU02Syu;3?E=&^~ZDR$_m;Bqvv3dSwbA8t=@wH0|2DV24e8W z{=ckhp$Nz6@_dlAsE|d4@LLVCTa|;zdov--mjDE!_C!nXy#!kXm7k9~d4wZ9;g7%I zw%7)4MD%zg6(c9a} z21ay#Q%}Bv##9)ioQCp}G2}2Xi>M+u-B*nDXBQ?Ac0dh65Wl?qnIgNvUU(?{P->gS z#)5yN$zX#mP(p^^o>=Z_ygM-j7}W=2Dq`9JS$I1f&F;{u2ccBGG7okGI*I6V18Ouj zNXd|b9x4j^L5&)?GXHp`jBhQ?Lj0}DQNYPQnZDSH<7*YYI z##_ijZ(gZ8bcmr8uV&Ytj=)KHVg7(r=EXM%;e@XVpt3x4JDglU90c0P+yB%B=%0uSm;K|cfG3vJ#GVQv|u;6N7BCOgv(+9gyJ0Z3xzy}ZF0?Vk>dfU5K{ z_7D0p@R1Ddoi%?yNqr_K+T!wrLow4_bI(&;t&GKSVp&pQtUIXj2ZUk*L1YBO0)|jF z>MJGQ+C1HfC;x9V=y)NGPY`egPWe>237{LzK9VIx{}~@mkF8@_%>R~kDkO&PZbT8z zHX)-Ar5#d+Z|q2GD~_1`O~!~DMH3|P#KcWVzQOWNmHhs1WB$IPBQ~H&N`Wf2>XS&x zhk@MPD})vvhvbXDH~4F?u){nz#JS?|6TNUCU)r+QKIsy)3xLjcE;YYB*(>0mql0D- zO4i`fmoDRB-{k;qYz(18TY*QCI@xHAqe;`d+7)rm@;~NxKafMQ>xbt_lnNuzCgIONr0|YnA4A|u<*ZA&KIG9wwWiI=2J@krUo!l2{CHex!%EmU|ulH{eAjr5O zLf+!}FE!^M0wY3W#X>h-R|ACnt^})+|2*b6p!Apz`TTR^J3i5e%!QQRH38r~2vyDxM8)AS#H)Y3@GoJ=@i4HHL&mr_V6b7)quJlM~!t;mW zaaK~5&$w9O?G2s*YSF$_2N$cHB~WQg@J4dGs$L*HY*jw#Ry{2BQbOEBgYIVDf0Z}? zt=5J-LzfhiHIlRn`*I^M4_Owtuj?hIH+C=7$ic=|^N#b9hQ4PEoQDw@fK<|}RZ#*! zJ1%(_2y*q2Sa9Q)m5zs?td9{liX$dwObzLnh6m2xgP$T8)3FYcFP#7S0mBJp%%E`|%pGVtLnP|X7~tdUL>8(%2OX*7(K2aW76QF8i3pGZYUUpP)&G%J z1p6`znFz{98w)LGv4X*J$tghmmoann>Pb=;vx~|ZXEe|nq=E$i(T{b}HI-u&?f$Fx z*66PQ0AYZ#{lSBe3r@j}v^(L(zeoIm45Vx{6yM-G4q!+*P)o#|-r^hh8t8t5+|S1* z|FZ|~@8J70FcqjhDkfWW091Q`uRn!!1V_GQvH}D+g02eryXELN1REH5w-rNsKw5r6 zh}x5lm{hky0p%|*;_OF@z;Ker%TGgaYK2s5HSK7oMx1`^1R;My@VZv3vq}GS2+|4; zc{tEhTcO}B#gINfJ;snt`cyOEMgbrCf6q6t&zQ@^(Xm0|iYS>!m$>@O%hLe$Xyu5C zLTf}KMA^8KfP2a819G?d%4P z;?fuG<(ISuAakJ<-J{wo=T5wJNL-I-_@88c_>pKO|K@Ak+;sqlx-TKoPR*Kue7rL9 z^S&}AG8Bd1X9jRA4cD-84Xf{q!l`6kCTMN%LPiwD09o-e2NLHrV_=Ay2XM!@8i8wtr`xJwB^_PU2k2^w^t0xA|47CG z-|4?7wKcl=zV2nNKMl+-LA~rq&cnZihK3a@KpUC4#!NH*b8#2nI+BB51Ipy&XpF|~ z@}Ej_o$v}?AtaSv$Z0w9VboQ&LCECzxsz_{mX)Td`(^}ziW#S~41zR~C^r=-F{ief zj~QPMXyF|@l9?B~TjV=Y3cux(8~Xa5qEa~Mgb|q!6iC%p?}toZ>|@sLL){9Ww({VV z?v6bv_4Z+W6sDs()#>BLC6I-U7Q~7>t*M%it-yfD!h-JlW2@~FUB%FL_p_JT+(U-! zdhdK>xAEa|8+`Ke3+Fk84mCbkoE3Kz1%f~?SJsYI_kkEJK+(|f!uWL;Ys!|W{_`ce z^=dF48eYDy4EGlIfA>5J@{tIPTdFDc>5e8{T5cEyLsI+BU zErYVnr*h2IlP0%VVJrw3OUzxEi8!T>ec`)TsOF9ogGy0K)d9pUmNy!ArVepN)2mB9 zR-I3lz6&QH+*43D&!>L?(=mn1BTs&wLb#Onpq#G^lw8%YggjnI#|!nx3q?}nfnf|5 zWHs_087y0 zZZYsX&?`WZz-wzZ#*%s?S8b`G{}NxLpQ;cR_W9POwpK9~y!rWgk(|uUea#Hl@$N>_ zA!YJT1Xh}e#Y+VtqwI~fsVTj8$Zh{stckO=xLLlW%Ez;7W>PfM>SP~RZ;>!bedWAb zw^&dZ(PIFng3}$y=jKD(iFkykXJDYzN(Gg1TnW=Qw}qSW*l%k9td+W4Z?`Q;`P zj~1@?zuuYc*Ubn0?q^*-{7i2w8H+2AP*3tcGI4m_wwfUSol=2!x*2(Igj!MkazTT6 zHQNPm4iKdL5#ZU+{29LUF#Ig)Jqj8iIXdDLTMjzpb;-{{Yjq)=XY7^C@S27OOoGUg zUOJ)Y`sh4>JbMEW*ZLjqNZ$V{(uUubM(I8Ikf~yW1x-P{bf|_!!`rvF3jUO}+F8lU zy_6V}niT9;fqQc}y^HM_iTP%>HJL-){XpAw7rSaY4L4#wXrNKBJIBO1zic|1xXH`y zRjS}ay`mMSIrW!f`c4v8kH+X5>6hBv#Uc*7+zpQk)14Q8GP8W!p;U7i^&xavkGVU1 zT+Lfj{jAAkb?JLN5$jzAL*&f_gk14NJzO-AG8>0Y6{ zBjXJq2y5)`syaYJ)5fh}xI|Hj7t`EZA0tXZ-7+od!^ER7^d+-YSl~276>hhK>n&b1 z7xD_%ir$)?n=JHs` zzBU+kkllRk8U;K0D-TmZf$eoRBEWIl*KbptTB+O0m}LkF!*aaX%0^jx`=@NttonMl zLZ6vHT_bu;+!v%qE-nn(Aw(>cBDn~!k4j7rY?phGPqIxV-oA6#V^Eatb?-3Ve&5e< zIK8zvaztG5!}FGQwV2>_CGz#)=B{Vl8d|iTf83PGPXrGbdg{gXK3CQP>nk@lYY`DS z!C1U#t7C>94JwUNyGMd$x>A5SdJ-3SaV#+~*mBT%L()`PdEbxVHh*ki2ArP5sumSf zJ+F{w35?|9ZT@-KI#hkiE?8cd7`*3^SB#k{P@REFzFQ@$o(s~)ohuz=itI6q^K-7- zHIfvqdWym(k81d$>3g@zkB@?aUsMg$?@ZM?SecA)vA$IBcfr0*8#?60;h=zo^Q$18 z0gYZZZ8XX!8*rkJ`5lTY=ERvTQ!oDnA`ne-Fd8%}P!h=YMS5wC8x;7-(D@_2!x;}3 znr^T;_e}Jzzd*=Hn(qpKe>G}ok zjhE8j=l|4ANjVVudN+luW>9Jdeb+i~WA=n~@yKk?$ZNWhscu)fWJ7qWR;s~^<7&QY zKO97To<3*BI-)?hVwv7M{Gy$VVXG5h6S6n@C|+CBP}k%j9Y_cow=U(Aqn_u%+JqDi%9{-WA7)h* zWO7LOjMj+Z@On3nSr(^JHW&+;3e1b^FeTU5axUE?`S4KVnwJRe)?%X7xghMr-=FR# zaie7*P^6fjUOKESx#42N;ahJO6`GY`Knj-2LKU0bf41CofH3*->U~3{(jhoP`3xm^ zD9F#Elmi)w=X=9`?b3SmV##$I4Gj(bjNsvyhuG(LihmJCFRq6*_h!oZ=qS~qx33xw zaBJ(o@R;ynG~ut>MW9U)!}y(zos>5Lk^dIzJah7{Ct4LXNPg$IU{ED4`vo&X4&0nY zr5VU;wlGy2=!>1^u;OvIpJki;#^@)9_+LKvpPEGfL-_&bg!-!)o-&Y5ImDCe5v>&i8ajMoZrpbOg!lym(YZBRvG9 zw0G!*nwmlyPoApn9b|D9C#W|xN?ItVAC|_4BHSR~d{dvJuBP$MTRmRRb z*UE{@azRA;7xy#G$sXTbruNN8j;6HX@dz;(U4*&GPo=acfSI;HMsDby<3muk0Fw(L zspK7#|5EHI8yJ`!|9QbnV%y5|s{r8zZdJANtA#lc zv?#{H{dV!sTlfE@+pn92ylY?H*=ZuqZHfOtQ#+5%LbvF*U*A`Kf5u4s!8^XH-?3gR zug8yC6xfa2W=uz)GIhK45!09od}cJ{pQ>u8?Eg^bHny_b`BBAGr2T{V(nr&8{b{=s zYE`?>a*h06P_Racwnowmxs>ww>`WOmHH0FskU?Zj^C^Z{RQ=AkcVXRSrS_xc3{^Tg z|D*^~lOpCihV^c7K*IT>y`axKeD9q;YI@P5lq<6SoM*b%(jE;kay{8w+fEo-|D)hG zP#O24Qsg|SMRA%lZXla*T2x^z`WDXWfIL224z}aC-R37Wma{G8(&j|{Hg1Af18^tKqT7|1ave4XL~Pjq_YSkfgdzd% zW|r;RB9JSC!a^RnmmSYN$(XsYt$t@M(D|rmq~UUymVw~T?60k%F;}n6g_|u$_`h65 z@JS@@9N+vBY99Q)TXex)!@x|o=0K^7^B^Xpkw#fZ|Gwvcgyro9_=z*dT!ul@P+ z-hec@GCdYP+S(=W6hS?I{yd^*Gy%WCjYCjy@vM&Nq+aBLC`x0^&-%u|s?$P<6KHXL zP;)`j-ntqlCWafTw`2Wr=zfPnvD}xMdv$Irs$NN=&N)8&;$OCY=4k1$bWf*7G8uBL zCh7G&0H_9|Q5VGZeh0Qd)A!^r&MtZ}veyBu<7#U zjqT*4cFW(sYMVyDuq%U%(R-f~XLL4a$gGajMV){CHP|50h~&U`Us3O5!_)ft^!zQ3 z<|Ay$Fm(jQ%r4}S4;AK%Rw#+~`z%=>0Ub0j1|kd3(0LHX8mk(%OCK1*Kxa3X>bijW3yTm2ITRNU|WyXm7U)eMNM>oEZlpA&jgT^hy> z&BnJbw430`pZ_Z(%r6=(2pL5QS8oK)Y)wbco&Qb{#kiEbn&uTJz6i`F1PG+t*}crI z5L~6iV!)@OF4<>82q|A&Kp-tzHx>pQ`mz+-b*mQ=<@goOz`okax6GRa1glcvJCQDJ zTT)GlE?hn#)G&PDQL*1&FWM4FTX+*92HYwyqugcL#}CuagdZ-Y9Y6Y>*m-kH(|Tu@ zg}&EMx^7`iMezx1mxHhd-=a-!%{$M|90VlBUVm&MgdebM|pzb{5A;S{}JKvi@F?gI2Y}<&RC{a{SM1m*O_@>fQ;HKQVofM-d^##MMk0c3iB9|Q{&?_$Z! zN7LlD6SE6EqmKjf$H~tMh5=q$!;5*Y>cz1UmE^e{dB*0hOUY(rs^YE;vWfT5ohdu$ z(WKWn2_wud>nafULj=^&JZ1XKbd}fiKS}a4*7{b6QVispt(==AFHZ5S=lS#Sol6 zlPF-&gp2PTeFOK(EE|2~+ph(^@$ctImbP5|=j;Xgsd(;kknRdLHv{B0InSnWLPr1D zCe2o`ehs)RWSz*eyJ7+mp*X14$G&~qdbfXG!(t!+=*eAnIQ%By%u4V&Z=N>ldhkJ% z#r?HeohP`yOD%l7a>=)qDtV}V>ShNzq0>(AL`u!kst^he&w_O0Br6(1pb=k7J5@V z{%>pw{T6`NEF%3msIi#vLD*$KZ^%TAp~w{ZEYU~m*?VwvGnS~ z7891>Ir`6k)P|MwX2C;o5TD8RY00=^cmzU2GcwewTzczIlNXDxMDq0CSU`*~ELjFn z01aBXDNL8EK&19C31WRCA+0Kf_%rl8Z^-!bG(Xm1nYFW+Gq)y-hc-TDPud*q^o8`fx0~TVhIIOa=vVlHhGM2Rz3?_#dZ2Ti) zyesZ^sGyn%Re5pv&(o(8DuiXj3#2eT5&}9~q$hOfxsM)Gscd_)zXftY;hw5`Egpl@C${nOnJ|~Igs+z= zc6MGS*iRjXSYAJ?!2?TT1tGL&H6H}y@G8g=U&vErt5X;KaABo%Ac*8@)cGU%kXOuQ zZS<9A7!n4n9i~8De|Cy*kotHP6%`hilarc@XmL?fy_M$|jK3_rL$trsHaD(nK6dZ< z^e0Nh`@ZL{$Mv{gf*|jS}3W3_@@Krq&Lka(pl+11WCjwLkNgb@qfhLqjVn&IP z{AkLVDDhRNxn-XjO%Rq-p+LwgZ{L1Ob`2e2)lo-&!^oWj$VhH+bm=F7?Ni5(v|RKt z=)-UB%q;))Y!=a*YxeQz0i;7DkPYjk#zs?*am8KwLFz zh3_w0N!JbIDX=dG$)8i9=xun;h9m~07|My2&x1Avrs+`tB5X=eH7^=NhJmVu{k*V! z63!}_A0L-{j`uT*4G(!(6{1YftEQxpP5T&Ddp;Qjy*!z75(9C}i>xp&a^QG)mWV-O zAUhGs82UZz7c>|HhlrN!jsA6fJ}NO4W~>Q2u0XC6-vh&3qFU1eqK=tXI6n)-vc=Eq zm3`XU|C;1L62w*Y>)HA0=++J0pku25!{1a9G$Evk>^CoIEw?2S~ zK&Juhz=ZvG;mZO4B>?0#$4xzo-g;xmq**ARNKgd90piHdKC{_oqio3d)|OY*wHLpJ zr0pAuuR1=;cki0+>~eKj8@SYlAOG1)(Rq#pmH*7L#ysLOaO^w9u+nIi@DHd>NYNQN{sLiW?v+pSF&E-TsE$V~CWWkAwI+2v^c%_ao zFD7Ezs|caoEQ-6FF6ehyV2jZo1(rd-ZA~D?R?L2ZT&hatO5j3amI-~vNW(4i@n%U- zQGNJNR6A3*^`AqOp9XoYi$OUuSs20+OuQU>a#86;6rdDI?mf>}5B}W;{oYYX+SKNt zEDGq%SYmYHI`--y>hpv>N9hH)4T$e;+~mo}Tw}w0wX*y&&qH>Morn<_D#&sms`%yZ zvF$>DMUFO0C70(>Hs5Vbut%o z!o>*@WOzwSiEo+=_#ZOuURBZ1h$OXjDGUJX`Q88RKCw3LiAeWdV9acPybY9pa40CB1~FeThd=o(KrhIdztJkn4hQcC$76Uf%O{lWp4d=r6X49C zXY7mOc{_*2>N!)v8~G-G*vR(sj(OKwnz7BrNQQHE`yHYKc~R#WNF$oIg4f#juBMTg zH}OwCKVF=BQR@*JDsuEr$Ixv`mR=%zWb=6CZ^0Jm$!o_-s6AqANbXTGUg!*nG94e65Bh5(YtI2)el3tgvGaD~MW1guHKTBA z?N|^ZEoI2S)}1%U$!c{4i;9nk1{h-ZI&aLsw}Y%plckG4ASOek<8|tvB2%CNgfYIw zC|J3IjS94KH`tP|=$O>efZs^0sqZHesYQbEwG5r{ZC>1J;l(fFuzKPBqaO(9|HOAX zn{1p94&p|S?aLyYYS`$E-$+p_VKMK9kk~hEYl?%3_8XdoYU1(*FB69ESMt7adNM$n zd$t?*ikKKlo*Ygr{tMlhh957ZMXqgGAr$&~8IA6OEXQ!~)s@YX`-Ut{kG~XAl zxc1}V7~8ebc~X&%^F-?ZJB&NBkYo)(rxIrN$#2CyU>mu3yhNRSD?6%)*`sKu(NDCK zg!bUZ_@k-TPBE$x!Wl1tCUeV|ABjV-79yQ!CAPuTj5a zSvk#d8(=T&zs*V8`Yz@7m|V2^W)|VS&->(G%4DVY1fryx_E!d~raQLl z^59}KdLl>JvD7&tS*uJrJ(akt`(-K78>)VPikJS_g5mM@_!bGS5wK*<-kK7#9%?c6tE{Jfw?wxtXSA?7( zTPTZE>2;f177nAg{VO$KFKgebKuHSgS(M`pS}$-Tqj-XIg?*h$2~g4m;Mt(T4J;u<@Fp6Gs?zDXn$`SP0%wJII8t~uI(oT*f(YA>`!}$ zhKU92{;vd<*it(0CX~Q>uQy6Z4g+NfLN4>Yq1N2jPJ%;dk|>vNUb4X@qP_Fy=jS(I z7UEbqZ{xhn5BE;ykhk+e&;T$Yhq2kkFbM=~CM*2PSm9^ak;A9f-dXe5478m@MMuSMxW9+ zfLwz{6@ZF^Tem5+3Dc)1=r^XA_=1dp~yX)5sqg8Ax;dcy4&thqh+~ zAbt%ndd!k*PDsehzj)ZiRb}1BLeFmzx^&zvzBZz(CHZ5_SP7~oVAB958mA3i5*ehM zvntpqi39<%g4ASv!RI9HyDgw|kck;nz z;!qP8oIlwo#bLuHCd|#esPzWo!9GEDuCDAf7*Ou?@aF8C1?agM0+QAF5W6<_Z!CbL zbY3etyL1Iid8*u=^omw9a8jnCroQc+UTHTxX6V>p3C19UVFdaY!#!fI0a#AqE8a7n zVCZQ(U(oN28d8E z^eK@5CA76*&ANC|S@>ylMtZa68-_t}~xU za%{#H_Ckh$DpU-UE3a7_1Xh~oc(9RRQ7bcx@=gLqGfU~3DT&Yvg@*Il-7I9+p?EC$ z7bDw83V_p|^BCp`DjCb@0Bh;QHoig;qamt%2KP*qG4&B&^q^vju%nwz2zW8GwWj07 zR4@QmkPcl;aP`XM1LOsM0zyJj*B~h-pN&}Y4t$v>Y*I?!OYvSZS=WQjp70jA(z({% zNH|ys5rP&J>G5Eui-8CsvrP0Xo**VBiY&lWFTSkyjPnCIDjVO(XY&hS7D4VSQDcni zDk)d6P9zrEW)_t{({W~>p6o$5ri{S8Ga|`8n4t+@K=TYJAW(F{iVBe0K5Sq0G*!j(;9%awO`w;VDdZo1r-TAxP19$ssG~5Or}FRB_=6xUXyx}=jZWr zWo>NogSet--2rki1Y`cyJKLakwRWk(3U^25Ioy8e22x)=dK zR1+jT4UK%NVk|#%U}w6Yyrk&Y&UdsqY&zA(*X)a5TwJUO4_x}X_4w>#-;LbBhhxbb z;HC&{C{m__U_WF5sz)#Wxr-ul6Z2+_6d?F8CRd!TbA__mqd86w1Vi~Cht(eBp{hWr zbmdvhPvIi~RwM9xO9za0y;o23iv&BzZ@H}FuFr@z1YL5Kr(ISNc+Vta^9Zk_-8u8^ z$zWpoqRh<9o+vIC$+;KKFNmn`zSW1^ZB8r=4q&Lrz@nv(-bOM?j_oW5pJfivo%^Kc zJ;?(mv5^YEW%kdK{7R1#C4H>5v|tKEDsR(4d2H8{5LWj@RLJ?q(*wR|vLbQs#W2BF z93(c7^g&zeEd+ar6wf`ljMwpw=U5HNz$M*Wm{QYUSn&gh zZm$Y&@1AQUM>Y#%c=UNrc*Z7+fZ+y#7yva=2+WbrUg6Ylv5$R3f?x~ z)Xr^I{CO3^D^1l6p1a+;QayEl1DYK=K~sMHrDBbp#rhw5IBHxeKq~VxgLVZouE${E zED6?c(La;lEZ$w3G=B}QOPle{N~7>gmpE8&(*rZ)+OIzUgRo}$m~gOe^6&yCi~vsc z76 z{E|0kiZG9s9O7BlO5%Sn$V0otXc%({Pa-98O_a1wPk?mxlUGttuv6-|rN)EK_E;UtPDoLvu}R#kDF;!{kLRsD z$vYikH|CP}+rCWjezzF60?ml8(~)fuwB>*&Qjt!sSpZ zLTcRI^1!pGr&CRpZ|o#ZJ)=av;eF>?mro|M?V(|#b`!7qekLO--LLWT=S03XAo=8P zR0r`YdtbkM*;yKlB)&LZvzWGFr-ZS&Mv1Q8L~5e(eH>fh33| zg=?YYs5nR$d)-^GX`GybdM;+a{pKus4p2tA2B0r}H*UjI3onMld z2Y{8@`SjQcAz!xqs@+;HR5RK;^3^L`P!=6QK-fX?eoKc{@AR8n;r`L^ZjkT3SWP9S})T6XeRvJuy^eD&R`UfBkhzo zjhMXGTiO!@yKiJ^otYzBb0w=#B3+*>pVum2VZiyyI&;{b@DrFRUqoA+6fXch(N5{B zJuNkp{HL41w3jQjvhcsa2Eq?Nkd*%&<@6ss&0sLKZd+8;JCCE0bWZ*%yd^i?GE^}m z?XfwkNBBht4P(WVrYLE@y@mgev9Exta_hQ=!$Crc14u|6KvGIUy1S83Kxt4wK)Sm@ zxUeN|cnA?w0)bbB^Boe((SNWBg+<7#FYi9-jTIz4uyk%{f;gS7vhW zqU}(LTYm%1?D3M=6?jGQO8}$T1^17mkFO@yTl#&a2O-b`AX#zCcqg~IaW(yRb_K$e zfP>eK1Gs17`>2kOwH_XH+;bPCkJGrCJ(;h_b_tl&2P1jOGk(i-%v|@2AfTRaHgqZ> z@q>xIjlhaJj`K<2zw24Uj0sNzAENWw?M#duli!(b3#N3hmJWZXM$iNJDyH}P?kt~5URQV=C8;n^ zn}jd9%pLtmHKFCA7csr>&FE<>RiVCp1OBd~J1G4Db4#3G_9s-dg0I{SsQ>OGq!0ap z2vchsO><_MX-*N5+4oSrZ#>Oetoh!@%;8~^o>ZZZe1a9q(y|hxhweRiBl8!jPucSM znec-JQpcHFzXfm}b{1AMjv`$J9C>aXYWFs&-t*vp=kP9ovNJg>Uw!#e%&0t42i_4+ zzp+nZp2()-GT$cl`%A2NG~3n&)6gn&%HoF2aD2_-X-wNuHjM!r^G`$KDoNbdjitWp z-Q&`I#`N$Ux~1NCa%(@AMoF^<72!AJ1C`+GvyKu2BwJKh8#P?7zGNh7yZSJ?RJNyK zqj_cDG&kFeEnn{Oq)U*9|N3IPRk~v@_R(en_8tdummR~*)k?m7djYU2fGyxxsqzB7 zDityiLiy&)Yh|$R6{>;Q7|w5#ZiI*ZkQ*&4r!jeSm_$0M;eUr5cQ_@^o1E1%>Et7Y z$=r=Qhp>+@UY9Mn-f(LgLvu>lP*igQB1&oyZj!L57%*?C6>OV2&LZXgcJq`Ko7~4{ z+-=CH$9i3m{JAG1aRkKyk;$-4zGkNq_Ih_*w3U%Kb|g(IC!(v}#ssv7FCGw?I~fv& zW&eZ=6-|@FgUXAZaMpr)MkGR`_rXCaMSz}}6!%`wSpn;dn*lzMXJ~#-JG*Mo@rYH9 z{Yh+mfK*^F)^zg7;)e~#`Sruln}idti=PcSMwrVUet)HOF20ZL=38jb8wzrfv;&^+Wr;t(Tf~o z<|a`K2DY@%!{?jbYQFS6#Y8Q-TfZV!z|`?{7QI85u91;_m*{F)yWQ?$e@Qk4`Qwpz z2JrbCm>LA+)Q)${h`$75pDIWf#DPb~w8mpiu zK0kL$RbeNm!DCf6-(r$DYm<^l8o;R^b1(}g$gDa)n!Msvck9mBc4vwMS3 z&-IrF8{uu5azmZ@o~)I#nwh7urB-UhRRJ1O7b+He7b3v;6@!zFo=q{=qi>q>aAa$* zmac#HVdMK|6W>R__^>bT`!=`6$=M4Hu*oS7u+or>|a!m8`WT<24=mohs5PKAYq8$x<{@dQ!l;h7ND%V2q1u!e)^% z#6g!3tv0O`afdQSLgFSGhe;%pJziEOYBMb#LQmW39n}uQ9`oo=Q*a!)W?oyR|M3E6 z!FS7?b6eOswt08qB3Z?|J5d6d6MFldQ~JzGbLTi^ zvU>M6A}9f)rPz$ix%;zqgOwF@Sz~L8CIQSz1P$I30;v6-3^NKjMK6q8 z?hss`l2u-*6leZ-psr(kVteutxU=@BBj~+1bdh(b=ASvN?}-NVN#l%wy}Qtn=#A^6 zZ2J!6&lWjX2Rf(OT0aHKQdn_iC94xfG=rVAM+^@_U8)E4@Yx0iCraP()? zh&v~o)UXA~yQ9=_16&O(JBy|2$>$>WnrWg6z>xcp)U-6};_*o2V$>r$a+T3qIlmlW z)51hxP7LQ(XYd&c)xuxZNwikeTyZocCP0wWGhNwr8EVLBDDjkETPQ9(C_jplQiSD? zuIew$R-@Vr=Je^xtns)f+md(g@_Jgn8TUU(dg@-4dcJqN#;>!pynbEdiiy`(4Ut)r zF+g-(={{Vado3=B*Y)rm;?y6N=JWp*rO;drBn=A7>*IGe&sWX-iv^rY{+>&&0UD0f zt5juE)GAqshB-uMHKd{JI!V3_WLL)6~X-!>GI7kFW>jm#5v(1 zF3XGUiMBoLv=k>!SLoaZwH_tgYSX&8>X&V~aLZFJdgwn4h)`Tw__V7DKLRDRJ$=Lj z*QemYKzl!?t5O?{x;KbeXLRfk3U?!UgE70u2Lo>fVZb!%7*P$0~vxkrtK3QN`og~E$tA>9`6;Sk){ z$R$w3B)mw2D?}Bk9qFN8W`gCz5`h**~pK| zo%1h>DYC=#iWi#NmBQuht7eBy)y|m|$Gp-eO{c}OK3QDyqm-S_6iLk{zC}-uTlL8H zd|)nwdjPOH&?!Z$2~6(lCj4Ezrp>YH+l7pBqz1!zK(Ask>le09&P=eNDlRQ2#t$t! zFJHdwc<>pVq9rc4Q--bSV5^*N#RpV@#8%`-mdi5Gu^fJVmKaotu^>eZ4}XCiA5O=O zgmUdYqTqc$B1%EEn$R#xEr9r3L*Wa(ghdC!C4v&B}Tt z>3V*$Mnx>OVEw6-w`jBI?}rCz7%e`h-`91%qS>r|uZnx*ka4Zl8~kGpUjr)mIx@-i zYk!9!iY==e+OwN*Q8&=y|oHYpNb5GZg{g)Y8C(e2m*FK(m zGqKe1c?q$D2_*eXs`($Eyd7&8yvA~i738DJh@w}oR30{7K=(d(g9Gd5#UbI-UBE9z zge4sCYF`S}y&?3lt2@4u<>QRY9Yc<~>6mwbLBoWIYNf6R6J=gF(mi?hsQ%M%d|(r< z<0YURYBxGxRFtd$`TD1#7mgaVerA*#mrObLQsV4)U&IOEFARq5xXfjE&pFbPyS@w^ z5}6mkoD9fzBscWk`nZ3x)7y}P*vDpw)dPE^c9Ov$O$D}9JfqDn_`Tu}4n1o8sha@? z1{e-cwG%E4DUL6+X0Q2oBZL2{Dog-~7?|yTo>5l{v8ykgxSAeznppx*z{k6`?NiMr^rrNwD!I~g_6l*k}RzV9ul-30R8 z?bKFkheorF=HrrbMI|vTnu~I4#745GkSVR6p;H*5n%8caw*gE+iw!&Z{83qIKMz$tOwbV{{clU^_aI!}GDLXpz;;iM<$euwX_jsBQ z8GjmqMbW^gIbT!-#6nfOb5g-uJT^?N{x@w%vt{1rC_jIb5)Dxj>eTSQ5WqU=4D1xi z=pTul(mNnDPb&j3I}eWzT}EFO{4~qIWV6!$j%{2P3O?Z9M+^x31%Vjr%brP;sPmg^sjeLn4ttH zV3OQmf)thy_ulMuv7bA1=olcHR?^>YA2A}dx}I}-QHf8EU=@3Ae1r%};7A@1(O;q;U$}+6`zl%s%4b z;Sr46+S^*F0`NT5(qpB2p|UfeT8~3V7@c+gwY&iRwyy9dk&P0{>*gQUE1QUnKFM;s zB+M^@L2SG<^ggP$)L-G1oh!V$Um%+evJSv*avkPO7nTdeKIL5_?(|fI*+Yk++;!6% zOCfFUJxiK(YX$5Il1H-`o;Dw`!rOyvm#H+TGtbC6nE09{hqf%<70{oa_igIyuL0D@aNHwb(*6X{YEF=VU*B?QV)Ye4l zXv%5$t}j-xwr8%~%;lt@J1i{hXMwY`%w7^dCZkajc@ic>&H#LHzCbCR{2VF&o>pTV zIf+}5H;@lu3o`x#3EzLqm_EF3(EOA`^q=>E&K~v|bMX;f^r)bCk7GaAl~}$jD@5{y z(_nxZPQEkJf>h&^gG-&z;YMAU1ha?2WhHQ7g7li@d;xjQX0$%chCXZ6CFez2FJgYT z)%48uOJ+vsDgx`L`h7Lr=Sz!FlH_;=C=VD_0@<+hNZKmqB8&=69v7u+--GWX0qWxr zZ)*gya=Dnm!GAk)C<4XebxiVIWjwMc&=m1!tK6Vw^&p9m@i*a#yQcoW>Tfbi$5@-`Bm;1yv5>Dc}F-1|}h8!2Z4W zY__aF|0|x+ugy6pu#?<-O>Oh(GhNc4Mh1}SVoQ5>uZ~o%DNdG_TU=OwLE?0w*2v2n zEq^*a{40ODe0_L@j#9$+T`b_VHy?d39R(p|54;2_=O3&sr3ePV?-xOfiJ~L@yQU5h zm-4_}>CTZ4iVgl5pG+MPlsDOfj2hex>bt+B^R10d7S{kXfIRTYw|7>!ejccb%$c}- zzYSGkj~@%IIuSL3400bF0>DF`&kjh5Aa{Cdv`BH`n@NhuzxD%v2}Y4uFiUssZEUtw za(Yq#@vpD1i1l2=XI2-*CzK!oB;ffMcY&G|ZMg74F}?;&vmJ%xQ<@UL^OF@n1m}^_ z;G?AOzLLumF_9CjXAg_#O5 zJW-9E96lh5TmK*-_PF@9a7eDLUx?J2G_m>xqo$3A|NiblDTstzI@BZrtrj~Ei2i1L zH`87sG4p(r;p688nol0w0jZB3J``9VJ7Gq4R|5Zz8i6qm>oZ&u+_T0(dtnLnFASD+||*`v_?z?HXYxMfRb?5<)I9siv^RB zY`x{&UwG@=F3XpUo89G2C)?fM_(MKC28=y0O>xE8R`K}|E~0frrOHsT5Oe7C_#i%k zS^G8`(Z_~gm4V-of!eAHxO0nF?>_t5FDRh+6>vds$g|9KJR1W@-6KlIYjU{}aQzQ; zV0M#_DcyPtcgh~bUcFI-+E`ExAuJq zPQ&}tu}oZ#ai<8;&gnIX(*0*|O^E{u?!yf?Icczeflb7sTZtg?Uo4<#Koyolt!>BI zv+t~*Q@`P{NU=6cM19PEC**`>bK!i#^mv~>3Bmj5EDXm{CCDg8(#O<>a}+$7rE%=3Uzz<+}wkq_9$l=f2*ZI*+btVc|Z@I!~&FyfaIZaHx9n499R^(Hdfo966b9`{=82$ z2~I*V)bu3s@p2yUJqpF!AL;dX=W)B7T1G+gSGWOq41s}nf^{(Z1rZUoT%Y|PS(o4B z=g&8)#)?a~yeA-*xbMX;J&)&m(yv;xan?Q{u{HBCdM!_tWzP~go1V-Un~Yq?I*3Y@ zuU=#L>EpI4|1oyRXnpQ>wrCGWi$u@U7ZmL~&9*>#(P)Ll_^lYItGM#xZMS6mH|y)s z4Q9GBLa}+IG9)4eNrCItx&tY1?1-lv`UnKaO}AS3E~h5MdgL9rTmTZCToQO)XYPSV zh{9E5-|JV%uBKx)TuU8dcvC%5-b5YfK9I10i#t=M)=RqtruW>uE=(XerEJL5SJ_LrN8?TPxhd}G}o z5Pe&-J=D!b717oyS~Ir)_Fm?f`p)c{`T(ENX5aadrXT89d5X|J^;S`}0Lk-%HtPG% za)}0&52$QJ05c*>9*5qytl^Gpa62L>ZLz4xhf8E5m47X>6?9!|6 zCua_RPU`k2W4pay#@#sLQ$Leo(clzKSJ|c9ocJZn&cX3GG#%V~94e{%`K2EHLoH6} z-vtK`+&tqu*Y4di$I00>l4sq0st*^bDSn4mDDWj)RX0C>uc8=h`>cB5++g=SEg-{T zXkydoxDBuHUC{9!y{TaDS@Y>~BF-9QjzUWq!Ru@`6Usw9%a%F$P81g8-c#K2aHg)t z{I`YsS$g3JX}q}IuIz<2p@B32wNlFZd1^>zQdNCZI2AUL7?Bt0<9Q={Axxy>7Nsn0 zn9FjWKDe{u@ zwu!Fq1ZtE1gy-#`f6q`NAYOP(K;;l{mUCb~d#$#>3a0#(Ey%6c$YTF|%@!v|gq3Nh z!UT3;>%ii3kR@pEw{!P+W|(iEGTdUi+@!@6&`7(^R#GWqy29vr{08N@`ka1nP0mk! z2BtP8+54;kzg>K4)#rh~!iY^_{LSSKzx$0VofmteGVYbQ?8!;-J~_%xnKlvnB(Fg{ z-FgxdLDpb9FD1GAp*T>@QYSJlx0!X@Z{=IPeW3$~GCjC^a7jGRPY>+xJkw#&Z32FR z4zMSKOjFib3cc%*56wX%2FSe39;!RjeSKb~4~5uB~)P|ma` zbAKZhnh6V*2SxGUj04p4Y7Y@+ZAiT2XQ9KGb$dt_M%wI@{?o(cNQZV!G^4 z`9?&|qrKV~GGTGu>p2ZxR-zkY_1|^A?Eij4Aij_uvnXgY@N54-aDOfB7ce=oj;IzU z5YgFfQm0-$OjZ~z;z^~}x(Lw@*F0MMMj>Yx>Kip*4Wv%DSfAY8LMYbN_ZS=d)%640 z6iXt5ti3h;fWhnSMwbck=II0WO&V->Wh8f9FE^O<*-X!vd|>2u*IP+=05O(qvZZI_ zk)rTmWTc_QgKToZt=J0(Z99`$UE~c0+|kkyWI(**Rxw<9cOtrEy&AMU($}l^4+aa5 z-_+JaCPUjVV?b&%{fWXyRy3>Md+P~CTqq{i1)tlywZPg?6>|!Afk`*0kOmQ0A z9X@YLMIK~h5W**35KWv#`AqJMp2WlmlK>kmyQP7T0>6%$g-L6CcN6U=vU=DMb+wa* zm5O`Mv2e!D!oteyU&Ps*w_Sd{IFtY44R`(e_d%y@TW-M|BB?>z(V#PM{YwGRm z_Bidkeh*tfi)+NZ!0X$21K+CyONk}*$1lKjA(|}AA@cq?dI97zAZ?6N@}AW1_6V}E z$x~T3g*;MTk4emr9pO!~y~g*C>3oCz%G`_nHrGjfPA3%~lv=NIkCfi?d+S%WZ}Ro5 zqATu=p+mZg{fE;VF7tNJMmvi_nc~Dc!pVp#&kgHfTIFru&R2C>%V!xS5E%FF-LvrX zaT(CMqh~Sczf-O8%;~|&Oz~tLzoNsVqYt#Duj>2v7L;gh;_jSz*8x)WO}W*VfSC1x z|H*%Aev4U%=~Ib6Yxuaz2qmHphX0G$ee`CI{l0~frHrhsY{%eLmFcO0dp}9nWqVKG z(jL?{=!)HUwER_Hb6}G?4gWk$Fd(2snY#H_0yxj$M^?!LXQF3g)X&xP#|*ueZ>Eh- z39w%AVIhv%TvqKJb7`@6w6`1f9Zl;Pc^^TT?^B9joB-^DY={iDU{52&DICocKC?A* zi76HWQg0sD=>mNn$G)79)9_i3jnq{cP~#rc-?CoZ5Lsqsg32ry!)pv z0~*E%3@yAI-|Z+`$5zaqq7Xunu6NM&@*K6-hob>*)Nkx2aR(v-vT4M0D%Uw>5pO-s?0gw#6m;tvuUJRzcz zmNRcaM-5qN0VP1g%;RfzKt9($ffpBP3@rdM18VfjkroHst@LO+KMe@_nt8KZ@>jmr z1mfvU+Zv#kt?lk~E=IhV0w?OJx5=^`7i(!yWO1h>n}~TRY$~d%+Vns9^N{8+r8rNL2@Q zpENTjSivnz{(Xe}Gz_0`X!I|b*iHAW=-s>F^>?#`ps7*|ShpXR-O$-6(0J6>*Pnja z(L<-A*|Xhc-hP>Ji=o07{mom>zJ3F#%8Z0cKQBTlTS`_oaq_mV z?rX|)=Vl%IRhWe|o#YivGw)^mwkumn&IGX2s{`7bH939=diZ<(`E z4ROEB92J$&{|DE=oEGMD|&b^vbSK(=AL*q=oLu79BDZun-h)^v4q z?;`8SZwk!{r(c(yK~|ns57hX@v4GY0z@%mKS;rZzUvi!nV}daR#V%z@4SbW6GXM7x zK*cw=;qd6rozOY9hks~M6%Yk6>2hM?)`$zsZfheb%;|Qu(70gMUUuxJXN8s%i3S{%Alw`Y82+VusAhVhVucP&vs8~9P=7Q%yDCmV~0iQ#9< z6mb8Q3z9i1#E0D%HKeCX{iUaBA8qO&f;8|vCxD_Fj9TxjAt1rGj*dRh-^h#nfMnPeL+H#!`wb;i@%R>b^ zQ5@`+H3F(xrA>jS+`Xfn%QdqB?gmayQF&xZrp3>5NRXp1-&sC=d*~K&dgS47Z-k=L zZ?uRHv6&(T)WC%q%3S{z+5qNqTV=wOXW2z$hNH#IfKCn6Z=Wq+>Lh?0C|uzem)gR! z_|^7^l;N8?ug5a+TguO^@11bj+=D;3{#4*HVv!&S%r3s?q#PCPo_-vXGn|xjUN6}G zT~HCC6L%w2E6l>@{}a~f;tzs=03d!=J7%%WXq<=Y^B`XqVxuttXUS36?((y$m~FMU zJi3QrJe`UE(Ox4gozKbuTo$`Ai5*l-eZZ!X&+6+#7sg9`+E@HGaL#A)5I`nl*x}kI z=Z^pZyO1U`pzyazDFxS7m)drI3|xL8i8G@`KG$1Op4IR5onAg)% zXin-#Tr|QW^oDct+6wp)4xUv z1wAqVSMJjQK=qW0ApQd2=aUe-XekfStJgJ)%@aZmRsd9Q#*u zuOYf9G4n{*>lWxJvK)bR!nI#o9bLAJh|Odv;<^1Rn#6OZe*P0q{R5vtzFKu_zNAKo zoW<87DG}$q=C`C1;e~7&tLp~8NV{>s=dU5IwYVQhfA@IhauA+&Ul|H_mjh4Qe=bZ^ z6pNsM8z?Jwq5;U#v@TLxTGyBm@_(k1Uc~d8DMBX{q9cNui&@!Tp zt%j&u06tjqbhwI!O{E|Z_xA1T&T8&IAB1$k?YaeGi4q_t;39Yg;Y^7~xQcCnT#;$< zJmCCn-j?dRd8ihIu89~q1@j9EcCHQp*AVCuJcme;Cwdj?vfz^lVvlBr_UV>dpnvD8 z(pno1#4pXoSN;CSGv(^RBQ>6a=^2Sf) z`rs7xXmYL_d1TvNuD}iuXwe-JUH@NDG79qR@_<#K+2y80u+>vY*R1pTg?QN_nmC#p z59I~B{VW7vJ451jT0h9)Mqv`y0R60gzk^-~ZrJfgi^)=)^P>1yY#qxGAWhDOzk2=p z;LYYP6mNbO$%VuDPf)K#iW_Q)e`a>EL{$d)&jRPr$g*4{275d-gBd_O2BB3?UO}gG zf(hE0uM9f7{y6gybHN7gzy}neHpEe=Y6bdjL*FgaTRUQ@V`Cqo`nqqx(99Z#$@QuX zew(D%7lZd}kk*m|IZjcd$3USQ3;vq#NjIQfH%F`<3S=E*O1V`2AoJEx6?aJvd;{q& zg9b?;)bg$lvpV|R{t4LW;Ck#kc@S4DAcUjZHA}s+h!*#6MHtm{KmfMpo zzx|P(=VhRKuUBi+);j{ADCtw@&>`@blIR(lx4kO9f5lR&>|`$l;ot`!0Plf0Gk|Y+WRky|1JTr zc;GauAA~ODA!8qVyR*&4=4OdILqo|XXWMDq6+zZvRylqG&nfeek>Q8y9xb-Jz&|R| z9QOrX8yoJ0*^?;|+)<{hi?ZU0#H?AaNzuNz!TSR91+l@6Nb6E7!Xq7e9wOQVM2%Cv z17IXMhcw4XbH`mCr3^V6EPNfR5em#phatr}Fq~P+;Ba>KqMsra{>YS9hH3-^S)z=d zt}!yrq8lBS@L(tzhxP{n)acM*Qqo49EiQ0iaslmB)j;`8$n+{wj*{0@>W$h&GvpWeEb``kRD(~?p zK8dhkAGo08g}x+spvKi^a6-yvX5b*$Oq)@Vz)7N*$>`s+2l*1N|3*UrdGQobi*Z8% zDdD5fT)Ah!a&h5ZdHJyNDgSl?!COCV%&zD7hu{#^085#EM_fl*Zc3M)nNxMapOz;t zOioDSacEr=vYa}o_^Al7JO=iIbwa~_^K$7}^ALulnfQ;CfROAfXNor0#z8>TwO`fH6EzN=gLPipIwcjqWr#UDh+opdVOpy`#%LlI_QedV7kbIO3r#7s20lYfF2L9%F)QLT&|Fk(n0<9 zno!2GqAa+Fj#1Q88k0`pxn_4+O@YQr%-84!j$PH;)5-z!51ewa4?OEu z4ahY4!-Zu#vEovKuy9zGYxHOn;bNT9SFUzcmeDJatbP|RnOSexaMzQ&%)1V#_`rKa zD-f&A=&C1`;G@hi3(C&pq^Q`JPHk!+$BC?(634$idxciy8gvP68RrRFysKr)eEl>k z=+EYNRe(AIRw{oO_N|)GRdk}Ob~j4uj5y&X>NHJ8QMj||thPLlc9#UacNb&Ely%>a zR@-tJIw}SMYR69{)(_tf4G*e$=|)y<11p2YFCB4Q>lYfhhTuvldRr+%L$WF~Yi<%7)+JlGW2DHcqt4AdSzGi7Rxu#%{!1^6)*8NR~H{4Rs7dW5Qm_ zng@g-aSf|r8qYLvUNP|C8hZUo^^up*N{iV#G2HQ4-Rh**REX&St3SsJ8J#QXFM#w3 zXf;WQbqe#ZqElMS5XdA!px}Gvp9CHW4D%cT#@ArnoyOzQY1dSidp7RUOLcKox-VNI z(q-9NRe`5t$7>G!;Z1GWG$%VJ=W3u=0ceTzFHL70mCUQ$r|!Yb#LvHW)X;J) zYSA0ox7DBQ<;Y1q9q2L*4B*y3C6MhV;^yEPI)7p=Un~ULjj^M>6)uB_;NE=|L9lIB zNf$X_yvL({uY$lO2WlYAo8JLf4!YOW1WwU-Z@vJL<+{9UV$+NY|`cq22c>3LJT;(PoE@T^S=-MyuQ+=JAGpb{@%7XJo zo4@b;w1}NWFVW|d-#hKx=z~N7GM!SQz=P~xpzDGNYHWXF(%1fzXaVw55&&_;+$4Z4 zV`J4M#S4QjP9C0k`!9gA+3e|Z*k3bY=IzQ&R zTh$eQiE75O8QA6Q59(;vxesaafUp#n2GE2Ei95Q$dyF>V0tpSVJmV5zx`+ql_wQG+u2Br`$#O?CCnc{S7i9DZf9vDc-|4>y_) z=+a825|FV5zV!QQW+usxALEbJyS+~SDAsjF$T63iqS;Y>R4~pg3j+80Spn~w1dnN8 zQB7&$45VChVM@WMXq4(e)qlf=8A?z#M1nwG#?ahKoP63w;hc#p_H$@NC)9hC2l46T zmBn-d_J@;-yFVUFap29xapqlN%lG*tb)CeU!a#CFgazbJ12z1d(#7j4$4Kr}ORMzqybhWb}74O-jgm8k>-*248ojO16^G@w391>&oV++v{ zitpa4uMg(K#A(MO5}PWa;5M{w4=1SPS)aKJF!O-SV)D^>?OimtihoXyR>u zKvI*17h@dW(4@6bIV{B8t1?mIk&G8R?P1jNzq?WUopbSX|LlQJO7!Tb^n~b*hZ}b0 z&vRU5$mnjFZonk2A;YlGz;=2D5_um-v;PqPc93?&d@L_BeGhwO5$3;ZMdNo!T?H^d z{fAdG@mG~e3WUt3K(@CmU{)ZxlnT-x5m`ce4iM+YBwYqC!K2J^x`BNNZCDkEW=2!3 z_5fTmhcbbz=X8F%{rc9DninU4Uf@x?TC?uXAf4t$*byB-i93>gjKt-Y0y@GhcEQ6i zO2z>Kns%l-&270>wM$@sY^P&rV9|+0@(h0DV+dm;V(yDz8Zr~jVY>51e_ER)Q6zCt zo~$P-Hy=orI}e9U0{n=80-L$2rshhH(W2@!A0f)8p|1b{bqUl^HN}LowL9w26s(Pc zt3!rXTrNXvJ)yD3tHd$y2M z=uH5r{I5=-dZ-hb3HU^crBXaOEW~bSJ#Q1x4(!8``HKb6g}s{6#m#}ub=|c0H5mHl zpU_n*bCkI?+u2Y=F3)Va&@}W0^*FZh!x$MjEJyLkoO<^6rS@{3*_$ZjXFwfFZ|Y;y zd{J3-kOZlcP#Nf0w0YnD%k$xZJ2C=xOeaKHT~K-l*yg@k>fWfE6m>QL^<%!y1>CYB zl?w&R-uQp^AT=8%^vU3>j~2BD>x)qCve`2Upe30u~&iCl0c{JctP^5t>Wq9!9zAS>;hN++&#YzjxMgUcHu-7}v zufe+jH1ihFZ>rER9e?N4<0Q3=D~zZ(b5}xNa!P_v3>OyPx&noy?oD2!)WZ1w=z2 z0dfLAWZ9m7-%{p}J)9bE9}W8d>!krAm!lW23%t{y9$b_~fyX*T0~{I;_Od!fQ;(p( zn?MtE80M5m>S-RZzdE9zi`1N~jO@8oMGMl2l5D0Sv6lhUVH8)iLUal>UOwB}m9SvX zZ1`M5apbJ7Jg7j+?61*rOEId{ft2Fe0?0l1=%j?s4rlGBT7&r4wAy|rTn{+&9olcZ z$TOV4AiElbL+7~vp}WJlWJwdR^A^2`k!cN(Hi=rn_O?#@)0K-Neo3D1NK!Yl@xF*9 zS^hYjbGBR9L$^$&Nb8~_<5d-AQ3TANa4#2gfO7_LQMR;8<5bXAoa;rUCFqnzj0Jd) zeX&@}&wzq+HxA<>2`4^r7%#9vnIO2c!H-Bl`>X~TG84D(_WUmO-@A5=jg2=CX5h%W z9f-Yt?@`@HkloEJ3pt_+`{Bh`6GNWMpGM@`V`gmT#TY$wNq@M4Drjv@EEDcHhODGJy7 ze0l@){rlV9JY(%dl6H`0ca@ymaB)6zXr+Foq_Uf*WqhgkY0upI5M1KjmyQ})q(z|L z7Lx1Nm-H9wf1d`NS_%=@`Zk8r2(WzP4Kc?3=(yaIM-8|P;Z4J#!L znWT&{9Ifm@$-&+<!(y#*v7!9kqF5&4= z(*26$kG^y6RrKk5dwcFcFHY@ur_^|DfOgv2=HbJ~OU}Rs<{p5kr#*8sJ!hhN(lI10 zZ;32lE)CGz))4+hX2G3F&=nLQrn+p{5BZIh;!a_#eh=hy+efq5m^m}FUD+nfAV!3 zE48Ob5jze6$79^~#5SIUqN}{KpL{#MrLis<5Gx94VF2?TZuMKXwy*C!EtKHwGN1PC zh@rafC3M5uDzVu@g^BUv(}?}DzS6R?zORTMPI3Z-3G6xrj!T(XTLLL*X`S<8+g(gX z^qH7K$g*c!W&xQz!}2YC^OGm@f{!lsL{wB%44=e{ZAV5+NR{^NZm7^{5RXP#267>& z9x{*Mia0TYy6{Fwjo0d)+hz&JU(5))MNlKz_9XOTvl zP{vDI!J{W=4DwmIh@yu$U+u_=1-u^d2$Z2;_Ij1=LPMB;!hruAa6h;Tn4+-*j`|~P z$&U;I7rJN$J1IJn9^e|7F+5}d+VkfYjER@DS;AZf(vrG*ZVB$+YOQgU$RkU5FGNH9 zC1@Ro#xbxm+WRQuMsY2(zPp487z; zFfw(w0kn=3F=ncl(KM~dL|s&K6E}f2Q{%+S&_0Y z0G04JV3B-XEfn7Sor`Y>M^Et>?zEl~RCrw~$9 zh;qNGoa&VknJukf<{<2tk;GQS(Cn%_{QB*Lb0GORN25wdzic5x<}v-g*iXEX_Ptgeb|clK6JNw86{k%0wZg&FAOr}T;FB(&aGVaoy%9CJ3dOmm>6=S+QBj@W$WymU9FeXIHk1HuT&4cYkHN$C350#}K$#^wU{k zohF|4$r4fd*%X>|o=x98WjCXDy>HjZwKde0e-02HAX1dLY={hj#LmB58xo;E&7Xa8i67qE>_4u`cvUS(?3)(sOk@x_$dqp7vhD#@<0VB+SDL3k$ zKnL3)PX-yIf@}+HbJKNA%Vtc7MjJUlK|>-+QZac+l1Kir>gUHT<)0{gKLS(Bc-j|? zgzSwt&Ca7*`>EgJs+8>yhJJed7+ya26?rVlGEheZ;V1u#xh3e8siSTC!COR`TgD66 z=(EDls+vEY+4#ra;}2N1Qnmk3-fq|Pw$g5>#E8Q@VZg`q*SEczhrhlF)UKV0Oz~hn zlh}JKu+XNpzo?n!>g@4KL=Q+R$I`DAbP*ypp9GTq%Tq?Bb0F>N;zDDme@J6jk^3^A z-njXljTF9}8GP|3*TfW1-0knb{R&vYpS!y&3zAuSj9|>n%nI2y>!pAiJo34a`ptuV zwbxBMcaFD$GsbTkK9PR{8o{?M-O^^DMMiyPkpk^nuoN?{bG-DzM!g`HHafkRf_tk$ zOsr17%8`EX(q%1N1V-k*jLR{NpB(7v`}sAMmvu8b^iZ!~36BmI`xxUQE2J{=u(+H%Q;t=IbqmnzNOFcUyem5OXy$YatwFa3l zPEh)~zc$v8c}?fYwK((&YD-{39e2FlHG@>|kGPzfCKkPtwF4Q?e%uA~w%-`DGA0nf zWZFE0ztvxE65IEI(-J34po3V`8~3L z)qDydn6nRUmO2qD@^~g+?mRx2pZ8@tapqI!5 zVA3qx?$0Wlh&a!v81VgzqnEeEq-1mu6S;>qNykX-~KBlP?!PNBC%Rab# zui~)T@TQi#d}iyaL8Toy?G$#^KBy-iIYD6`*D&x;TL?OCaIp+A0c zXIGBiwf#9SCtpi8i4Hcx}iIr2%4&Yc@ z1Z7uAr(EP|>|2th)(8ga)jQjLiK*k&entAVk|+hFAL4YFl;Mp>Ebvytp>%1*#1UG+ z(!!0Pb$_ZF@swJ?UfSZ~fjc$P9ckNvQQC zdQ?3eFyA&^rRD}R+!|{V_=*< z#dx8}7Ziy??^sXc#rO3+F0CvoGWBERQ_avUv;yO+Og7>bxS$x3c= zyP)O^V+;H7A9fhB)s{1z3!@i z+FQx62#ECk2BJZV5!N0)t!Aw;m|q_WS<#D_Ur{ysQ*00e-AIBd^uuN*1_BGhM8OEn zR{*e$1^J>k2!>43tWA)L2(w}h?M`O^r#HwjB$KEM_m?|O22Mq4ko)cKWi#^ z+WG&369nD{_+nHKTnyp;sg+NVxb3AZrC^lWJze$N0m|-MMX*Lxkq799_Fh{Wvo|l8=3Re&>rip4$f~MtMl}6TvYXDLd zSBPcof4x;y5QYYZLhTvQ1c;O{M)M|q%R|BHPsM4AzC5@8Tki(sD}cfDSH1%2neN&` z1O)VuyT!*)?@RCNe~6&|?#)PCOh8)Jf~KjF;Z>s>?Eec)`zPxHGjaI;gF^%@U#kvu zB8%N;sRloZFz1H*q9$B4GbNnW+=+PxA&!$QK4$WbJ z9NIT^6`@e+B7#-?*Hpe%Y8V$6*TmvLDkJ(}Y8WuGXH2Mt1JLnM1&}+^OJ6DT{0+K) z9+CzVJ(WiS0Woh#*_O~C9Kg>(&id0sn+WxB^ss;60d!!1@Ab?eG5uywPcH)A2hg+Z z2PL|K!s`YYP=Pc-mDth66@RHn;O`G;OntsqS_Lli4KU32!3^MZj~#B!uKn#%O0mLq zQJ$6~%+LTkBW!P>2K0WS%Xt0>y-*U+u⋙oB4nfBh`#ZXhQ$-1mYb)YwSAXXiFgO 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 z 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 CH=>9D literal 0 HcmV?d00001 From ce332c71bc7fceede3aab9ddbc893dc5b7ae347f Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 11:46:12 +0100 Subject: [PATCH 116/148] Revert to svg images --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 36c649f..24e2b58 100644 --- a/README.md +++ b/README.md @@ -719,21 +719,21 @@ ctfp/ ### Overview -![CTFp Architecture](./docs/attachments/architecture/overview.png) +![CTFp Architecture](./docs/attachments/architecture/overview.svg) ### Challenge deployment -![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.png) +![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.svg) ### Network ### Cluster networking -![CTFp Cluster Networking Overview](./docs/attachments/architecture/cluster-network-architecture.png) +![CTFp Cluster Networking Overview](./docs/attachments/architecture/cluster-network-architecture.svg) #### Challenge networking -![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.png) +![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.svg) ## Getting help From 1a183800b4ffaed4fffde318db8b2a3c83b87765 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 13:39:29 +0100 Subject: [PATCH 117/148] Update architecture diagrams to correct colors --- .../architecture/challenge-deployment.png | Bin 105644 -> 105644 bytes .../architecture/challenge-deployment.svg | 2 +- .../challenge-network-architecture.drawio | 16 ++-- .../challenge-network-architecture.png | Bin 65472 -> 78173 bytes .../challenge-network-architecture.svg | 2 +- .../cluster-network-architecture.drawio | 70 ++++++++++-------- .../cluster-network-architecture.png | Bin 83443 -> 97778 bytes .../cluster-network-architecture.svg | 2 +- docs/attachments/architecture/overview.png | Bin 154619 -> 154619 bytes docs/attachments/architecture/overview.svg | 2 +- 10 files changed, 50 insertions(+), 44 deletions(-) diff --git a/docs/attachments/architecture/challenge-deployment.png b/docs/attachments/architecture/challenge-deployment.png index f5caef675dec124b99f182e4a1091206712c9809..a75cefa87678128c612500c702a7c409fa72ed66 100644 GIT binary patch delta 29 lcmZ3plWomTwh70WOiebP&?sez3#n&l{!+UAODW?HH2}a549)-m delta 29 lcmZ3plWomTwh70Wj4U>u&?sfuP<~RX`Ag~cFQtq-)BwsK4X6MB diff --git a/docs/attachments/architecture/challenge-deployment.svg b/docs/attachments/architecture/challenge-deployment.svg index 8373da6..553aaff 100644 --- a/docs/attachments/architecture/challenge-deployment.svg +++ b/docs/attachments/architecture/challenge-deployment.svg @@ -1 +1 @@ -
ArgoCD
ArgoCD
Shared challenge
Shared challenge
Instanced challenge
Instanced challenge
Deploys
Deploys
KubeCTF
KubeCTF
Deploys
instanced challenge template
Deploys...
Deploys
Deploys
Master
Master
Container registry
Container registry
Generate
deployment files
Github actions
Generate...
Update deployment
Update dep...
Challenge updated
Challenge...
Push docker images
Github actions
Push docker images...
Pulls
Deployment templates
Pulls...
Pulls
Docker image
Pulls...
Chall dev
Chall...
Commit
Commit
Kubernetes
cluster
Kuberne...
Github
Github
Service / Deployment
Service / Deployment
Cluster
Cluster
Github
Github
Action
Action
Background
operation
Background...
Github branch
Github branch
Challenge deployment
Challenge deployment
CTFd
CTFd
CTFd manager
CTFd manager
Updates CTFd
Updates CTFd
Deploys
Chall information
Deploys...
Text is not SVG - cannot display
\ No newline at end of file +
ArgoCD
ArgoCD
Shared challenge
Shared challenge
Instanced challenge
Instanced challenge
Deploys
Deploys
KubeCTF
KubeCTF
Deploys
instanced challenge template
Deploys...
Deploys
Deploys
Master
Master
Container registry
Container registry
Generate
deployment files
Github actions
Generate...
Update deployment
Update dep...
Challenge updated
Challenge...
Push docker images
Github actions
Push docker images...
Pulls
Deployment templates
Pulls...
Pulls
Docker image
Pulls...
Chall dev
Chall...
Commit
Commit
Kubernetes
cluster
Kuberne...
Github
Github
Service / Deployment
Service / Deployment
Cluster
Cluster
Github
Github
Action
Action
Background
operation
Background...
Github branch
Github branch
Challenge deployment
Challenge deployment
CTFd
CTFd
CTFd manager
CTFd manager
Updates CTFd
Updates CTFd
Deploys
Chall information
Deploys...
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/attachments/architecture/challenge-network-architecture.drawio b/docs/attachments/architecture/challenge-network-architecture.drawio index 98c2242..27f10ed 100644 --- a/docs/attachments/architecture/challenge-network-architecture.drawio +++ b/docs/attachments/architecture/challenge-network-architecture.drawio @@ -1,13 +1,13 @@ - + - + @@ -31,7 +31,7 @@ - + @@ -46,9 +46,6 @@ - - - @@ -89,15 +86,18 @@ - + - + + + + diff --git a/docs/attachments/architecture/challenge-network-architecture.png b/docs/attachments/architecture/challenge-network-architecture.png index c3f942918a52de01b6e642e947c6f3abf01c7dee..ef62df97790acdd7708eafb1aba0a8c094d3247f 100644 GIT binary patch literal 78173 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

t6qX)*#G9^I^*@Q`O6k>*8@%&QC@Vdj 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 OG*$Ifo+?@0`2PT2t_F?( delta 44646 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

User
Kubernetes
cluster
Kuberne...
Service / Deployment
Service / Deployment
Cluster
Cluster
Hetzner Cloud
Hetzner Cloud
Request
Request
Challenge Network architecture
Challenge Network architecture
Hetzner
Load balancer
Load balancer
Branching
Branching
Challenge
Challenge
Yes
Yes
No
No
TCP?
TCP?
Yes
Yes
No
No
Available?
Available?
Fallback
Fallback
Traefik
Traefik
Traefik
Traefik
Traefik
Traefik
Text is not SVG - cannot display \ No newline at end of file +
User
User
Kubernetes
cluster
Kuberne...
Service / Deployment
Service / Deployment
Cluster
Cluster
Hetzner Cloud
Hetzner Cloud
Request
Request
Challenge Network architecture
Challenge Network architecture
Load balancer
Load balancer
Branching
Branching
Challenge
Challenge
Yes
Yes
No
No
TCP?
TCP?
Yes
Yes
No
No
Available?
Available?
Fallback
Fallback
Traefik
Traefik
Traefik
Traefik
Traefik
Traefik
Hetzner
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/attachments/architecture/cluster-network-architecture.drawio b/docs/attachments/architecture/cluster-network-architecture.drawio index 6b721b4..ce002e2 100644 --- a/docs/attachments/architecture/cluster-network-architecture.drawio +++ b/docs/attachments/architecture/cluster-network-architecture.drawio @@ -1,13 +1,13 @@ - + - + @@ -18,7 +18,7 @@ - + @@ -33,7 +33,7 @@ - + @@ -48,55 +48,52 @@ - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -104,13 +101,13 @@ - + - + - + @@ -118,13 +115,13 @@ - + - + - + @@ -132,7 +129,7 @@ - + @@ -140,16 +137,19 @@ - + - + + + + - + @@ -157,6 +157,12 @@ + + + + + + diff --git a/docs/attachments/architecture/cluster-network-architecture.png b/docs/attachments/architecture/cluster-network-architecture.png index 63fe1eae3b2008ae05bbd28590d39f39b55bbab2..d01f6e366c348f01d967f5af25ecf847e6a2797d 100644 GIT binary patch literal 97778 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 G{r>=`K)+xB diff --git a/docs/attachments/architecture/cluster-network-architecture.svg b/docs/attachments/architecture/cluster-network-architecture.svg index 1393aed..5b190fa 100644 --- a/docs/attachments/architecture/cluster-network-architecture.svg +++ b/docs/attachments/architecture/cluster-network-architecture.svg @@ -1 +1 @@ -
User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Hetzner
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
Text is not SVG - cannot display
\ No newline at end of file +
User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
Other K8s
resources
Other K8s...
Hetzner
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/attachments/architecture/overview.png b/docs/attachments/architecture/overview.png index 58fc9814711359052a77a6fcb3c065f3ea8e26e1..87a00baa8a1103fda197d1f4f1cb14a0bb23e52c 100644 GIT binary patch delta 29 ncmV+&0OJ4qw+Z{V36RDDIW)1ze6s|jK#RMBNVK;|v;niH-#iYU delta 29 ncmV+&0OJ4qw+Z{V36RDDHaM}!e6s|uIS@;ONVK;|v;niH)!q%b diff --git a/docs/attachments/architecture/overview.svg b/docs/attachments/architecture/overview.svg index c488b15..cd1b8db 100644 --- a/docs/attachments/architecture/overview.svg +++ b/docs/attachments/architecture/overview.svg @@ -1 +1 @@ -
CTFd
CTFd
Redis
Redis
Redis
Redis
DB
DB
DB
DB
CTFd
CTFd
CTFd
CTFd
Chall
Chall
Chall
Chall
ArgoCD
ArgoCD
DB cluster
DB cluster
Redis
Redis
KubeCTF
KubeCTF
Instanced
Challenges
Instanced...
Prometheus
Grafana
Prometheus...
Logging
Logging
Chall
Chall
Chall
Chall
Shared
Challenges
Shared...
Kubernetes
cluster
Kuberne...
Uses
Uses
Deploys
Deploys
Deploys
Instanced challenges
templates
Deploys...
Ops
Ops
Deploys
Deploys
Platform
Platform
Deploys
Deploys
Challenges
Challenges
Cluster
Cluster
Deploys
Deploys
Deploys
Deploys
Configures
Configures
CTFd
CTFd
Pulls
deployment config
Pulls...
Orders instanced deployment
Orders instanced...
Deploys
Deploys
Deploys
Deploys
Challenges
Challenges
CTFp
CTFp
Service / Deployment
Service / Deployment
Repository
Repository
Terraform project
Terraform project
Cluster
Cluster
Github
Github
Action
Action
CTFd-manager
CTFd-manager
Deploys
Challs
Deploys...
Configures
Configures
Git
Git
Architecture overview
Architecture overview
Text is not SVG - cannot display
\ No newline at end of file +
CTFd
CTFd
Redis
Redis
Redis
Redis
DB
DB
DB
DB
CTFd
CTFd
CTFd
CTFd
Chall
Chall
Chall
Chall
ArgoCD
ArgoCD
DB cluster
DB cluster
Redis
Redis
KubeCTF
KubeCTF
Instanced
Challenges
Instanced...
Prometheus
Grafana
Prometheus...
Logging
Logging
Chall
Chall
Chall
Chall
Shared
Challenges
Shared...
Kubernetes
cluster
Kuberne...
Uses
Uses
Deploys
Deploys
Deploys
Instanced challenges
templates
Deploys...
Ops
Ops
Deploys
Deploys
Platform
Platform
Deploys
Deploys
Challenges
Challenges
Cluster
Cluster
Deploys
Deploys
Deploys
Deploys
Configures
Configures
CTFd
CTFd
Pulls
deployment config
Pulls...
Orders instanced deployment
Orders instanced...
Deploys
Deploys
Deploys
Deploys
Challenges
Challenges
CTFp
CTFp
Service / Deployment
Service / Deployment
Repository
Repository
Terraform project
Terraform project
Cluster
Cluster
Github
Github
Action
Action
CTFd-manager
CTFd-manager
Deploys
Challs
Deploys...
Configures
Configures
Git
Git
Architecture overview
Architecture overview
Text is not SVG - cannot display
\ No newline at end of file From d8b66a98438620d4e1f7a992b19307ca8edf0f95 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 13:46:31 +0100 Subject: [PATCH 118/148] Update cluster network diagram to include Cloudflare proxy --- .../cluster-network-architecture.drawio | 75 +++++++++++++----- .../cluster-network-architecture.png | Bin 97778 -> 111523 bytes .../cluster-network-architecture.svg | 2 +- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/docs/attachments/architecture/cluster-network-architecture.drawio b/docs/attachments/architecture/cluster-network-architecture.drawio index ce002e2..83be5cb 100644 --- a/docs/attachments/architecture/cluster-network-architecture.drawio +++ b/docs/attachments/architecture/cluster-network-architecture.drawio @@ -5,45 +5,58 @@ - + - + + + + - - + + + - + + + + + + + + + + - + - + - + - + - + - - + + - + @@ -58,7 +71,7 @@ - + @@ -138,12 +151,12 @@ - + - + @@ -157,12 +170,38 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/attachments/architecture/cluster-network-architecture.png b/docs/attachments/architecture/cluster-network-architecture.png index d01f6e366c348f01d967f5af25ecf847e6a2797d..bdc6a1d87a52e75c49603e5eb5d1119f5728c608 100644 GIT binary patch delta 68192 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

H!~0bI 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

User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
Other K8s
resources
Other K8s...
Hetzner
Text is not SVG - cannot display \ No newline at end of file +
User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
K8s
resources
K8s...
Hetzner
Platform
domain
Platform...
Management
domain
Management...
CTF
domain
CTF...
Cloudflare proxy
Cloudflare proxy
Cloudflare
Cloudflare
Text is not SVG - cannot display
\ No newline at end of file From 4211682e120e4cac551d7c29890e47fc711cbafd Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 13:49:24 +0100 Subject: [PATCH 119/148] Add generate-backend to quickstart guide --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 24e2b58..db3eccc 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,17 @@ To create the server images used for the Kubernetes cluster nodes, run: ./ctfp.py generate-images ``` +To use the Terraform modules, you need to generate the backend configuration for each component. + +```bash +./ctfp.py generate-backend cluster +./ctfp.py generate-backend ops +./ctfp.py generate-backend platform +./ctfp.py generate-backend challenges +``` + +*Replace ``, `` and `` with your S3 bucket details.* + Finally, you can deploy the entire platform with: ```bash From 1d6755b25e0128e20fd004e1d02c916feb203a4c Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 13:49:29 +0100 Subject: [PATCH 120/148] Correct formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index db3eccc..861d652 100644 --- a/README.md +++ b/README.md @@ -700,7 +700,7 @@ CTFp is composed of four main components, each responsible for different aspects 3. **Platform**: Handles the deployment and configuration of the CTFd scoreboard and its associated services. This includes setting up the database, caching, and storage solutions required for the scoreboard to function effectively. This can be found in the [`platform`](./platform) directory, and as the `platform` component in the CLI tool. -4. **Challenges**: Manages the deployment and configuration of the CTF challenges. +4. **Challenges**: Manages the deployment and configuration of the CTF challenges. This includes setting up the necessary resources and configurations to host and manage the challenges securely and efficiently. This can be found in the [`challenges`](./challenges) directory, and as the `challenges` component in the CLI tool. From 8767675d0e67accdcb05d55596d0fbbc99a9075e Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 14:33:20 +0100 Subject: [PATCH 121/148] Correct heading position --- .../challenge-network-architecture.drawio | 6 +++--- .../challenge-network-architecture.png | Bin 78173 -> 78144 bytes .../challenge-network-architecture.svg | 2 +- .../cluster-network-architecture.drawio | 6 +++--- .../cluster-network-architecture.png | Bin 111523 -> 112375 bytes .../cluster-network-architecture.svg | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/attachments/architecture/challenge-network-architecture.drawio b/docs/attachments/architecture/challenge-network-architecture.drawio index 27f10ed..725a121 100644 --- a/docs/attachments/architecture/challenge-network-architecture.drawio +++ b/docs/attachments/architecture/challenge-network-architecture.drawio @@ -1,6 +1,6 @@ - + @@ -44,7 +44,7 @@ - + @@ -95,7 +95,7 @@ - + diff --git a/docs/attachments/architecture/challenge-network-architecture.png b/docs/attachments/architecture/challenge-network-architecture.png index ef62df97790acdd7708eafb1aba0a8c094d3247f..125154d2cafb8fb9177fe13bf8568075e90c1ffc 100644 GIT binary patch delta 44128 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 delta 44198 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

GKnfw$ga@{FOPO|aLP8a9aDvoy9ryVpR+|lB z)@56b>Pp-SHrPy(?|;ui>G%y@UI5UH=2QxA1HDofYljSM?f{DQ^iXYaDA3NJR-IH* zuSB(>B0`c|$e#eWCyfxqwFg|Y>$h*v@~VK$&)lr45#5XVBMxOwMOQ7=mI6-dLvY45 zqnuX|lH}r)IUrkysx|@sEjPg4ujxq9R)$=tKLZZAW&d`CJPA}@T=Gl)|?QFsiBGCnVA*`5E;4ye^^ z3_f>vIhGWt?LxijTM;YKCaO9TwOvF+=B=}X?G`0J*<2%AJ8HV2`XRGfHI-ukLzn`P zl1H?@x%rRbuSt(NlzXL1>k~$hv*xH-!}NpEQUz+YY(qPBS(NgMhu+IDsC%0Fy1H+z zyOUZ($EhG}!}seXq2_es86&sN=V#m)?V9FKb9+0(y~se~d5#7!>JOb||V<>L#j*X_+;%Tx+s;H0o2{8`r7_Q}W5cazk0P5k~7_47m8 z;53vq)7UV5&qG=-tzE}_o%t%~>^uE{P#(SVJ_I1#t|eoDad1{F3T3(M#cIP^uPhpN z1;|0&(Pd09-l+Usj6y>_1MqIcJpiG`?>;}-$JWkBKHSn5{M{<%AW<1bUvp|f=^#Ck ztngja8GFGsc#1tvDQnWP+=L>N8K*QKehg7kqvkkhN_j=*`q-)k5e~W?{sfV�geo z){3j6?ctr-1=d)hroj&zn=BHn52;zS-5FYcd)jVWITK1_M%|aFws@=UdK=Xm261)Z zLrNCQZ8WGv<0-}aG?n=sln_L+CkpJIgktR9et@VLx(6>UZDrz>0#LY5TKzY|p#-`lK>H_yO*=4+b2=G8n^1xq&RaqaGuXy3?~nV~8xz^_ zzju!~!qla3)pc4PHR2=w51KDIi?8DPQX)Ew zrOG^p@5!p=BhhgLOlvHK)HnOJnpccEKd;K)99@1)KsSz5Z%4+Qvkl?o7rY?Q7i^EiaWQ;EQvh2t(9f7V`b| zm!*17ZS&l>sk~rrO+dLnCgK&duV(NlH`1~*2AOHNmft(>c4~P2h}q#F!}>L?anWj- z%b59Lh^%aPxahtDa=H&hrui4wD0caV9OK6Hv~OPnWqwA?aZhPc=bKyiE&kKy^T$7} z0#Zfim3E#DKI#H3I)Ae7>CN62|4MLF{dHRW+dP?LitN3Wxwo{3G;_p#CJG?jZO5L9 zj#tDjnhIS5NJa#w`*Pn&Unk5(A)()t%@pa}wk-!krZNLmCR4c?lCckl9K{-L*5~cC&jLWY9(nEJKDXu7`QXSOzmJdPosn*`C)3hR(?=2% zC!X6bwK`+e{!T*D2UoXLPtv|Y8SVH%6Wv!1ZcK{PM0it(l{GvOPK`#J_CjDgMkJt(1Z$8WBA8FV8njh?|j{g4#$G#?W z|9|=;*w*aX_?dca;Cd{?|HZju9@A2`vFZy6#*zRjJ!@i8nuQk0o_C>4o)uy-m!u4-BcVybY5y_Pmf3{kQ zC-1JCIlON$10I)3qC^ zK@|e)Fqk@GiSZ7SiiM#`*qIg_DAJMt1z1X?F@jk zlERsiX5pjlbWnCn310hIp+Q_IcmU=7TD;-x7!JlST?A<^!H0F8R;fC#mI_HiDJ7a$ z0e7jhUB?k`GFHD`SU@fuC`cM)y2!(}G4v)<;QYILE+ppf1%%4qI)Q(pE>#}D+FujB ztv#7&q`vqE0+KMnv(e(pWeaW$RF{Ojb@DTVi0+w(dOj$(FXA1tZb00sU~Wz+azI=e z24@vue6HA+3-7w25zb3^OOPkS(sSm?AKP?V0KKeK4WK_dgu5L90h`}rbP;7hB|~|g z3U6!ysjp>$4e4vr!)f4CI`M75uybn9_B;@U?eIwU{%7qDqTmD>-ne&~eCS|f3RtC2aB6g=FD-yq(XLJeoqS^Q6KAiX=+YTSLwr_OJ7;+dHOiVt)|7qg~4S)KyE)Gv1%GRtRQ*OKpH4%Iv zQy7$G_zUny1c!GNCFnZdP^Y46XgH-fNt?UmYm+IXqty>xFv0ijOFSioy~!sTA}e{J zyK_*NWQnQcb6sIP?o~XV6<$=X1v4b#^;ZaHfKka54)iIKtqOq|T)A!N7vY3tUMl?O zMB?Vqi}m!DpO17VGE$o&j$=BfI#eFY2&ybJ=ULED#%vCX6y`rFP zitEI}xQlaf-vo-?3JdJ39*p1MgFRm@9I158z|>RGN>SavZpm~SzkBmjGGd>6(t99& z!|@?=wcodyRjUX_$8-$_tnt61wlHjd_&>uYKj7A0{v^3_YPnyp4uvCy^9H^K)R}v5 zEmn?GYZlN}f>%T=Qyg(P7sn(gc_I73q!G4&a{g*q><@3Y2-lvaNY|L_E$c(U7)#!d znF#(PC(3u5Ux~@H43U>6W(8=?n3j0j%D-3C(~-j1oYMajfp}YiXR&|^8wsLkLcNKJ ze5u~R9s#?;K7^&R96~*R#9+MO?|Uh@@KccGV15c08v;ZHw{d=QU?qqtfpZy@R(Ja6 zH$j&zvNc^

User
User
Kubernetes
cluster
Kuberne...
Service / Deployment
Service / Deployment
Cluster
Cluster
Hetzner Cloud
Hetzner Cloud
Request
Request
Challenge Network architecture
Challenge Network architecture
Load balancer
Load balancer
Branching
Branching
Challenge
Challenge
Yes
Yes
No
No
TCP?
TCP?
Yes
Yes
No
No
Available?
Available?
Fallback
Fallback
Traefik
Traefik
Traefik
Traefik
Traefik
Traefik
Hetzner
Text is not SVG - cannot display \ No newline at end of file +
User
User
Kubernetes
cluster
Kuberne...
Service / Deployment
Service / Deployment
Cluster
Cluster
Hetzner Cloud
Hetzner Cloud
Request
Request
Challenge Network architecture
Challenge Network architecture
Load balancer
Load balancer
Branching
Branching
Challenge
Challenge
Yes
Yes
No
No
TCP?
TCP?
Yes
Yes
No
No
Available?
Available?
Fallback
Fallback
Traefik
Traefik
Traefik
Traefik
Traefik
Traefik
Hetzner
Text is not SVG - cannot display
\ No newline at end of file diff --git a/docs/attachments/architecture/cluster-network-architecture.drawio b/docs/attachments/architecture/cluster-network-architecture.drawio index 83be5cb..7f8e00b 100644 --- a/docs/attachments/architecture/cluster-network-architecture.drawio +++ b/docs/attachments/architecture/cluster-network-architecture.drawio @@ -1,11 +1,11 @@ - + - + @@ -59,7 +59,7 @@ - + diff --git a/docs/attachments/architecture/cluster-network-architecture.png b/docs/attachments/architecture/cluster-network-architecture.png index bdc6a1d87a52e75c49603e5eb5d1119f5728c608..b7d5d51e44a10c5cc4c47f848117192eabeac1aa 100644 GIT binary patch delta 67754 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 x@-)`MQ;q>Cl6!gThaUVEUmeq_ zWYp-2G}Uwq4W5EN?1x8k%5MBjwUe19kily`z<4eJqh+$uM$CXcTrfQ^Ouro1HYU7B zzoZ1cbsr?mz@jy>1$SsGdE3esb@QHv0i$YNQC6kUzLI}^fWDB1H8U}5fpM6 zYtx2to-^jh(N}dhd~-Kye^{~_y|{lMHB)|Ghkgv`1}1Vzou-(M?@zi{=ty3tcDyzc$oT*b0x6PE6%k$AXcn74=w5jpIPPS^o+n_JHRF_je zRy}elyvYK#tcii8niZ7l3J|SlV%?zUR9*4p+NmN{Mq)cLkLRwH*h*$(K~2rLdJwhS zJ8GvE;3%cQFW`@PdCg@C9571dV5afpP# zx%&P=2H^xY?M%nn%-q$({f6>o54|r^Tpk{pBbDZ%M;W!HbM>@F_G0+A7e?Pq(t;+_ zLw4`uSfOE+o9@pKV+4x#$bWJ|4Q-iVFM34R?t(3PmiY`aFGQwfQ97zP!qYC1_~d*o zYLbTwy8KgzCx<(y;y|}Qcu6@(%1eeHwgAyYS!k^{ou8fI;r8yR@I?&U!B!-|+xw>v zDIz98Y@8yyKZa?mdnCtBz9s6&|8}^PvbFY8ymyp?av*YIzy4N34~?E_nVfiWmda95 zkzGu|lE<0K%~2`69$rz5YA2PvexFrQQBnEiP>WL7=3NT#+pd4XXRa`lg{xiZ_U}`< z0wX?i-|T&}ndD+U@*={*%=`O{RCJXZC(w_FzoK#ULvf}%mqFXmOzQkT;1=YP8xfOR z&BR+9q753iXnAc~o=sGLT=VC22F(-V#xMMdB2MX9V$+V*H+vtTG51H_=tTttR)eLq zfHMHxm1TT5y=8(-sx!hopGnKJI)&%4ziNu)bV>t#TXVlW?X;-ZJlJhna-F-rPsncU z(N!u(KVy%@=xp?NvrvfUx{cjme)OH zSqTn}>R2PHDou}2bh9zv3fv(@`7W$akbndyj3 zaLy}dpp<+r>HuyK71o4{_Obcy@_3iqbi;^lL+MmX#jk;MpmOc`exF9Rg*qNUQJ)Y@ z4ZKrKvCwBlzTW=?mD3Ejv$Q_mlvtlF2|6g_7Pu}G0^O*gPw;nr>Ree_ZoZh?i(l1M zl)T0*7Cy+VFWj*t3uF7dyw+t~_3O^N+r?~ZVD+W!GZhY$0uBmoN9^29j2SR9-5qK@IBRN50RR66DYho) zae#?dLlPR<{Q3q19mf?=L2SI*1L1j%zW9gt2iO<}JqpZk=J29_ zghqY446vW_K{uY{iannZCpuxp4~oAYuIcUGV3sryQ3o?#{`O|36MWe?@=+ft>%V-JZT%H%lL?s5_%g zWy*wGJab!w>yM*KCzz|sG-?SWjm4OkLFKiPe&S}e!E^1YwJ6?fqG!o9$=wo z#Y^11Ip((DGm`Zjg0wZdnc7F9mOH7mrBK-oW|%D#=!j%6KbSZ+$o8rx^4=Y1?Bo9P zX{tY-Xi_N0eK!>EP1YvxH{1|UxHugb>0tF~ch1%GclAQ4t5T1@%gRVaR403Xkx9eI z3SdG#EHbV$C@M?9!wgJD^wfJ93On~aFgaKvzJ$3DVG+OhqiIVVa_btmOrqtgwB3p3 zHL)*kgto*ZOilY!@1Jmm3P)B4;DI$pA*E`?7W7KezaHUIx+al7!t*6Z^M>5B`>|h0 zcu(o#wpOGn?D06Ve4+wAiF0rDp-3=BF3m;9-QvuQ3$HeHO%#|hgn%koBo0E3o=caE zIZZ|j6FU~TJ^N=@?h%uY)6*(*L=dm}GEhZB3(kuMHO}1C+t^a=14mVmBx=uI_R-Az zQ_OZLL;VkTNKXBqwk5yjg1aG072hhz07ndLT-yuX27RUEi1V)>RkXf|a#)m+TC@%G zHC>yikm;}O9i$~1pmL5i(_W-uDAg6<8^DxbzV8-mlyU`GF@JwFA)}^76k-otW~+m? zdc;n+!g`jOE8^K$?c@`Q2+GO&Ec=JpBkU#%Ef$b*6S2L7up4`q44D2Hd0jddl_)Ou z2-l&es(lY>Bm<58H>bw2`x^Xh%pviAgUg)H0dg`97zWm!B&+-C9E@QDi>y z!I;&(o5B=5!|Rf?kS1%FYllEM6=X>~+noh#kPcNqj$b`7VGt)&)_4o@cqv0uBTEN5 zh<6oqFyNvg9SZ|Lt83<$s!t4WO&pK-hes&wGP3UmMuEJFzvl{`;h(P&F*Vx#;%g!s zi)o>;w@xIAFeyq4AXza$`(g^loo_}vw)H!%6jWh7ES$}>-3=98 z&K|*V$oMM@*SQx%gPYe83{1QdGWrYgAA;`nCjxbd%ov(j$E#?f#Lp+z5n8=lN8Y&& z9V8vPYw>3K0Sc&ND%k#ViIg55x*try5{We{rO^cCLj@5MedR3v^zQeF%w@+6T z7aLzhXQ;j%F~D#ia11qY;PluzvsmEz(uW1VirVN(5Q9cLs<+d1G?C9Pl2ksYad|_F zIwGuWr6ew@F_k4*cmFeb9;hTmER4G>@ltrVDPHugdTBFVKpQ0>(j#^1 z*>D9M=5}V`y7dbci@F}At;3`FS9@*P2dndVA*y!wM9apL>diZJS$}=rOJ(o=>UFW< zmb^#JNjeWm9ZmU1N`^6MQ&a@#JdoO9Zt^P0jv6nOodPqj@_M0yW+&sG@REuO_9eU2 zN0>8SgDw(HtlH`Pu&HQB7!Y0W>}E`~LT}=wv{f7B**obDD9h#?qx*Y^CO~-lq{|`3&8u#T*|cQr z?oK`*e}1~2i8eJ&HhKhdwDn@v?;2W7|uEYDjUf85%$i zsMo%?3A5FWUjBG{asu56n%1^c8!-6LrVClWIFgr-E@RF3UIA zd!+u26|Agqk4fo?+?n6 z{cM{|F8^&KjzQJK2e1^AflUF4oL;UICZ`MIm`{V_2+h4?J>nI^-b>~skL4-kC3^cX zMeShwet-$zt9{5v=N!<7JTL z;kE6~%y?>;N!ZfTLe@R?M>l#t)WZ7F?zBMuWaZ?Ykn5Naa85OyT!Ubwb-bB{Rs}gK zhfsQ=#n!-Q+Vb#^uPU%j`pREVS=H-+KT4qlw!9R6)nu?OPK)xP8s3-tD`! zQSMkFLl?A8?^fln(X85itP5jZ7bZ`2VM&`QV{mH%Y2#d)IGSo=r&L6PTdM6Y{@f;J zb%>V*Yc5ApRTJg;FnlCxZ)q^gNb%lyOpl(|Xc5Em`xXro7C|*#rTih*4M6TVdH;5q zZOOCYcz3IH9~x~qsaO)FoEe}RPPeXg4;Xf6v}6c>Fm}qXfWZz*Y9>3hTAH z)SR4lm=TN(UvuN$%U!tUdepS`!l3F0qe@i0kZI<6m&3<+FbkUY^(*$#-F%sN-VW0{ z%%7u`-Ar6F^E}iFtal&Y82nN{Ic|LZa}-yw#A6c3<8W=K;Kg^pp=ugxk>2{2keEA# zLJf~tu~!-5>=AU^z8^TFwQuhmsU#_2yIx?%Lu9JAchj|z5wKn4XZ)n>smsx$=%y=SrC61o-5^k&v-^rJ{Rr1e} z-Fnu+-f4Y;(Wz5+sO`M*@fsa;n1^ud{vMXx@yKXNv)cL5Ni&h{aK|;*G0W5LqO6($ z27Ojm{e)2Jimam9CA6{DZ@j0}n0fM~{WsRJ>5{eRTW<4L#pc6g4cgnTCi{!K&FpZN zd#{ZW?}bN(-AWwHQp{B)8NptV21-v5Nx(n`{)RRZ@on3tDwA`W^4yZ9R<(_adB(laiT@=<~v^mF&sR-YzB2QTGnzp-cCdq^2++_aad#T=woxZ~8 z!RFdY9re;DN?441!}mp%z%r$>bNm~T`4&#uys;sAmGVZ*!DaU$q7P;wXFcX#a=OfvbJdN+PDR>y&|JDTgH= z%dLl%Jx+62m7~1*${O8W*JmcI6G#?kXHT4uU3P=hLTtMim=f`Nq<-v&Ppf3q@`??* z9Bs75Ns2UmENobz+nuwCQ;v0Agr>8)uZ&gAhPV!tJFP{D-Li@>=^*lQUEGpO;>3Cq zE{~90!t%)^{gkovrJ(`ZZ&w0;c@?h9G%CXm7GCG;g`TUo4Si>u9@g=fX}~;Z9-4C)DC+PloQbX1*r9f!Ti%| z8Z!9m9}>IoV&gey-q5k`y`@5Zc)*LTxp-x!`?trR^~$ZD^K0ABy@z=Y$JKU2udR>c z;0N`)1E@f_JsFSR@Ym8RV+~9~VlUsXMkX1PBm-%%0yz!_(&yS{yqEr}vc5AzQ z?_sdlP3}7v@?H;Vuw4U}?3|>S{akTFGW-ovN=I^!B}Z-TL8$eCNe@$W@QX@U(KZqz zP4}cNA&n5hUK=XT-bqmpb`yy<1>Y}w-rJ^=0NFf}cDT!;&_b*C$iDOUX<9H9;`UA2 zFUkaOf~48g;iSsH+ zWmv9J%bfTtSZGS4+S}P-*3H>S@ys8cO8UXF@Wwkk2gn!roX2;~?m-xD*Qrazep;80 z;aK_{37FJ6*SoZVGm1!$EUI5Qj9FfZ1p#fj=r3Z_B4Z#vG|^~EQu15EeL_SXTi9*5 zpSmXaSOb}`++P;Gez|&O*TmqsE#>3@QWGsPf~Fg!4$)GEKhZhlY65~xMXs~cbA30; zSAKgRu5qvk`m(VaUNp90!_f3K7t&m+^Vr)RePYkPOM-Q2+%mW|K~dS;JGH6ML)eWP zLyf!l6bM~UB z_P~nKChDG|vHNQj)^>1@w>{{V#6%BbfrjYh3kBL`p%98CTy{&$Nm?%2On@#cfRKL- z-aK9mxYmsii_H8!|MI9Ge=WV8Tvb5oD5zX2eDLxWW&{$u{>dp8FZwNMfDt-^9+FV6k=Y^3HEL%V&BhldNNB$c~89 z#9298eJm1kAE63Wr5-JQj*^UH%3J3);8^ZqE~<4#L~^QQl#%}1(BdQWa3!!#S>2df z87C9HvDOi4%BAWUQ8Fz=0{tkd;&mL6=DCyuXfuIy;_%+#Bddn@^zS5gr&?z`){Q)V zRz&W$P#2A3U2Kk?awGb0-QeYRpD|HR&0zJGnMi^T=N+g8Uy2;UQQ=9=ZHnDaMPG_f zOdaoH5Vw5sNh+bk^@iE`;OMFuOcvcvXI!R0=E4itf~Cm!*SR;prJJl-DL<`|RCCj* z_r8WMzQwhpk|cTLGOa&=Uu+R)bu-H>BTFIlRzciH4&D&v*D=*ohB4Y|-s* znI4)A$ZGFJ$~#_!Lte|9ckfB#oov5fx%atTj`(h`&B6VL(0Yvf^T=)#&r0+|_)6fB zyBRrkn6}`SS^U1EGm58v|LG~Ed0kR&V#MIew=DzbtY}0`vRz`8i+&-Gk|1_DSk+)sn8Chq{2euTG1)*Fb-E7O%*J`= zNTnf>q*d(M@$}(?TI3aJ7#7QTuaqKRsWrl>jwe9B25zQ{t+0_A?Eak>gI5hkx;xoQFm%ipC#t)JF<-#4?V!A2QY8aqT;;U;uXDGJhv0xXWR zqBN3_`?Qarl-X`E=h8&7cBj((HbDoQ`sSI+m-^B>%F&NbexE#}7YdoPumd^iPFX5g z4)oJu)zwrGbsACQcBXT%cTcOooU6)T2=8p|dxONfEL4a~^UhFMki6j_LjrL^UdRB6 z_n-5#2nM(IrEJe7!;+_Omxyu)e{|^Zw7!|pi~;|aJ9#X93x_i&2CE}G)Yis1YyztMzZb;(Fl7u$M$`W5d1YtZ!Z1jln>k=_j zeVXeyH>nWz6`^50x>+C_lc}#<1E9l$s-vRn>Jy_1n2zXLnu!TTjUxOyo^7m#J~mt! zvsA~ppBTlHMs*GKxYM2+N1GNQR!*TxOlG9wKLbI|g(sgp!De}2qQfFKtdI~fLH4lL z)(r&Mjiw_Pic}c*X|?}CMrm>QIg~&$CNiDYN~Iywy5;Rc#nJ6T-mh3)Zu%y65TwE( zm{T^<~@qp==O9((@>@XISYMs105BFUx82ApT~!^!1b1~Kz5VFlXg)R&2nAlvGF-lO1+#e=!o>K^i$x=eI>X^d%?9ZFYPa=Cdh|DdI<#SB zw=X8y0U6qZEp}Zqh(NYOK%_B2Ore~H)SY(}MTWtTpy6;2)stwqicu^UAFQUKK{=ao zM06XxzHP))O~n>7-)_SYmd?h?E%A#NBw_S$w~1g0b?N-Hc#4p>v1fTrm1MLgvS~sE z!OCy9nY5x{TUiHK=vc{OKt!yy{Eddd9~zG}Cx$vyOqnxi}>)bc9+G?bzpEaNFo?b+Pzib>14;5gOvC>^#d%@dEz zNenk*jz}yL?Rep;TvVuJNBBq$9Z@O$iGvpBjIaMn8ZsmJ%G065-fmMK+N8Ct zHk!fS36dTUe!C7#7VHXnzNf>valQ9W;LUJ=ISTywx;v5?^rK_*ceYT++0HnJn{gZb zkJg9$^BARQIXS`0Zm ziWq{xxEOXZZIZ%KLYb9vDn&XO)|)m}$n4kJzfwC)UVeK%ZmXe!n~ZRAA1VPoB(;&#^(_vyL`UKT@C z{TJcl!vsfKC#;e@H`^ia5aAbMTU`Qn;M#mr5l;z3?eEx)WLET|?za6%@mC=f6cQQ- z5fjVYtnN9A2M$0}Fw?<%!U4I1`Pf(OS`i)cj+`~t^@rZ^V}o+5541o{n}y0wBTMb~ zBQcq3w#4SDeNEpd8<*1x83;9zsgm-#6;cercwd-xST-TW}l7U_Y7xNgA~sL0Q?)e{2{<)?OF6lL4^P zl3slw&{;8|oya&jK0H&*@I*))Mcg>@dA{LG&^$+#ii5j^_sLH>2Tu48$@Sy^^7?Niw=Tc8yn%m3Hv~n7|7M%vBAjjXl}RW1XUK1Z7c!mGASOhx;OZA- zq3(Q&x!!^HHHARNK(reHXXLibcQkS_Owo@}@1O*<)E{o|^d}F+n>%dSyxxNhmxQ*U zd??5qrHWWQ{7bbbT?_($i!u($R7btHTEA3f`3>Y$)jUkI18Hu?rZ^6&i7XAw9zI>{ zOR3zO@i-hkIO$KkIib!t>#iB)|9m4Yn!peGW_s^DoLFHH=UpZZ0WEAD|@?pnN4 z58qnO2GBs6*ATmP8}x%_kO~!gy;txVScYU3SDR;0FysW0ok|Yt?*Lm3%65Q&88z=Q zE|2Bgs5gr*f#_?j@D_RuXw0kj;77J!WnlFO%8UBL!#h9*W(^2Q4&>zYfI10w;ksH9 zdZ%!Nd-yEEjZRFirC?L%M^K$q=GBPb3@ssFYk287^_MUH=yJ z(BLk#xNb>YxYeMr7*An`s}jvq0W|ZhUf{04pmGWs7<%v}?HLR1{27G_K2}J4lob%; z@u7}wezC8-SHV4%q$2nRK8)vzhu1N(rg~BSBD}?8;6>K;N|TNd^ln^KmAu|PfBLRR z&&cJT+bYQ*mtv2iVeX@1|ZV^D~3lS0&lpF94g0bUaL_R%cFv=~+8;6nyF1SuH?#AY~dU_{d6Z>r=M z+UHvz+%qIBY*Li|IUi58wa`nF_HK;g1@1NpE(84Huqy<=U<(=`>Lclem8&BfOL}|G zH12}xhw;b|)Ld7j_Rmzbw(0&N--Bq>Gu2q}ZzcvnTV_%2uHUOy@i%TpfObU)qya5? z2a7k4A44cX96r#;HmmU2r3VP^I8X9VcuT>z1ex$Z%dl!#XI>TbuGRST+2z z5!ye~1u`e6X1mI~#n$Z?jQbuhWQ8wm+}y2oxD!U+ymlV&`@%mx$BjLZXQqVL!aEpq zB}cCbYjh-Q!N|LLg;|L~;D0R>YN=Ay?k>2wo)EU`EstG-4(!$eLBX8 zOHk&muWT>so0_ssU*-=Zmlfcj*Vx9-=%(X=j~}Ku&?b0E-vTd!C!W$rvd$gD9O|%u z$3crpcY1HYB8&MgRz6jsqk=VP6@(4Nqz|?lpP#%sp`r10QU;l zW!}>NnoiMFB#YVkq}1B=B(AABjse@ zv5_TmoO#VpM$hP`INsadQ*Wi_ojBHY6+Eq#hVV6AefA$0UMm0>uwVe>?&%| z!>DKWzOSXavV3?npyzkV4ru-9?zUWSs2yTaiw!rqpDz1XS&_D@=MUc>praDN0KWHM zs%K}ll}NL3eTwA~S1)xBeA?fn)=YI?&hm7-cauJWSOS6mBZd0QQz6ytnqxJ(a}EResfy2LN`gx{b)Rvz;?{Q4Sd# z(eu&X2@>eVuGM}{@R0F^5|ZFN-ogj*BJAb)udt51R3At%KOxshf{Qcxe1eA=7DBrp zd#>e7#8mo-@QqN163b@2>CY~->Cat!TUU;{{RtKm7_p={Y);SWXJKaL=O@K>1dnyt zInXGjFZ8V0@13s!c_3-fD5~VJ$h+Xb*`e7m=+}2qFwa~j=dB8F;BXs`+wDyRRw&4x zEfO_&k+2w#?82g_Og`{Rztgc@A^?876r}M36Gv2}+#Pl`%yfB>9bdPttBD3)8~X5R z%Ncw5g4w@j`eA%r<)738bL+G*~gkr997)DG60v0 z62sM8Bn1cc9py1N+&~X^80SVjOB85*hsgE>+xGUlx@DC70jZuGv{e$3IaWS&_dz-z zta&8&N(UEy367l3nV$&8?;NO40SgjTN)rETX_X}?EF&1CYVnp=S|=3Z;$jl6&M-9b zC!K@n`~E?foGJbMqZXiKd5H=10JjnIP_0amr~b$fSjcz7!@)w_kV#t!$5UOVWXGEk zH;YK5o>d~Mq%`iUSk-XhX#m?_4%=K;#WQ&gAa@Gj5qtZOjm%+db`*~p`oxAAx*_Qv zG@NlOoK-FHDL7laIiQvDFiso<-%ahG{9f|D!^5E!S93O+V*2leHULw&4SvkzZtC|9 zo?x)VfW~ov^iWznp8q}M6*~^Zp2$}IY|rP(YD&SegPeJ{M7LY*Te-c`0{ntJ)HBa3 z-H0eD%et8EZl4v7$-V@(q0R-dVE9gv4ewo@5Fzfj(O|4O#Dw7`x+J{!UrUtnt#9M` zQ10E<`yp8!GuO&5!u}t?VFPP+%=ZNsUOR5Eb8P&HG=v>IOU~gj-#_v@eT3j|XWW4Q z7K+tC;${Sb1E0G8F9DmBfDwGN-RO^FFioeX9EA%4|wr7gb+PA+f35?@a8%)ee;$yn;6F+ z_zL_U3$`W2t8l0A#Bh*iZz<MXCr-!X@0(fBwDzyFNI5x!P|ZocU$);L&d#(;n2qOUd}B((`}5 z2ZF;$bi_S(1zPywptKb_;s!B?Q3rfN{;NL&#&MCbTpmH_lVNo4F?5~<9yiclu+IWv zANaoG;9r%zkNS`3!Q*)tm89Uuo56PkuE%Xi3eaJuXP=hNz~x%x(k7~KD_G7eiGWA^ z2bdasebtuj?6f%>fq(mX;h)!mM>0u6=!i3!cp!^y2quXho=XDi$W&<{1f3!)AjY{~ z%ny1+3f?JaReYzy79M2bayCC{U4lNgGvfdcA1IrYbIAvQY9>L27OrW>6r0|y+^6Kh z_mXeIKe)17P9+R)-pPQ+>Te1AXy6|{gnJ8d`9~t(Nqc$K6&Tk|@YHQ+@p zxa`@H6GHfi{9l#lTtzVKx^I#ytS_PPt^NLu6`YMd{Uz`ovbUN|Ew2fz{94STjVz2l zyH4Db+JcDf?z>iUQ zZyyR0T1B`wMMU;pN^NST0~51!8)q1j-uce9`E{rT26 z=S&}AeVCvQrKOnEt7k6=hp!v$Hx`2GK!}&bblGh~iWuN!Ex)^MtmiG*t)2Heh%DKJl;EllSSbE& zH&uOrbysg$Oc8u+EeS+melH;R$1bu4ou+zfcN~WK~BqNlO7KXj9zIo2Fbor z1J-HdQVvCvs(TergziTsRUNB zm4g4tO0j`q0X#4KIyFW$Z;3W)Mn@A^zhcp-uWyBlXNbvRzr zRZMzDtC0aQ%(4I%pq1RX*w`kko5C*llEcA8pYQEcAmG561Yi>x1Lq=NjUQweoSwt#zk+cOrv2m;Y=*t`8Ixh$Pb1}3>Qa?_@Wr>w+u zrP9Iyx~`9_&7xlVHdOHbi1%ffLe2O}#}(e2J$^uKkX~B40gv0LMlh`TZ-5v5S~?>w zUVKv{!g|Dr-Bv-tg>bR#AQID=YlBxH3o1Sqv&n#$geNy<=8>}by{ZQ&g`}EccmkG7 zvM_&uC2t=od067f-l;>*(c8X#xSM4l~=j$neeLQH2V1-nHu-t*cPCr*n~ zC!ri39Y*MiYa)1raK1x z_wCDvhQKOH-pD8c2FU}=4JzA5v4RlI-_ic@Dr3HSk@4;__46^!toT6FvnFiE=ykY79hp@+<*_!po^>LNoi^g%70wkT7Nq9I1^4Nv8%+w&O92{v`@S%IeptcpFK;DTVbBo5#ilZ4wSYpk_%&fTIjg-`bl%AH`3Vz5CsSY>d-dcAefg)23Vtis6Sx}+;XsG z{|ftYb7Ga-mvJv#?IWXrs~5!4&;Ugm`Ad|(QmRL#b*Dq*%qk6vUWni0bsj6nsarT8 z^p{lF&{OX?|DoU@o+(P2O$>Mi8pK5Mx*!B_bH{Y$owpXwh#OhGw)GgaHP2XTO zoH^;QU_Y=&>&J_&l^fG93KaI%)Ix&93O`LnO`j@I3!vlz@`J+J$6}pf8-}ITI5|;| z5pTaPIiPdmXLY8X_ZGGQ|CHF3&LBi{bi7*~2@l_)7TymDnl*O3??OYx{|3)T9DEOA zQhyxK8u-e>x)y-)^09K3@JUDk`%Z5)(qNLg4+j~WIXWV4y{6T~peZPbgP2W?QVx86 z;R)c5{ylt$;HAI(qwy|qAui!rm7d3iu-{y`xR+j$+$Uv&+3c56j`(a>|KkNmG)m5zt)bWa-f@A~%t!BTS|xenj2so;+pqn6Ei3?FP0 z4-E?>Mu9z{aWB;7PuPIc~RieGxfGAk|^R2R^<9znW+=QpL z={$JKYH&VC+0IZBJ7CgqFw}kjK@n6Zm7S1$6aj%6s~`p6kgIQ(@dWhZMgkT!PzzeJW z3x+3(NedN8e5ps%FJ`CBe4Ul6HpQW$`y&`T&1M|j6nc$ZHdY~D{7I1z(I#TV3Et#S zB7gs$k`HSY*?WZDwE#{vn&a`j?>pYCwtd5~tGT%vrHN;WRGLZ=H0<=+yl~7orU7k%!tHqx5TB2Wa~e zX>*v!#+xRE_Rz6l2`mIGSo)g)&QY9#A?t^fMj>k>b6q;;e=&XF+3t zT`2{%qb5gF_UP-P6|3SeqdID+EXY}r(HZ~)FDg)h-ny*5~f^8}w87eVtjivb)M-2pXc5+A#6D2&b$l&2T zJ^zssKC?E`l}LB#W!tXpZ*XGmgp#BkN_3sD-relvjf=lj(}Uh-mN!}5`NMF1j-6GW zML$~P_RbfUEdP*>$rrO;E>>zVZSe~7SgYW&*N>YZaSc;PS&pA6tmPO*M$nUr04BHN z_>10R`$>D7b?A?b6%DmJ6eb{DzkbC1S+<%cD}RG)Z+#uvGFi!I1t58ux37!UM&1OkbV9L~=>4w<6vB>lc<8m5}4 zkgOzcgRIPd@C&E9Y-jj|RN3SqO(d~+_Kdspy7lvhikXoDBfEwJ-Etc0no~M}&83#| zu9XU?nVr7O0U;>oKN8g60%gP1O^_i{35z@FX=d97!-ua51?oo7N!&{=1*35x<7RFC z(tm5ak-}Fy`e4$8&YXTY0&Ba7Q=5~M<1*q0HhZ~c@5(-#f{$dIQI#`Kt6!AUqDpaR z(XJzqb2#CRNElDyU0oR|u(?JZkK*`*9|4VYm6LnD`*2deisbbjtcBSr!kcq9oxfPw zI3oqp+d0hNH4rQhd*gfrOnOle9H zXWr08^+KY}AbN%_-sxT$n|P6VeB`=!w;H5wokqWQZ(S%iF3QgRPpRH6vFe+md$ z2|RNq3tNe)m%z|^yaTX{y3v=33u3-Ix8|`)-94(Jm%0yuCP?(EF$czLJf`rsP(@}2 z!4uWI85^c#2MPRl$R zA{1n?=f$^aGsbVzY&x(bLu@$%R z(9+4R(rAxF%ctc}4+g@g-)Gbo5sm0JjXtmx4w88Mk$uUG{ z!ZEU(KhiLP`0Qq&F+Zg-K+*X~8%mP&ns}H$egR}06W7_SwKXz`_BaC%b-%@=EDf5R zY(`J6rljz){l>YE!oCh)Qwcsq@hbRA#xp0`3`A3UIfi>Xj-Y>sEH955v{zFcRaB0`fj0e1;_sNDJg>7hcvMU50!N}@^`Euu*-YB;H} zj(AcRB6QO6qTyF2ux4gOGd4PU{6JoHAvUovjfQlf$ak1x#Z_VTLgl8LlSGb+Bw>sZ{C8l7?!J#Cod+)*poOBI{ay zjpxiJ*ryTambKop-%JQ_9Lv_*gDw!_K7lvXoe<@Bwy_|E*>I3C)czrbfa7`nL8kRD z|IMZ_!C-Bh$(=7LCdOtFArqXkHkLaov74~5B4NYcOSAF%;n*qRdd0T=#%-u^H) zYY)E;e{m;!G|9NiotyZ^o#bFO_tQS!%8nr|K7?0fmQ7vOqJ;dyl9GqE)w`HYm&^5g zKnr?ZVnSVZVLZHy+pxNG9aJl6*eHp!DB8QB7Cm}CYL`5$01%>PAXbcEzY9e~6yMU! z2LaU{eniVqXkEk0boSDao}Y;TK_i#%eV7-wYO>x?q`eY{GF$xd?1xj)0g1l1MZoEz zOM0&ZLh!XkhjotmL)7jClk@J!HKXO;O#NLfbtKt^yD9yfMoU93JW5GFa~I5ZZl&x| zzDW3?cpYzobj-ZU01~@{>H7F*Sjn|r{kbaQ?#XJj%`b{FAx{b*4yy$G(Dd}X%^9lw zYTFrigF=Gn*^f(s3~2qGmx-3J;;#$>A@%#yPvSTso2Z6)d6SprTh-WXmsRX$8m~Z_ zLg;R^_otc(Y4rtA>U&dU?`R;pKD{$vsnZe2*|{!0QKW``TUUp8ZzvU|n|N-?}i z_r4a+=tN!dd3Kde7fn81#LT=RV*7`ds-bQ!F!wJ16pEvR?#nt4FZG-3n zce`aT*>%XF?zaT&bNtCN%(HhN$PdvocKZgce;914+q3eEx|KYX zIpYC9$q>yT7xyv8s`LC-;@t8g+hIm0V5u(G77nY^`W{TA>#Y0alKV3OtdrHz(+9q^ zn+4>`BN>1McD49@X3~>skL_*R8$X=P+L~6C%0hK=dv$4z21p2ctaD;Cu(hB!+ciTg z+9>zB6_3}FRRH+iz`JO0z|k8u&()9OL-0fChDDA52`iaeS0b91q{e$dN{VAQ#F;e* zu%~`escfMjIHUvPK5|;h2S)*~*)E%Gh6v;f2h3_{2Nky!1z--6Yr) zktU12Cj3(rnivz8VcpD?Wa8lug<|WqgXz54VuRK5!y|BEb#Gr@?pksQeE30vYvY=M^>V@;N{I=h6hKl>v^(`zo5S>wSK+7*)+_(e zu!t@)PnEyslmszPI@rZ=Ou#^kM4E?>f|8u-MY46?&IO~v9tBH}HbpPGaH{VxslNsZ zlX5)F7vJSz4C}Y>bV3G%%)?rRdTlkC3w{!L6+q3sg+%GaV|MQ z1&)yK#c^;^8L+QEOiW#?1<2XJ5uew~Upr3 zw~Kb`YdF|z4hQ2(Q||NOh08+HJr@cNUugU6T+sC(vkG<=y?{W77h$6BCgeRLzFO5E zcUXaR;~4a_Ph1#p#HX{JIFxhC%?kcmE^m->E;4I=+1>cPclTw@#CexOVdugt7-w)n zu|6-~&7*nzID9I);mBMYEPGYg>_-P3+Fl7N4TTcV^LUm9{X#`+P{WX(7VVPeIB*J{ zm);*eNKf(q+1KW;*IJ5}kNh%c-YIK?H6r05F0i}tH$6p4AOLRdZ`_=VuYmkcGCu0K zCR1(f_4Htk`NDUH1C*APlkk044N$z~ksDUie1IFs5`Jo&>e(}(Ea;{dlKr`rbn0m% zG`{)XhuB@%{ zBE98xP9C%31u`WQu))DcMeZMk#>;H1KYnwM$k2~NcoaD1Kbor*rc{Kv-AO=o4PbJe z)|C1L=-01O6S)klORaR3K%i5QivqOCoUypmzCwO~ zr1S?^KB9205P202)H%yZcd7`Ummyh{K~$yBRg4-FS-fyH)~UA8*j^s)1WI_w3y8b? z?C3$66g;84dpVD6D!-ZSEJMTfzOt@ue77Go98RyRhFn2Nqm!P4y%ga&DSr$18(E*^ z&2P&Z$H3xhnka$r(n~@sy5K(s{^q0sw|p$GH(b4$_W4Z<*<~MTTf#SKrm@y{Y)0}& z6d7Zy>9bx(xn8EZTRg1T{jhJ~O1B2QECl23ghnN}1AJ=!3!i;=s zG1vU|Cs*TBe#~S3EDmOcS|b8B0o^%5#%@vc-N6;!&fglvoqYP>^0o$YifE2%O|yS?2;pxgFf zAVZ%xJo=~f0*XiR1fT!~M+@hjc?`{DH3|V)0##ZI_>3pNQc7iA23?e(EayD6ES1Vm z;)*mtOiy>}7VNjkK-DkSgvo(SZnh??Jsg^>x|$*NH%W^l0FvcuV`d_)ZzeycXOzPHEaFKY|CjI1Zg*gf& zy^j!(dH*7l)%j2J2;TUA@osh{4&{4zmUWcDTYR<+$MdkJiX?Ef))~&C($1v-6*iFa zfWmw-A4uYxJrxAn+@bj7tcVpZY+$4v zblzVB9sc?Zy1UeM9hZilR2UZk{_bdPQ@vTM=g5!%X=r9OXE@;xbOFJLpW|1110zc+ ziTeRwvJ5b_3#i%=?~T->K|OR3$Sql+q=ltf0q_Ih#*{B1{%sUJs51@{38jbyN%|Y0 za;Yj1H1wZVT?M+0^cupr5O@)_0R;>!0#!zVWjCcMPOZV?Aj72B7|R4+7b7j_^e|D+%TX9er-XR5^LzEW9&+RPz&&h z;MF*|si3$wNK*&K!6l*ZUQOnWAp{}=tWG%9A6UPPMOhMX(qg{u+(QPx2Pm{3X{`^9 zI&%1|Hz^>K;FcD7)+RnYJRpzwQ4kUo{1{K=;^N{bz=(I=mk{)b4K@H2%^DJn;=KaB zjBdt9H+0quJ0|!q=-$?7XJZBKc(L_h=0-hP^Bx6Y9(CgjwdmGFzJC_Q#u~HHp5r(` zHE8$y`7iy3gcns#yjI0qfZ{&4SH0-gzZwyL18CK6<2%6Nkd?lM?-0q^z|X@tFE>V_ z=XhQ(AO0IGc>C*@1>@TonT^V~d%v4YmC4TX-d=mEF!5NC&-~9s(!%%wD~(C}WR;m6 zNS>Oa|2HJ90e6->@RW-uDkc6#?1vPpnLMR!H7fypoN7M9Qfd6_5_P-VcN|{qUg;xaR z@Go5Qh*OKv8Lowr4=M{dyFMX>E!k5-G#`-Z%yeo0eDT^@d%+F>Oc+8P&JHEZvH!qJ zs-ibZ{{t@>Q1Dd{WO%T6xr67yM)nsgI73R^fRn;Ne=*C70M`w6ixa zboRIG$q#f1gJ{J@o+ph{NnSjWi$C{EmYy7Z(Jb}et+P1?2AMP)B83r?ntMVjKj%81 zDU2CJ( zRuJUx*3}o#dUxRcGMpsdX+1{-za1S37Eni^2wz9qj3(TAJO$`4&NnInP2zZ8(5AXw zO{AO8?!@53Me?b(H$}+Owuso?WIdMUO$ zX1WHaF0vBcJLE%X^h<@6pIW_+1QQEa;sz6I1+Q=2mSA~V_)x`AliE?ZWB0NIRMI@5 z0M68dF(qwVgJ(3H0}q?1&%|AJJ`b+t(sc*o5h9F%AVtrMCb_2XT=Fw7e+KV@OVC)U zd8aL?eOm^?;mr-+fS6+oa#i}z{tsP$9amKs^o_&BK}teGLb|&}K;j@sw@8=LDJ{JT z1xe|WZUK=HL>g4OLsBHA1rdH;x-OuwrpZoQ*{{T5>@3m*m%$oSl%xYG%%d(s= z=sUCx+P2k#0?- zyt#R@$gE*1^-XeSu75iU2Y&?gJDKI=OrEB0O*d@U!L?UX>w`((66b0cb@?Ty`@XP% zW<4n9asag+--^{~$F^}qS%n%q2G%uNtPJWKoLE3VEY=fV=%mjRRLIPNIt|rRrhEZi zsEayjcgES93KzCLI;J(@!FzF`EB)%Y-ct_`bJaew z-dTnjWohKH5X)T?+JJEfHzz2EgkvO*5fy-+4~p+iJ(g-19+dRATTjVm8{ZZ-Zk(C9 z`>gX@RTU__I|E+W_E3e5g9G#-`|5n{(gA;9GZnPGGxk56xLog5tnxc?O=z+*H*z0& z$Z`UQF95i}JpBZ8LXnKV$o@<@KrcAFa^VD6uG0u(IaL3yrnA#LJ+; zLq4_7dM-=Ym%YW$s)I3W*^tRs>LO*{=5bzYiip>_7O=G;kjNH{EXNxDXUZQ`LKsvc zFN*ziR0i}v%Q$=E{q5P>_nkRh%I$OQ{eR~0OjzeoP*A4e?cSShdKsy2Mbs2j=3Ft^ z4~J7z&JS)F9`6N%R}_J6e3?QIg?p=na%B3Sy6j+yc6EZv&gYHQC6+e4?JGjmp0E18@XAN7KMjze6y!t?l!>R#grMGX#^yaQk&6l{zJ-)*-gh zVZMF~vCNrv_a~P>)C5!KU1VXu?lKlD$0o0NRMANIra{{hm*cybkql74PVMBW|8O-C zsA8`sk}~h3<>u6EQJ!&jSJv$OT>0elHmLg9xAyF&h}V489|3E%hkUF#w;BN8P8*J= zaD=9ZK7hfj^fXi|A;<^VHn!nbPuJPPvNI@@X5K$gYp;AFy4z3emyp4@T=Zk^8D!iv z2wA*0B^;AV*91+@#wEA~M_dy)E9yIhB4sRb&Px67YR~J7{Y*9A21$}bnNOZ9%HFU) z5`U+-RQbt2HX}I3elu16sLWiR=Jx>yJ1AHd6cpN8OupaIN?&$#bZi59*UHHeQBk;x zbLm%q0bM?<;d*YV#s_^5SD-J$G~|uDEsdr+j~~Y(_yJ&x~2!to6mZ&H~Yo+S?-DoeQ2_< zcd8!}ie&nwtlS}iR#?X)c?vwY?5YA**xrKmziccr1K?N*rC4NgmJl?)UO^X7m+tGs z&$T*DE|gqRD76oi={z;nKrO;zr8vPIae^CJnCL?xL2IO4IV9OMz*oR{=q?KbQSu~% zrwD?M3H6Sh%`KACNt7~u2(dVW6o3Wo>!AKMw2qAy>|F|kRfVbgCFhwFi_?9uVf@DRcCu=1Q8a;dl#@<9&!A8sf zK*I86!Ob-(xgEGgQAt80+JJ_hsc4)Cg^Ns(S}?WP8C9jg0W<|~nnP%O%)b?7%4Uxn zUVv~WR8_&2X7+}9s^5eAF9;O%Q4Hirv*VU%45K?GMaV3S+GA_o&<9Yz+3 zp~~{Zj9Gb$%|9`r@{rJ%Zder9Chmh+{|)0Ehe=__7}^Y&=m5*?`s4Vp%0?G~NUl#q zFn|-L1O9>r9PSQ7d6x_qODOdbJC4A~A)A;xI@h%)C54vkIQI!l( zpo*!6#z24w5AHEtLr}qF>08K>M3}*p($|$pf@m++>0;DEls__m*boGi@N}+9$*je$igOdUg(=a zoyfopiu7q$5MtHsL(FgkAf6g!4YemU)h_HfqaApo^qQeEV1}}O0emVANi>^KWFk!a z-u$qgI66A{PUvD1I%z$U?=XvUkVl%cg>`-=keaLD!l3}X+rjq4{1a@f%)J;P2>L>p zlMmK#y>}fUVH*?lIhc!}U{Jf*#ekav)sd)_S%&$Mo;36joMv z*3IIv`ZGkw6h^EG3w?oC3yXL(6{-Zno^iOFBBp%@%jP6pOWWG^!xkY0c!~|o6hbn$ z@MXXZuv^TPR?AkAz72oX|ERM6y3oGLbG-wm?r<=^%#35oF+CvTHXu=)^~pBEbvjRN~GO^fKXR8r?^kfRv`Xem$*+?m=I z$=hP{ITSC_g&<+#ZuBZ>H^ne%3skIAH(&itSZZWtqSdQ{$3IPlo=_d+nsqlT3{{W=*tzs}+e z)ybmHvXI4s>-jGKBb=P7(88S`$8v>l3YrCo>p_un-`s5(<9Dp}rOGPqKQ-(oR3(%!j)j7f@R{$nfR!s(l@yFlgZ(Ujzzt{}1 zMu}1uKU2$e@Fgg##dWrO;qnTu>?z>`tfEaWGQb?!mj7BZy&Qkc5Gyeog&Rrdi736b zDhngT89kBa^Yo&3i`3E_W ze7qPIHdKPoEda)Gum-ZJ!L4vVQ2*q-^i3Yf6l`>4(T$_0D-KP@k~rE6?w*Rug|%7- ztdgIQV>p1Hd*yS82jbjq=Kh|k`GD~J5?qpC*roU19Em!H@RhV{{Ob;7Ubd|3IUgmc zzh;9AcoIVy4d`Oy+JZ1?Qu(FS(he}Lb!G%3TpawqxuL%^<{jstja^u9TVhXw!y=<< zZY9RGN2whG-lkMKQMxq~%9sgc-T<(mvVqSlJpzuTD@7yCaM{bTHwM@Ih z@yWcq=MV^%Uh;Q5fYtQ`3k=Ag_$9cL0)+-z*>p?a1tk)X{(;0M-rzO>$Jr}F1;50E zS5c4d2(I=Y&#g&XN96nlzF+F-uf|;`7#O0wG`}SxtPjy_#ZL2XKcYACWcmqt?&r)5u)8N*sWDRFp>EtII5^ z1YBU-NHX4|KZP}gm5S1o<@C!cE2QygcaYvzZ^kE9>>ZJ!Vgj)dQ~6; z`kcizP4YFsKwvF{EE`<1|9z^&q5z3KpiapxtmbB*vMvZ~?}4TUtG{Z7Bw31s2g3+x zf87^y*PaJuNLB{F;3g}iZz?Vl#&kNG^ZDO~DU7nL>HN7q@hOZ; zu!pCb$|wEsouZ)R%7L?5;;#R$ujg{~y_p1c$&J2xzX8IyIgoTY_xmgFv#LjN&ncr% z34qbEWIaSsGhwj+al*DtMY8*F3U&}nlLop6N^y&cc7Op^j#6jrS(Udl{0=INTH%eo&t59CC+1a) z|JkXsbRyKx-*v1kyRxk@8WppkMbyJ)`|}H63wyZZeCr(3_uqV8du9nHJw?T!D{3m} z7SS3{#Q+)X6D5wR;5+K2SKMPGUUM5z3wOI(zjJM(k-!Sz89Z`}CNy6AN6}ADZ%lzGDgK z?5Q!5r#6N)R;u?8cU#UXY|}2a_vdz`X7wMQbr;(F|mfPS6d zE9#eU&Y*s4$+3#4s3mOK(OfV|bP1CN@82}k6OsFbx5|W+mqBq4!&7c6*s$(~eJ@vQ zQVvB??hjvNlqC{B6fmu;0QJXckdjd^AjMx-J6HJK4%y5JI)B5{k+$@F|>4i9T^&SmLJe@E)Nl8 z5EASdI6htilXrQW_JS34YM{%#FsToNqZ(NWkM5^gNhtg@{BfGhuJ~;Ox30{Oxwg!Y zdLm}#Y>@z3pXK_lWGBBGpI|&%HqBO{;*&UDVc~v0GudKgr|WTb_#R@Ph14rzPJdZn zCk2q*Sljm;)?)RwCEbSqanA_ZnBmhgo~3~-;g8!-BEa~+<^a!0`8)#2`sA4apI%&f06NbGc7X9AB{rB1?m^IJI8Psa%yQ_J%A zBUIz&NI;YCulV;5&5?9@pfm}Y#q%Nv9DL<^P|J8{H094?MdGfiVma~~H|_xK{(q*7 z7+|f_o74~)jiO(HvOfcyN6_JLw%CW0VtpzIywJHm72sH@3rl$e6DBYV5YLsUS-CgR+XgM@2T zBh>`DtQTxyK{LtQuy+UhLR(xBIopA#=myr2h(8`Y7p3o+x}vNi*nTiiGtI87Yxr(P z9{Fk7m`!uYzqB$e*@4Wc2Q*wJH+Ga?ci>zt(_Zam(yPTV0&kU$Fn7$7Q}D9hPb7uNr+eq2;~n?@D{P?f#PG*3Wg$sD}RMk+xeAzMUX zvMxzsPIqs55Dfo@^uwvp4V)N~V6cVzUP@RuB%NnT8z2`dYS#PLevJSQ~VPlBdEaBeC zq%T(RF$U=!x8+^vs>up(#;&d+#;}j<2u4g~I*FZSU!7zZ;b~2H z`n-8*BtM!9X*L7c16>h5LV!q5XCcB`O7iFhAvE9_J0{&H@qtTr5O>Gh1qe=^9PdZ~ z6`7H9(5_i*mQg$vxvOl+yQ|~ZSC!D!U+CWI+X}1KHlh(!ROzY=qN^K5`1m{dp!vK- z(u&H6X2XZ`hk_Co`TNz(Nt|C?iU>bWZ))>q!?jNih}+`73rM%dyNeoUPC&|0673Y4 z&szx#jJPMifTGtuUy@7~qw3*OlVgYUjn`2uV- z>{U{?Hn0mFnPDN8U^j%G`YT)?aCf`(=!pmjnwIMo%PBNc+SkBsY6*TRrJ408f=!+4 zOxc)p#qWNykbyM{E$)9fm$5WG9ZgXY)7JG#+?@gSR0-vqO7eV)ofEn(Gp>go@7yMI z0dyyksTiKweeQpUUEZ=pvQ z!b177refOi9?V+~32Lj#DORrvQr>3e>DcIh1`bEYtG1R{jh*OrT zne}(a)j6zkG>RVf`OtSVG57XVG~!hmIyH6T&pi(`c5kP6nS-O6`NpZCN63D6B|+V( zwkLBaTdMW0x*!gK=;ws%t5J9F2$B6?d0qYLLN)@=@fAAAc`J@K-{%ORddB!ULc{o?b@qP1=fPba>8{q+xUs`8) zCA_RzUU|FZ!~OUdGj!gojRgdD1IZd`W$$CNI8w}|S5Fyo!(2qQ65lgfzgWQ}pmB&# zc=TY>)vUx1q)u5RK%2keLdDh@C{EJyU~6(HLy|TBT!eLPJ=~2p;oC?271*7E+Ld{tOu`0n(f5);^CIE~PeNjjPo?8;;xuQqXB{ zK@2MY#293cW1!Cm?jU)R0d@Rl>FP|41MTK+?!5aA>T2H2?#1bkv38B8Sy6@Z8~bi7a9D>V{u+f@Z8pAa=_g8(uCID*C|^+jyK{d4ovp62@e&q@qCf&0ZTIPbXySm zV~hBSaLIl!VX0zFnEgBdE%#)b>F`aP@0%d+dqF|x$no5jzN`Dnde;bA$S^{9GhALT0)JI`FS1Xi8`NEdhOY=r)4Ij zxB~6+s6kS-IHcZ8O~0i(#sZO-e`dWpVlCpmGu*6p-2(ik1g(!ufLGnR#Uxl${ntcq z`pN9eHTal_F}ZYtn~#(p zdznOjEZVqw^!BF3g4I)c)D&#KzVN<4a}CpCtic?(JC;hCz(Wl3N=I~Mdd=5^A{oSe z+`sAZ-jnf~lpno*n)Ef9+mPd9ef+fY{o^v7puxc)h2ivb(dN_j}ZQhi%Jq!wH z-|KElwl4gMzepcqLZX4|k}gL&nHUNzx4(T>5CX(-!{#01L9$%Tpy6T8OBHyiI!p7PyT(I5~N&(EQuC1LpZCPy+EO+s%Gwb6_g zpP+Up<{NDgp+oVLDctu0-ygJpDnPhaVKr#C4s*L=i1bqCsZo#<|@yKI4zp7&QqhJ1F(Q5>&i?nb&#RH`B0lTI!#J zD@IT7h7Xg($oyt2iQoEupOFXw5glMs)aO_tX{#0!jSu6m@)e)D(lN#hNRrVJ2E8L> ztCyp;s8sa?b+2|W*-e(|G|J~Ir3{hZeubcqjeKkI_pN{u##<5a4e=*Umhrj$zlklhLkm5~6$GMQNB7f<>I zOHhu5D2202j6vEWi}EH5yeg?%3r4{Y1NGH}j_zGD1U)9uyNG?2<_WF0c`%4gg(1$> z+PVX8)MOlA)UBL9rHjo>qX^Nqla(`#hceTHL^ZWgjN{D1w_j2>e6%B$;>~;~$w~iX{gh zQ;fRf8;4YZ7tW$0hs~{5*~aGk#DTy{6!k_c$snUNAZ$FHh$pP-;OpO&gQ=}9*+>I} z^s^9W#1K}wL9G&3-@b!*@r}(}>s+jAngujtFyS_63NHSAoJJ;Ci?($KC6RL!($@BE z6Nm}KY~<&S6HPUF*8sFVXRTblgxFyM3bvOTcwce(K~%78Kv=&A;8(vT1vMFUv}DVk zt@o`w5NL5ZI)*eLB?|)ndAR|Skq3jFXugAsm7QVei3bB9znFJO9n1vaLfQeYP_(Oe z)0V*o@c=owXi8=fY8H0C5%b2e{DA|l-DE{(TXB)C1Ocg)u` z8xDiFcp2AV34w~|vA}zJ+y{Co;Z2Y&`JWxx+Ayn@hNJi$PMN=c>!z^@N}dy5-SX&n zefr{!O+g>nmM@lV+Ul?l1lJ8hz&7X^B(KAv#KRbr7D6&IGTatADKKhOT=(<+1MFxE z{p6Dfz_bO{S)Z0@c0D8x424Vp7^7G?Qq1mpM)wNZ!d$$7^E-J_Mxmb#58gv!#Qiyr+Ah*RA5q z9y6r}Te%eJu=AVGu@JosiSO~k{U0YWW_P14^^+t8FM{>eRQ}TKJP!u+&M5NQKVW`; zlQpnS+sUb7u_osP(C?By=Sl_0FGM@?0A^>A5cs1(A-rfxfI4dT1NtYHEe7Yji#fe4 zxq|ZY7<&5qg}wz)s-Xq3B2g5H&5{!eZfT){)kD`rM8RfKYun;ty}(>m&@54SkgKsI zS7Y6aZPwV+-x!0{qAg8+^S{D~L~CJK@JJ?Z z`^*sypx>nUb`D7y8RG3n7~?hTswe@~Ar!#)Rc@~Oa4msaDlqqKqu^h2q7$(hluY~) z`%x=4ysCy2!tqoJe7gf`A#K+V>FD#{7@J1X8f@Cesdr-t6=bo4$@dhWJjta?jY~*i zylKVsAl@HVm&h0TYF=d5%=iS{=L=uhh>B=$(GzTHpxEoQ*V|P+y$~Y(l)7 z<|YfTNg)DkFVoA{nT%Meemz{QzkPQt&W)#J9-(2Wtp5CIXGcB2fa;IEJ!Qi1Ts4c1 z^9OgS6;m+vb2-~GmVTT`c-QQ0-lkrk19S@r5r3jr;Yg0^4Cu?#snoKro{Ud_p57{q z`iY2$h+q;FeB{PyOM|^GZ?^DNG&ra8&^)xSfHuIC@C))2vtQKv8`)v`sp-Sz@5>iKKpBhPW26X*1<&uw4#r=i;F{+)KOnEQ{p! zZ98*fJbKgwENIU4OxyC;{k|WGlcxGi$|k;wx5KjE2rH*F?p{UPEr*E0J4$6bq6MhX zO8m+t0Nj_6E#f2{(BsRl2^ERh$=ah&N{j*S28V2HY_R~DZ)-^$4d2@gqx0p(jox;2 zOV^b>dQrlF(2F#EYd4P@$b$NRwXh--h-hMLi@Rncc{DK#(eTc~-L~9nTji>iH8NS^ zC(WIR{r&w!52MUnzP%s>5zAhUua#oHTr}sIn}-~8M4~CU6%kJxkz|wf!ayF#EcPck z=^O9`@@g!Q9kXJSHGVtnBPQMd_%U*|#c|q z(6L4r6`uIs%L<&>3K=Lmwg-||!AclLnVR7p^|)4|C%u+0U%m|H9q%Q&i4KM@M=!Q8Wt_};~ zQtGY$RjL!~wT|2~Af$^+)xProNcFM!$}v(~0S%Se?EUD%=FMg&6kuDl(RwAc*^YpQ zh>tP30K4P3`Ld#n7V_+2;G75F^Eexz@>MXV6kw?(xU$S_I&i8=q^XF_@8T?YSkqbh zWjyUc7O@CxO(xyK2Vc`q4p-w+Wp~9fdtj>o+GNVHPfk$y{QpN&*j9;+Roh@nW~*gO z8h;wNj4Cw&T%gc)3+;#l5A@JR6q_r7^G_|&s|HEtr`VZdW#s!;e;pXJ}ko6M@rbJh|Gl=VjUS#J(D>TP|lUD^=lh3WYeK2a_NBMM(W*D+UV?XWDDL%A%KMlc*j_nqM;|eD$d# zn`Zt&7M`V6GI3Y#)H6b_8fPO>(gvkAd^vi6H{>!L%T=R7of^fX; z6Qv2XF1Gho(U!`Eqq=MCr!)So;}7LVCZ>?)cC2tIY;OjQA>0RDU1xgNby@T8Rej|{ zJB~V_T|`NGVl`$IjkQoC+$Y2zBJFW+Gr+} zE0{UX*=f#YilQ>JlYIGz!kpir)MXdkb7a~LsGQx&rJ5Y#VvS)ee1Hy{mA;VL9k)Y~ zG{NUtzUUe?)`(^gUP!aHleFL+R+^iu>x6)SY-${Wi9mGg*XsXA1$H+}mrQ%0O*!o! zjQv4`wKhnSV`4!FRJC|S6CJQK`L#OVxH~yHMU9e-r2$c1P@ToSb8nX;VQ|OLho7Wt zFA$kh$$Mt;@vj*ZzBC#WArl&lh3MM>S`jw){h?ylBss^}n>Ke+Oz9qV1PHZhx2AV{ z#{tU%2@ap9FG6|Sjv1)(MZPK^Z*^}@MZDD~#l8~PhF)3DSpW?yk}_gO2A>kR8jRXU z|5*WhDNC>HOei1F-Mgo*mQl{<;1NpiY|8>1A{_);=Bg~G`f%zCY&yP|D33dEme##2 zo!*NSu-l;pS=#sChuK=CzQ!hh7FOZL9eRcQTZ_fuTz?mzkicrvJk)TXGaIsf|3qwW zY`C(uMO>5qPnM}c2%_v-8$W|Rv5$}qHF%m8;(@)AP|d5K;3!U2Ydhd#6^0OZtwKX6 z{W0;9GWJqdfUOYB-N;rKr8pE&ODcWy*kbd;gPxv&gn%ayf9j&{GdB~a1qkMCh}JEX zovG4kIg2D_Fc(r>AOR-2=+~gv?JTgbb!Lhem~#U^DdVbxsK;hf^;gS#jbC_Vebry8 z^|&|>Z-Qe!00=ISLWmln-FjT@!2NurFe?!w#H>y4c5hpEiI?vrVZMeSNee z-C(MH3bDzWM(}J;w{K$)`RA~Gj!nXpKG{gi}Cw(BcmI3>E zisYGf)M=}-hLcS8#5(*cVh;$-iQL!yD_F`?Q7?8J9?13Lj{(apvY<}%@m9Qx}OKxj;{0^sBE z0tZx6vhWY}lkPjfz-jOa^^8OAfbSQ0N;>@G0nwon!$KQwl0U!%yg++#*gc~O`pL&c zNC!WX&f2Z?pZAI(0`DyEqLn6!XsQBy*tFQMX$Sk#wSS)2^TEdw-+8}2Fj*kjY(zT3 zj}aCNa9W-tg71m_WUPU>Cc|cjhEg6)6Mk)NA%||%)aeG1T#MN{$Ne(Xi_)Ki<%dy$Jo;*Ui$da+v4Pnz1+$xMe;EO} zGWs#%x(5G;8`sKKB+K`kt5w4dau;!+m#+-BR@*cg3hcyGG$ zNr+vf(I=N9XxbY0wCskYEMsi74`2X_)2SPSLqob!Ho&1~oqYd9>yB@E(Pnuaw63fr;4JMF)r$Y`w0lJt*!DG+ zUdCeSOZZbv!Qr|0rjV3)Px@W^6%gXLdnva&-~4zl59suFb0VJGsheSssg^kn48nhKyc2YIiLrAnk$L;A4F`5C!a4PtbtBm5gDK#Khz4jFxP%Xzz>ubu zleO$FWm3idfq}=I_qh5>Z@Sa1Yq~e3KJMcVAd^)jj>RoTZ;sL3uQwDPZOt({wx++$&~emUKhM zl@2R-GOmnQ(hGWLG~E2sU@^$LDfAqYa{hRJ2$2Ej51H9a#H2;0;}S)4gf~I!lbiij z6iCPtm9m{cN}GC@iFL*A4;g@6AcPSM@~{{i;C&yxt7{Xii=X1I@9f1x+^r-s2RY-- zN)3cqme&$?yZ0RtKKC!LZf*Lee1ARU1N)uL=bE;7M=tcKE&$*iPVI+W1|en)j7h1J z9(5)`s>-&_Xh0yvck{?VG{3yRSVAPKAbe=1^JxxQ)yYq3~drCiq0$O)hYo0 zSl6NE`_aXRa*N45**V~H-iUG-Q9DIV8HzUu$ZNB(-W<@&#tPIYP zAAOFQezL*CCz$v4LSFeGSj;p`OtRq8E+rc1{p<%@#!NAfjzXXLYRgEt2S@2bSPf_g zXNQZ6aE`V<&&k6xG}>!$oUJS6t_H$7l=a2wCghVPPUZI9{2$anFF70JS$h&>P_#a087TIyUFM z?7l!skGvJ%(91xf_GbNP{9=)`y{6Ke;f# zS?coGoEek(I4IP_s3wEK>_IY=pnB(ChJbLpJc6F)@^iLDCojueZpKd+KC%*!`Giz* z-%IO{TCT)h#<_KkRFdu*(cA8357}2kU^R_{PKyRd_|O8-Q-%F!l+l=SFi;)tPd9j! z8M%BVjeql|4FCK~#B&Gk_UCUgaBpMP;5~4b?(h>&=G?)th6M|vV5Co)tn-*zmCwd)iS4c*wi#AIV%W^b6Hs{wdTX@}c z9P{F{WYs$a#-UA^gGDoUq|fcY@R4{HB|(8IrSj_b(oH;HB;p`PuO6cZsae@gYZ9djA$CS?Yv zaf4;gd#ib4+xz&x{I(|vy1&ZEYk!+2yE6D_Q0FjTf<}k+B8P_EgqL<+*IDj{N&ff; zf5fRy3w3GYXX%_c%#gfnZ07f-$=Ft-a==sx!78BZ3zv4cxsJ7iLsu3)jYp;-^2w*P zm$v%JGdjWV=+a+(?#V?F5LTP-S4C>gU&ae(TldK{}5z(QNu46b6F8} z@)ITLo5xi5qrdn%dMcmXLQBK&AqR=7nj(w8;_n6eun$yy_ns%YV!bje!*LKXr)zPB zEg+GYL>xS(SR)WIWmQS|Cz1@zoKm0x6YLZYN|73$=kbHB@28@2R;l1{@@Tw%`teC$ zcZ0{)I0t@r?o{<~*raC=S6y0;RMu}wN31C1z0<|Jee7rr)}ldB5*8bm-*44gW5g`- z^3x&GlJB#V3?F*6$TqRS6o-Jvw`Y%!h{p4&57}&FVi~JxE*MO8lVloyO{}0lV+BL%r4n3NkW!JD?hm;jE zAn<}rgE10{IcnG5gc}hFUsfOiNHgpbtF`&UXNH7ZjKaU>Vgs!8zl%Tc{lEf~{N19h zosxjd=l@~_8EN`zJY$J0AQZI=ACu3^MT!26p?SeEwI+rE7qGZ~dX31!epfAAD&aT? zn3A&KPedJ^YX6vSOmzLO9d7~=MojP{ii^}Fd|-|5Xz!c$ zBVbF+pw^C=Q7$T0CJY~YlTz4$jm?vKJF*-OXIlbWs6@w~4%3^YJNF3~HaFS|T`7r< z%M8-wZ`Q|#LDa)f`lJa?bMcuC{0k)3KQD2?`YxHZ_9O!d5|5JEwXMZnT&$Nzyw+=` zfE-$l&z?K+Cd)$-GW+i-c|K!WC=cNu>I<3i`L#_yfik$4TkfmS;GPwG@>VAy6wH3K z`0xLG^QM^95{2ne@SA)f^i4J<9gT4yfb7V`zJdKTX6X4_JYO14=Yxv8G^< zAm|;SLzsaycEs2}st5Rm#5>QLG^bH7Kzc3V$JOOuC4$b^U0E}o7PIVNRjv`}w6!eA zWd$TH_$}`>uMIZ#@k?ZBO@{d88;W=ku>0Wfw5R+G*V21L;B85X*+^i!VP7O-6>3it zEOF@O zB{@0EB1lY&B9K;AML8=gD{NOJFGEyS@94YkI^HYjWr|ToGa!Z!F`;MkO{1NweJQX7 zH{&W`)lYudSOI2u?}HA{-Jq6MySe!gos)B~5gPs#m4mLMJN4e)H-_pt$fsyNT&`j* zB@$+O*p{NN7SGTz$fSV_D`t$CTvd=yDOtAhT!3AILqnAIsMA9(b{uZv8~mkzMyhTE zoG!ljJ<54s`Iotdj6B&0%K(;HA6A|Jl;xkF19lklfEfzQ%T+X_TvrroClEn|2#95y zeTq|fdopgQ7K7X8?=Nb0pH2_)ql}R_-aH}&Cg6EkDhd}cPiKFJHBXxdERkYQ@sVgj zjIj}1tW#FNkVeCt+rwro;m_kw4DumC&~KAM6tBhiUx`tX`0Ktm;T}d$Q+*?hU6(*NT;=uqsrH1Pn8zlQ zVV#4fwHFd)IS)zTK4a~ggI^{CZ)7^jy8$4X@4m|{tH9d4$ak3b2pnco=iT~D@?aYW zSN?WG1FY?q;S3#JOkZm^G9wWs2Zs5+9mcn1t;CEQyPC_h#k@V9pNZ~ zAn{=9e)aTkyP!!v4aNoiYw)8^(#BY^ZVU#+fwXCNZVvv1Cc`YlmU$IsgWi8$Cnu~3 zGo)dvw6&7~1%^+!C)Nl04`(vRr=%2GpDKUH1jjw_Wl`6#xp)m&*r1wUyom>$$x&FCA^}%# zCrom=We`X_zMMCELaokhIN&RNe5+fRfcQIQ(?clw06;+dV zBO!1MadXghNeE~24?Sx6II?|h;>?;vNmHb4vjn z2Pi@8O^c8(|Mwe*+`OTSH0W%fe#K*fl>T0)q;p}Z-5h4{@Ws>V!zv=89lg-zUjrR}lP>(L_`r2hp zwo1;hP#YF17DL}`T|bx13-~xcm){N2W;0Jt(AHQ>{U1ZGi79ZXV)i3x-yZ68>5A#y z3~M{D4U`xVKg_{{CG>3%x|drU4hlk8gM)+P#Y2J{SWnwn_taHelr5?2asd1)#E8G3 z@q<$`3&j8z#Zh$3exsq=<4=p#Zt#Mdq5oEtn){u(IgS}`t8kyk#dDhm9CKV6c7HP3 zhA%G)ye2COIxRuce6OlT9qU?g{6Bg_GA)y0Z0b|9tl0Abq0f2-iq{y21nv9b%l`ru zP_V}pb1Wu*iCLY~}gmu<;eSJtjo|Na)r%$d|Q4iR0Fh$ZsLylp6;hn%gzVvwa zc28iW+s)c6tAxM+L{y(^9_<4IS=boC`g-Z5H1mWEA z|1kv^EB^*15O*a%OWxQNK}!%7@m6Ol>)W^t8gzx0gj6=KF)_Tb#Y~nc8aj(mil1`@ z2oxvZSEs7XfjONof`5-zwse1g#F8q8XTEK6;-qln)cs}WT9_J+axaC*PxMCtC4h5D z)JN3dWHGp~*n+lrX}SA!-^W>mP&O=qeVvi0@%`)6PQMKzd!Hv=q8ny>A3lTpPgYYf z40-UW#Yl|V%($`a`cqBV%I1w}D${)Le+Q{(%MsJ8qt)aDL#J#GtJ^ciZ+0|v4Q-1kiMWu0duIR72aM&zN3$qe+@=^02?Y2G80VIr(27qW?+^ZPT9 zg!z6MK;9rh3re}v@EkcO6P}vv`t^7BS%2iT96aAhUtMD3DJ$|J;)8uK@?Xew02v?@ z2iluRJp0V0Q|9b%O1yESrS$Rpt?+(cfUj$C-{HYs6X!u6vgZ}tZREbCZE!eUYoCH+ z5e3RoM=QWd|8@)DV#=%$!j*QPtGR09(ceQOfl1`a9(Iqz0KjgtgV*4Ecoy$VX1 zP*#Sd>bue7kq;ft&WxXgDtO-9N$~;DEjsFz%rTmHbj+i^$g4AVk;)Ni$cln$Y8^nh zVL#?H4Bf!9GS|E-MoUYJ0kn_4jbH-xeFokjDD-#%z3kqvhAkdo#YUg6uu9!RNC1h0 zv7L#gSErLolB1%r(tHR3-sXdN7=m5;tUx8UqDslUt1ez!ZTh+S?wkqNbG0nG=c+Bz zH0L{yTHesI%0g&w;k3uwBb2Udm+)V}=i7Nfw6sjIo;&L2ntSI1cOWh-2eYqoTx<3S z_P(CBNVh`$F?Z3*Ru!ec{w%!bU+3E700Q;J1A6JoMbL#o*V;z!+s4FgxT`~AQqtQ% zOs6lPiP58qpWxcf?hYJTFFR}dX=^FWs77#zx_Ww677W|mLwBr>f<=Z=` z2tE*YGi=q^=jl!6F|M(g_I=~$OJ3q6nIplZaR;``qw!~fouh!1fjytxM>bj=2*UpQ zzg_oA+d6^r7=My0Vz?|nR;0Y#a*>}bDdScENNA8O|Ew<|iyGRzud~l1W{6M4R%YSH z=NH}98T{#W-xU96&Mno3HoKeUdC2xs-171^j++QtC1qv%45fg80811^J0Y|7XY?7a zzIagbr>v6(aczw!Q}jh#Mp5YL9C-Fs7XVB^wZCDN`QN;RzqRZQoiP;j zSQdfzdI5gq6EpzKE`>H`w5z#;aOL!6-8Z)(vZH@w7#3p&RD^H^t%N~Vh6&p<@e}#S zy^|HmHmeGXRaBCGdVST!2PF&2uM-L%_URCvy!VhZ{N&yHz1n$Xw0l$R{Uh!`M;0s< z*_kKLWTg4>K?ku>5S8e*@`w)5n?|KTNLO}1GIzDTk)|MNV@_7SU&2syp*(vulf@rC zGqWSyE$YpCLzPVD>B8D}^84z5Kp3r7a(T=CW4b zE}EYAjk$hkGHOQBp_`WLRQxbsG5!Dg`s%PKyRKhm7-CRKQA!x31pxu21O`D$2@#MI zkdRQ25RkkXP!JU9?v#{}ZVW1?_AgY2TbhQvG&?){nl@- z9q8afjBLbK3;lx?(2#26pr5fofG^CsT)&l!iN*QSGLWh~O_&6^4dLIv8}c049Ok?i zXB3km90tVjTM1nU@nVT?kixE5q3BhadEhUc79x3^fAxK%5U?E%t_oiqD zxami8gvcCLT?OcYBgm7^TcjaIqlA3`i($Ek71Oaskf$8a5iM>vNLq4QqhuFBh7hGI-UFXJrUf%qI}@5~Er(mQI=QSD*pMk4+Q)kc6u3t%;Zn?xIij`F*4z(_j&LBN0%TH>H>lUlsNDZsPZ zEuw7xn>IgGH?7}ZzxU%)&m!Yk=tQk+E}maf(0J$SPlBU|qkJhBDU`cI9)+rQ@+ozV zEKJiOE&v7v(7I?B@;rJR#zMUNq|_JKhJqhBxTrixo4-^I%1IkyxTJ@8X%$0MCZY?q z(4wo2{;1(CX?6a(O%lhgjDbp!)ab9yK=s^)%vWti??9j-G$Ly+{Zk1D1mmZ?j&s~Rnx@Eh=E>iovxlm{ z5=n9pB=AaU`Eya|i6TZuMS-O2gjOS~(1hc!UUiEZ#v&5?wM0h^*bLN=>U2}2CRK@! zXcJ%mOZ5f~kKaTBd%@9kOxhYG8^y_Qe-c9~x-5pO3S6~J92JR0QF9bel^@+G7|KCDu082tOrenQZJ$KB8rf9s``M471t^R$FE;{J z#s&mrxcVt^mJetolRzQ>&rnoEYtW;$V058Bu1~jDVvng==NSW|o;4>fg6m6Jg3e#- zZX2e@ZGTEiJc*u_^N9|{62OA+Qtfbf(BYDbX2)ryfzgy~VD@&93#X#bmb>Hr;0Oz> z#~>E(Nj{|}Rb-zk5+(?MDgFg*5kK(n*r()!NZcyw2K*MhzK+6&7UlYoZSC z7hqaJU`c1Oz_+1_lr`!luVy~3*9Wfu4+<>%_&Kl(S6^LL zbc`bqmZ>@!J6F!y5u<-y&pZr&~Vq4o`O01pcD+&0A&$MCoDrZ$rrCr zgNvPiQq=k~$X6O(u-8c=|0^99-v})N0-A)@?bW;(;=Z>%Q|>gG-vj35jq%-NVVS?k zOOy5(PR^n?+O)k>py|e$_oS0kQ#>!KCFJgn6M{bbz#I1ES4*%2vbQ>9P+~N2!x)4* z>64@>RK&agA1k(ZL{}feFC#O&*XUS59>mMklhjTU>wdDgU*{s&zx9OspVh(OQ5!v7U>NeddzbiExm49TH-rA9T$UBK^}f_)D*vN$^X60(eZtswJ){x z&*4*y=1rGnLZwo?&gk7;+-;osQuM7dKJ*4{z$>P(qy6sSo~lQX_Sc=w92JkniX(%D z?_u4|h~1=~v({Q&LNey&=50Z=5bxw*iCJD`WTX->;p)~t@rLO>S(HP{)(&{hUQ=sF zz^Ys3L$jPC`)RU|V_{NdY{#2uc<8zxJf5aY$V(SY;q~9qK8bk2%cUM>cTQ4eXShG_ zs^dWCRJg+guOCBphtzBwi-f<|bFV7Kcd{be3xM$;WcmSm{)v_;3`cG_k}ny0o}8Db zI32kG0)t(vfcc@apv{samoQ3E8Y+u>c{k)in-O%du?8-S_zdQ_N`)|#iBGn1;GDd& zHZiOPwv$|i4*3_-co3*;M<=Jt<*)98jg{%^W84DLgy^K=^7y!P>#_7b&6f zC?>r>KkLa1f1LR5Gqo!Gj1pS@o6B4$-fwE@cst_`=@^OJ(^ypLYUg4@uQQrsUPuod z{j$?4slpN<&;?zyqqUVi(jW@*`&(gxemzuT)qS2GTs%O=hqYUqZ12bs*Q|} z&XX>elJ5;@YN@atF$c(b-ojX;@fEx8+#o$G6qGV+1NGN<0Ar(2^)lW8Xu5;?G4>}g zdY%55(HC9OowH3kejqI@EZm?L38+fX*_$4y;&<#TBoWXpPlvOZ~*;+e&rY zpRxPnau%q z6b0{%ERE(dd)=2>%^XA!k^cGJf4i^&TZ}APEVXLaq^+Cnswljhm3*&tabmr{{D5Gu zsBzxT?Pu6nj)Mk7vA1eyP+}oC!!r;Pwdk?g$C+$z0K^mHBWTr?`iYiq+eFymmpnay zqA^xD3Q4n;m$$CM!4|hO;NGzC%MZTg)Oa84=FX%X{&@||`v=bF7SpbHg?aV#_D+HO z6HN%%sDn-n0C~QPRf>{^5xZ5%lA}~69x_^Gz&{Ua6!x%^BN$NyUJMuD3J(v;%lTq` zoY)akV4?!wdpBP=Q6OLZ`QyM{fh-d8oLfH(t5s@TfAdMbr`q_Ftx9CPMD?_ZjF>}X z3W0dKJmylne{a~!l!U3qWj589aaP80#iXC`;B4ditMSWod;Uk)psD9SOYeQw>3P-Ahop z`H4GH5SKbSkM=WFG{$e0(Z5Fn1gA*Yd0Ex_Dh05@38=M0hw8C70-@JHI#+_168t7kz6<^PJ(s)lr_x+(qW?9%G)Bej@VDx?M}x zwnvb|YYcW(yWHG)Dbp9dIp1apDk2c_kQO`~EX^vdb`4pI8qgMi&QIl4TyOY6NrjR1 zYJK}PysbLzjZO3S;;bwiDPzeS1hPblDL5xydvBsLv;OOoX=#H-j;-fhQTB%3)d3KIh@ECg08uQMbHq(Is46 z-BpVumOGX?fzT?F+Tc3NjoS{T9EyW$wezHld#|ev>JhHIZ zM)C@M^Q zs9wS(jPYW%hlI^~jGzWih=ehrBgmJA5-|k6SD1bU_Mf#U)p0j5Ku-poq=&H|h4KsC ze?}koszyzijG%XaQFg&N1E{(I2tD0So2;{2HE%_7zi5F2D!gh2z@Fi8KT{RPWaT)x znEKjSj8nh%W3g8q3Tw7RI;By7xcu(jHCaJI-b?fyf}J5{AQc27jr4*_M?MZTU;iy~ z)|snNf>uwh*coM@WdDY^!r#Q=gRk*dO#`LkUBrmIU81y{tz6)DY2;9iFAboQ~R#@FUu{+PDe?@Sx<6oE9@CKCYO_mhuZykUCq4_NTwTL>sK zmru(emg)p^~bl4k&YaxQHqOp%$YPQgpW%emi-=_ScI zKG+ANL?M?SlpTpOJVITzYp~7hfbIpFm(6p!j@^N{xBOpN=$Yz8>+swqY8VJglxSF5 zj-lX7Fn##ve_2nbZr{4a%9m^5gCW*27Yt3aQuy~?^`VfDi{Cy|1#kA^nrJWt5u|?>-J+GJb>426mowGOmjA0NP!vG!19DdT+aV&{b%C@`I_YzgB$fZ!{gL%hN}aJSHz%Fhb+W28NNG8h=L5G2pk@CkX(-?c{0 zIqVP#aV#j_UdT#0bbDX~ ztziS{1mz`a! zH1qY~U-0#5iIVeB%Ky`HUpBb-Ouoo{seLW6oDywE$2C;r1Gad#Tpq->wbh`padJ+m zTc<#OpGrKQTHcRd*fnqTtkNr5jMp#f0{VJRG_3t{nN$_3w6ql5`qFV0b-Bi6kpaqS zQn0W88W-Vnh$7yw;|-oq@Dk(pWe|QZ8{FXMucEen!|`w%xgmK)31NP)EJ6iqvfN%W z4lYsk-Dd|MP5jZx8F0in40YJ>i_LnV^q#r)%>gh-8qP8Fj^nkC{qY->Eh4!9P_*vP zi&_{gk>jubqxrih>&5u{yEmbvQ>TB`+okqel}^AzHdU{1sHfS?D(M6vMUD0y0lu)8 zE~Ar6dt_5T>;Z|I@5RSb%}xSdq45~MDcVx=DNZ}#OPjBQ{UfmUXmGOMu@G&hXIRw) zBaVH3s||$&KDBSm_YzP@j@f(r9-QDB858sQYm%hbxk2EEL0`S(3-42PiGL1MEv?%> z5qH#cVK1w@^Fs0JTadT7lO-f-cx6ePpU+)Tx5g%7?F3R?Va!}48EEffRH>wB%maVt z0jtB;H@n?Gqd@|wBQ;ABn~7fT5pS>s(SSe5|D^3Gma-wa0+QJJTu#>}!-WtIF`?b1 zSqmeT>hnD9xmuwDk0Ax|*#fOW&)*lar0yPW-9?Fm36>AtCJxjGp#ka~`84q~b&@ zl-tdf3Mzn#vnfpPc8kk8btC0TnW7RQPzI1vlPgLr?KTQTu?1Q~yjX*&zo!RwB8HMA zk~DQ%&2}0(=RNj-6AJ2x8*^&4X` z8OBU%HZHWSuQ>V}E53rQ2Q=oDfs$ZU$9X|G+Rn>M(?}xJu|@_mv0c$xOi~vd_p56C z#4<-q<$D;JW6TsADLs9hlrHw;HSTF-0Vm=BU03ax;VJq$AaS(%`n#5f!@2y@>MF79 zL%-K!ED?s8q2Ca}8z-QYS7lKHz%1{ekhVAVy&Wq9H4N+g8la`uE?F@uQh8eBrM{GgRW1o#a2Xw8zlyVIqGPUTu8HFsPXMdvC2aBb%3e= zh`93mi>YQedxk2kQhru0wZ+tNf)NO=`_+0ne|zO5v=3F4vokpTnD1^YmR3Ru#f!$g zJaf^!H(Rykrbkn?ZKiQ&#f6-EFS|t|Q8t(+I0G1lUXZ(kzC-EVF^+08y_RDf=_kB4 zqoUB#6yw9e>CRX`J+<%l;?Bn@?>??TPF*5ZSswkd!;&a4xZSdh6WcYk9@>67Zte<^ zXlXt>L|L)ICkraMDG!QjGmdEx;0if+ra=GZnYIRXnJ9xd3^Z>Ujx5CKMWv_PZ+D$U z0?H_k-oHnMS}7dhK;T#!^^!BuaG|Jvykasj9nqa0mMJR6Pp_)pHXS}%DzwkqS*2WM zBN%mz76vvAQ@pU~w6s`*Ud;1T){#hDs=N}+@JZZuq^01KtDahc|0%QNt3(^10Q3PA z6qUV^jyYiMxn>5#s+ZoRr}5Khe7;G+{MhaER-foiN!A(5D-rc6(e3*&I_+s}Q-P;i zj9~Q+o>VHt!;3px-|BlZ57F&b`&lm;`fl0P>>JMT735IPL!o=iNBU4LpNyyOCa8(- zq>;;$h5F>O5c;xcOF8!x6HL+vAcF_e`X_}*)`-e423@Y7FQ-8*)#XIq?bLyZ0{5Qu zZnU-GQKpIYlK({y3vjqf#N9A}zXrp`Cy}H^g8&opOhf(|)KAwre?^>Ml_Bx;=QH-R zJ0;N2+pNa}Z>q{pZ5l+=6m$*l>VSw{>>evOKgF)$_QJ9uEB#r+*>ycfcXq%Rq9Bq2 znNrDAUEZmd$&k^R#@)P4mv8q?=Iu=~WQUclf2>rMS+8Y1w!UpFy;rC8?TtWmbnvVE z-5`MwJQ0D2SAudMD;}5bKDwYx8}OtR>JLt^+mxTkO-QO4lC$H|!iq5jSZ#Fg(-lg` zGgr<#S6B{x3SrM^cd#Cr)ZIRquQH4nxNCcneo%6A5mR0-qhl+V_wD8VFTbQumN_JB zFDmCtRH+puB=)P77VS2iZrrO-yWbs+BGpw*Pu)GXxy`l`YE|#moX`p-M1;QK z$s72RtmW|B)o`@x_kDGd&%=V?fsjSKz9y`%E20?r8w(m?EY|OVxz4?YX)xkOO?or!-x5%wxeZnHW z4z^;~g8jRwv7>?dzOlCzape>vdu||}BZy!3 z#~dc-1_hp(6}|9qZb{x6i`h6a-70l)k&t2gh4-$wRF?M)zi;DO474op!XtbklmWTY zUt@X1?s26c#+{x%_mXc$I5voM!Khr*p3*MJf2No{l>>*HF~5-z6medW{DCz~@cHAQ z3$>DOQy80RRCG|HSaNK5nY+VQuXO6YPVPeO2jWj=iwit9>33$!@3_73Kc&z+Q^u*9 zIa$BOKH>YQ%?Bc3?gK_c78PPFt|!xsYiPZhq#@xfmprnay7<#)4%@Mr(zztpaP5zW z#~TWZjHhg3P3@Fk=PLQssMMBTniR+pD#)3NxL~`xK>>*$W|h$kYLGKs&C#*#qkycp z%}1`Fzqn(gLZbPp`qGLUB21|S0@NeMt8GUmAZvS^;Nca0eFqxH!%b;(M+XvC9#QF6squNc)B00t1yGgtjlTHG8Ju{n<+UMqJpqVZX zbn?vU?-=Wg`a%80RNu4I(>U12FCq5d%w5`b3{jl2XEz~6B)+~>tmsNVm>Bi+r;XH7gP96A{v&Zsc3f|K z1WOzPT!Mzw#?bvIMC3TMGZVNm^!hxP+Uz5di}7R8xO#J1Fn%3-jz*`F)r1s(~-N ze9h?gPrf00DlMW;ed54yh#wh@k7a-a+Ft^&7vZzffH+G0y(TKbOhwj1LG1 zl0H23?8$QnLBlV4oXV-H_3lEyOK(`L?{2Iu?C%(sgt4o;{Ee|-5Q^wTG1fsN%^Ifw zL(#|2U)G!3GtFhZSaLh^YSvkNS(LSgZxyjgP)EEVN zF?SjiCEdDrWEy$4>k@O+h()M5TD{@TM9GFjK*B7M_oYhBBC|mV3(Jlu7^WLsXQG$< zLrovquUgQfS#IgN@X^nF^8=$ceP4CN-yYojw+DS|9@vAW56c=qxa!o;FGt{hZr#p* zoT5uY(N@{$fHNE}D$2;ebbf5i;ck(wf%1cXh8Xl&&;^CoSNJYKk~nwU;h_F9>xFaN z%Euus_EaokrF2a9-OY`bkJTZzCEYbXIjoL4?9;%+S`p^=n3BMT%+OHra zX+OLx8Mnc19j*ml1J?2RrXjp+hmt<@+Pm0(*sQCV-QnZUMwbi9CIQwXr%$Aoaf)X` zWP#zWNi)i4=ZA`lO-e%9k#^;~PQ;*|Wx8O%Jqp^C*fqf4KjOJWtF{XOI`ecYj63;73!QG`~L-1a_xc|8> zqi=ao8CVOr6&Aq9xR0_TKUM{yAUwJ0xgyXEZu?NValL0wJo{`AT^9k-8Cd#O;;~{m z4h2S1&CKsYj6C}1yTzFyw3v#|r@uqM&n+i%Z)@pRz%Pm?Ceh}JM;r0-;dy3QHo&9C zJGX%}O>qo_ODPrp`rSh(uaYUlbe#p2} zQPoNa;?&txO%C{DA5;h$zrQW2HF7_MR$6;7`L&tl4>>%399b5O5}t3h{#Uhu&&Fa2 zQq1n`A6^oF{3oYxc~~{x<+Ee|Lt+ubk2e6h7|e2X=%xKNrD9mc9N(PA@2toEFL%s4x$T?EgnKAtd)7^p>5wp}=-xxXY?>bid4 zx~n`vq(fTK{m?2M!|`CI?p|`UJzwfQ056?+VG)LdpPhJI5%f)JZ_%I?I|v2!Of!1* z|9DRvH##M+U?5%$H3oUc6x`+AkaUiAKPT~tYotQi>KkF6XW8SxPl>(@Jh^#Ck(=Bs znL&-WCwfAXsMpx40dc)Cz^$?0yYY7m*YL9C=tAEYX2<*k>5v%p(!;{Dhx>Ys2H8xH zqTfhoe3Q&iN{ou&wYKdfJ6eSbmCvQvjas{~r{03`Bl9K?%&=6Gd+*XISMO2I*X7E4 zn`;}&m3dK&UUs!|K(bRC?41w3MhUG|pVj{Dw3l?D4Tn|Co2%_&p7PULb+s|vEit?1 z%`WP=zP2T}TsgcfI=U^tcTn)xz$+h{SzS~9*0}LLMP|O-nisO=XW$-^q2nDIYgAZK zteTXb;v5(W^HFf*EEYv;u{OoPYy5vb8YBo}kvRhMYr?kw9#=rVj~`qr6{#tf%}RP2 zro8+|wEzAM+)Yzbt8KfliIlKt;Pb!#5fWHbu$PhdwFOh}MK@Yq{EtW<%aIR1%1TiM zEtndnxqob#|JN`0%J~R$Ah^KWKiO61{Ddga24F5@FQVwWa)`(i{&o!dw|_U! zV+~+*Ox_0(jB!hF`q#vOzmHQw0?!SM&=0hJuXx=`;=lUlQ;8B|!2jHwwwWYE8WZwmjC9_|TR1E2|ihXJ@Qq*i4?+OdxRnR0MqGsyk3 v1eChSBNhUJqa$x3vuOeX0%BO>5)y2-+vqyH?Y{(GAyB@qCjaJ|iSPdaRoT5V delta 66893 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 )n2DqA{iVT)W5KmIEOcdt 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
User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
K8s
resources
K8s...
Hetzner
Platform
domain
Platform...
Management
domain
Management...
CTF
domain
CTF...
Cloudflare proxy
Cloudflare proxy
Cloudflare
Cloudflare
Text is not SVG - cannot display \ No newline at end of file +
User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
K8s
resources
K8s...
Hetzner
Platform
domain
Platform...
Management
domain
Management...
CTF
domain
CTF...
Cloudflare proxy
Cloudflare proxy
Cloudflare
Cloudflare
Text is not SVG - cannot display
\ No newline at end of file From 1ee0a94252fb0a0dd1b7bfb7971f44ce37bf121f Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 14:33:27 +0100 Subject: [PATCH 122/148] Correct formatting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 861d652..6d1526a 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Overview](#overview) - [Challenge deployment](#challenge-deployment) - [Network](#network) - - [Cluster networking](#cluster-networking) + - [Cluster networking](#cluster-networking) - [Challenge networking](#challenge-networking) - [Getting help](#getting-help) - [Contributing](#contributing) @@ -738,7 +738,7 @@ ctfp/ ### Network -### Cluster networking +#### Cluster networking ![CTFp Cluster Networking Overview](./docs/attachments/architecture/cluster-network-architecture.svg) From baab6aea6e99744c93ae4ed06d5539239cb441c0 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 15:01:04 +0100 Subject: [PATCH 123/148] Started on cluster architecture overview --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 6d1526a..4c65444 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Architecture](#architecture) - [Directory structure](#directory-structure) - [Overview](#overview) + - [Cluster](#cluster) + - [Cluster requirements](#cluster-requirements) - [Challenge deployment](#challenge-deployment) - [Network](#network) - [Cluster networking](#cluster-networking) @@ -732,6 +734,46 @@ ctfp/ ![CTFp Architecture](./docs/attachments/architecture/overview.svg) +The above figure, details how the different components come together to form the complete CTFp platform. +It highlights the central elements: [CTFd](https://github.com/ctfpilot/ctfd), DB Cluster, Redis, [CTFd-manager](https://github.com/ctfpilot/ctfd-manager), [KubeCTF](https://github.com/ctfpilot/kube-ctf), monitoring and deployment flow. + +*The figure serves as an overview of the platform's architecture, and does therefore not include all components and services involved in the platform.* + +#### Cluster + +The Cluster component is responsible for provisioning and managing the Kubernetes cluster infrastructure on Hetzner Cloud. + +It deploys a [kube-hetzner](https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner) cluster within the Hetzner Cloud environment, setting up the necessary servers, networking, and storage resources required for the cluster to operate. + +Specifically, it handles: + +- **Cluster provisioning**: Creating and configuring the Kubernetes cluster using Hetzner Cloud resources. +- **Node management**: Setting up and managing the worker nodes that will run the workloads. + This including configuring node pools, scaling, and updating nodes as needed, along with setting up the node-autoscaler for automatic scaling based on demand. +- **Networking**: Configuring the network settings to ensure proper communication between cluster components. + This includes setting up a private network, configuring VPN connectivity between the nodes and setting up Flannel CNI for pod networking. + Opens up required firewall rules to allow communication between nodes, and outbound connections to required services. +- **Storage**: Setting up storage controller (CSI) to use Hetzner Block storage volumes. +- **Traefik proxy**: Deploying Traefik as the ingress controller for managing incoming traffic to the cluster. + +If an alternative cluster setup is desired, the Cluster component can be replaced with a different Kubernetes cluster, as long as it meets the requirements for running the platform. + +##### Cluster requirements + +The Kubernetes cluster used for CTFp must meet the following requirements: + +- Kubernetes version 1.33 or higher +- Traefik ingress controller, with correctly configured load balancer +- Persistent storage support (CSI). You may use whatever storage solution you prefer, as long as it supports dynamic provisioning of Persistent Volumes, and is set as the default storage class. +- Provides a kubeconfig file for the cluster, to allow the CLI tool to interact with the cluster. This config should have full admin access to the cluster. +- Has at least a single node with the taint `cluster.ctfpilot.com/node=scaler:PreferNoSchedule` for running challenge instances. + *May be skipped, if no instanced challenges are to be deployed, or you change the taints in the challenge deployment configuration.* +- Enough resources to run the platform components. + *This depends on the CTFd setup, challenges and CTF size.* +- Has correct firewall rules to allow outbound connections to required services, such as logging aggregation, SMTP servers, Discord, Cloudflare API, GitHub, and reverse connections from challenges (if they need internet access). +- Flannel CNI installed for networking. +- Cert-manager is not installed, as it is managed by the Ops component. + ### Challenge deployment ![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.svg) From 54e6e247af208779a2a46b7610747a370bec993e Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 15:38:03 +0100 Subject: [PATCH 124/148] Add ops, platform and challenges architecture overview --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/README.md b/README.md index 4c65444..7aa337f 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Overview](#overview) - [Cluster](#cluster) - [Cluster requirements](#cluster-requirements) + - [Ops](#ops) + - [Platform](#platform) + - [Challenges](#challenges) - [Challenge deployment](#challenge-deployment) - [Network](#network) - [Cluster networking](#cluster-networking) @@ -774,6 +777,71 @@ The Kubernetes cluster used for CTFp must meet the following requirements: - Flannel CNI installed for networking. - Cert-manager is not installed, as it is managed by the Ops component. +#### Ops + +The Ops component is responsible for deploying and managing the operational tools, services, and configurations required for the platform to function. + +It deploys essential infrastructure components on top of the Kubernetes cluster, providing foundational services that other platform components depend on. This component must be deployed after the Cluster and before the Platform and Challenges components. + +Specifically, it deploys the following: + +- **ArgoCD**: GitOps continuous delivery tool used to deploy and manage applications within the Kubernetes cluster. ArgoCD continuously synchronizes the cluster state with Git repositories, enabling declarative infrastructure management. +- **Cert-manager**: Certificate management system for automating TLS/SSL certificate provisioning and renewal. It integrates with Cloudflare for DNS validation challenges. +- **Traefik configuration**: Deploys additional Helm chart configuration for the Traefik ingress controller already present in the cluster, enabling advanced routing and middleware features, along with additonal logging with filebeat log aggregation. +- **Descheduler**: Continuously rebalances the cluster by evicting workloads from nodes, ensuring optimal resource utilization and distribution across available nodes. +- **Error Fallback**: Deploys [CTF Pilot's Error Fallback](https://github.com/ctfpilot/error-fallback) page service, providing custom error pages for HTTP error responses (e.g., 404, 502, 503). +- **Filebeat**: Log aggregation and forwarding system that sends logs to Elasticsearch or other log aggregation services, enabling centralized logging and analysis. +- **MariaDB Operator**: Kubernetes operator for managing MariaDB database instances. Allows automated provisioning, scaling, and management of MySQL-compatible databases. +- **Redis Operator**: Kubernetes operator for managing Redis cache instances. Enables automated deployment and management of Redis clusters for caching and data storage. +- **Prometheus & Grafana Stack**: Comprehensive monitoring and visualization solution. Prometheus scrapes metrics from cluster components, while Grafana provides dashboards for monitoring cluster health, resource usage, and application performance. Custom dashboards for Kuberenetes, CTFd, and KubeCTF are included. +- **Alertmanager**: Alerting system integrated with Prometheus, used to send notifications based on defined alerting rules. Configured to send alerts to Discord channels for monitoring purposes. + +#### Platform + +The Platform component is responsible for deploying and managing the CTFd scoreboard and its associated services. + +It handles the complete setup of the CTF competition's scoring system, database infrastructure, and management services. The Platform component must be deployed after both the Cluster and Ops components, as it depends on services provided by the Ops component. + +Specifically, it deploys the following: + +- **CTFd**: The main CTF scoreboard application. This is deployed as a customizable instance that manages team registration, challenge submissions, scoring, and leaderboards. It deploys using the provided CTFd configuration from the defined GitHub repository. See [CTF Pilot's CTFd configuration](https://github.com/ctfpilot/ctfd) for more information. +- [**CTFd-manager**](https://github.com/ctfpilot/ctfd-manager): A companion service for CTFd that provides automated configuration management and administrative functions. It handles initial setup of CTFd and continuous synchronization of pages and challenges. +- **MariaDB database cluster**: A highly available database cluster for storing CTFd data, user accounts, challenge information, and competition state. Deployed using the MariaDB Operator with automated backups to S3. +- **Redis caching layer**: A Redis cluster for caching CTFd data and improving performance. +- **S3 storage configuration**: Integration with S3-compatible object storage for storing challenge files, user uploads, and other assets uploaded to CTFd. +- **Metrics and monitoring**: Deploys metrics exporters and monitoring configurations specific to the CTFd instance for tracking performance and availability. +- **Pages deployment**: Automatically deploys CTF-related pages (e.g., rules, schedule, information pages) from the defined GitHub repository using [CTFd-manager](https://github.com/ctfpilot/ctfd-manager). +- **Traefik ingress configuration**: Sets up ingress routing rules to expose CTFd and related services through the Traefik ingress controller. +- **Initial CTFd setup**: Configures initial CTFd settings, such as competition name, start/end times, and other global settings using [CTFd-manager](https://github.com/ctfpilot/ctfd-manager). + +The Platform automatically sets up Kubernetes secrets and configurations for the components deployed, so that these information is not required to be tracked within Git. +This means, that critical secrets are stored within Kubernetes secrets once the Platform component is deployed. + +Backups of the database are automatically created and stored in the configured S3 storage, allowing for disaster recovery and data retention. Currently backups are configured to run every 15 minutes, and retained for 30 days. +Backups are stored as cleartext SQL dump files, so ensure that the S3 storage has proper access policies in place to prevent unauthorized access. + +#### Challenges + +The Challenges component is responsible for managing the deployment and configuration of CTF challenges within the platform. + +It handles the infrastructure setup required to host, isolate, and manage challenges across the Kubernetes cluster. Challenge instances can be deployed in different modes (static, shared or instanced), and the component manages the networking, resource allocation, and lifecycle of challenge containers. The Challenges component must be deployed after the Cluster, Ops, and Platform components. + +Specifically, it manages the following: + +- **Challenge deployment infrastructure**: Sets up the necessary Kubernetes resources for hosting challenges. This includes namespaces, network policies, and RBAC configurations for proper challenge isolation and access control. +- **KubeCTF integration**: Integrates with [KubeCTF](https://github.com/ctfpilot/kube-ctf) to enable dynamic challenge instance management. [KubeCTF](https://github.com/ctfpilot/kube-ctf) handles the creation, scaling, and destruction of challenge instances. +- **Challenge mode support**: Supports three deployment modes: + - **Static challenges**: Challenges that are deployed as static files (e.g., forensics challenges) and are only deployed to CTFd through [CTFd-manager](https://github.com/ctfpilot/ctfd-manager). + - **Shared challenges**: Challenges that have a single instance shared among all teams (e.g., web challenges). This is deployed through ArgoCD. + - **Instanced challenges**: Challenges that have individual instances for each team (e.g., dynamic web challenges). This is managed through [KubeCTF](https://github.com/ctfpilot/kube-ctf). +- **IP whitelisting**: Implements IP-based access control to challenges, allowing restrictions on which IPs or networks can access specific challenges. For public access, the `0.0.0.0/0` CIDR can be used. +- **Custom fallback pages**: Deploys custom error pages for various challenge states (e.g., instancing fallback page for when a challenge is being provisioned). +- **Challenge deployment and configuration management**: Deploys challenge deployment configurations through ArgoCD, allowing for GitOps-style management of challenge definitions and updates, controlling it through defined GitHub repository and defined challenge slugs to be deployed. + +Challenges are deployed and managed through Git repositories, with configurations defined in challenge definition files. Use the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template) for challenge development. + +Per default, the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) deployment templates use taints to control which nodes challenge instances are scheduled on. Therefore, the cluster must have at least one node with the taint `cluster.ctfpilot.com/node=scaler:PreferNoSchedule` if using Instanced challenges, to ensure challenge instances are properly scheduled. + ### Challenge deployment ![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.svg) From 50ec6a6c62262acacba98912c150d8dcc769aa44 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 15:38:40 +0100 Subject: [PATCH 125/148] Update formatting of headers: --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 7aa337f..242b4f0 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,6 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Directory structure](#directory-structure) - [Overview](#overview) - [Cluster](#cluster) - - [Cluster requirements](#cluster-requirements) - [Ops](#ops) - [Platform](#platform) - [Challenges](#challenges) @@ -761,7 +760,7 @@ Specifically, it handles: If an alternative cluster setup is desired, the Cluster component can be replaced with a different Kubernetes cluster, as long as it meets the requirements for running the platform. -##### Cluster requirements +**Cluster requirements**: The Kubernetes cluster used for CTFp must meet the following requirements: From 2cb5a1abccfa878d34548c794b402599c8e4125d Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 15:40:19 +0100 Subject: [PATCH 126/148] Clarify challenge instance scheduling requirements in documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 242b4f0..1e41e69 100644 --- a/README.md +++ b/README.md @@ -839,7 +839,7 @@ Specifically, it manages the following: Challenges are deployed and managed through Git repositories, with configurations defined in challenge definition files. Use the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template) for challenge development. -Per default, the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) deployment templates use taints to control which nodes challenge instances are scheduled on. Therefore, the cluster must have at least one node with the taint `cluster.ctfpilot.com/node=scaler:PreferNoSchedule` if using Instanced challenges, to ensure challenge instances are properly scheduled. +Per default, the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) deployment templates use taints to control which nodes challenge instances are scheduled on. Therefore, the cluster must have at least one node with the taint `cluster.ctfpilot.com/node=scaler:PreferNoSchedule` if using Instanced challenges, to ensure challenge instances are properly scheduled and deployed. ### Challenge deployment From 5b145a8f0b7618e0ee4f9bff4c9ef3cf1f7fa4f5 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 15:52:19 +0100 Subject: [PATCH 127/148] Add overview of challenge deployment system and its components --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 1e41e69..8e1cf4d 100644 --- a/README.md +++ b/README.md @@ -845,6 +845,17 @@ Per default, the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/cha ![CTFp Challenge Deployment](./docs/attachments/architecture/challenge-deployment.svg) +The challenge deployment system, utilizes a combination of GitOps principles and dynamic instance management to efficiently deploy and manage CTF challenges. + +It is built to use [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template) for preparing the challenge definitions, and ArgoCD for deploying the challenge configurations to the Kubernetes cluster. +Here, ArgoCD continuously monitors the defined GitHub repository for changes, and automatically applies updates to the cluster. + +Static challanges are deploys as configurations for CTFd through [CTFd-manager](https://github.com/ctfpilot/ctfd-manager), while Shared challenges are deployed as single instances through ArgoCD. +Instanced challenges are managed through [KubeCTF](https://github.com/ctfpilot/kube-ctf), where ArgoCD deploys deployment templates to [KubeCTF](https://github.com/ctfpilot/kube-ctf). + +Container images can be stored in any container registry, as long as the Kubernetes cluster has access to pull the images. +Per default, pull secrets are configured for GitHub Container Registry, and are currently **not** configurable through the platform configuration. + ### Network #### Cluster networking From 606449679b7d50e4adde31d5b4c56187da3addbe Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 15:55:25 +0100 Subject: [PATCH 128/148] Update challenge deployment architecture section --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e1cf4d..fac49b2 100644 --- a/README.md +++ b/README.md @@ -850,11 +850,14 @@ The challenge deployment system, utilizes a combination of GitOps principles and It is built to use [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template) for preparing the challenge definitions, and ArgoCD for deploying the challenge configurations to the Kubernetes cluster. Here, ArgoCD continuously monitors the defined GitHub repository for changes, and automatically applies updates to the cluster. -Static challanges are deploys as configurations for CTFd through [CTFd-manager](https://github.com/ctfpilot/ctfd-manager), while Shared challenges are deployed as single instances through ArgoCD. +Static challanges are deployed as configurations for CTFd through [CTFd-manager](https://github.com/ctfpilot/ctfd-manager), while Shared challenges are deployed as single instances through ArgoCD. Instanced challenges are managed through [KubeCTF](https://github.com/ctfpilot/kube-ctf), where ArgoCD deploys deployment templates to [KubeCTF](https://github.com/ctfpilot/kube-ctf). Container images can be stored in any container registry, as long as the Kubernetes cluster has access to pull the images. Per default, pull secrets are configured for GitHub Container Registry, and are currently **not** configurable through the platform configuration. +Any additional pull secrets must be created manually in the cluster, and referenced in the challenge deployment configuration. + +For more information on how to develop challenges, see the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template). An example challenges repository can be found at [CTF Pilot's Challenges example repository](https://github.com/ctfpilot/challenges-example). ### Network From 3febb1d8c0f8c53fa6a2b51404c2e2f73095604b Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 16:01:35 +0100 Subject: [PATCH 129/148] Add cluster networking documentation --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index fac49b2..9c945a9 100644 --- a/README.md +++ b/README.md @@ -861,10 +861,32 @@ For more information on how to develop challenges, see the [CTF Pilot's Challeng ### Network +To visualize the network architecture of CTFp, the following diagrams provide an overview of both the cluster networking and challenge networking setups. + #### Cluster networking ![CTFp Cluster Networking Overview](./docs/attachments/architecture/cluster-network-architecture.svg) +CTFp requires three domains, as it configures different services under different domains: + +- **Management domain**: Used for accessing the management services, such as ArgoCD, Grafana, and Prometheus. + This domain should only be distributed to administrators. +- **Platform domain**: Used for accessing the CTFd scoreboard and related services. + This domain is distributed to participants for accessing the CTF platform. +- **CTF domain**: Used for accessing the challenges. + This domain is also distributed to participants for accessing the challenges. + +The platform does not require you to allocate the full top-level domain (TLD) for CTFp, as subdomains for each of the three domains can be configured. + +Management and Platform domains are configured to be proxied through Cloudflare, to take advantage of their CDN and DDoS protection services. +CTF domain is not proxied, as challenges often require direct access to the challenge instances. + +Domain management is built into the system, and DNS entries are therefore automatically created and managed through Cloudflare's API. + +Hetzner Cloud's Load Balancers are used to distribute incoming traffic to the Traefik ingress controllers deployed on each node in the cluster. +Within the cluster, Traefik handles routing of incoming requests to the appropriate services based on the configured ingress rules. +Network is shared between nodes using Hetzner Cloud's private networking, ensuring efficient and secure communication between cluster components. + #### Challenge networking ![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.svg) From 14a2a332e082a71f16054d4c2c86f69e5963e00a Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 16:01:49 +0100 Subject: [PATCH 130/148] Update getting help documentation --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9c945a9..c21905f 100644 --- a/README.md +++ b/README.md @@ -893,13 +893,14 @@ Network is shared between nodes using Hetzner Cloud's private networking, ensuri ## Getting help -The project is built and maintained by the CTF Pilot team, which is a community-driven effort. - If you need help or have questions regarding CTFp, you can reach out through the following channels: - **GitHub Issues**: You can open an issue in the [CTFp GitHub repository](https://github.com/ctfpilot/ctfp/issues) for bug reports, feature requests, or general questions. - **Discord**: Join the [CTF Pilot Discord server](https://discord.ctfpilot.com) to engage with the community, ask questions, and get support from other users and contributors. +*The project is delivered as-is, and we do not provide official support services. However, we encourage community engagement and collaboration to help each other out.* +*Contributors and maintainers may assist with questions and issues as time permits.* + ## Contributing We welcome contributions of all kinds, from **code** and **documentation** to **bug reports** and **feedback**! From 51befe361339e88411318546d6dbde206135f5a3 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 16:02:16 +0100 Subject: [PATCH 131/148] Correct formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c21905f..97782a7 100644 --- a/README.md +++ b/README.md @@ -854,7 +854,7 @@ Static challanges are deployed as configurations for CTFd through [CTFd-manager] Instanced challenges are managed through [KubeCTF](https://github.com/ctfpilot/kube-ctf), where ArgoCD deploys deployment templates to [KubeCTF](https://github.com/ctfpilot/kube-ctf). Container images can be stored in any container registry, as long as the Kubernetes cluster has access to pull the images. -Per default, pull secrets are configured for GitHub Container Registry, and are currently **not** configurable through the platform configuration. +Per default, pull secrets are configured for GitHub Container Registry, and are currently **not** configurable through the platform configuration. Any additional pull secrets must be created manually in the cluster, and referenced in the challenge deployment configuration. For more information on how to develop challenges, see the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template). An example challenges repository can be found at [CTF Pilot's Challenges example repository](https://github.com/ctfpilot/challenges-example). From c5c788e5448940a3cebe7e71a3ad10775642f027 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 16:03:32 +0100 Subject: [PATCH 132/148] Update cluster network architecture to show traefik as being scaled --- .../cluster-network-architecture.drawio | 14 ++++++++++---- .../cluster-network-architecture.png | Bin 112375 -> 113385 bytes .../cluster-network-architecture.svg | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/attachments/architecture/cluster-network-architecture.drawio b/docs/attachments/architecture/cluster-network-architecture.drawio index 7f8e00b..9c85d9f 100644 --- a/docs/attachments/architecture/cluster-network-architecture.drawio +++ b/docs/attachments/architecture/cluster-network-architecture.drawio @@ -1,6 +1,6 @@ - + @@ -159,9 +159,6 @@ - - - @@ -202,6 +199,15 @@ + + + + + + + + + diff --git a/docs/attachments/architecture/cluster-network-architecture.png b/docs/attachments/architecture/cluster-network-architecture.png index b7d5d51e44a10c5cc4c47f848117192eabeac1aa..8717a6ad2e0c5d8e4dd19cd764469cb76b5016d1 100644 GIT binary patch delta 67634 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 delta 67339 zcmZ_0by!s0_dX0b${;N&(jX1e<&YyF-5@Bb(lNwPlE(oBi!SLdDd`49B&E9q1f)T! zq4}M`c>H|d>%IJknK@_gwb#Day=rflcQ2vWFNHGVyni0X97-l|o0*@VnV;9pgIPcp z{F6^eFtyF5^sv|kJ3`|Sb^`z3dpZa&*VpH^xoP83bA`X1GG5^MQv7Kpm83>Iw|Y9=;lFgdr-vSN+0_X>r%wivg^R)1wspPI{m6mf9vkl zu#yOXZFp-u!AQIJi4Hsa5~;j&K~q<@R8Aq|eJ1SPxoXFZYOST%pw1$P5~ zEh$Zf0Kb+)!i94m|L>Zw$YKS``CW=HJRb~hfMa+r&97NyW58LLf@z{ z3s?{iKsx0eQP8WMP`Q7;{qtuI2MQ?|ER#;H(}(w0Ie&g+OTZFxrhh8Kh^z48FIIj( z1%He8(BKN*Os~SXoB_-KnU{VHvLpd{3JaIM&x4;PsFAU# zIys-mJZxEd;?cmCX)G!{t`Sbltp~04bC%b?3Sl{xe*lai|^jY9E;XBc% z`BF4r?MrcXif(i$$qZk&uu&E^^w}MXef6q?Y5_ael?K}U*NvD+Ub;%$zreg#%{QTU`X3%BzUFjP&6w>S*^y*zr#EtygN$Aguj}p ztH1XRX%}_-UE`Xe0OjS1mx+fN4T{N!@66%jm9wL!A-ibDHl5aGZ7CgfMd93}4wu35 ztth5!@0ez36`k+l^Y;LCew*#X)gtu7Ncq-~J1dp=r6V@jGG26ca-JcwpGu{lEbX&O0^2M@7Na^&l(l>bcLXF2b;+e?|ZPF;zIEX8*4H33qmNAy{!e9K_wdyR~9)xc?3?bYm^AY4 z%6Gr)OP9Bt8r4Ev+6RV z!+%kkbowJu%C4XkG~lrpq>RV>?YM~qz7{L$@<2`V&HCW249%k)bl4Mi3M zK?@{z&Mme)=dtJ>Te$0%Y6+i@cX__t`n!`9N%tbFeE)+44F7@@dYJ*FHxiq{bEklE zgB+qa9mY9RjPAq=&_0<#4NLItI2M|7+l~|pNs#1OR~}>~uou;BDcLsAn#FJd2H!Uk z^tHQBGjTPCoIG{7VG_?=1tXf;7w5`gM{A`9bSveH&uP1d+_EC`5J+lRe5O1>v-FWL zwcF^Uf^~_r<86*Tv;dxc@L@BV`siz(hNYPH6*<3=hkHl9L>+ zm$dITm^J@OF*tTymu@g=JW7ows>nA-s$+F6DTd%T%z7z6m?Z+FY1HRxEV5f_`P>kwo(<0|z;)|Dn_8YkCULH-F?56`^}}Q;r6bPY0!lCD)E93{ID@ zyV@PvJC=Eo^9s(vI1-Vo3_{wUAKT%tOxPCHs~|WwLxAIS#Uk3hk^Begx1^S_Jr_En zPODAX7yT@FfhFvrh2`0M-DG&Lc7ZcAaMXQesHdzimp0$OaVF20cbT?7p!glnn914U z2n^P`Gm{|MQt!E2!XOg>8LAW`cZXFOqV8Ay)B|?03i*T1Ua}@xbr?h=eXAw&zGTkq zD>oDtcJo}(`?<@rb(~Hi7FV5aGmxcf>$>KBG%>vgY^pER7Hd#RogMT!(D=vwFZ;N=kRE?E z(PB6B(d*SuOMU4EmO2-Y z*dJW^6?oB`fL>yYIY1l?YqW7(@DS5`h|QZ`nt=sCkKnaS|B(@H2{&t%cKn8o9Hc9S&cDI^%^p~p~Vg@a~=GdABr;qvR z0?DW}{r-nM6Lh6nCCq9QTf${!G%H1x{P3EQGO^#$rQ1UX;`DjJSxdGBjXRmF<&3df zEU+tsSuQr#PZJ0M(Vj>qy*G%sgA@vq{sQLfajL1#UH2WkiD3MNAWNL-xpdL?A<5b! z#S)#nVW;7E<>_M_aULn#!Qs?!N|W|B&Get-bWU2v)Zxb3%jncXu{fbCS-z{Z{YaXz zgd{$~d8lKtsoR+|?q1B|;vc927z z(SD6HDHRztHqAc zHn5iqqQ;K9c2oXo0!18GTIL00&ZG}|s)7A++MX)|Mf-|)o9@WCf=)0=Vl6X=XphkH z>021!oCPB)QATUY?=JjsB%*-v&J*4sM;IF7S@)fvdtuR^j zT)dPb2t*h$u7kss^Az3+*?}(zcSiylw6a1}pE76SJYbUiT*R6ZF_(yG*Bp3rZ6)5< z1Tt{R^vYCi8TEgPH$aA-_ELC5@g?bNYCfjZewaMH;`!E1ncVLX--DN+qOY%7_}Y++ z2TQEh1WUHodz!A%-Yo@I+Eq5Vcz9iQo##uv?yt7Pd~a(*d)8Oi7n0E7IePIF@rAVcn zViEDKiQAGZV?a%E`HqrnR5F)h2(>;(jI)v&M`B)5i^1XnlY8>Z#0}yp)^|LCS%;yg zp7;K^_)s2@dk?PQ7l;D^cRr{P5v<=TGSTuvZy+Wd8ta#HY!~vV)kxTIQFcf7Gb))| z18EcbxBW*x;##74vvBFrp%jrr{#LwA0kopbdGqrNY`rB!LlJfHsWUEO^3TnfIAIbW z6y+Vs$ENWM-hnvN$%4Z!Oj9yPbq~IFv-ch4N!zsM)>ly&xQukapXr*1b7oU9< zb17t^c&-(ttuzv@YRUBdd3SiITKVj4EAI^TgV^wvCfGx!yg;`xviyjiHLR&maqMXo z{6UQyghBot%29sZVq;uK3ppCoJNlYpNNClO{+Fn1KF5j`eYbYWL@|kP4BQEtPOr>f z;-+_NnZ>~;VyT^w8r_BtX7bX)fy7Xf-qR~%dpy1VLD8rshHL@nT@A0rME8n~Y->oT|`CIC;V7y~@_ICT*FNtj4g!&O0fi_7(DCl1M5^g zw~JLv?zH;vT(sm}dNd|c_3qZF#g9jNrF`>-;ct}R<<(R5Q!HZ}Y{D`l%0JOliN;8l zXEpSW{z?C1xfv5{M4E-`^WR4N_{0jN3p2BxsOicN^&!x0*iKd z{z^`vOGalEvVBRsUYvYFN1nLWb190JIgwM{T&cgno4HGiJ2JM*E$(7nsi$}Dfqvd#Py5B>Q?^^7%G zajGfQZ= zdMOg~4!MWfpUf7Mmuu+bSoy=PF?9_>`7(G42PB%?$mSJ}QwxRV{7}REJ@wt~cQ1H< zdB^kAX#ubICWJmT4CK2v_uHPq1))~yJrA$?dFKPV5<@-NL?yq%1~A*BzYKX#dY(&C zQ?}o*^rX!b0Kl)_#UowqaAx$wq z{|LPR(#fM67ww+HQ4kv&8@r$1K3^OHXXVM^kD|u{ekGVBptjCM(6|I@q66z%6iLoi zls~cLC?-z!i_`w|JotD@K1Ab}vjr3IMKJ$$>4xG#pGCtFh=QS^p)qyet%y3i$hwtW zU*l_}0id)bYB={Vo=*h6frCl?XHo_ho{(0}_yPE9`K8?@@^n6e*};lxTkm4DFFpg; zbmK~j@_#xx1ZR;22$t_O;bC$IVXr+qv8^`7^(3#2<$}(RgJgOQ20Ym?CyWI zN?IN)B`gI%{OLobP^Q7K7EIPDwmYc3ZfkNBw>f&h5%8@qQjmx7M;0O56~FS#6GJ*= zvC99>4DKGA2#R~b9_s`Y-5hH5{d3yg*D6###=Zv&Sx}Ye&PB*>zY>H84a=iQ_aE*3}r-F-4<+8Ad08g zV8B7%5)=_JU{GYauBGleShy1LkFL)Le*>o_ob7G9a6Ro`6=IAA3&l&SP{MUvFkJj_ zhvu)6e${y3CD7i98>AqN(?4c#{-0Nsl(7&bP>ffcrJ=g@Z$%9Yk|u}c#eju+1-`1u zHz)}C4YFUW&LEqi1Sf*0f||Ul6zuu$xH;n3`WWSa_Rj_WY=`|3`>#xJ`e4Ws*}3^P zM}gX^3chvn$LO$do|B{yo&N@>zlnjRS;{xF@BNv|1*xeY)lFN2%fam9?V?A5Xk@Wk+S(DDx3r}=E(f=Oy6uMkHAyUp zk~D~VMc%Xw0i2+qpwmy8wEVlFlfAt&J-uL(RyjUC-u+IS2&IO%B&h|&iF=f&s%4Iq zS{`-tPdOvgtSvOMl|Zer;XS7Zoy;CEdq~D=v0f)*1j*Qk$ih zQs%Dyi#z&P7{xjb-ZU$vy}8?2wb$_?bnGpq08U(Ps+UIm^XL@YoO4Y5-EL`2%@BHt zp_^ri-04KL5VzwtI-s#RR{}1~$Oyn;=*FOtVI17#cxoyo-%ID^xbr80bJKiBnljrDdJ#?iB+#=B;aR(f&!l&6t>ZV z7%#4NUeq@=P4D~;{kuo>Bw{!&P@MLgU`g=7oT`JV8eCZ!5EMvnXlPJXUl@GLXByCW zoS}+M{?}0H+aXJ0p#QK2ReDpx`vx(p03s+Lqqp2mu)uD;+z!Id(FS_ychv$5lYv!PF-|515+$6my{cKPzM-;reBYRX`iH$(e>cs1uL_-52iRX~h{jRED2 zS!>xZ@zwaZl2>;X^K3ebPolEBRa&0*D*uOAR08N++9@ipGXV zC%sRWW8t6uqWtb29hEqJu2J^%-)+Rgc>r!m8%}^7^jR3Z_{hR+H6nphzvarO6F{)U zHP-vT@xhIu|Aw)CZ!A737-ZhDca31(OLN88cKPK_ccR$HydZnI@6P`*3pOEeHJL20 z8q7#@9y%zOJIDz~EDS!B3bJ!)`#*Le2X6DI9Mku`-ufn}l?4!BGdwb~s!@@V$-0)U zDO}g5v*uxe2#@;SC@eZ8XFZtq3m)edP7ebmd*+p2OoWrgOs{Xx51byq>IGr7UYsm< z*D18XQ5mGXoNH9pTetg6 zp*lfNEtCdFM)b-6ADDokc6wS+|NSS>CrLhb_k&z%sv-~r?twZhh>0D=%)#oWY_}tH z{TF4J#AUwGi9OTdbo)refZm~2uDbHdcw-R){WJCAs?g~_u6gM#oIu&hs@YLv8I!Yt zO~5DL5v4&MNtU@%xn>r*+5@f~VY7&VA>d2oBagF1#K7k|Ln96*^N(X;V+RLk{``_3 zGu2O)nFlLp_EY)FJsTI99ggfRj4EG>FnQ|n=*N8=cWyrG!+%qh#vhk4i1NN>a8TpPiu5tJEIzVL*d8047H881CV9GB5p} z8qML-(+_ppA?iR!#qtzq`bvjoKbSlHj;Ei(Bf&j>v9ZZtQs7Av{Bnb zB(}!8I#!w;bc3&nMJ>UqM*?k_7XTKyDt8mepk_>Pmj6b80mjTt=g8J=|i;#l9)_d+g4lp?&oq?cC z$~*)tP1&6s?%Jm7mGYEDHk%Vs3bwfvFth5J_>|N5?DXw*i*3q!X$<`-1nhrypK^@> z8uMk%mE!7W$)C*;>;6^iFI2b1f{0AeWPqYp$d-okVs@7YTJ`rnTNPV^q2b>CT*$=! z&$*Ma8{_zlRirr<)&w_rKbY8W&C#8Wn)s$Ypz0WDpyblu$461)35rYvcIu5E0YKl^ zvk82UY~W;@dE$5j@PY{k-1t{BB^bAsNXYEONaBBLrPwZko}BCX@S`r;irM67e8ldf z-^J608*zzFv(L%L+rk)6GMV)~k?qVe`w~*C`K2mq1hBh;12SQ%k&EP8xH#2UM(f7D z?$4H2ti71CTYjXiZLMT%eeld-0$16js~ixT|GT#!o_Uuw!DwGZECFp3{+Gjh;nq?1^MD-d5vOQ~9o`-z!a%gz;8gR|4_dKIRY#Xeu zrwipFc4hF_LQD=qgqPDtbYqo2^2cPGdiplh57iGGcOS8feO1gP1aiyx8q0nfl@d#R zseEATi>7IGSl-K1SDLSUBSAfrGUBg1NjyosVcZ*bggUx|M*aE77jyJz;9XS3^6tp- zW@<K zvWl*X&V{b3`Q*dN%*I8U>OUWQPklq@=F$h<`+@+k78^O*hhjS$2)Y8#AxXoEX5k`Z zMAv4crR~{IG`}y^m;9PnL_uFJh2dlJe=y1#K0R?@DMDv0D6jk9eDI*95Eg3 zKQbvpB~S9STVdj`zeX5^=dwE0dN??^Kdp?rR$7EGt{s*%EdKH}!M#z+*7W1c=?;G9 zbn$?>UN&OAG7O=%#eXneOk4*@2u7XNkF2^VOI72yhVNVi&bjkff_Q`E#eH(m&63ya zzz*=BD_aUN;ak=&Tx6mbMhg-<)PfSz#|uq_Z`W>)XI<+=Y|0Q7_f1QVp`TdBWI_IV z3pjlu*2oZR${1CgW}VF0>E3KV^C@~l?`$Wqgfc)TJ^zU&rhY*$hK<+*s=p=G9$S0T z75EO+toMr5@-qK5@e~(GF9fx9#mZjTr_pCI^YgIFHxbooug*Stq9CNkT~<&dC^e{6 z(VL5`i4VJ0M`A6kTyCoI{ZZ6CX80UPzYGu?Ibi&f@3(2YzDbw`JF7f{sOUR?nRG_c z$U6WAAQw>IyW$bMP4bAdBH+`djIT4Dy%3Jy(c=&^d=(>71xb#?erW}|$|>uGPxK*e z9dj)cF6b0KzPN(XA?%7m{Y%cZdtEkMkjO{pVw zuicF0z42J`iFX1#CHJQ5Z@c=26^PGzdu_G@840AI4zVqSfgh-P->)Hvp)$JBcLG{9 zjm}{OTB3-og4)xH$aGU@Z2epuY^aMG7TzzA!FMsRohXCcRSVL(4{pHKt9<~*^QVyE zyaqfu!SfIRYAhIZuRdyQVK8DM~Qz^ZB{V~2PeJ=y{<=;C=!iJARo$YN3 zdw1w?es0+Z3vK*>EE^_X=#75*wF)hDxQY`Mbq-49f0nf@oHElwST6yHLYi z(+GQguFmE9&*OJb(x4g?f8jo_^l$=ESsF5&-+fO!kNOvXf@5k>;>3>>m4;laT9mcE z1y_dRJ^Woi2QjB0hzikdw0;fP$7x@^KuvjfHk}^>=U}`#^r7uu-+1}=9)+d*k^Mw? z8vmwnA~>t3Kiv(E(2dt7xu_{Grl(_^GpZZ!c)f$uZoywQ5kFcV$LFsNfm1#qv92K+P!}mrG#{8 zxVDmT)RxEA_pIJ;92|y3nBxxEv20@v`|v@XuoA#d-mmO+s|Q1=VTX932$rYWYFJlV*-w7f?$qVgLx4W-9wCMGFFnUU>V zIhP!%Z~j3X`20)RZ2K>F3XOgT^p20)Qm$!usfbt2h)?Gi;oX>jWTKQ0(|JZsr zAZTe{TeJSG?U!G1B>NzW8I%<44Kt=)2psz(4fq`}!~oAb|E%GOlN{^u=&O>G)qK#K z%b4#(S$TKf^SE(eWx(qiU2^aJIE>g=nArZocpOXBX!8C;cjFev(Sk%H+S1ypBA~-x z(c@j-7;W;|@K0Z#sJz0}28%$jU$WIPy}wb*a&V!+v&@8*tq2KPAqhLqjq@{wVEu0? z^26TAp*u$eHz-~r^!x;$68z!P>`x~vtFd;yp5vq`@Al7or`qbHZ^IbZs1_4!T^Z%a ziPgl08ho3{*8?61tk*`c9xQ}EILwwBWkrv7O1jUmC4c|6(7E2*wc}iQ6FQn_}a8(-*f*0%8Vo>=8uNJ>?Tl-J2Dgl zR}yEOIP+ABE8H|YyAGo@mD77(C-crd2d$(>8>@8ck|1pi2Lv_9H1#MPG2*sah9uev zsEPIkY?g+Yj2F3Y>MpiC`#E=9=l<2J=X5`&aX&aXX$q)4Dme?|@H$;!_N_i$6L85P zVuHG;Jk|Pz0ua9HGpQ8h^HZR!`wzKw-VQFp7w8ur`0Y!F*ZxhYlfWL+pv zVjll=ix?&*IQMuV-r>;vqg0%0>KGO4abwYaN8RVtl>YAH>}=znh31^yVK;%+j%}XC zN&SkRUWS6jgvem5xc|bOQ7Fj*lO3a<1c?2|c(UvriroG?W2B!>%o)08i}z*&k14J# zyQ(+NYXL3KqmwgY%xh1(u1WojiMl`^p>!bl-^o#W5^0<-z0lc|=Fb_K$}xvV zPV#4dfD}0PJk%<;y(pMe5s=U6uM08c8JxvKt`yMj6q z#g0zMAe*f*gT%$fc^iQ_UDf1$k>SYD#)%iX!P&>XijR54LKABadS|=zVhbV@6CH@d)+GOy`)F^;WPe5} zk#6Bl@ejhpUzvAfYtp$qz5J)pYtx8X&Z0x9TakMa4fVol31ybjYRpi;%*wPQl3m!e zg`iQkHCDj#aPb%g*Mc^4btr1J1o`U5JoUY3voY2wOiXDP?>;f*u5TE6c#Fcd_s)>e zn_s0+`W;A;uSZ0p=aih(6nY-_!Ftr>S?xT+L~ZfGMKWVvn?W|Q!&Q^Y2z99`xnL@X zc=xHPGw+6l)J7c_U|7!b(}(bgZfI1h_y)(+n>PUgmm2fHl0+5-rF~04ay#8GCirk2 zM_(7&=}$byLoRNXSGj55CAK3xVG+-jN&VXr#_-xaW~nI-ZnH5NDAcb-H`g)kR_FID zN}QfVoG$Ki5uawx(*)Z_MuhuIKk|UKS*ONQ07|8nJ@$xL^m-sroM25#mT3F3L$DgIghOq#G zR5Aj5kpTo0k1=%zv-T?k`EOFdO7iw1@01PJpJG7|y0bNy#ujO3)e^9xtgaF53~ffi zBSC9KiTJ!=L#fAhY*^+pVjsi^XC?^n@)tpkrY)CofOn?j21Zn25MTo^g+IOZ5gP zpU&CmyF^JY7U#|R7n~>)$^B4?67Rd6Tj5o1;lAfZGyvWX{gQ-k@Y&1XWeJp#><*YC z-ZWP#2yj7uqozW}&5SNVbl=_Y)VSVI*KDuoQ<%;D`cOVHBJD@H&ai(bt)M~Sm$KAM z+7G+$)od#0FLX7NQ1NLazle%;6!Up(nE@29RyZ&2vMd;$3~N!t+?&ldCabf&)QQTw zZ)nl_;sCNRyt5mR9_>O6501PLUA3EIIsQ@x5{y>A0&L_>4Fm)1#(Z1V&r>u$zxhy{ zYLaBm664;)V#d4Nf4>s8El^3UC3wvc|i79D0Q(ZIQ3~7+M4D*yZFCR&DS)a{MJt18JQy zgxn_@(u~a8+pkXwSa#bb^1Ww%eF4Iu1`j6e8V@xMVrIM6s_qZ+Yc-zBx{Z%5V6(pD zKy9!Q-AjxOl?}iOkfs+yNr@la*=wR!DsvA|X55?XzJ%r!_lvf8HQ#IY`0k;#_U6V^ zLA8Skr%Sp2fa3N@9FyaSX zE$&hS_jw{rrCT5N2wwOE<8z_Cg`bqC5=@lo()xTW!+`&zsOZS0RMBuz*7vQFN2B%X zqWb&41~wc2Sn|SZ0q`T}CQ4(pHQZSL`LaYbBphZAssWjHq)g zhuRk5+>N!W{>B4olEGp%c+!$aqWl7>1r=Ipj`|QsEIbDEnVfm5)XG<$^H~Ztq+~sr z!SWe38DV99C8NAM+S0bInt^Km%?+m&CGFVmC~}n!?bz_09F<^#uC-7B8PBN5WOnm7 zzHmkEWe>3axh-T{h?{csE8m43M!Mp>c3yaj zy#Lj{<4~DtIhx)qyURs|cllyWCdK^POEFb{{SX-2`JQQ(>xlCfli?<2H|)sMPH%=Pnxw z053WS=E=Bbs&}eI#&B7;w_kKV4g2XMX@@rGO(LXoDMkw;OzFJm$L4tQD`NB-5({24 zcbOaw8@Nq3wIYWVBO}1lGJr|$Q|cgTAam7G_a@olj1%k-6H8vDg02c5cb@*(&)W-G z*c^>ROIj2)Zu3nP15#864&`p+fX{G^cYZC6*C5;3q*3EZ5kkdUD{AYLg?jnz`3d{c zs^?uvcH*BbEJxQ+1{<4!XNF@3(l{`#9qhU2q}#2}w&vgV5)ed`t&jsM*2xm+_I*jZ ziX$$oo{6)=w*@{$dG!Yy6?+|Vs&1R6dEU5n)YP$u_utj|o*X`CZ17Q?##PD`Fj8N1 zSMnq*#H71Su~OeU6=n!u3Pt7LRkjTx07=T)i=vx*FUc@8G&- zx13`?+7TDewUD#y+c0;;pJw5 zyg*(7B2XDrcS%RnvxH0fRIfK4R!Gt4brW=TjHb$0rrrNtYCyFtweh2`D7#l3n%7ahj{pR?p){^(sf^Rsg|bPUxHdnNx$9tTt4t zICG+{sm9QNCg?o`*-{KfVzs!1E#8rT7l&<>Q{V06C|_*J>dHJi2E5h|7M)i_HPw(! zk&X;CpeNLM209E5zgRhGwHaWuo@f4gg@}W;Gx=(Tk2~P;(zn=#=gzNFK#>toE#NL0 zfMX-#)@}T`XP>-cw{Cp`;)5}h10K)B$x00q8GXmLIVUIf0q1Kp?_c;lDha$sZ4w(o}h{TayMoorE`qePA z`&}Ot#)675LBTiKnQDR~%eLy2v8!>xhCbk5hEPKyA|(P#uuopoIKjqj3Cp6!Znk6t1E zQ~==gjM(e3Kiiyb*XoH6Y$8cXV$TB-S^5bi5n__P^*J>uQlX8MA)-un{*cSdyp3-* zyu(l{Nz7e-5n6rPK*-&hp-n_uo?5{_6yYdLb?7$RBjj)-u_h~Du=EOTE@Gn zb{5mK`djFLGN8P!H`sQJ5WHd}vO7BU0m)LiS^XV3!6N0Yh^Ko!p+t$D)eMO?G6?o% zee+DkzW$rpEiqC6NDD`goV*_UJW{bIB1}t=T|NDv50XL)#yaLcX4=BYZs=-9l-dbh zyq0CZ)3|BB7OUB%nR0%)lnb&~aB#o#<*8ffyj9gvpSuqp7nze99!z-7TX>F$>) z`O$d_atoq*Mv2P0t%h1tzU{BP%YXWF?-i-eCVadBsO2H6U!T`lu&|Rq912B{6Ju6KB&+zui8Dw~X>EYXWr&$XJ$ZC~%`)$hr3rDNzHz|e{ z&SQcnR6eEGT1A1GV4usuo?C&q&dyd~(POxhBoTD>X|iCs%q0J9wGVjv2TUMs?d@4U zhkh)x9^i1}iKw;a2 zyHw#LoBsH5D$C)oEyjp)e^MkPg^l7Viws+ZzaqhF$We^EC!-B8n2=ztG!Y)+8%3G@ ze9}bRUaO;GH_^gHb1*2QrTSQBx7Z-SXs%tcRkgUupHjKI($@u+4)t360>yl^cF+ZwL8WJeZ+dYrRJ)R4N@k+2N!wzcP`%9WwAsjrIa^@m^S zYIaoY)QrsLXNE~5T`d$OlOHgKhZyu%-bavNsURLgYQ#kQtc<}bLGnr_RE zUNyxTS7sa`yj|XxO?@3{)25lKD(SirZsEw*{xdawJ_`Af)iPkOyX~%UH-A@`1)X=~ zgjNbYE|8W0v35Lf$>Cru4t1xgxr7Yr97(1cD`C|*rP_Eqil#aGQrVYgm_UWuHLn!k z=g_uXKGzPjiHf|j>VYiXoO^L#I4ujOa#W)pkjbWDLF}+&!vyA=ds&i2-4%lgm|+4g zg9L)4AbuESY02&Fp7Em6FgQpoAyZAnUzQcXxQ~|sb5fRG95XwU zI8fhm1zN^T@|}&9Sp2}oz_inQcRa7tZf&%NztZneiR3N6M>v;vM(!2WJ`iUl^fO{N zg4g4nA~ayklln|k>t^0YSmcLj3q`wId(!Z>l6KRdR}%?CojBbQ)f7AAz8ViK&4?p6km z`*o^{#;7Dc+e;FCa%Qx>a$8evtLZuBcs+P5f=xX#Vg-$lPajH*;+2Pa*v>U_)G#c) zT2OvqAKugDkNM95z*DMa$#UHB&n$AkT35KteOV&V_T9eu0@+WY-T{V=0MzWJ#%FP` ztVb%T8W;e7o1Q>@F!m^Q_8CLp%@hhsqy~o1B3V?aTazgJD5dj*ut5Q@`eY0mg)z$1 zC>My>AC~c1-9L;>wq?GhYl;in;&q(!zFz26oFl>s7`stsviZ&TDb@|_M&6J7ch|m; z?MM30jVFKM(6-G1zz$$Po>d4wh${l-`utGa<$kRQ=@piO?kQrf?z~P}SUWy>6G_eu zY)U`4E|^cZzY6e!kb)Kx0U^9##6^bPHK@`5m5sdH=6zPAfBKPIQX5SayK*BCn~%!fu+@UDvmJxNK31HGV2E323y+D+y>F#8CfN+`7lU3^m`T|59$*0$NW9RAK@9fWc$GuB9>gVyO&8>z-`fvII&>gh@>rz$TjU@YCKsl;y+!TJp)CFM9lPN77PZA9zc zQ^-ubROt>S=74yS_W~yZEN+>xIkbf`=$Zf~$;QurUH1X+3xMc`1U&f)$x=x&!>6^q>hUXT7- zuAQYO*luCV`8K&wuVyZCquZH|BP}7)ac7se%5&GIcBfytz+t+f!J=n-E$ub;Ru2^= zzr?nQDbo5V_+|5-#@DQ;0;+RG)qefq-sis?? zZw~sf?;c6XJ8{!hPNtgae1LU7)0qT}j@d>VF%2Hrg6Vl-4#$CK#6)%(7L}ma?}8`+ z8`Q{Vyusn&an8<|yLIGmiBaOvd4`(DV^_qi`+I*vy4(*j?x2+t?Tuis>&ZOXJi`dU`^6 zc<1AXS7e~AehwR;2Z@*gM^SjU{9e(1Deb&nmEG`zf+OqsCh-2E0X2iyT6IW)es$CL zp4y(%(=QQOnVfMHsp-`C4s}1{iqyJaB;pe7^-Lufi0T8>)SLB3!4iUJ>U;a?#N*g> z)9t6zv$zL)^<_&QdY`4ZJv=mrE6hR;(`!m*>*x&a#RzZAkG!6s11Zx(cK5?*fkCC4 z?vMAQM2dGPesDnzY?)!tyG2)Tf#>lo^B85G-=363>8Rp}Ou0l5Qt&scNgmAW3QQgx zA8en9cliO}mEu4tFBt*YJVX;^p|#d{c6v&H*R!p{A3kIUTb2Ot+Mhh244(k8af0mn z5UQ>2krXrWhNM03>%n63=IRgeo)J!}{)q9ty6g4bw0b6`a^gjqDvO1McG3BZ9;YhT zMx^w*`9v|Moml+pU1oWCdBx)cEh-V4x5+H30EX8;6S9<>%EHwyb@}zGT!N9Fx^MKn z-bi$@9)2EfVe0*DS}LkijSJ`_AY9fs{H{3NmBXlQU@CQX7jO%7$qA3nsbc1<3DyS5 zElOUSj&}pq7u)nHjZyQMwBfTr!fmItOtC4)s%t&>(3s02uk|AR1FFEjng6K|8kn+- z3uCa1mq~F(nB_5Rc~&L!KJrsdmYhniXJ~Eelc$>!^_l~ZRu<#))b$G6jXuPsc6@5& zu@IGo{$?5?G0a^(f91^36TCxjihunU@3IegG;a-a!)eNO#K>17N{sbYcWGw4L&Mm0 z9%DOo@t@C5)-0t2U^o3rkl}{TK$lPjazSZYxBk0XNW_B*yV&WZ@u4fcL+&S-TZYG? zU^nLaT5?{e6DQdJvortr0Eu_YD$YTP++_5jM6HLgQL>*URPzpeGWWR6I&PbI#`HNRl_r~{r(wqs&Pyn`G$>0yFWbdqd)G^-G27P zK;h1z)!8osJ%TV%W3K{p%KyjMTZcvUMSr7$C?EleIEi+D-MGipT}*-vCGq_5rCJn-*O&tDa2NFW#RVz;-S_Ex#RiaI*VT&6;} z)hoYMxbZlqe3H4QQm#wXjgG%NQ??6PX}oYUcRJb>^ULkJSbP7;P1FOMjzB9t8(!j` z&2jfd-_e|>5Tvcq!_+Yvv(iPWErrT$vcPPa9g#pog(1Z8!S+`*k@xPPu@48%XQ+OE zq)D!v@XbiPFIAf$!03;7(#4sCXeXNwyYp^dziJoD-IRI*Tvtb{V!GG|O3a!@R{`xw zzr?h`u%sdh4>LFw+1u!CB#iETV0N%fdJ*7+7 zT9vAD#N){EjS2i9&b`%-BEgurel9ug5ocyxe6gu(rofCL1iZi!aT<{9UcO|?X*O1z z+_}i@H88h&kC=3Vo<^A?l6c*ZfiiXx4J{egp}A|fvE|wa&Z;0G)RDX5tC{_~l?uIT`eXY6%2w`C1+F$4~>@O!noPTLt)%GgJX-P(E$v)iAe0{P? zW}v=rh=yp85*=@$y+qAWt}DPdh^f4M-#y+W?FzDL;r>`sR$ZMa#2&Q5*09wJiJfqT z_pUHkC9<(P$R`sKR9@@19vEhibeJl(T0|yH#`h7z$Jo2qfa{Nw*QH}oiQ!_8bQ^A| z+4q!2GEh5yb-Dbc|MOmVoPeK!B_hl`ij13iz%$$BB0-|?=;G(=s-wLfypc?)-s#f`8KarP?#?{(^f|(QDQmz&Xm=o zhul1Bme)09F+%M7fcf&+iaz`;7 zG6BlM4IZUKo3V^IIP8$_dZPc@@1!?bj8>#U_@}8L4ka4l>*aWI+uaIL~#?A|4!T>BB=_#Qf<7Y?aR1?Mxj_ zyDh)e3sWl1(Xevh4p<%$;dyPWBv9BGd@;qOs?vMPE^1sI)M z=b!PoW3syP06IbsC7P+Q$PuaPF}TLk!HJ-p*xepdPgv89Smzk~1GHrXm#OYNd;y_d znQ}z)3xv_=V*vE;WVK`ok78r}_)GYUW9^??Voq~7ZUYyrE&MfGjeCLyX9b!Gq<>BK zunlqr(n{cZ?8mC%;?O~>tE=7$Z#KnCzSb^pW(sJd1VnnJPQ9A0fc@CPB0{%uv1&=z zv%GC&tng}|9s5vi0WW2TN3wPEN$utxx}3kK)=L#{{_10~;g-Ec%}b&OrH-Zpq9wzb zw8^Uibsk9VFt>P@<;F~u%T9y2Q+c^qMZJ@CPk31cQo%lFmwFF#yz8P$M3ZZFy54On z8W9G@Hlp24iB{>&yp^`mxt7l;@-2=0L0&nNqW%BuNn{W)N9zKAz zj|}aKNaXbLT`+mw80SK29A{|$4eJrF81`N&FIhZKF)z_;NIy;OVEcZc8Q;{So!l}L&(xlG#T#O=lm?D3eay2y7`0I@RLvweBexsAr*_qS%B zcj(RoKDRY}$Y27Tkd~XEMxb}(<6bRgcq5~aug6hq#(nE&)z#?pEc*trv%C|mVwM{) zZG3Fx8ATGCy28#=5QQrWy@7cLXFxN_APs`iwu#RyG%Cn3IfT+9Ew&~;^H%-eeyYH| z7$|=^WmRtgM@n)fupGuX`klcorRG?>V{5V(xBqjxPseV3j0aZ8$Tdo*Z>##}SWf*u z)|IiL8&jaVxU9{THMBK}v_qFCkENSAD3ws-mTP;6KedZn8|G!fTFQ~s)Il*``Vpfs zd&@&PCW`ka;(GPG$4VGh-nMF(u?VW^Disd1{sCl+lee!|*p|JTj(4}(_Q#q|s+L75 zW(O%pGHvTU0*O!JF}*X}-*R`09)2v)C0xjI?((@u~A`!FxJH(rMCJKC?N28g@Q?r>K6td9}zMMQ_wCJ)8> z;p1=+kVtHYH=JNsW2^9S_(&cpVKMSe-meM^rtOBQl#a5anmZ@?z~-sa=hEp zOis_0@tV0%x4}xtWj#iW)+W-dlgQg`X-h7J6YE8|GD>m@%O{iaL&n;Vni|NTT?zW> zUA!UFtPCGo@VZ_u_Flbhk=)NfVl_im8+YeUVdfgp5BnjyuS6%;rA_;cCi>mU_c+_2Ov&*bjipgf zVp{QW2)nbIvvT60hM?bGEm#ng801wv9#|u#Fzt_C_Ip4VnD-dEl)agyPO|QyF;+!= zlOwi5Zv9D7PVtC;_T!cFrelV^-&-z`@c)ofIForUJ8NqXwLLKFWr_`XR_!JVwUd}=dZcU#X@m;)*->)#O^JH4n@O}Q z_***2d7(BRRG!(A4IRvJBH$F5(e86lL2+qY;wDHFU3mS)dL%(ZJe9#PvZ*_#{7 z+fhUCBaeTfjrOPQvNKW)TBWea+X{t}6($0c=T%ZFuv}x-d5KrB(;BrtXs0=Mw2310 z_l%eFon`TrPi`LI|NEZDcgyWXm~J8mxRZph&r~o$7nx&UGSj>GHGR?B6j0)?dqSz1^`#j_kW6Sl8w)L)uC5>b}0|O@&^<9@IE$!lS?BYVw$t0^NsrnzWY1WKU#3 zq0&%KR^OJmwh`7Ts>bX(%6nf&qHpWy_shY>$+cKie*2B(OLjm5O#m|7?vi(Lsc4Fo z&z``Y0P1(VZOt8l_kWM2qZ^mUicJ$kmsK~{6B<9c2|qYkk!wWbDPPCP-I(#(o2mBs zd~xpGpDE7b++0Rk51SV%>Fw6{Ad7wJ+p&MRY2+}xKb)&=<*e21QNUP!6hCq_BCms zNmL{~UeALM-~FkMvRFk)VwP3%B+;1P{eB5CoOoRVNvmBO>_#!sxuGj{Y8l(jiFu)2 zLjk!fA2Lb$dwp!3T)AD-@wd?dFA_S!IYPfm^+37ptu-0M*%f0Smd4UUSH4uKn$ZiL z|9Y%^eJzLjdEJJed!j-lY|7JKV-hEW*Q_k$woDbvN0i>6Hpf4!2`?Mv=-SK6XL~A> zY+z@v9T8`UvvRiiS|#Nhql#6fjF&#fNG329Y;YTLtn@OM)T0s6oaz{5WWe^4v1No3 z1niO3otc$!D%l5X8>yyTu8tACriDnNA0t(~fg|#HKJ5V7Od_2;ymx49)AW}9jl}MB z+pOn?iRX{1=-pPTk_oJ<-O*!i!~pG0UT%+BGv)LwRv($k6zFiliAwOf$RQk2ozmPU z-|bTLBmcnE`6doQ>xWNLeYnvyw*ZC2*34pZ=ytjivIVjipScw+N58$nz4t{s&W$s^YpgF*aKs|1@{ITl$t3SqQG3GX?0L)qJFGEQt0iQ?iX z-}SP^cDQGIYBnKjeU>QhcoPnLuWa7ECyjTq{dV=<$4WWkyM1;C_w_g8Jf22FJt&^l zSbg{`?wP+CJ$;z5=$>8rwzDgSr*Z%BDTQT2N`7+W(CXJMLv&6oA}-Y-xyIEXW>e@; zwqr|NrG(c=qU3&!MV9)p)~R@jL!|kRFzCy>4K5%_m>~3@lWM zC-`~3ykHUAg(TkJM`lCJ43mrVWTz*2!dDOhugoMt+;6C+$*eeweew7kYUpCBp)%@p z1G1Y-@X3?PKq5(N*mD!R1a}u42)=k4Bx2)-8!t<&y)E0QGhOsB8(l**jOBlxT+<^tjT0t`sh?dWpHbQf z!(MpcTF#8f0EBA?=pbRCYA+Ps)sZ^(c6KaFwrO9~x_l_B+Tv}3P)^7N47r|nhsC6h z;Q=aYiLD{uv-twWewS&c3NaC;plPLMGQ4n?4h3zT*8AMIu&cwy7*(4(#n|8`^-L=Y zQi}o{3tCYcNyvTL&riziu#|UcGF7`vX zyiAuI6)Z>8;}O-hbP%x_RpUm}IXQY{G+xeE!~Lv6*Wrm8+f*{ z8V1-1Va##^<9>1sPX^_6)Wa@EZX9hIgjglHD)BWFm4I0YU3~HZ@&b$Hfte1A*oZ<> zUMx{z;HT043$3KV;pb2SiHzt>8XJ|SY}?k?i&aOri+R6bb-C$V z*g=2?hhRbRTWw`X%#OxJB3b-}eeIFaNH$al|I(Nh$Slx<*B2Ze^Z= zGusz}hdg*e^wLq0x4&K2)@`b`-F@M6MQ@PT5$jrfGiewmB!<6%9k#_Foqzqd0ZFgH zAE1M5RNdo;Np(Vo^OhfSPfsrXPW zH8l!t!4cVG`0}<1Pc0=|+(L&PLwF_|E4Rc?UXZ}h!`&tVkds8|!i+@n(AV*2IYE`w zC{1L`qzZzS-(j~vBIu50h(~}{Fhlmx+Jft{MQ3x7WuRk*R?o`NAUWcG#D`^+#lR)BNo~*0GHZ3HIuZ{4(TY9{1EIm068>Fhxa}v(eXE!lzFt(Bt+U(_^ zmER!Fzf8}23wEqbw9XMaCX#S8t>hP|?hJ=^0_{QJ1)bUj3%|$#%`qM`DtVOvh?+vQ zlVu{!r6ZTSLosC~AMBQzp5-GB4XN%sT*(Z# zK-h1AbcqS%pQZb?;jaS&O$LdY2-Z@xvxpIh7>i+-GN#Dw$V4dObp=GWS3lqhR@@Wd zYCOX59#!W8wku{wE6G0osq3? zMoUgDr5dO|d9kn9SpQIRduWOKcHaUoizBN2i~aCrf@6@AHYr}49X_GL&&0O61suR- z^rR}00*JHUaTv|6>OKOs1AC`E$*_rr)DKmothP2sM%ElJdG$ zQVbzv7$%1EY9eP8PI^r+6k1=Lx@=Q>z5%KUx#zlt8G!%TMUkZg55|J;$NyjMV*NF^ z(qyq8&4TQO+K!;C>__Og9izzl%My=k00y+I*H{b$yUl1Ovrdi=&x9kq5E4g`H;;Uu z{_!LDJWrX9gS(9P!CyKLPK6K44d9>6o)T=Ax8)5hILV87BQGHYM~jTc44D+}H~AbJ zmI!gmJ~}ZJ;7?q7q$40E-nSK-o#?$gL7CYNSZa{K4f(rdjbBM% z)i%RLI1lG1lS%Y%$ZvxeGM&>PCPc8{8WiQA?tF;5(HV6XN*C=xz^SS&%N>n;3{$Kz z>J5|>CH0%zC-afM_~%YLHt)9}^&_DzC?5t=D(NCt`hN-Mq)Wl)G$EuYynb`c{0J^N^az^5ERzb+Ud!?A;tf#h40>Wp(9nlS-S|764x z_JuOgMa6rJ%VYgI=GD@3AYK|TycIPLw9-|3@uNGgGOz{! z{Xv6~ksTn0vJTV~2lMiJf!@CZ6rrmnp?3<$heyuh!%@kp_2g{o{K%Tk^u%G39fiy2 zLR>SK9}J$U$G`AdgTQT*q$=blY)GkzupuMsYM10M z!b?03HnQ#)nskJqdlRCnWQ`t$Gj~0EM=$r@R!IdxDgdy*Z71dbw~&T!KEQ^c12`v4 zWo2cV6I6_iesepvQ&CYB@kIv>-CKVGkC{;JkQL0>v{(%crZ%xw3xU+?bDrM6)*-+x z0*Gy)LV|*FgFeA9sydv=x7Py3|D;`t=}SaZyewia0WL}8C(z6DI^ZT|sDk@w!KASa z!slxb>~bJTv_gX8G( z13BbfmU0T__HoZsixU#f!w7w<1BiA_X#lD7ur?tA`I!f;fDjjrH)%nKu4KpSt2k2cycty*NSG zA>KV%A&B9;gobT!fsZ!DLtr%gvXR<9G6k}yrsujVARn;}$3@frhl@E8i+^tI);rw^ zC;Pm99}Le?em#KdXn1=S@*p7z#VVk%~B&umy<7Uby1OPLnYhZ{dADF!T)DaTrvWnhp<_ z=Ct4$uORg~m{4}G&84fS-UAdQT-OEbfa^LX)6pzmJNh4;cN0+}ekYemZJs@myW4uBseYJ6Ek45R zex~eSeL2KfuEb%YogGKiB$KfcS{;l~AV=%VwkbkM1bQ{Kqc7y&Ffv z{v?YTjO9@rv7~49x3aMD_m^Tjg2y`i9B7o%CwkW0x9ICYB}W=GiZV4k`Y!l0`&>Bm z^P4D`XD+kzHpMq_xQ!+p_9la>6lBkAL=CnP7SqvPSo)L67dG@eo!eys;HS&M8s9Mq z5K)P8Px#ev^OYfXeBJi$7HZfy4B*|CGyd`gi~s!gIp1qA+BVX-PV{?0+hj<^;==79 z)po+}lsOB(XGMgy(eGgw{*cqRTplqosfx5VK}DS5^v~gK!iT$hCkATmvlf2}J@dS3 zTw?5W^H7k@C@M0+G2VRgsOt8WLAdUb7_R0LDcGs+D38Oz1bVo`I5!hnVnFjdMYbo| zwzoIbtz+a5NcH5l$|AGJD~Ins$mD~Sf8<{1;KDD#k<&RlCW7HR2kI=qwESvG;{P12 zvIMzxB!g5v-pVS}HmQ)15SMgymZ60|U}OZx}Ex4hrT zNLba?ygw~*1NXw3fGgYvzh?3<_y79e!v7K|J)Dt<7jO@G#eoB{C$d#I*ZXm*mO^m+ zAaB7v*`2myE5A=#fM1Y@YW8WhI}rs%MK{yk?Xv1bf%R>f2!r(ivoOMdi1Ud=f?f=ys z&FcoZl;Ba-y#Fd4fgyMVzwxHX6WBU);k{Akig)*|P`78`e6;@Ov&X+X0t*=M4!PH7 z{to=A3@?`i$eeuv#t>>EfBDSYL-ZQgV1pr)kAe+`fY&z{OcJjQ*W2^pv0omMFT=+> zxR+&X1oYtmd|978DN3j7``wq?buaHsIZ^WljA{{1oReE298>oh%ZZC*+1 z!#^uz^fQTuAE!K+&Hb;FXU4>zXqzR0gF0&|__-oH372qB|NZ|0d{DsY&)0ta;Otx` z51#zmIpaY+0V#xGGX0_S^xqdj=oc9kdCx=|EaKmfm|Z2 zlt&QyW|`c3c%B6wHqczK&Vt}G@O|f@zfy7U4UFkQlX)1Gq~NDpz;^_$hwVsm&|jt} zAC}Kh*IE#nw3#Yw&@AVbM8Grt2h13Jf6bolY_~ZZf&V&s;otk}71lNAu-+$iM-ogxMAl(Q+lQ)Lg2v2Z1uzqBqvKO1N?;E?p0Lkc_ zjXjWb2e$iKv!(SVfsKEwWvq#n$w#*;P${ftWxUchvgbWt#!fsL!Iva374woU+q@L8 zalh8O%C7_O!C-{tUVx8o-%))3A^OvoH)H^B>1ZTSoyaF@ZAw0Es#2(i^78=}I z5vdT6^TqR>C$tzU_4({?e}AFv)j4xxtS^WOEXSn-Eqw5T2>80uadR=a0mN@f%vaq1 zND%`dsr5JaKN|&$4(l0nwkNNArF9*0EutdnjGr}JIhuYBtpdqacK^$Q)q1BJMY;_s zL=FI9MHA`i`c>;AwynZ`@QbssWa0@`n>`U3BwBhcCL|IQBDPhX5Y2f2Zn-^OMJ5Jq&zj2NT`GiI*cpKZbSm<7=3jvp=tt+hZ zo|2Ec2kv^itXNEW>KB>3&}IyleWeDRLp$uK7S8LiY-s@6_SsGNw)pe+ru?AIwr{n% zR1*a<90>IuvC`+X4zol50oZQ2xs<_TuF~+?Ba`d7CfGgyxfT?oMhF|L-q9Xcf|#|N zNPOS<PFxRSSicF{06SPW3)`a%!|ECt$O|&*v0OUN(0eh z@RP){>>N@Xg@=QgO2U?%QEmqouVBXwbjJ1dC_$6*E?XPsap4?~)8mhP(EO}w3v3%& z%hP)}t69ug%#FJGtw zs_sl=x$;qwOk1Jma}E@daK0e1AQdkzxK2O*)1vUS9FRoJSE4$q9rP+(a@2Zm-xr3r4;%RXl~5bWDuad) zE;gMhrxghLt1haksR_IMRNWE}Bj>+pf`6qN&?{rOyzN?5=i{H^`Dt%MGAZCYN5em z#UG|)W=<8T1WxcK{IBqQ#bHmQ{xGmD=!KPSJ%G9uV&$yiosb+pI=$A& zfZ5l+9M{+^qaqVF>e|c)iWHbbP*&h3qUUuuc+9=KZ0~iPaX}2? zL4q>&>UxX8mC-1a6MC+?(bRAw&7cp5Psj`B z2;a$F`#%g^jp@87IM6c%NMFuKd}aNcMLpR*KJDEXOI9FXo#&oB77uGk4?$ESj+Iyl z>)|ri$;nb=Sk(!_d4@Y63fA#-t0LvNkUcs->9Kt#51z6bob6F|Fp|U$nl&8^_uPL_ z0yRkGCZ!(1wL(FHy(w!E-*T`|<#;x_IA!$+ZaHK%Qk*ts0VJ(eOPW!W=^MCMTCHB; zmg0~9dzp~BX+wWE9V*-d1%$13LneiZaz6LV&h1D1KbfZ5RKGK7v){M*R!$2{1pb#oW7XcMsO7G8CM8+4TPTdnko|-m$kRSe(NT_HFG2#So>IYFkKyTT*^{U)G!k&77i5koEeA@pF zZ%*5>>DbNEQjNmQ3o4V!P$_{%TwZ!K{i(k(vuufEu}N5TFCsR=*zE>HfVO(7Xw=y6 zfX^bLB^x%D8d|p|e-!?HFxl-oORPy7E;=*|iv|lCtxAG!H3Eoa9v&8Ta=< zw&QnEG6+J#k#+|w#pT&iiC^x?%T=jkVZR{Ve$5hDsL$?HLI$M_FvL~aV;gEBGZwI< z_pY1ce2D(N*14GCwZhxMmS5>VtE%& z1vrRJf!*QW>UjSe!Hzoe0*#3%s6td!RyJ^o+SuH_VAb-9P}m!bhFBE9uC@WC_Q#U2 zMlQor?}rmW@1|6sPkbl_upVaWTq^)Eq^!n%OPJ!tn<0he&lD|J6Of1LJdrM6 zs{oAsmQm?L-wsLd@2K*@5^UY*J3QY~QrPG5G0N+MljjrzhEa)RwE?tpQZ|#4{|h#G^-S zQ3iYXdSSa-cu&ws3#wnM zn;}D`l9qNdGc2|ZM-E>U3p9*IrEo917L6r@PFS=DNdK*HMhf5T=tD@8y7C74`=N&@ z`z4(Eyu3WuQGc-5E3Nxh_t_MDCEHDE&^&GaF)m9grClYv&Oqqkgf}v2B8_)#b+pLt zI#nWy;{$%=Xm=%<_nUVom1{^|zoB}VwjsPZe+&J|#vZ-yo~-m>J#R)hIv@Sroo0?P zS)q3EYf3YPW7AOG6*`0R?IKD@z}wpYV^Pu>nt8T7t%S2+_)qO(vfWVBEM20@y$Uw* z63fKs4WAx0NZmGre*NBtP<}#;gU9cDbwn5dMV$hsQWDR++2U4m`Xw;5o^JrAqG9ZL z@}ijE4($RqrKeX_^it0u5X*>NGv&Z|PsA1f5~|A1B6y^F{?HM!_PnxYST4BxLSk*b zW~_$z&;N;#mMUa5?X8&l4>f>In?E_+*o%GTVcKO~Kuv_QT5|p9wmw;y-Q}J;mcQuk z~8iV9r z#@Z>8-jn);6Bj_nF?oZ{R$C*BXpb}KQ1@$G+VYUu$!6@-T3Q+}+b^8^DD2CKb(N4q z6t9AxWFm8l-C!(*w=)#s<2)MmD|BUL%&?=D{HUrr!y}r_McCPXH;0Zj`g67}=lXF@ zGMfSRYd1JAffFis;s5P~8ZEArLYX#JLY-RDbW&#<`KTdO=%n*m)6Z<+%q&V~?R50` zfyn4$d~$IHHR)i9?P?uyw%44tPEE!h57a=iBB8xryFlBbQ!~AML>9_B(U5t#`P%c> z=0=iPcjCJ`D6}-b*{^WeAvftqnAGg4>cFh(BbBka9TLw!Rcg#?V%v^~2XlXs)Mr3y zwmojGN+0g2!NSYWpjG`86!nu5q6Wvq9lJQo=sbpFf(6sC{G77U%Q>p$(8T7?-lSE? zYN@I?%M^=@P!r}lSh2HKs&-?^Ky^f8eN=-Q55jnn4Xr;X^5&8pGl=sm+Grg&lLDQ` zbM^Kv5aK?97u20l_*}JJ0J9a*1mn^&p@Qh|zF@giGHCxKE+q#f20m7Sr%SF-W}g#6@TFC!ZJwn*YWITK zd5`0|u}UANfo_%tlHB6mw1G{NDsSc=ZXWb1B4W?&5E(cJ$n(0)I(g{#~f?ug{|cI zl_j>rtS-RST&XV}QK#`cn9S7K@XaR+V8RcuvN?MEz>j9Lh)j7j3vjEhmcGqSc{Jk* zZEw@u{O)4W-m<1t5vG&hr%PipNJ7wSn-`~ntq0xNt{Yy}MtL->dcKsb0r2f6-X+5W zj=q=$t^pJuf^Xcn?TPaU6y@bLIiC)ITPjEer&QbYR>^ew$=T z%#I9%UCuEYx3aKUeU(TN|8oh7C}u9eC*Jw$Z7>$=F2SaVG+XjB*T!WF{RrH(7<9z58|l-;bxNMV#7C^_IeOx|w6!PRAk>4741 zu3=5$ska39w^j6I?xmF$W=@`?E61{K#V72yjz>}IY}8*5Q&b)gRym=4yv@X)A)F1kRKzwVR-u|PW1&2db? zK!ZeDLJ?!2GNpQnY=aNFXe`9DXxVsE^r9=L`VN!&OOP?C#KU~@TM5CievQbK#G8ae z-bS=#wIG>Y1AqWOqzGO`zewQNaOZ9+UybFP$Y(#OgLVo&789v0dt?Ie{S;uAual+h zzoM(I(3nvw-pesF)*H!UR50B>Y!-(nFrXx#j zVD_qRxyA>b+TKYjO~n#V3wV}?{KG`+P$PO;G|QS3;7ste{QlTMW?I0H{`LU9w(=C?5HJ$Ib9U>qW*Ua zkx#9#hZndyswH&N0_vmN&WPS2YVx^O!k3Oe--s@rv>T+Wv1F;ZwbNb~GbxwFE=>3H zz8%a8l!O38Uf5y#IVM^U%Mh(wTs1Sfv-(u))Dc zMegs#rYmf$-+ytB%Fs{Xk>gl?|6Hputs>0rK>}(_0GI0`uLweq%uJ~^q(QeM-zrOV z+X?&8&PSt*>+M~Iwq*GubQO{JE6!@8$9hVDYIz`M6I&*Ma}$!x1CfSJTM7e$sFyF& zlevs)%WZU(K%i2PiyS=DlCiYQu}Xe_wER1meNi~qhr9|0+R$>+T`Gd-Wk{A}5H*?e zRb!?^R?pl_b!x3NwpT{FfW#Z}0^%+|d(@Cj8lF(Wy*y*P>aP|%DMae0jbi^LX1kcB6%(ij48K^f@nM+%8k!Egezp(eEF;(xU;-s$jys z&L)F-vV9e)5cyG_#q&^>p7LI<=Ucz3Pz zv#O^m9#zz|5^ItL>LD8tsxw6L146p?@sCik-s$d3(X|-H=A+c!Z?!9Sh&PjC$yA2v zw^@}mvC;ECKNkCv#2LulF9k93}zV+N5uY+UPSRIo&XS^;8^hjn#agOR-+g& z8Bpc*fC_l>Gp$_Kb;wmI2VJ0+qf*^PT$KSx*_n__kKlk+7OHWnE?f>IVskZF9pTVo z&DAWaze!dc0g&w8<4JXeqj&~{Hk3isl!ie+5@`%xvx6MiZ5R~vjz$#DM8L66vOxW) zNRFX&P#7F8u$bBRrTun{UbU5@4NCxeQI}8cJsYd7^aoWY;1!rqP;y+TWZED2YUS72E^fv`atwTPKYiN$)EJ#L7R(J@CPd=aBh{O2w&yt2R zcuP;V;dmU@T#*ERG#XAC(#)p;c`=Z(2=~oS09K74Or^qo$0`j)MYfYL(7?$&I7h8S z<%~^u1B?X}l?AyWHeTNao2C_oiePf7i<%$&KsXE}Oy&Q^Ghyi{@RKqcL(v#soCQX; zR3O>#9_blzSEJ^u3K?sE4HTxxn!VO)UMyQ2kdQ@&bn zxuXq`wL3Z>j>NzJM9kH6TbxuK}l&*{KZ4VO$U>m6@Ih zy)vgRO|Qe_xQ5BBGwH6~AvBX1bxf3CwRXq*{2%18#g0A(ZW#e3_{A%G={sjomB0B% zhR%ZfsS)efFczHa?=n^k^U69v4yVQnbpwG5YEfGEQrp?s1SYQEgdwo0I$#R4I3kwd zoFbT-s95OSq`7kH;8}QP6nT1rwz8Ew@`>lS3~D-f{N< z$RyIKwBL4-h*JR!zk>nEe;}Lx1I>uEvfjVBe^zJ88g#k?E)EJF)gnW`2weFc3QkxJ zO%4IszSaG|18i(Kn{{sz+9W0ln1hOTj|8tKz-`rm)w(%?65ztdcdw@M#t{PQJ*x{& z?K{>_<1y9*oHUp(JNJ+wZvlwxdq$hSNoO9P?It;77Sh@(&)UL=hX-{3-UCtc_lcCQ zuCD$9jCkk$2qDlTHrN4>f7Xy-6z>!0V{|t?x~a2K+&RgALHD*s2OBFmPM6vaW&dfs z_IZyS(1LpKg<5s%qTfD=VPlQ^(~;*qNIB&2>*-H}rle;zF1$9STY&jJzgN5DKCl*< zcoQhDZ{s_`(Tvsprf(!?1HS;nwA>kqo%ac_(~9wRoXnr<*L%M{mn&a8 z%UgTzslx1EMLx^llPQZ62dva)9aA+H4j}ny9(4vO`yYt1Cfqr)pi?fMn6$*3@$b?o zZ=IyV*gG)thVVMLet{J%I(&FMrBm1Nu-njz?yhjeD4e2P=-d;)A2R^@XT|gFufZ9p z2weXC$M310S?*0>dgQI#H>#FERiN<8VCs}w?+$f^!cR4qj8hh|?quSP_=|cB%Ur(+ z1AJ1W@FJ)&nNidO7vTRrhK&!@<0gL$8wBJ8gB>9l?Rdng#fXM0dK3bEe6-smQrMF{ zCXC_(`jgqN9Um`VKWi{p0f44LsKeQ*WIg^r@Q$ico235(?-*G0MG$0Yuz2}H=fOe_ z5Gy(ZI^BeGx_SCJHUzlt@S!&Olh*ts8@P>}AA)eA=90pAN6kGUl^^q6PZbf6P>jCGjQ-H(1x#d%+8KoR z(!ni9yRu?cJPjgWIXIzn+dmEox}d}YRv}ZEr1vv0j`NBOP}KhexI3qpQC7wWSq`&N z0W_H)3#e<%Vhv|VzjwFeiHB{(s>e zAHzyBr!1*HDray-4HGb*c|4He*%kz5n*KXp%PkDkQ0abXh=6uz$P``aMBX-Fp?j6V z7Vg@eI_(7(WANpke&{#;YpFD5r*AT@yPZK| z<8Q>9r^94=nAg;Ls12cjBs|~Fa+F^_^yP_Wto%bjX51bBOf-n91yy1%!N4W36aQT| zpb0i)1n+i$2xkOQ_J8h~Let_N^5Mx53st?HT6+ux+!gOPjqDY+Y3)b7+2D^H+(|PIjG+PQJra??0g(r z&!_7N!XreO0-c85XDxCq-?-#wpZ^Hi1(%rda?37zP{FkVgtI?4`2b>#J;*f~Ji#P4 zIRB9M1cEwiK#!~nu&*ZIe9GM6>>f(*g#`?2(1J^=ovmD3!qh9c9n7RzY^)RVw*S@+ z%*UK>sH#-x)wH4!VAxut%$xEx=EH%iF|&8PT0gmMbSBRds0P4`iCHkkSU$du)T{TluFJq@Qx7&V3)UZM8iT;ny`L<_rmzjK z37dSJoD_Q6I$Bl+SaFBo61CP>Y-4XB2pqqE8U=kh@`Y20wVdZ&3!-5`~BbdI<{kooU`}ZYt5e5 zobxve02}vMokn9;R?M@DR~8pYl{5n)TF=(8t%JyC)F zrcZiTHfA=yw?o?#Bx1}Vs@kj1wbt*Dtbi^Hg*3h!`K;j|_m{fWM`G7=A+t|3C8~U_ zQ+&3Rk#YG}5Fh}-WcCnb753Oav+95VEK?N2c|0Kp`g&!ZL9cwiJYD~`JC8@TbFO>v z&or6Owt1A4RB8BocfWpk5oKsY{2{o~rFwb@j)tb4A7UDx?1z9?6ooK9&Y*|E9Y?}A zvja|Dcd^BKdqCOcvzFR2>l*+R;(N5U_KK(XMd}MX<%kKN_3ZWa7U^cawh5kh^N+K| zgm3XdW;^L(5Lz!CJC$N;K- zmvClB*B69>6>zaav1pBV;-aWEskn5Ga7>O04ckcM&UE|UyZ)vjoH_3$5C3_GsZ=#S zb=~tVt)ySZj^yRk9##}16u8?keHs9b)e?ge^IBqQ%RxFGF6}neITsIA?Vj$MN8LN1 zYUjY#tB*2K`{ht1EYfT$x7-0qjw8AEUaYDuuh ziG_rOx0h1yb+t289v`3Bfu^hq3dGcuZW7#vwI4yN4O_U_TDnDc@IDaxv=l9SZ7t?{ z4<1;0dP1IJ;kFOGXFzD_?D9|h-aYx~t4LO&X;4NKDuW8k|0ejwO%xIXN#SWYpT-J# zfgSCD=4P|gY^-i&b9eq%<*V~z7T5FyhSCs`^QoR(XA@5;&~y(f)Lavzbg~BwBGJuhc2<+;ZO*vBCBh zEdS->ky!wTN+iuHm$!@nP0fNYdiCRux*PO9xKeRPqcq4us`Osk8laY7sYZhEwge$& z4i@@oXz)5&ZysqbEpQev?t93?K#~HfkQu_@6C#5X7fY+uOj4C>Um|R-U?l*tb9NRX zzR`r$gjEnon7kDVcBz;#B8MGTM9GSTUm1w}&^*BVu){zST!iAC|M66C;YmmEBbcL* z^?sEsY#`SV<;7M*i^c%!!1x;o8`x<1FQltb9^71;ipP;h43#u2vI9uyx$2hBVQ`HI z7!L+^i-SpB8YrL{_|P1~5@Y|Z2vfCq*bF-$C}`s_EY&WWMDH3zc|B?U0h$2Vx5pa!h!r8NBZ5m)?Ua`27c!|(gE zD*gsXFmd|*8GzhwG;R z0TR)Icf=Vruz>pn0%D8;Pbj!!_W5lp0EGuD!TVJNg%hr*!l%lD7D4q7VIZFa4agkc zDF@F^MyA8}RVxhU`3yUO4hf~E|9s zdUg7MFDHHwDD4GXC5tn*D-ROiq?r-;*> zZV+b0H9ciPFhSWq2R@aS6q;Qa7-SE&d!R6UH-VmBu?M=ChW@r6D|T5#J1QbA*~5Ff z38m+&xp650@piF4vV0F4EA!9C34=cpT1M8iRwgL z!6L`i*5HeLPyuuzJ8mC5n*?%TlGRO?8k-fHD8J@En!nXKeqQKa6}a67 z<77CQUSuaQ=b0ap^B9pTe-&t!43;C5oQgY`Y-64m`?oL#`WNG0IgaZD2>COc%ht0} zW8w*5|7GY9pH@ieU5#+IVgM-x%6WUzJEHj7?7GA7qg)A-KHrI113jM@r|p4=b?)PD zx&d=7?wytXN+6a#OXpZ8vBuW97G4_&iLxCQ^JS7cD{sST&T@0>a*KP|Ru&U*t|y&$ z!iTunS-QQ(Rs(EG;>?Egbo*;yA|+0;`4Nb2@j^^A^)ADjHnYIsFx=afYyn_C<1yjy zxiKa93ChC{t$!_sus&#BEii19bMWse%~GE(>8T7|D!LwA@?YlUQeV9N{Y0VoRZ*)T zNfRhBh6d+lH$Q_hONVOXGpNDwx4_Je-yLc>q9Zv*O&zJ+hL7uPL<|6K=<}B^HU#a* z1OJH?++uxqJ#R{_9292K<(4GZmH*q|o$TEU%xfqpT>Q1nA9)DgggqU}OhXk8y&Dz@bDen{U;bUrNAx?qv~hyueA{Chw#k7PK(!*2f1nT9V2&o9Fj z1;)Jw{~3_b$QZGjaSdPHr7FmkcRS|?g$e9%`Au>dlM#JJ+ZI3M(iBILpqZClN^_r)bndFu02ZKQ1CVtGT9;|a`d3_*vCW3YMPkU zlDzN$f$DCVcKUcY-{N@xs83G4#E#Y2T{MalcnIFse{0J!(-FxdNO^5oZw z(@IZ_4a-K{q0)EBKl2aJoOn1(G_oCvr=|<;oI@a3dMVH)3ybT|tT3Mb^G~7Ow6Gu> zyFQsapuph)3}}V?9Fj44jDR|ONu=bTob)pK!EK?np_BP_Dci`rU%>ZEpZM9b=L`co zRF|hVKvo_|#?e^O<(DG6w|w6^fWg6S{_kTb1PZjs5?+@IL{|Nx$Vosl-^FN6Q7{I^6AV?ZCEEicagJYAs|T}ytjvzBJw2ylbr z?~rv26ShyAGRWPy%N&7wRFXk4pwA+v0$gDGD005z-^Fr&-IR9T_R?Oe5y35f`&$ed zJN;8Il2-lx{B!~dbzB>Ff?y@$mql`**I2@bY5ryi2J-3{<=Eko|L@}xmIO(kfZZv( ziQU=?WYz^y!(GsmVC`q!s1$2y$VfOL-Oqc%9=Z#lipa+3C)@#r>{aze(o`;J);XH{ z*%h?XoC6lHaW*}n#C}k~K~io#Xth>SWOUg-n;*jiL8p_sjbFs}KKxeGh=gYVysneq z+S*ccOMF`X{^-qNEUo06@Ndf}&VnOANJYDrTu)(+WPs`P(~QbDJEC~^l~)hgzAkh- z?4o5+jZ+9$MdP40YyUH0@fUIZUAr-QxEF>RNCV*te=Puwi(Zf5ixGtp&^Y)(yA5f3 z^63CE*}l4x{cPcvaT=36Tc$w1Z(V z;tAYJy?vhf+b@3aeTyWs{*qGAD>NN6duUIjW;8k=PM%aFa5BiOzROOs4)Q-55$;zT zx35h!Qdod}5|Wn#c>{;3xYwEvhexYX$;wSpK!b?fom7Z%p75F0x?))&-R zoKGDc9eMQobe%3YQ?ry7Sa?*&Ye{%0$JGeZz@QAaRyQ?v%~;aS)019l=ggygFO$HM z7@57Ml~}Jq17;{UxyY5{zBwOg3YLV+_y(+va};mp+FvMSK$!MW|P&GvYq z;=S!}wMpN+zP?tUB4q>S&l~9C$z(L)rp~>IJB~B_ct6ZRyyUWB)zox1H-C*ab8YEB z^p?gVp7}|LJx7A7B8_JRFIanFWqr6#MRqnB~yC-T8KOlTR479WJf z;WX4E(R)NUD}_~8Ky41=V;&pWuFE?82*^O8`sIXyWrgo z5X4S%!e>A3wr`FuLnqgfp)nW5VLg|sP$5QPp|0VRlVvbJmhZ!Uh_YTCbh#fcT|Y9R zm6P<~UWSdN(huYBr>PvuUpDa?EB#p-D*b6b$IhKC5kebZxxcA6D6Sz+Hw7t#=Ap5X)pu3Li@rJrm?qmPBXGH8QuzE~n zZKjV`$jj2!5dLigkbaH3@4hcQeVE`R39J zfTT%M{e1!qy-{1Laj8pX0X!Mn_Z;>BI4!LVw0QFioOrRd0S_CwEok-fXz%-Xldlsj zY%jA&9rWGif{xjw|3>f?wsC2mP7yk#R~8;bswd2of^LhS2<{zOBIyf2Wf3xm_jxeT zd{qW*Q#~C4*oj{Zr!$r(x~`=4+FIRBTI^5IwkIfVaGYe=229;d6E>D1Jg%O0 zGi!2xmK-Xh3av87TOwo8J}0;%M~K#`$7>1o*)G__gXdCrVDAq0g|@jP^LBzz(T!}Q z5WhWnFDl+J_eR@9vVUipW&XOlq2;#~b?mQgXEDpE_`=4xY}bNjSj%;Kb5}L43-@Y; z4!YXUX3&Ub0^TY;QU0Vgm;U{}xpOrzE+z0oJa1NzjRQN#pMHH&M^(kKwz$#uPx0cC zN_hr(FaSjksM0ho zeIXraimtyzL+CQ84Tzv^R@;c}rwW}CLQL;D*g@EsdTnPpSvRgDR8z`OcLg{rxO6F& zD#($T|HGL6=#jj63%uqv7#e?sfe01Ns5&3&6OI&T>P0s98+`#GxryJNJf$5#iEQr` z=Ress%+H{4W8G-3W3~;kF=J=t=V7d5quZqi_Hwj4P-?YD)nC1N`fp2DdU}VFzX?w zfcx{|Spt({<{^1;*^QKLN8`8OK6m@S6_0%V#+q&JyKG}02Yo;|wcAUu#5z6=u{F{M z&xwY;;wNPWCF{B6NAPxiTw%s(RNxyAq$W3U3Es1a&oN1)rf`!ldwq2hcU1#j{ekYT zl~>!u*@;C`Q)j9(imh#$5D@GZf-%=tDXVJZ+RgRn_k|>_3J+>oQn)_4mJq$0-O}aD zg-e$l5w|3M6_IUE^_8^D{RKHkDYVmtg0)b%e}_*=F|pfNCDik8K@nv3-APwp94=Ji zDr%dHv8;Igy1`KB_70fn_VKMBz&68PB~5!Xhw!llHewleLl|g2!TA0=I~B)|L_yHB z%Aiz1sfFrE9o#FH@TW?KMZYpQ)cKyY&1pA+zDJ8$*b_?!_2+Wd=BE>BYT~;3zEJYs z94LoMs@hUf?%z+4lbr@#On1v-Cv2sD zQ(B+hj^Q+48`v14?I+D-ye33k8t1JSFFXFS(LR-K`gt0{qoR^ba&hYSInIV`h(nER zZHPkBw>}$GfP9&s-zn`TxCadZQyx3Xl2NU(r}Ngo!{nr6doM_O1jM!vS9YfMG_IT2 zfB*w9&&a}z+O=|@E%;obm!6@dxpxmQQh?HctIle{?9p_A?$g7~mC=Kyo!UV`qpb3_ z16^}pU1cFQVN>(l0-VyOdsF2PE<}V2<;}%)6+KzD9g`r9HPtkmmqlsiIR$!lhTXv7 z$eMn`J)t@fhcfYrQX^e{~D~TVv-BJp}X5 zf=oR+DPQE_s%O7)ZtfR;vbUO~>DcWsFXS!{u zzo`&vYq$Fmw%#y=&^FaiEdt}HVQ0_L$S_vG{KO@xwfQk6L5y3+ecJ*!FvHj_?L03E z;zF%XGc}#7)S_^oNTIu5s!g~5Ag3>tx8g(!{4f84aFqIbnm=dDQEv5}6}dWO$dO_+ z#nQW9%Tau%wXh$snRfI3E3F>$OXz^y57<3wN2?Yf;vS%ObYv~(q@v(u*WU$V=8J>n zG!3P-pQ6aW=7-a6jY?>@1#0hou1vGne6AUIkOZ4fvw1S1%v7nm^)yt41u1#JW&ti{ z6>B1@O~-#?1xJ7PqcVif4XM6oj>o$>S4HEe=zyLr@UR2u|Cx3`Ez=uDeFFr+a_tqu z3OwI;e(?dF%V>O769Q9@b>;xNQ(Z`u6^yE~&G9L@Vv0*mHgUfm6*Tar7Ssw&yh+*| z9SuAdnw+?`sZG=Hn$A?Pzeh~7PT0&)!(dz|_7}`YhNYSfb(+#6ywKlG{9@f0UXV2Y zr;(EO8JlAFSiYRoo7Q8&fEvwLBEzIVbzJ7IKz7>AqRbzpPFW>Eo4&DP<@UJ*DeJ@S z>Cr4Hw!(8!wsmMD!ksSZ%R41!Toury>s!g&-KXkXzt=UV&27bL!C(&7-Y^@-)!KH2 z#DXkpZ>`p&jfKgDz$dM*ja%jde^+jZiyVXEID$P}9?hv8O`QJx{K7{D&e`+{{w^IoR&c6UpIF`oQD!Rx>TuSWfbqmmjK%EY6!}Nk6)~JBrlbJ< z6|z_ZWwtbJeIGDhN^i!S)@ph+AA1a^q0`-j7}frXG0309b{~3x+({PH^^3K)Cp`fI z?c#3izW()^Qt0*9{RG2Fw%(}>8|p9t(|{fW&;nsPvG^_4&ZDR8t!-rA{;xt6T((ik zKPKTeTP-izdW-Nj!znLnmVI*CcUE`mMm5Oo-2=WD+Wwu+nPz-LNgJpjf9Y)&^(`mu zqO986?AQ$eX;`GVpj1Ad*;a#rGjYylka%8L?IU_xOm6=5qaaBzy-c1OBFFGtAwzim zO{1#q!$cF_(x6l?yNEDRJFn-do-ix(&2|K%KeS5x93eFXMh8_-ig0`rxapBL-c8}^mA^Jh zAadlH!?kNLq9f8OTUgLj{oLrg#-RJP@^PgZ8J=LLB5JU711^~l^M_wDU6Vn`%imwU zyW*`9eX`tb_S^&gXM}8z&HQ#7+P9g7>T3U(8O%QV`eGec6R~EOP6{?RuSQ~pm{5u7 zgQspHEmok@@xrzK%@+qfNsw-_|E@!XUTiWco}S>A!ErJzng6D1ZN1&B3gcD7h#l8m z&G*K)c#WtGU+;SQ`ZmuwhrFgcksz_R3k+ zsXwo*!kfO|q7J)dGE6@V^n)`z`}1hrG!~*{CSO1)6enUWYscj`tQKt^GoYs7@DD}| z23cyEmtv3P!QHLYG6e5qP}De~t1@WEjfiHE4D$S9z<)!|XI6C__c-NKDvvSeJD)s! z45;6r)wcnOCB?kR9hH?u-M~%4#a{EmYT<*==hOVBP8b1i$<_c&;Njz%HxDcIf=5P{ zbyFb{jATM1BK`v{EgP}%<%Rs4w zRXw?N+=%b^twRMT>r;@h^+tG|fJ+V87Bp_AtIG`LqiLzykH-edD=37CkCC{=E4G^M zQ=Rf-u4)mANfhSM(vdR0dzB}ZiY95WgxX?3M?l!H8~cSWn8>m8QEHOibAvY|s7&Zo zLv96=Jo3gm^7QP68bQx~Onoh~uk#@5;1`Gw7Ry`wjj9*GNb$(9v{7Y|%F`}XtsZ)e zC0A2Rma7=}E6N;uWUA)s_*R&C@4ivPZa!`h`mXH#;Qi$3zV%oyx=ffq1UQfsF5~C ze)$=SJ~{rSEx@lqmaI16B7ZVfk*;2j4G$+C1&2bB-U3r-P`QKJ?5oGy3j}1WU(>Z* z^2)hFJ=sZsxI-nK;y#BBHYyV#hIJ~5+)v)jP1fK%D=|tJ*I1*BC05lfR(Ms?Hy2Gp z?gtra2p`|QWDI^tXmAn#GQ$hn=2g)5Sjv}aQd43)$OQT%^LJdh8iZ}{iyYfNXQaCSo%Ih`WkcJ!uq@^TL(8=HPa6i zn0zfHP}%CTCq%BTg4evG{AG7%4;@XW-);Fr zWm&>Cy#yi7<=~~RsE#}b4?+EB5zMHXby453y;IkiQC*-Rw?q_Suj>T^nZ%E3BZecl z2obwP!7qD1)mItZL^kL*(W6w71&n`qiaDMFd`vm|wqF8L4PH2_ngR}wK}`p{-y=st z8!=SOcG3|h8L+X5^rBv{tb?!ruG}>jX=Idn7V3f+#jb*k8dSIk4;&>*F}H4RaI z7tv0_8@EYYbn)-ww6eikbZxt+h+U$Q_V(o;fD;|NSy(VdJoCY)4xsJ18Wfr&#gCFu zaJ)6Z`${N|pn_$CjrErWKl(2#X~=1!rCRrIziH0Ei~EE&!bAWrqywM|WrrqrT{#>O4^U8up<)4{W)Tm}*jG+fvW|2P z)79A>r6uZ*@#?bDe+bTPqk*k)Mdf^$`0ouO!OJK|(sV3cXfDpXkEWcTpCd-i_ND1a zY+gFa;X_oyM2I_THEAN?3+XUL4;_%dX1U(8v2gf^SMZFM5vcf{i+raigP;`>{%7*# zfU{$JI~Mhd2o(RL8OykGcdac@v7Gesrst5`qabm0tQ$PIbxxFFh?}6dQtWft{0f`>hG4|LAtM*pAac-N5MF|)T*n$!Z6mQGB@1LgmR`B~3Qw@~S(YL5|e#Wbxkjzp)l~ zVyq3J6e*#L5JL^MKlHoLLco4!m3SW|DK6m-iR!-z7udl^Wn*h;CdK zCTG!5_@lw0d}u0QcQoz=4t-v+8kzSg>nBu{t`%~ffme$L{TQONKGob zse^jGf!Eu}tkJQ}&31tWttn}jDaqz*Ei2U74&Yd{^ndXAT~t%Uzt%X0LcAY|(#sHB z0XDpd^Jd$%em^&8AuKWPgB%m6_o332>^Xhd~IoH$a?&qIW6p9}zOtdGxtM)imm z1+{-3C-}xBEEer)3tRtXMkHDrZrlgvx7o#O^ME6BS_xiJZQ&(7uB3bGzFzo5;z>v2=utj>dO}hX(+wMD*~9=?Tq2(vYIu`h6XPRrqmTUI<0kqr zqYfL~oND^pST1<=f+cah`qg}ZdH(!u-c;)d>*5b%VKszAAJe4*n%x#bn!pFUjIk37 zx;{;q?=)N&nf~W|ik=6#8)^{WHOCMMyhY$B(XRU=Y_LbNH>EZ*68DiZ4?04ih7p}h zIQhZJHG?s0!EC~*y&nS0opbny^G*E`PARlIHewN)_ZN$>2>TPX*h!mw&Rn}@8WU4e z)_TibH6Z3i&fx5?QKp|Y*B0R2*~@U3hu5SO367WfMO-!$cDjE*H`_12J*#um8My~& z*lKG(emFSL3^Sqz;OxwpF+Nkzq38O}Q(?m#Li0?)zLIqacN#vmCwArO*Lr|{5g{5t z{4xT`S)29o@l1MUZ-3VN-;Zw=NB@8jk&%(iLP8JRx$J3iHWV!uKZ%9p^&DA-4HnS_ zniG9QzGv~zex}`ceijPhS{>F&h~5$yRCe(n&Hw@nksKsusDZXcukn=xhSy>-jMk_4XtH3@s(?M&DD=e@!2$&U1%qH=^6;`%fue}Hxul~$2~J@Qt`j*58kUV1$r1A zv9q(s17yCvHAxJ7Zwri`7Z;cVo#@tXtNRRMM1f%!8HTp*o|wp@rhla{&2XSz4knCxaozK4?j38#+uk*7}E>wu&I&=3_ zV2MmN2e%^T?I4!=AhS4J05Xe1Db9vQ{6TzLi{vM4IOHu~js{7{4&J?sT5G#9WOye* zUBS)A{hjlSK))>HNelioM_C$utg-J&5w#R6RQv7_Z~A<%;jxX_uT0V2$prY=e~?v8 zq#x}+Sjy|s>+kO;@jq5pBk}W5MhWa%r;mlZIxgzyb}M_BuN*IDEo<@>|Z5KIIj+MwEA>!^NYW#8Use&EV-*}!MD}7GiJqL8 zfms6a?JSC9LyfFFC7Kez?+}TtL>by`X*?{E9CJ?hZtInt6_vLHT*9Hw=phs*{*lr@ z*h|3zB$)SGu5uV;*d=R6Qx?|EiC(_%%B5Y9&B3?UNhRscpLt5;UFTvVM%Jv-LEvCP z^d^+5SL5aRI+VqvPp}udWO##l-+CszSW#o(9^H<>nD0#+F@XsuVQB6VVBBT?F>KG! z@Kq&t(#W!<;&McdQ}L5`+z*CJzj3gRu}Z3+o1Gs1*dltG1!ZBB7?#Lp)wx#?lc!~ z3BNGZ*pl~9$*ZZ+s&QiA;@Z~5po?Kfxq^w~oP+jUt|U4;H^q;iINaqGN@H%(T_@(f zz?!eS`P9>++-$K-#j@zI?(@^Q?S3ShA@nTA4_&Ly76GO5a3yM(3QFFNLr)0OM%Y#H zVIxEam6eqv7VGWIy#*GN`bI~|skgQ;!lo~^8ChBeKTt$kdLu15{$_;iveDjH+aLlP z$gaUDlnhw6X}#&cBCxMjrfk*|ZN~ZFaPl`IyuDe90t*{~s*#BJKo87J0iB*#9+0!M zbMyr1WCmd6MfIMmj3QtO^vK7en-zR1{Zr) z;1KB{(6ZO$xirVppX1Q;zd(7|g}c1rZSDL>GpOUqowhC;85S|wH#%Sfyzy>mz8z)v< z^|Jl_!%2aUAc6EH{ip6`%!?3A+Yp_57zcBW^GXhB>`1=yA}O%Y#Xbecb+Up(fU3{T z@q_X(2~x7Ix`_MjrqzG8z0vxKPd-@tvDSc_>*xkh^MQbI8C3P0mqkcC*AIX+pl3&* zy;@^tC-@|S2J^n8$aNl3Vg&*i}HqzEsh1b#+<~&YcXjbc5 ztk^!VzWl9f?cC%z?`~pnXi)=sd}duEN96dswYX9N6AP7fjIAYGq)v0PhnPwhHr=(c zSC0lGC%PPfKGerKF?hcTT$&v!jhJiIj&z52?R}_S&Mbm=XSQ<(XUx|eHulY%1b!;l zbZ3C0SXHC-zYpIETr$jHaWaSE>shd?r%VB5H_~LR$s12IKcP4~%K{X9GT)g+du<3I z>0#f6{@aK$F#VZfL)X#G9~hx7kwNCQS)!jpec^Kh7rh=>1Th!KKmrmCJdYXGE^s%o z%LmvAxJ&MKPOvQ^Qk}=}&kC611;M8i1rMob-b-PYm%aOW)y>{3)H#a9<8{eI+c@*jpW%Azjc-9v0v=UQFo+-Z87Z3nMc{+nR zGCHa+Z3i5>^)&N9vBubBw--S;h8nTdQp}cbs97cNP3g0|GbjA(vFH4$+u84TjQ-TN zH(ieRVf*TO67KSDNrS}yzIQL_gE~F|m&m>m2fW<1%qP`1 zG4I^)C{bi(vT$9Q87zJTCKp{^r^WbfY)lj-XZNM3H>_Al`xB}n}sr^Z8sv!Ia$2&oTmRJY3GP&|E9k{S# z5y559svpVD5JCwzG&DlPz$3c<0SsAM{kxvqt4gLkG(7x}>n`_T#SIVoftM@~*fTOR z4lec}b6VdGhR;}pO$s%L>3F9vRT&eghJIpXA*>s5BGIUR^mNc+fnRD8+1_)j-p0r8 zS8@zfs;Q2LwFNC)Vn3YBhYX0DSfpU;xzS^XOea+GNqIxBO~zVZ7%c_ceh52$yhL9^-$e^UcrwBnCKSnOfCBFs(zg*UYy1SA7;tE+(wVqf#6I z_kHlXu|o)IOq}6q>KVXC+^He91Ucia8ZCr)j`uQ7r_XIsevgkYZ*KXeeTy6Qh5gRf zb8UP4V^@ZBR{-#ipz%kpfDkhV#Y9$N7_dRguNRO(XimA%<$DQGsuq&h zcQkFz2l6?Yr!w*uXw%ry^xnZQ*8pzYwgEyz1V=r2)2i1$3#z!ydGoc(1j|BB>B+y< zWqr6Dz~l6G)jNx|ZQ5+N8BMtjo&FZuDriLpt#sPI8|;Ss6<+Um{GPn}eNV<>_1Mq0 zpX__PSstNG?B=s?j8O{)EZZV!9G)-Qo9;`loyvv(i-aTvr|9UK@y z1jmBpH!cC;iyKjAK%RUI=*fw30azK_V}FJ`3&T{S`;V~h?uWkgMX*|EnVIFlrClnt zZ@$*sGi8f=b`|?B)LKVDa1V`&#qc`N&dmW258)DHd!AQ-Z)~#P?DVy)K_V;&MaUpeg z_J@|G;0RXk+fpWreiA)H3o=YJ{HC7Nn_pi7{D}#Mc;?-wyf4|S&D+iHFi_cJ34mXqKxGn2Y3Mhn^0B=y^Ovjjyt z6%h=ym)*ILRSzF)ISfj68cRFq)y{7q%E!0vTS}{8w7WX;ULSZqD zi%y3INBGbJ(bGi&=2X#`^Ds~y@69%QR+_keB1?Srs*>RRW8^bOp3Z0G7@3#1_a`aNqsrk#y^L^5qsR{>4dn9Efg09|5cy(3~ z7%v1io-OU*A2#TF=Fw=YAXIiP0oe0a*5_c6)GdU^sN2D7?e8fM_edY&As4CflDCA! zO-U%RJ5y7}GI;=v5RkQcE_q=@G@nVwh*=vgereK?7I-Yxa^H3H+vF5J1ny~MaoTBcRS@J8R zcSenl3uS2Z*w6E5In4Oz7W7>dFwF|5>H`p`dTlfn$=xz}30R>8xi~Cu%u{h}CKQ0F z5{g|!KNun7VRs#D2WFf+tc}NKDW)IOUD_L_&gq4`X1Mzu@~@kz+%a-StO5#ZF^LR;hiyQCW zS>J2v%i#0Prwn54n%{+3pVtW}#9mfM|NViI^2Kwe@4+7eJp;A(?qL<-1dft#>q@Nt zNW2jo#5q*=gZ7`LxM9DvsKj*?wWM!zfh{1>*koLM=6DkjF=bOr0_x)*Gp7Pbz$6Ey z!wRHU_X0tP?Y(qVuD5EqT)bLwkKa8S>}&Sip5i3v%b%$o3!nB1=5EZ$lg{}?<%AuL zynDKIXOIJ}*;Xt#1)H7Q|Cf4$DdH>g^8FEnwC3-2mg7gS72P2boZ%Go{POhSG4WI( z%@MnuTs%`P?FFN`eu`Yn&slW~Y23Q3AT&IjDH&w94M>-baR^^q0APKIuwAE&X7vm> zRu=F=E&hJ3oOR2xfJ2WKXSsE3*CAyPG63O)kyuIPJdJB-0&*uN<!7 zEbmg;8vbS>LE=#=x1qhXmz(YKn9p|I9BhY9%hPAhe5s0%q};(9D&B4^E7ehgBST>e ze*ccyM^F~eO4~g(TD-GTFTUCogp$Sgwt#~lubvJhvZz`Ce7>Ol1hXL+dzweMWv;K$s7-0v()Wm|Iue>X?U7lNXX|~cHz{k?(XiND{gGOwJwxYjBNqo z{?4r%(RhEs&?)wdvCtIivhcv@&|d|!p>5orFcCIWX0XXbHNYo2*Tgs$25juSZ=+B@ z1QSXdn)aIg`NMAO-GQ}Q&uzU7fmW@3R4Fd5%1Bc4l1QYDO-bJB>MHvc>5EWx_1lJS zdro(Y2AE@2(Tqrd7HbiHy6|D5XKgSIw&3R61Z{>Wj#{e0AngOtp}7y#((1Oh9-(t_ z?YE5mjLt*X)1P_s#4ncmSvM4huYG^zEn5Y#2=o1pG((L<#;y@&Ej&OIL8y| zwqobC2)hJF#weZ9r$^izxI8470u{f<-(m)yE!F5mdr=^yhBKbJ$Ft)`Yc4NSd z_3t0U4j77n7>cW^)U>4CR+Sn)BZ7$#tUDYtTp~Nu38M`dJidRRpEMl4J)RQBS>p+O z1;k3s!1J(H7Aazx&HV;To(@l#BgG#RAkl)E;v>1)W^8~VjfOS9kHb_ZkZvy|5O}ugghDU`0Z#KhXkY!=i`VM5j0>C$*7+erOFmN) z!ow-bP3o%!(1x@!rl9eT)&629QRUmi-LLw2y|m=(R*QE$7H6yDKIKqhSy<};wa(1p zze6pIrvKv@cP=?V+8jhxTcgBea)Eo8G#PteB*}H9$Ng2mJ+p!tLs`PBG>9us=nQm| zEgZjzpl+tVfL>*Rt3VU`TMD;=HWK!6bt?TZig=VY@arlqX} zt?4E>pkb^)zpdA#BXyiGd;g@PgoE^#s5%W$YgwmNf0;vc$1S>4|k>>6zP0SfUPtH5DgJsw}u>@yKtmnd7Wbq6{SSbq2=; z&M9Oq2leiLjeP$NL2pn%1X#mfs8QG1umx@ zye}FlJv@o zO-k|tq{iSY(a7A2VD!;+uCH_Rg*zOW?<+M6>q*3g@x z;_*`2B@j}Gr$_zW?32CrQ9`y})C>*V`P`+;dov}Q<&QwwO+Kjs5CoxsRo7z=J%G0v z%9WN@u#enS%PT8W5)=}W6!s?niddFl1Y4BDaVHa&B169^dAwqgohSZ=f8!X}Np(nI5~Wa~uQAkEf)ErAh)o14K-sddnfT znAq49_gN?_@5tuIKks7Ecu4vz0p;J+%>I)kk)}s5rNrc5Q?>UI)LKVJ=NXnT4{itxNhE_+(J0ZLO`#=!6VPzxvb*tYg)$9;$Oi zrTxdIxqUmf3p&<+x8fd}9vt1KnOM>HbI6%)mzraxF)mgv{bKLdUA7?T>+(!-FIbly zvP@0V)ml&guc7yWIcT){>pQZ+ebm{CRr5PpwhrE#D6t@Zn1>H@Xn8-nw|fUJ3PMDS zlaurLeL_s^#~p0@n(A$;)-;WI0Dcu}B2d&)@0`j?Im}IY920xcV(k9#{Zg$vyr9FS~$YmhYGe_N-C{l5tgsf=vO z$(i@9^5V~ig}V)ml&>)kNxJtUR{jMlpx{g?=UGkv6t`9*16u2Wq;1YOLqo$vGT!&^ z-@9E!%W~jii9(rSp(n7s@JSMwT7I~9t3N2p{YFEMO;S)GB6`rRfKJv(9yW&mYm;uV z^4FV7`#c%WY{qz!0I1$*v4*1tK{S8zKc)av&7a^TlHSy(sheA(Xh~wCKAOyxgPWJZ zu*_21x`xE?!3Hxqlq?qZ6`_(i?*?r&%L`=_t?JY zwH-d&%v@V$=dCR9CFX~H6pEk7OaK`mk^s`1SR(h#wMXvkPg2eiRf>} zgHk9`Jslo~1AN_dSscV|P#pZ3D%De^nl(eor`?=9dNeKNiv8eBSTZ|Ta%SLJl-2q% zG1*wn_K6zsK_q}!P|jhv154}ct4R%|QaC3|O8xc3$#{L2iwo0x;cDJjx6^z9bc>#5 zHG7ga5gqGzFzV{eL$qdG#)gu5W&!$Pjn&c7VFcZuZy}gLeV>sJ z2ns!1M6bO2vw52rSg|qZt8CIY5t4x8U~Ffy`PJ#PiqwQyybM1=kgxSH5r$w_JS|d- zuYRjy+1r??t1i7%cC`4Uz`gzi!7b!gBrynixI!F`CN<#uI>%UybFqQ*hycw|L>s zY@Om}8wj832ADzr#Kl4$GE{WE#G)#{jf&t0VK?J;tpnbHR9@3Mt69HS{(cl?&Qf`j z%v!f$yF7;AE9{XYIQR5ZG$r9C-wu{14DLMBd zu!TmcicbfVa%i^h=^gNj8xv5oS6cb=`$I7uJt6Po24@7ixwh4tI~;CQ6(Bn+@T#gh zIBy{6R8&=;WT^xO23n&aI*C|xyV2*k2NOZbpQ^TY0w_vt!{lhKQ4u25bdp9nS!V1{CH^Wt9GI?7wOdnCeoHOoZ!lO}dRVro8kbaje^8J3 z?;B4AulotumY!bO0{Qy4U(Ahdwp>tkw7Jt zgeY6=NW^}Q+Z2Oe4d2RwmR%IZ0~7=wz(Ypd&aer7nt+ORaXR!}F!cn|3HLEdWr>r% zY?1)c12<47U9n4o?Op%$yIDh8N=kocC{})BEgZ+*3ep~|of4x4yZSIZMi2KNsE-oz z2~36!esFj&|K27;MpOM2e{-E9(QmwiHI2&Bz$f)G?DjP25TT zibRJ$4S~NMAP?u3STxNUn>yI5Ihs@uA)Wl`drNJtzr;GU`Z4EQ#p0ZE_nqzkudnZp zr}F>*c8(JcWfgHKn?wj@g(I@cNQej-aTFOL#N|*#$=>UjnHkwz!zg8ECCbbm+3S8^ zr#_$W_woDP_x*TWe;nsJuIoKtIv0O-D<1b{r<6gfn_vwyasa1 zA@WP|8*X0uL2`J1L@?zXjapa8gHVkQLDkOT`6(vES-_wGQWxz?oy&}8x032QD*G8W zqTmY-E~*VMwk94Jw*~%HOjsVoO+cD`Nk4q$ z)_wDwu*+F5xOhI~Fej!)6L0rE>?s zg6~zm)|?LoGTGL!ZA^|h;phvGp6Nc`&fIYA#?m{$j7dp_FzLT=KuYmAS!mlCsDClfuMIc>)|{U#*&*8fhy$!>tm=2dP0`z10Iq=A{{_vD5{{f znbEp1y3iNbYtSRT%i5&>6vJYO4!LIDq$E3ug=ov+y=uj*PybxY! z9t;gQo>$jtKY=tdnN$eO+6r>zRq@&MaM~LfW`pGzoA-pEYNH0SR|5(210dKWgEqWf7SX%a4lPZUUCt7fbZAiB>fG?k*lSaAvN=1Az+Tp8y&- zPH{i_cz~2n5*hfh0x4f7N{eFIWD_Mu-j#@@ElIRlM8>Q9ou`*Dn#L67$|`wASuF_m zafufb7Rz$H?jPbThuxHxB0ZnBv9Tbqc>@K^wBY?hG_BsxV1a8x7pY+0zGnu7zOk{@ zK|Lzu15`r+bgWmCAm6KfxBm+TmV4|J*o7-EFQ_=hk%-Ax9*vy^V4kP!mDgiMw_wD% zXOZa-wdVUTN)zU*fBoX%vJ`)K%CIG{SW@-{8w0TU;8tUOQ3Jsc%2)jl6ZRh>tajM0 z`S<^Ly&oP+qT!~iJ^_RFUNsoZ0m>?rNlcz;LNNZ;aeVCDqr#RKLB4YEguO}{{(sV8 zi4S3b1qp-u%Q;EJU2g~0oGFOX@80R9^|75~G5J5pOQYT>UeT&2+PtkpxbZ6EJBo?P zN&e?m(u#M+NI|20;0^oYi#3=6h3oC|D9PjSgV_;U6pxZ7Q4w$df2`P^VS`)5Sv(DI zH6{*_2LZhx9Hn=bUh`A9@hS(w1;*2idivo#Sa|jovuB?dW9^-hvB{oz;)mHDr|PZ3 zfB&6lJF3|rby!hH5 zT(xCpe}-pZHb_lPd`nIoOuRbs;IosC)D>%?Z>>&F?|N~*^tc~G$5<>IFUW_=rg$+L z-d@;gnEqV&wIV+BDnr0a*0969uHf#<2aw*E?Tu`8&xP_sqxx@QT}_Ccr0$bA-A++? zOH0evAO?Ynfntl?$jC@lV8PX{LLUXf3?3~gA{A=-y=E?Jwjp5AE&skr(TV#6<%iKQ z*;1~*n`mh8iXYsc#`DPYXU*aH-_|>dcrL)F6=r`*MtyszFZYsDf5&9FK9{; zx)6)BuK&goAJw}JU43sC`~efo5sy?h4W^pe*w8C1mh8{v`VW8F>lRmHNf7A#&Y6*# zif%a&1^M;0AmLVBu}v2zGdOsFQV?suI?)FCO6M;Q)O*!LMn~t$mB}jg1T;36+YMU+ zPYT}qp9uRX`t(gu4_tt4Ws>EB3hF7-{h)@+!WZ^=IO`8rw?M!ER!R^fCJ1q<2Ykg(=B)f$TbN22(!bYohjUvgg25@5do<4=14h<+N`eJ>Yxe>BppaS1{H=U)* zS1S7Sq5rmUCOLKX_3y@2>eX()1ZCgRt^dx_IAXd;_oR`Mj7NJCfq1es>RPjRXUNN( zoVD6@hTzLGBk#0q)oJzotYc(k88DLK zeU>5(n)tmW%tXE+Y+9G~wSd3qnDZ0C9l{dN{*H34%e=Hr92;jxXQS=&*SBkO^_ar2 zbe)a*w5>v>^~UoHXGUt=Vn8=kf|?)Rj*vrXgF;aJOr#cf64bAKJQFE`CydOY{mfNN ziCbmlKdRvwTavyk40{e=fyzXKnt{220XFOtioF}@dM|P4&yDGt_Eu70AUs`;Bp2UG zb9v4f^~U4g=UX@#%j}_pl@b1moQ2a{-KPA@eMhJ}YIm&NS|31;uh_9GdS#X_iy6M? zjk#89P!WNYpCbSMKuKmvm0QSSRKK1u=VWgAmHO|rbnFUVEpNU+;jLAvuWg&Y6=i1H z%9_etB~dt%n1Xi}aPU4-YSDLPA~kiu#HrKR7KHeUkKVp8wz85}sQbFUXqSt^=MAreq;=mp&hl#-9RrlU>_NAS>@s+(TC2}< z`F~o)07?iIm3qegJ_3dQ{ee6!&PlU|Q|}vdkD~5y4v$n*drSqyiCI%389*4y0ej|4 z;UfTMFxY2kUWvbn!ps-&Qk@jiwF?crq-X(7LxEKe6ozU|^P{IN@~WoKbnC zW|H)e@DDslAnR_wRWI|0&5QS}{(6`e zCw{lgQfDX^U_y&po;_}6q(!kV#_iX9v`wtTi%FV2u(H}h3W$DvTxk;Y7SMsIo8SFN z-Ed{P@2RjKzdk3>(Uxt?vSS{lZjdy7=$E{WP#{lC#god@y?{v=V?~QOJJrQt}8ODB;%g%QF9(mZK5jAc$jNbW4+X>?g zpy~!7^mI9IaLjC0zY)**tP4A9mD2$B43GOkP##rK~jgB^*$OMDhTHyd7A zcKWlo&U|BCkEaQQj$<3m0C?X=J$mk{`Tjp(!TYZvpv+tz14feI3{Fls`w3m{NBDf2 zZXXb@bXkz6TxFA|0e^BfbvjJtG+nK-m$;Wpp9Sj+@>7Da4@QeZF5N3Vl)yYdU9hjW z%Wa451X`BP@w$!PJhSQl+{)10Fxo(nf7*u~Y6hoBL{SwZkK>Q6`x4QGF$>rPNOvA- zb@DxT`b-!vJ4MlKM~AMxKvVAX)v`Rj=CIZUUe|pw8rd`16;|`sC_eVJO}-Ru1nmoa zb?b_Q-KTMkD1`xmX6L7i#)s(Q-9^vMg^y7j-kc+^vkB15?=D*KUX*BHgh~|{ z?LwRG{eC9~@Rw2VF91YI(W%|G=8V?tuqkO}pxZ(Z8t!%b^I2>)Da?%4= zH~GFmkWa`VL;a7X)_9zqB!c4if_t>ul>VD_%QB39tUCJ%$~Nc;sczPgRt<2feXp3c zeBUpzTf$fJ?(eKtn1{1n+CPY$;Cxz24cTOz{iMdtZX$ElCyp&~Qq69Meyr}a--9zh zJ&_^aQ8{WYfmwlr9QgA$Ln5S{r++<*zW71SY_Np$S1*s)7n2i2^r4ql@=>MntydC3 z_Z)z4Lv`}03vXjPjkIS#Imjgm_8d{$&3+D8_XOwYh=ml^Q0T$P_z|+3%`@`Q8I_>R zSH8Laq`bWpX>XKz~~XC1eXi1N&<+?dZyf>&l|`VFeK z#okLow4>D%;jt6YXtJy6HK(x4x5&kTtxGTEZsMs0MxxgG^zO6Bhv8tp_;5NgIdHB- zceOj$AT*QBDrszg*5ubeGX96~hh|OAM*r!lskXof2IC~UF;AZxe1JMFo_~|BVh}85 z2~pS1D4#-6`58(>#hRjwi^9TMAeiO|#A9r&UzLJ+6579Dc+W&St>WG&wuey+gRKS3RCcE$>6m?^rfGt~4xM zh`&|X3FP&xL|EH}QrSvWNlD3DNTK~C>O!^a0w%jr*`e-BT!ha7ifr9p01BQ<@RB^^ zixGRL5M1x)udcau)#+dhxh`{26=Au*Bu)oQvYZ}D9zF@Jou~UB%>2>G>2Sn240TZd zlMB~ha>r8dT0iLIao$n%w$tVIy|Jqm&Eh!#P{j4+M$HctD+<;9*7?<)`F!l%?Q2QL zPW-I1C-m5qj1%)u%(coL>yB?^7I%P{t;LQG|=oftt6V5F)KQt$Fa|@x1x~1srHTeRtyTso%Y_k zgZoxl92pby=}VG~*Qo*Ehkfxelt89?pbP+OmhPfPllN#gQ5w35kA^xpA?uO^?U5XBM=OvNA5>=H^?^ zd&=IGWEhgJb@YfJC4&L1w9JM0(iVWCW)+ILa&eJ z_7?}E+BrqwXuE*G@dk3y_Ek!diS35gWtBbav{zZth!f%dVFv+B7% zqQpIc%;!byqid_2(jQB_3P>EOy7IQU{$Mt*q^eRf>%i~TC|iVaM(9^W@H$n>%hITR zV3oJi$T^t%-hg7|q52{HpZyHXdd15oh3ZcV{Zt7L5523PscRNlCh~hZGd&&ZlOL8% z)0uijn(f5J&!8k>Fh>h5o&)LB4>r6xDp^s$y#_$_?-3V&eKyzW;!f9qMaqwg#nzZw zUeE)PHNTq2E?-|di|$pHabujn&vmsH$*H15;~|Nd7mVjDd$Kf|uX#3B*=3k^l%LJM z^P)>U5@pNY2xkDp&Bz;`gME5=E4y2pByCymN$BPt3lM>94QoaTb{Gt~U%Am#Em zyTZo}tW(!W4Ymir?Xl#_829Veagsa6xWTO_W0r2@x;jq}P&OQ}VnHQ0Lb*^>t7%NV zFki?i#{65?7+dSLLEM=3jKxHAh}Wy=Ocvn4e8M7rMK2cnhi#lz(in0ZO7n&e}QrnW&Gh^Pw*aH`Q{Eu@Xu8 zs(^`!`#~d~IjsH@W5D<0#T2Nex|AreMd%;T_vlXRLgS1NGt6*{{^vZcfO3_NyK3|Y zj8+{xili_Z0GP<*ROIi0TWR{77o~(WFo`EVF*?j_7Z1M4eAxfGvh>)7QS|ZrPH139 zA4Ke8cR9`o(d-y+%`X{qFrPG@Su=F<;0Amlnj^9xRVtaT)0<$O44E)C?Bs5^e!Xip z=U|quFrT%DlW}qWL}LS z;89CoaDx4Y(s)inQuUysJs+ftmBa+ttat4(70AV(u9$Nvw;uc$!kymch#Q_T*xH|~ zG>+)MZFi1&KxSisy{t}N-%c_2>x;Xef65&#bxhb=P|K69)GSO$?9(hM+^Ij&uv@Nq zw<{V&VW5$q{$@TInZnzdbX&Di5-BUx@K!Rx&q~C4?gGA8Q8)}hSIkh-fygc%<^zT`lqC>cN}@ z*$G$Uea#om9Jjp0<(2b7vRW2Ak`}7AWv105cK9-`?^EK2(^7xW^rpS8sLU7|rD_<3 zcHeH@oHAOUYmt5Z6>+Q`;vIkcptavy{Cd$X(n7;_t|GVn0G*}$4f6~3_mfwE)lSbH z+A2JO3RulzQQ^RIqk;Usy1N;1@fZttP9TvZNDM+e;=gU3%>UQGy!;8dZ$hhq!o{2O$kSM_ZS<1&wm4oZ|vjtwvMaNO*X zBi!jYQ=oTG>d{P5zUKz>_DtDL_t*Z%lzXO2c{MU7>NdH@eIK;?ke}`aHbW*IVl=Kh z!-5YQTx%k)PdEtz({1!cA3yQfjZT-$Cb@;{eK`2Lp|Hq!+C~mShuj)p@y7<$*0hqO zK%P($UIOB*-Of4#Pi!fja&UqpZQz($2c3(@ta7S%_X(+LM? zMT}M1jY#7hT+4fx9Ls-KjSERlS3`3@q#0Y#+Nq1E_|gZ$le?R9_Ph&R_(<9_BX3le znGDdVGoC7T&yHs7=E%m_Q+kP;mjhX9!Z@2q(m4V^+om0vqa0#*~w!nxhk8!~#Q=8jKy@}QHaPkdv?ox2s(R|L_0%hXEc;T}7W z*n2&De#a?9WzvD$j0}U@!k8}mS3jNrlWXuC)}oVx^T8xpiG7d+JiI?}k_ioCK9 zH^RP(%bs3QHylS|qN3N{>LGPCVWt9J=uk?B8{ZQj!4`*sOVCi*8hiXW;xxED9k@RD zijz-s=7BhLju>@;#k@93zrY~%dqa=hw$ic+rC!|FyDr55?gA-N4Em>Y21^uC*Ihui z&fh4s`eeJF55vM5gr8fEYP)-4{JS=O-{ifj;lnOpvAF+}Z^%txK-6lD?|ZC~vt@5S zQ2R~N`OI*?;i=ZuUsIpyE)+wiSs#REGq_!d5djg3`^TO>dgdr%{MnFKji6EIA^NN2 zs@2-g`s)1NwsCP7x0dUl7z;Z>1)V6#F<_!o?Hph%@$lJ;I!g!E+4Sd&?uTAY`U@`# zlh|%&M>CR%+v4Bx617+~HlcohVzjV6JRh^chsV-3UOa(*R_}H|>G7qxap@IVbm>A#47jZZSK#(^$t5NFMcTlLR`H zS(B@j2B$P`w3(<4m)%gWxF1$pqEm($Pn`N?9u;%5UPZ>eYg@j7f2%ezTa!$jo~Okd z-b}Pyc!U^R=rQ+J zMf_>OwSQaCyXuMEUwpr${hhB?i*qRg|6}t;-oq3Fa+=nP21mT{P+?(u-g(Z^QODbb zc1CLV`Y zJ!V&-VT%^xYS4SGdtc+I&HLs+9r$LbIKG^eyzSt&Ox!v*E?gIjguvGaoQ;FTwH-=v z-)r|=+d-3oN>;m%KNnLDD4PUWkGwvSx~3_Q6C(4CuUClh{86-NbY`-|4e67Lnk`#m zmx3PY&};PEav8$u3H(#AOBSi|w&A^S{5D}Mrphy2TMYgWCHgTU=@xvR0W0$GD5Kq5dlhLP2

~$~G+I*K=i^^f z0Sqlf;6?6kE?y7#N%P1o+A{HQJzgn1*8xf4nm~fyO7O8#t1Guw+i^FINR;0T8FMbITn<4TJ4w*tfq(X1ouuL0o5C6s zkAvlwZuY#IjI#SYp5G6x^Pv$^&KBIiq7D2u7E6+1adYqBywtSL$g2K3V(gNj7E$R|!KBXKqD+a~+Fe{{S%P@GoQlVRO&UATz3JLJ$xRM| zggXFVVtj5DhKH~1czhxBRd#p5s0BL^)IG)G)%X1!S=`8^lClvbRRlEzJ&wu0EwC=* z676wH`Xk?Pxth&a(%i?T4+B3Y`p)y`kR?!4Sn7X zznb}mmaIqSdq1Cc%G;Lvaf3?D(3jh^g7vW*@+i^tr&M|fYAsqJp8uB%n#MzMI=*C>LOO{jNp+5ZwH zx>|Ko@0as#(%Du#RwZ|~rt|cqpYDpAt?^E=#bs}938%HyO_8OFp(TltEv4Q4{67|6 z+357js@m6v^>-=qb7eRIJ0Z-OL3t*D!O;fgWtB>3LT-|`e>luX*@?GE0pbik>2j

User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
Traefik
Traefik
K8s
resources
K8s...
Hetzner
Platform
domain
Platform...
Management
domain
Management...
CTF
domain
CTF...
Cloudflare proxy
Cloudflare proxy
Cloudflare
Cloudflare
Text is not SVG - cannot display \ No newline at end of file +
User
User
Service / Deployment
Service / Deployment
Private network
Private network
Hetzner Cloud
Hetzner Cloud
Request
Request
Cluster Network architecture
Cluster Network architecture
Load balancer
Load balancer
Server
Server
Control plane
Load balancer
Control planeLoad ba...
Control plane
Control plane
Control plane
Control plane
Control plane
Control plane
Agents
Agents
Challs
Challs
Scale
Scale
Scale
Scale
Scale
Scale
Challs
Challs
Challs
Challs
Agents
Agents
Agents
Agents
Cluster
Cluster
Kubernetes
cluster
Kuberne...
K8s
resources
K8s...
Hetzner
Platform
domain
Platform...
Management
domain
Management...
CTF
domain
CTF...
Cloudflare proxy
Cloudflare proxy
Cloudflare
Cloudflare
Traefik
Traefik
Traefik
Traefik
Traefik
Traefik
Text is not SVG - cannot display
\ No newline at end of file From ad46afa9b4f16e493d27cb3f952f8bb7820bf5e9 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 16:15:42 +0100 Subject: [PATCH 133/148] Add challenge networking documentation --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 97782a7..6b980f5 100644 --- a/README.md +++ b/README.md @@ -891,6 +891,46 @@ Network is shared between nodes using Hetzner Cloud's private networking, ensuri ![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.svg) +As described in the [Cluster networking](#cluster-networking) section, CTFp utilizes three main domains for different purposes. +Challenges are accessed through the CTF domain, which is specifically designated for hosting and serving challenge instances, and are therefore not proxied through Cloudflare, rather point directly to the Hetzner Cloud Load Balancers. + +This load balancer is set up to forward all incoming traffic to the Traefik ingress controllers deployed within the Kubernetes cluster. + +Traefik supports TCP and HTTP(S) routing, allowing it to handle a wide range of challenge types and protocols. +However, a limited numebr of middlewares are available for TCP routing, so ensure that your challenges are compatible with the available features. + +IP whitelisting is implemented at the ingress level, allowing challenges to restrict access based on IP addresses or CIDR ranges. + +By default, HTTP(S) traffic is configured with fallback middleware, providing custom error pages for various HTTP error responses (e.g., 404, 502, 503). +When an instanced challenge is being provisioned, the custom error page will inform the user that the challenge is being started and automatically refresh the page until the challenge is ready. + +Shared and Instanced challenges are deployed within either `ctfpilot-challenges` or `ctfpilot-challenges-instanced` namespaces, while static challenges are only deployed to CTFd through [CTFd-manager](https://github.com/ctfpilot/ctfd-manager). +The two namespaces are configured with network policies to restrict any outgoing local traffic, allowing only outbound internet access. + +Challenges can therefore not talk to each other, nor communicate across multiple deployments. +If you challenge require multiple containers, they need to be deployed within the same challenge deployment, and set up in a sidecar pattern. + +Cluster DNS is not available for challenges, so any service discovery must be handled through external DNS services. +Challenges allow for multiple endpoints to be defined, across both HTTP(S) and TCP protocols. + +TCP endpoints are handled either through custom Traefik port, or as a SSL TCP endpoint using SNI routing. +Hetzner limits the amount of ports available for Load Balancers, so ensure that you plan accordingly when deploying challenges requiring TCP endpoints. +*Currently, configuring custom ports for TCP endpoints is not supported through the platform configuration, and must be set up manually after deployment, or manually in the cluster Terraform module.* + +SSL TCP connections can be made using one of the following command examples: + +```bash +# Using openssl +openssl s_client -connect :443 -servername + +# Netcat +ncat --ssl 443 +``` + +*The netcat command is the one displayed in the [CTFd plugin for Kube-CTF](https://github.com/ctfpilot/ctfd-kubectf-plugin).* + +We understand that this increases the complexity of challenge connection, but it provides a way to easily and dynamically allocate TCP endpoints without the need for managing multiple ports on the Load Balancer. + ## Getting help If you need help or have questions regarding CTFp, you can reach out through the following channels: From e573c6389829f7586c1dd3b3fe4daae8451ddd73 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 16:17:04 +0100 Subject: [PATCH 134/148] Clarify TCP endpoint handling and custom port limitations in documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b980f5..bd68c05 100644 --- a/README.md +++ b/README.md @@ -913,8 +913,8 @@ If you challenge require multiple containers, they need to be deployed within th Cluster DNS is not available for challenges, so any service discovery must be handled through external DNS services. Challenges allow for multiple endpoints to be defined, across both HTTP(S) and TCP protocols. -TCP endpoints are handled either through custom Traefik port, or as a SSL TCP endpoint using SNI routing. -Hetzner limits the amount of ports available for Load Balancers, so ensure that you plan accordingly when deploying challenges requiring TCP endpoints. +TCP endpoints are handled either through custom Traefik port (only available for shared TCP challenges), or as a SSL TCP endpoint using SNI routing (recommended). +Hetzner limits the amount of ports available for Load Balancers, so ensure that you plan accordingly when deploying challenges requiring TCP endpoints, using custom ports. *Currently, configuring custom ports for TCP endpoints is not supported through the platform configuration, and must be set up manually after deployment, or manually in the cluster Terraform module.* SSL TCP connections can be made using one of the following command examples: From 58c49080b22f25262034292352cb118a456c26d9 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 16:28:25 +0100 Subject: [PATCH 135/148] Update grammar --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index bd68c05..ba5dbd8 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ > [!TIP] > If you are looking for **how to build challenges for CTFp**, please check out the **[CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template)** and **[CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit)** repositories. -CTFp (CTF Pilot's CTF Platform) is a CTF platform designed to host large-scale Capture The Flag (CTF) competitions, with focus on scalability, resilience and ease of use. +CTFp (CTF Pilot's CTF Platform) is a CTF platform designed to host large-scale Capture The Flag (CTF) competitions, with a focus on scalability, resilience and ease of use. The platform uses Kubernetes as the underlying orchestration system, where both the management, scoreboard and challenge infrastructure are deployed as Kubernetes resources. It then leverages GitOps through [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) for managing the platform's configuration and deployments, including the CTF challenges. -CTFp acts as the orchestration layer for deploying and managing the platform, while utilizing a variety of CTF Pilots components for providing the full functionality of the platform. +CTFp acts as the orchestration layer for deploying and managing the platform, while utilizing a variety of CTF Pilot's components for providing the full functionality of the platform. CTFp provides a CLI tool for managing the deployment of the platform, but it is possible to use the individual Terraform components directly if desired. To further work with the platform after initial deployment, you will primarily interact with the Kubernetes cluster using `kubectl`, ArgoCD and the other monitoring systems deployed. @@ -44,7 +44,7 @@ This platform deploys real world infrastructure, and will incur costs when deplo - [Updating sizes of nodes in a running platform](#updating-sizes-of-nodes-in-a-running-platform) - [Deploying a new challenge](#deploying-a-new-challenge) - [Updating a challenge](#updating-a-challenge) - - [Deplyoing a page](#deplyoing-a-page) + - [Deploying a page](#deploying-a-page) - [The CLI tool does not seem to support my setup](#the-cli-tool-does-not-seem-to-support-my-setup) - [Architecture](#architecture) - [Directory structure](#directory-structure) @@ -524,7 +524,7 @@ The workflow for deploying and managing CTFp can be summarized in the following > [!TIP] > When upgrading existing clusters, it is recommended to drain node pools before changing their sizes, to avoid disruption of running workloads. -> Along with updating one node pool at a time, to minimize the impact on the cluster. +> Update one node pool at a time, to minimize the impact on the cluster. When updating the sizes of nodes in an existing cluster, it is important to follow a specific procedure to ensure a smooth transition and avoid downtime or data loss. Below are the steps to update the sizes of nodes in an existing cluster: @@ -564,7 +564,7 @@ Below are the steps to update the sizes of nodes in an existing cluster: > Changing node sizes can lead to temporary disruption of workloads. > Always ensure that you have backups of critical data before making changes to the cluster configuration. -Changes to the `scale_type` will only affect new nodes being created, and will not resize existing nodes, as the deployment of these nodes are done as resources are needed. +Changes to the `scale_type` will only affect new nodes being created, and will not resize existing nodes, as the deployment of these nodes is done as resources are needed. You may need to manually intervene to resize existing nodes if required, or delete them, forcing the system to create new nodes with the updated sizes. However, this may lead to downtime for workloads running on the nodes being deleted. @@ -584,7 +584,7 @@ Challenges are split into three types: - `shared` - Challenge with a single instance for all teams to connect to. - `instanced` - Challenge with individual instances for each team. -The challenge should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and build using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenge Schema](https://github.com/ctfpilot/challenge-schema). +The challenge should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and built using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenge Schema](https://github.com/ctfpilot/challenge-schema). In the configuration file, you will need to add the challenge under the `Challenges configuration` section. @@ -639,7 +639,7 @@ In order to deploy the new challenge, you need to deploy the `challenges` compon ./ctfp.py deploy challenges -- ``` -Removing a challenge required you to remove it from the configuration file, and then deploy the `challenges` component again. +To remove a challenge, delete it from the configuration file, and then deploy the `challenges` component again. Challenge changes are automatically and continuously deployed through ArgoCD, so no manual intervention is required after the initial deployment. @@ -650,7 +650,7 @@ Challenge updates are handled through the Git repository containing the challeng If a challenges slug has been changed, you need to remove the old slug from the configuration file, and add the new slug. For this, follow the [Deploying a new challenge](#deploying-a-new-challenge) guide. -#### Deplyoing a page +#### Deploying a page To deploy a new page to CTFd, you will need to add the page to a Git repository that should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and build using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Page Schema](https://github.com/ctfpilot/page-schema). @@ -850,7 +850,7 @@ The challenge deployment system, utilizes a combination of GitOps principles and It is built to use [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template) for preparing the challenge definitions, and ArgoCD for deploying the challenge configurations to the Kubernetes cluster. Here, ArgoCD continuously monitors the defined GitHub repository for changes, and automatically applies updates to the cluster. -Static challanges are deployed as configurations for CTFd through [CTFd-manager](https://github.com/ctfpilot/ctfd-manager), while Shared challenges are deployed as single instances through ArgoCD. +Static challenges are deployed as configurations for CTFd through [CTFd-manager](https://github.com/ctfpilot/ctfd-manager), while Shared challenges are deployed as single instances through ArgoCD. Instanced challenges are managed through [KubeCTF](https://github.com/ctfpilot/kube-ctf), where ArgoCD deploys deployment templates to [KubeCTF](https://github.com/ctfpilot/kube-ctf). Container images can be stored in any container registry, as long as the Kubernetes cluster has access to pull the images. @@ -861,7 +861,7 @@ For more information on how to develop challenges, see the [CTF Pilot's Challeng ### Network -To visualize the network architecture of CTFp, the following diagrams provide an overview of both the cluster networking and challenge networking setups. +The following diagrams provide an overview of CTFp's cluster and challenge networking setups #### Cluster networking @@ -897,7 +897,7 @@ Challenges are accessed through the CTF domain, which is specifically designated This load balancer is set up to forward all incoming traffic to the Traefik ingress controllers deployed within the Kubernetes cluster. Traefik supports TCP and HTTP(S) routing, allowing it to handle a wide range of challenge types and protocols. -However, a limited numebr of middlewares are available for TCP routing, so ensure that your challenges are compatible with the available features. +However, a limited number of middleware options are available for TCP routing, so ensure that your challenges are compatible with the available features. IP whitelisting is implemented at the ingress level, allowing challenges to restrict access based on IP addresses or CIDR ranges. @@ -908,7 +908,7 @@ Shared and Instanced challenges are deployed within either `ctfpilot-challenges` The two namespaces are configured with network policies to restrict any outgoing local traffic, allowing only outbound internet access. Challenges can therefore not talk to each other, nor communicate across multiple deployments. -If you challenge require multiple containers, they need to be deployed within the same challenge deployment, and set up in a sidecar pattern. +If your challenge require multiple containers, they need to be deployed within the same challenge deployment, and set up in a sidecar pattern. Cluster DNS is not available for challenges, so any service discovery must be handled through external DNS services. Challenges allow for multiple endpoints to be defined, across both HTTP(S) and TCP protocols. From ac50d96e925f1ada70716498dc9d0135bc0367b5 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 18:47:33 +0100 Subject: [PATCH 136/148] Fix punctuation in networking section of documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ba5dbd8..710863f 100644 --- a/README.md +++ b/README.md @@ -861,7 +861,7 @@ For more information on how to develop challenges, see the [CTF Pilot's Challeng ### Network -The following diagrams provide an overview of CTFp's cluster and challenge networking setups +The following diagrams provide an overview of CTFp's cluster and challenge networking setups. #### Cluster networking From 2debb3831af2ec5b8bba28768b1e49d298f13aab Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 19:03:35 +0100 Subject: [PATCH 137/148] Fix grammar and punctuation in README documentation --- README.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 710863f..53607b1 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,22 @@ > [!TIP] > If you are looking for **how to build challenges for CTFp**, please check out the **[CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template)** and **[CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit)** repositories. -CTFp (CTF Pilot's CTF Platform) is a CTF platform designed to host large-scale Capture The Flag (CTF) competitions, with a focus on scalability, resilience and ease of use. -The platform uses Kubernetes as the underlying orchestration system, where both the management, scoreboard and challenge infrastructure are deployed as Kubernetes resources. It then leverages GitOps through [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) for managing the platform's configuration and deployments, including the CTF challenges. +CTFp (CTF Pilot's CTF Platform) is a CTF platform designed to host large-scale Capture The Flag (CTF) competitions, with a focus on scalability, resilience, and ease of use. +The platform uses Kubernetes as the underlying orchestration system, where the management, scoreboard, and challenge infrastructure are deployed as Kubernetes resources. It then leverages GitOps through [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) for managing the platform's configuration and deployments, including the CTF challenges. -CTFp acts as the orchestration layer for deploying and managing the platform, while utilizing a variety of CTF Pilot's components for providing the full functionality of the platform. +CTFp acts as the orchestration layer for deploying and managing the platform, while utilizing a variety of CTF Pilot's components to provide the full functionality of the platform. -CTFp provides a CLI tool for managing the deployment of the platform, but it is possible to use the individual Terraform components directly if desired. To further work with the platform after initial deployment, you will primarily interact with the Kubernetes cluster using `kubectl`, ArgoCD and the other monitoring systems deployed. +CTFp provides a CLI tool for managing the deployment of the platform, but it is possible to use the individual Terraform components directly if desired. To manage the platform after initial deployment, you will primarily interact with the Kubernetes cluster using `kubectl`, ArgoCD, and the other monitoring systems deployed. > [!IMPORTANT] -> In order to run CTFp properly, you will need to have a working knowledge of **Cloud**, **Kubernetes**, **Terraform/OpenTofu**, **GitOps** and **CTFd**. +> In order to run CTFp properly, you will need to have a working knowledge of **Cloud**, **Kubernetes**, **Terraform/OpenTofu**, **GitOps**, and **CTFd**. > The platform is designed to work with CTF Pilot's Challenges ecosystem, to ensure secure hosting of CTF challenges. > > This platform is not intended for beginners, and it is assumed that you have prior experience with these technologies and systems. -> Incorrect handling of Kubernetes resources can lead to data loss, downtime and security vulnerabilities. +> Incorrect handling of Kubernetes resources can lead to data loss, downtime, and security vulnerabilities. > Incorrectly configured challenges may lead to security vulnerabilities or platform instability. -This platform deploys real world infrastructure, and will incur costs when deployed. +This platform deploys real-world infrastructure and will incur costs when deployed. ## Table of Contents @@ -101,7 +101,7 @@ CTFp offers a wide range of features to facilitate the deployment and management - **Environment management** for handling multiple deployment environments (Test, Dev, Prod) - **State management** with automated backend configuration, with states stored in S3 - **Plan generation and review** before applying changes - - **Sub 20 minute deployment time** for the entire platform (excluding image generation) + - **Under 20 minutes** deployment time for the entire platform (excluding image generation) - **Fully configured through configuration files** for easy setup and management ## Quick start @@ -117,7 +117,7 @@ git clone https://github.com/ctfpilot/ctfp cd ctfp ``` -First you need to initialize the platform configuration for your desired environment (test, dev, prod): +First, you need to initialize the platform configuration for your desired environment (test, dev, prod): ```bash ./ctfp.py init @@ -127,7 +127,7 @@ First you need to initialize the platform configuration for your desired environ > You can add `--test`, `--dev` or `--prod` to specify the environment you want to initialize. > The default environment is `test` (`--test`). > -> Used in all commands, except the `generate-images` command, as it asks for the Hetzner Cloud project to use when generating images. +> Used in all commands except the `generate-images` command, as it asks for the Hetzner Cloud project to use when generating images. Next, you need to fill out the configuration located in the `automated..tfvars` file. @@ -152,7 +152,7 @@ To use the Terraform modules, you need to generate the backend configuration for ./ctfp.py generate-backend challenges ``` -*Replace ``, `` and `` with your S3 bucket details.* +*Replace ``, ``, and `` with your S3 bucket details.* Finally, you can deploy the entire platform with: @@ -185,7 +185,7 @@ In order to even deploy the platform, the following software needs to be install - [OpenTofu](https://opentofu.org) (Alternative version of [Terraform](https://www.terraform.io/downloads.html)) - [Packer](https://developer.hashicorp.com/packer/tutorials/docker-get-started/get-started-install-cli#installing-packer) - For initial generation of server images - [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) - For interacting with the Kubernetes cluster -- [hcloud cli tool](https://github.com/hetznercloud/cli) - For interacting with the Hetzner Cloud API (Otherwise use the Hetzner web interface) +- [hcloud CLI tool](https://github.com/hetznercloud/cli) - For interacting with the Hetzner Cloud API (Otherwise use the Hetzner web interface) - SSH client - For connecting to the servers - Python 3 - For running the CTFp CLI tool - Python package [`python-hcl2`](https://github.com/amplify-education/python-hcl2) - Required by the CTFp CLI tool for parsing Terraform configuration files @@ -194,10 +194,10 @@ And the following is required in order to deploy the platform: - [Hetzner Cloud](https://www.hetzner.com/cloud) account with one or more Hetzner Cloud projects - [Hetzner Cloud API Token](https://console.hetzner.cloud/projects) - For authenticating with the Hetzner Cloud API -- [Hetzner S3 buckets](https://console.hetzner.cloud/projects) - For storing the Terraform state files, backups and challenge data. We recommend using 3 separate buckets with separate access keys for security reasons +- [Hetzner S3 buckets](https://console.hetzner.cloud/projects) - For storing the Terraform state files, backups, and challenge data. We recommend using 3 separate buckets with separate access keys for security reasons - [Cloudflare](https://www.cloudflare.com/) account - [Cloudflare API Token](https://dash.cloudflare.com/profile/api-tokens) - For authenticating with the Cloudflare API -- [3 Cloudflare controlled domains](https://dash.cloudflare.com/) - For allowing the system to allocate a domain for the Kubernetes cluster. Used to allocate management, platform and challenge domains. +- [3 Cloudflare-managed domains](https://dash.cloudflare.com/) - For allowing the system to allocate a domain for the Kubernetes cluster. Used to allocate management, platform, and challenge domains. - SMTP mail server - To allow CTFd to send emails to users (Password resets, notifications, etc.). The system is set up to allow outbound connections to [Brevo](https://brevo.com) SMTP on port 587. - [Discord](https://discord.com) channels to receive notifications. One for monitoring alerts and one for first-blood notifications. - GitHub repository following [CTF Pilot's Challenges template](https://github.com/ctfpilot/challenges-template) for CTF challenges and CTFd pages - A Git repository containing the CTF challenges to be deployed. This should be your own private repository using the CTF Pilot Challenges Template as a base. This may also contain the pages to be used in CTFd. @@ -242,7 +242,7 @@ If the platform is manually changed outside of the CLI tool, the changes will be > > The file can be initialized using the `./ctfp.py init` command. -Each component is not fully configurable, and may in certain situation required advanced configuration. These configurations are not included in the main configuration file. +Each component is not fully configurable, and may in certain situations require advanced configuration. These configurations are not included in the main configuration file. These options are either intended to be static, or require manual configuration through the individual Terraform components. Changing these options may lead to instability or data loss, and should be done with caution. @@ -571,7 +571,7 @@ You may need to manually intervene to resize existing nodes if required, or dele > [!NOTE] > Downscaling nodes may not be possible, depending on the initial size of the nodes and the new size. -Hetzner does not support downsizing nodes, if they were initially created with a larger size. +Hetzner does not support downsizing nodes if they were initially created with a larger size. In such cases, the nodes will need to be deleted, forcing the system to create new nodes with the desired size. #### Deploying a new challenge @@ -647,12 +647,12 @@ Challenge changes are automatically and continuously deployed through ArgoCD, so Challenge updates are handled through the Git repository containing the challenges. -If a challenges slug has been changed, you need to remove the old slug from the configuration file, and add the new slug. +If a challenge's slug has been changed, you need to remove the old slug from the configuration file, and add the new slug. For this, follow the [Deploying a new challenge](#deploying-a-new-challenge) guide. #### Deploying a page -To deploy a new page to CTFd, you will need to add the page to a Git repository that should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and build using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Page Schema](https://github.com/ctfpilot/page-schema). +To deploy a new page to CTFd, you will need to add the page to a Git repository that should be formatted using the [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template), and built using the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Page Schema](https://github.com/ctfpilot/page-schema). In the configuration file, you will need to add the page under the `Pages configuration` section. @@ -682,7 +682,7 @@ Page changes are automatically and continuously deployed through ArgoCD, so no m #### The CLI tool does not seem to support my setup -The CLI tool is designed to cover a wide range of deployment scenarios, but it may be that your specific setup require some custom setup in each Terraform component. +The CLI tool is designed to cover a wide range of deployment scenarios, but it may be that your specific setup requires some customization in each Terraform component. Each component is located in its own directory, and can be deployed manually using OpenTofu/terraform commands. @@ -751,10 +751,10 @@ Specifically, it handles: - **Cluster provisioning**: Creating and configuring the Kubernetes cluster using Hetzner Cloud resources. - **Node management**: Setting up and managing the worker nodes that will run the workloads. - This including configuring node pools, scaling, and updating nodes as needed, along with setting up the node-autoscaler for automatic scaling based on demand. + This includes configuring node pools, scaling, and updating nodes as needed, along with setting up the node-autoscaler for automatic scaling based on demand. - **Networking**: Configuring the network settings to ensure proper communication between cluster components. This includes setting up a private network, configuring VPN connectivity between the nodes and setting up Flannel CNI for pod networking. - Opens up required firewall rules to allow communication between nodes, and outbound connections to required services. + It opens the required firewall rules to allow communication between nodes, and outbound connections to required services. - **Storage**: Setting up storage controller (CSI) to use Hetzner Block storage volumes. - **Traefik proxy**: Deploying Traefik as the ingress controller for managing incoming traffic to the cluster. @@ -786,13 +786,13 @@ Specifically, it deploys the following: - **ArgoCD**: GitOps continuous delivery tool used to deploy and manage applications within the Kubernetes cluster. ArgoCD continuously synchronizes the cluster state with Git repositories, enabling declarative infrastructure management. - **Cert-manager**: Certificate management system for automating TLS/SSL certificate provisioning and renewal. It integrates with Cloudflare for DNS validation challenges. -- **Traefik configuration**: Deploys additional Helm chart configuration for the Traefik ingress controller already present in the cluster, enabling advanced routing and middleware features, along with additonal logging with filebeat log aggregation. +- **Traefik configuration**: Deploys additional Helm chart configuration for the Traefik ingress controller already present in the cluster, enabling advanced routing and middleware features, along with additional logging with Filebeat log aggregation. - **Descheduler**: Continuously rebalances the cluster by evicting workloads from nodes, ensuring optimal resource utilization and distribution across available nodes. - **Error Fallback**: Deploys [CTF Pilot's Error Fallback](https://github.com/ctfpilot/error-fallback) page service, providing custom error pages for HTTP error responses (e.g., 404, 502, 503). - **Filebeat**: Log aggregation and forwarding system that sends logs to Elasticsearch or other log aggregation services, enabling centralized logging and analysis. - **MariaDB Operator**: Kubernetes operator for managing MariaDB database instances. Allows automated provisioning, scaling, and management of MySQL-compatible databases. - **Redis Operator**: Kubernetes operator for managing Redis cache instances. Enables automated deployment and management of Redis clusters for caching and data storage. -- **Prometheus & Grafana Stack**: Comprehensive monitoring and visualization solution. Prometheus scrapes metrics from cluster components, while Grafana provides dashboards for monitoring cluster health, resource usage, and application performance. Custom dashboards for Kuberenetes, CTFd, and KubeCTF are included. +- **Prometheus & Grafana Stack**: Comprehensive monitoring and visualization solution. Prometheus scrapes metrics from cluster components, while Grafana provides dashboards for monitoring cluster health, resource usage, and application performance. Custom dashboards for Kubernetes, CTFd, and KubeCTF are included. - **Alertmanager**: Alerting system integrated with Prometheus, used to send notifications based on defined alerting rules. Configured to send alerts to Discord channels for monitoring purposes. #### Platform @@ -813,8 +813,8 @@ Specifically, it deploys the following: - **Traefik ingress configuration**: Sets up ingress routing rules to expose CTFd and related services through the Traefik ingress controller. - **Initial CTFd setup**: Configures initial CTFd settings, such as competition name, start/end times, and other global settings using [CTFd-manager](https://github.com/ctfpilot/ctfd-manager). -The Platform automatically sets up Kubernetes secrets and configurations for the components deployed, so that these information is not required to be tracked within Git. -This means, that critical secrets are stored within Kubernetes secrets once the Platform component is deployed. +The Platform automatically sets up Kubernetes secrets and configurations for the components deployed, so that this information is not required to be tracked within Git. +This means that critical secrets are stored within Kubernetes secrets once the Platform component is deployed. Backups of the database are automatically created and stored in the configured S3 storage, allowing for disaster recovery and data retention. Currently backups are configured to run every 15 minutes, and retained for 30 days. Backups are stored as cleartext SQL dump files, so ensure that the S3 storage has proper access policies in place to prevent unauthorized access. @@ -839,7 +839,7 @@ Specifically, it manages the following: Challenges are deployed and managed through Git repositories, with configurations defined in challenge definition files. Use the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template) for challenge development. -Per default, the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) deployment templates use taints to control which nodes challenge instances are scheduled on. Therefore, the cluster must have at least one node with the taint `cluster.ctfpilot.com/node=scaler:PreferNoSchedule` if using Instanced challenges, to ensure challenge instances are properly scheduled and deployed. +By default, the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) deployment templates use taints to control which nodes challenge instances are scheduled on. Therefore, the cluster must have at least one node with the taint `cluster.ctfpilot.com/node=scaler:PreferNoSchedule` if using Instanced challenges, to ensure challenge instances are properly scheduled and deployed. ### Challenge deployment @@ -854,7 +854,7 @@ Static challenges are deployed as configurations for CTFd through [CTFd-manager] Instanced challenges are managed through [KubeCTF](https://github.com/ctfpilot/kube-ctf), where ArgoCD deploys deployment templates to [KubeCTF](https://github.com/ctfpilot/kube-ctf). Container images can be stored in any container registry, as long as the Kubernetes cluster has access to pull the images. -Per default, pull secrets are configured for GitHub Container Registry, and are currently **not** configurable through the platform configuration. +By default, pull secrets are configured for GitHub Container Registry, and are currently **not** configurable through the platform configuration. Any additional pull secrets must be created manually in the cluster, and referenced in the challenge deployment configuration. For more information on how to develop challenges, see the [CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit) and [CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template). An example challenges repository can be found at [CTF Pilot's Challenges example repository](https://github.com/ctfpilot/challenges-example). @@ -892,7 +892,7 @@ Network is shared between nodes using Hetzner Cloud's private networking, ensuri ![CTFp Challenge Networking Overview](./docs/attachments/architecture/challenge-network-architecture.svg) As described in the [Cluster networking](#cluster-networking) section, CTFp utilizes three main domains for different purposes. -Challenges are accessed through the CTF domain, which is specifically designated for hosting and serving challenge instances, and are therefore not proxied through Cloudflare, rather point directly to the Hetzner Cloud Load Balancers. +Challenges are accessed through the CTF domain, which is specifically designated for hosting and serving challenge instances, and are therefore not proxied through Cloudflare; they point directly to the Hetzner Cloud Load Balancers. This load balancer is set up to forward all incoming traffic to the Traefik ingress controllers deployed within the Kubernetes cluster. @@ -908,13 +908,13 @@ Shared and Instanced challenges are deployed within either `ctfpilot-challenges` The two namespaces are configured with network policies to restrict any outgoing local traffic, allowing only outbound internet access. Challenges can therefore not talk to each other, nor communicate across multiple deployments. -If your challenge require multiple containers, they need to be deployed within the same challenge deployment, and set up in a sidecar pattern. +If your challenge requires multiple containers, they need to be deployed within the same challenge deployment, and set up in a sidecar pattern. Cluster DNS is not available for challenges, so any service discovery must be handled through external DNS services. Challenges allow for multiple endpoints to be defined, across both HTTP(S) and TCP protocols. -TCP endpoints are handled either through custom Traefik port (only available for shared TCP challenges), or as a SSL TCP endpoint using SNI routing (recommended). -Hetzner limits the amount of ports available for Load Balancers, so ensure that you plan accordingly when deploying challenges requiring TCP endpoints, using custom ports. +TCP endpoints are handled either through a custom Traefik port (only available for shared TCP challenges), or as an SSL TCP endpoint using SNI routing (recommended). +Hetzner limits the number of ports available for Load Balancers, so ensure that you plan accordingly when deploying challenges requiring TCP endpoints using custom ports. *Currently, configuring custom ports for TCP endpoints is not supported through the platform configuration, and must be set up manually after deployment, or manually in the cluster Terraform module.* SSL TCP connections can be made using one of the following command examples: @@ -957,9 +957,9 @@ To administrate the CLA signing process, we are using **[CLA assistant lite](htt ## Background -CTF Pilot started as a CTF Platform project, originating in **[Brunnerne](https://github.com/brunnerne)**. +CTF Pilot started as a CTF platform project, originating in **[Brunnerne](https://github.com/brunnerne)**. -The goal of the project, is to provide a scalable, resilient and easy to use CTF platform for hosting large scale Capture The Flag competitions, starting with BrunnerCTF 2025. +The goal of the project is to provide a scalable, resilient, and easy-to-use CTF platform for hosting large-scale Capture The Flag competitions, starting with BrunnerCTF 2025. The project is still in active development, and we welcome contributions from the community to help improve and expand the platform's capabilities. From afb665cb96e1e90ce84b66171fb97c3b98a3cc08 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sat, 27 Dec 2025 19:08:22 +0100 Subject: [PATCH 138/148] Fix spelling errors in resource descriptions in tfvars templates --- cluster/tfvars/template.tfvars | 4 ++-- template.automated.tfvars | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cluster/tfvars/template.tfvars b/cluster/tfvars/template.tfvars index cb7edab..e8cb3cf 100644 --- a/cluster/tfvars/template.tfvars +++ b/cluster/tfvars/template.tfvars @@ -49,8 +49,8 @@ network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central # Control planes are the servers that run the Kubernetes control plane, and are responsible for managing the cluster. # Agents are the servers that run the workloads, and scale is used to scale the cluster up or down dynamically. # Challs are the servers that run the CTF challenges. -# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically if there is not enough ressources in the cluster. -# Challs and scale nodes are placed in region_1, and are tainted to make normal ressources prefer agent nodes, but allow scheduling on challs and scale nodes if needed. +# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically if there is not enough resources in the cluster. +# Challs and scale nodes are placed in region_1, and are tainted to make normal resources prefer agent nodes, but allow scheduling on challs and scale nodes if needed. # Server types. See https://www.hetzner.com/cloud # Control plane nodes - Nodes that run the Kubernetes control plane components. diff --git a/template.automated.tfvars b/template.automated.tfvars index ce3ce5c..0ee1eb3 100644 --- a/template.automated.tfvars +++ b/template.automated.tfvars @@ -28,8 +28,8 @@ network_zone = "eu-central" # Hetzner network zone. Possible values: "eu-central # Control planes are the servers that run the Kubernetes control plane, and are responsible for managing the cluster. # Agents are the servers that run the workloads, and scale is used to scale the cluster up or down dynamically. # Challs are the servers that run the CTF challenges. -# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically if there is not enough ressources in the cluster. -# Challs and scale nodes are placed in region_1, and are tainted to make normal ressources prefer agent nodes, but allow scheduling on challs and scale nodes if needed. +# Scale is automatically scaled agent nodes, which is handled by the cluster autoscaler. It is optional, and can be used to scale the cluster up or down dynamically if there is not enough resources in the cluster. +# Challs and scale nodes are placed in region_1, and are tainted to make normal resources prefer agent nodes, but allow scheduling on challs and scale nodes if needed. # Server types. See https://www.hetzner.com/cloud # Control plane nodes - Nodes that run the Kubernetes control plane components. From 9ce3853d65e32cf81244db2931a10bcb9732d05e Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 28 Dec 2025 09:12:41 +0100 Subject: [PATCH 139/148] docs: add links to command list tip --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 53607b1..e192331 100644 --- a/README.md +++ b/README.md @@ -290,13 +290,13 @@ Both methods are functionally equivalent. The direct execution method (first exa > > Available commands: > -> - `init` - Initialize Platform Configuration -> - `generate-keys` - Generate SSH Keys -> - `insert-keys` - Insert SSH Keys into Configuration -> - `generate-images` - Generate Custom Server Images -> - `generate-backend` - Generate Terraform Backend Configuration -> - `deploy` - Deploy Platform Components -> - `destroy` - Destroy Platform Components +> - [`init`](#init---initialize-platform-configuration) - Initialize Platform Configuration +> - [`generate-keys`](#generate-keys---generate-ssh-keys) - Generate SSH Keys +> - [`insert-keys`](#insert-keys---insert-ssh-keys-into-configuration) - Insert SSH Keys into Configuration +> - [`generate-images`](#generate-images---generate-custom-server-images) - Generate Custom Server Images +> - [`generate-backend`](#generate-backend---generate-terraform-backend-configuration) - Generate Terraform Backend Configuration +> - [`deploy`](#deploy---deploy-platform-components) - Deploy Platform Components +> - [`destroy`](#destroy---destroy-platform-components) - Destroy Platform Components Below is a detailed overview of each available command: From 87d5b3dec3c37ddd8bfb6220de5820f44d409454 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Sun, 28 Dec 2025 21:47:16 +0100 Subject: [PATCH 140/148] docs: add restore guides for database and CTFd-manager --- README.md | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/README.md b/README.md index e192331..189861d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ This platform deploys real-world infrastructure and will incur costs when deploy - [Updating a challenge](#updating-a-challenge) - [Deploying a page](#deploying-a-page) - [The CLI tool does not seem to support my setup](#the-cli-tool-does-not-seem-to-support-my-setup) + - [Restoring the database from a backup](#restoring-the-database-from-a-backup) + - [Restoring the CTFd-manager](#restoring-the-ctfd-manager) - [Architecture](#architecture) - [Directory structure](#directory-structure) - [Overview](#overview) @@ -691,6 +693,105 @@ However, be aware that the CLI tool also manages the Terraform backend configura Documentation is located within each component directory, explaining the configuration options and how to deploy the component manually. A template tfvars file is also located in each component directory in `tfvars/template.tfvars`, explaining the configuration options available for that component. +#### Restoring the database from a backup + +By default, the platform is set up to create automated backups of the database every 15 minutes, and store them in the configured S3 bucket. + +You can restore the database from any available backup by timestamp. + +To restore the database from a backup, follow these steps: + +1. **Identify the Backup**: Determine the timestamp of the backup you want to restore from. Backups are stored in the S3 bucket specified in the configuration file, under the `s3_bucket` setting. You can list the backups using your S3 management tool or CLI. +2. **Create a restore resource**: The MariaDB operator provides an easy-to-use restore resource that can be used to restore the database from a backup. + Create a YAML file named `mariadb-restore.yaml` with the following content, replacing `` with the timestamp of the backup you want to restore from: + + ```yaml + apiVersion: k8s.mariadb.com/v1alpha1 + kind: Restore + metadata: + name: restore + namespace: db + spec: + mariaDbRef: + name: ctfd-db + namespace: db + backupRef: + name: db-backup-ctfd-db + targetRecoveryTime: 2025-07-17T20:25:00Z + ``` + + Replace the `targetRecoveryTime` value with the desired timestamp in [RFC 3339 format](https://www.ietf.org/rfc/rfc3339.txt). The time does not need to be exact, as the restore operation will restore to the nearest available backup before the specified time. + + *This requires the platform to be running, with the database operator and platform component both deployed, as this will set up the necessary resources for the restore operation.* +3. **Apply the restore resource**: Apply the restore resource using `kubectl`: + + ```bash + kubectl apply -f mariadb-restore.yaml + ``` + +4. **Monitor the restore process**: Monitor the restore process by checking the status of the restore resource: + + ```bash + kubectl -n db get restore + ``` + +5. **Cleanup**: Once the restore is complete, you can delete the restore resource: + + ```bash + kubectl -n db delete -f mariadb-restore.yaml + ``` + +If you are restoring the full platform, you need to first deploy the `cluster`, `ops`, `platform`, and `challenges` components, before applying the restore resource. +After this, follow the ["Restoring the CTFd-manager"](#restoring-the-ctfd-manager) guide to restore the CTFd-manager data. + +If you want to restore the database to another MariaDB instance, you can copy the backup files from the S3 bucket, and use the MariaDB tools to restore the database manually. +The backup files are cleartext SQL dump files. + +#### Restoring the CTFd-manager + +The CTFd-manager is responsible for maintaining page and challenge states within CTFd, and has local configuration to keep track of what challenges are deployed and their IDs within CTFd. +To ensure there does not exist a disconnect, and the manager can correctly connect and manage the challenges, it is important to restore the CTFd-manager data alongside the database. + +You must manually update the challenge IDs in the challenge manager. +In order to do this, the following flow can be used: + +1. Retrieve the current challenge-id mapping from the ctfd-manager + + ```sh + kubectl -n challenge-config get configmap ctfd-challenges -o yaml > challenges.yaml + ``` + +2. Open the `challenges.yaml` file and update the challenge ids. (See CTFd dashboard for challenge names and IDs) + +3. Apply the updated challenge mapping: + + ```sh + kubectl -n challenge-config apply -f challenges.yaml + ``` + +4. Generate new access token for the CTFd manager. This is done on the admin user in CTFd. +5. Update the access token in the secrets for the CTFd manager: + + ```sh + kubectl -n challenge-config edit configmap ctfd-access-token + ``` + +6. Replace the `token` value with the new access token generated in step 4. + +7. Restart the ctfd-manager to ensure it picks up the new configs: + + ```sh + kubectl -n challenge-config rollout restart deployment ctfd-manager + ``` + + *If it does not pick up the data, you can empty out the `challenge-configmap-hashset` configmap to force a reload.* + +The CTFd manager is now updated with the new challenge IDs and access token. +The system should therefore self-heal with files and missing elements of the challenges. + +If you are restoring the full platform, you need to first deploy the `cluster`, `ops`, `platform`, and `challenges` components, before applying the restore resource. +*You need to restore the CTFd-manager after restoring the database. You may restore the CTFd-manager before deploying the `challenges` component, but the configmap `ctfd-challenges` will then be empty, and you will need to manually format it.* + ## Architecture CTFp is composed of four main components, each responsible for different aspects of the platform's functionality: From 5ea1c581ef85a3cede1bc1b565cdfdc102731440 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Wed, 7 Jan 2026 13:04:28 +0100 Subject: [PATCH 141/148] fix: correct spelling of 'timeout' in mariadb-operator configuration --- ops/mariadb-operator.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ops/mariadb-operator.tf b/ops/mariadb-operator.tf index 3801b3c..99ac19f 100644 --- a/ops/mariadb-operator.tf +++ b/ops/mariadb-operator.tf @@ -13,7 +13,7 @@ resource "helm_release" "mariadb-operator-crds" { chart = "mariadb-operator-crds" version = var.mariadb_operator_version - // timeot 10min + // timeout 10min timeout = 600 // Force use of longhorn storage class @@ -36,7 +36,7 @@ resource "helm_release" "mariadb-operator" { chart = "mariadb-operator" version = var.mariadb_operator_version - # timeot 10min + # timeout 10min timeout = 600 // Force use of longhorn storage class From 6687532808f5086a5d0d75b0fe41162ecb640934 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Wed, 7 Jan 2026 13:19:41 +0100 Subject: [PATCH 142/148] fix: update default server type from 'cx32' to 'cx33' to match currently available servers from Hetzner --- cluster/variables.tf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cluster/variables.tf b/cluster/variables.tf index 4f326af..f0feb5d 100644 --- a/cluster/variables.tf +++ b/cluster/variables.tf @@ -105,48 +105,48 @@ variable "network_zone" { variable "control_plane_type_1" { type = string description = "Control plane group 1 server type" - default = "cx32" + default = "cx33" } variable "control_plane_type_2" { type = string description = "Control plane group 2 server type" - default = "cx32" + default = "cx33" } variable "control_plane_type_3" { type = string description = "Control plane group 3 server type" - default = "cx32" + default = "cx33" } variable "agent_type_1" { type = string description = "Agent group 1 server type" - default = "cx32" + default = "cx33" } variable "agent_type_2" { type = string description = "Agent group 2 server type" - default = "cx32" + default = "cx33" } variable "agent_type_3" { type = string description = "Agent group 3 server type" - default = "cx32" + default = "cx33" } variable "challs_type" { type = string description = "CTF challenge nodes server type" - default = "cx32" + default = "cx33" } variable "scale_type" { type = string description = "Scale group server type" - default = "cx32" + default = "cx33" } variable "load_balancer_type" { From 43ab200a65839460d03e11c43cc700a113e63ea7 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Wed, 7 Jan 2026 13:20:13 +0100 Subject: [PATCH 143/148] fix: add 'challs_type' to CLUSTER_TFVARS to correctly set variables --- ctfp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ctfp.py b/ctfp.py index b5f1137..bc1b040 100755 --- a/ctfp.py +++ b/ctfp.py @@ -53,6 +53,7 @@ "agent_type_1", "agent_type_2", "agent_type_3", + "challs_type", "scale_type", "control_plane_count_1", "control_plane_count_2", From bb5f3507538f8f6350f771505485c3a58ca642e7 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Wed, 7 Jan 2026 14:03:41 +0100 Subject: [PATCH 144/148] fix: update team instances panel configuration for correct team ID handling --- .../dashboards/ctf/team-intsances.json | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ops/prometheus/grafana/dashboards/ctf/team-intsances.json b/ops/prometheus/grafana/dashboards/ctf/team-intsances.json index 096ebe9..38121cd 100644 --- a/ops/prometheus/grafana/dashboards/ctf/team-intsances.json +++ b/ops/prometheus/grafana/dashboards/ctf/team-intsances.json @@ -18,7 +18,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 56, + "id": 14, "links": [], "panels": [ { @@ -108,14 +108,14 @@ }, "editorMode": "code", "exemplar": false, - "expr": "count by (deployment) (\r\n kube_deployment_labels{\r\n namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\",\r\n label_instanced_challenges_ctfpilot_com_deployment!=\"\",\r\n label_instanced_challenges_ctfpilot_com_owner=\"276\"\r\n }\r\n)", + "expr": "count by (deployment) (\r\n kube_deployment_labels{\r\n namespace=~\"ctfpilot-challenges-instanced|ctfpilot-challenges\",\r\n label_instanced_challenges_ctfpilot_com_deployment!=\"\",\r\n label_instanced_challenges_ctfpilot_com_owner=\"$teamid\"\r\n }\r\n)", "instant": false, "legendFormat": "__auto", "range": true, "refId": "A" } ], - "title": "Panel Title", + "title": "Team instances", "transformations": [ { "id": "rowsToFields", @@ -133,9 +133,13 @@ "list": [ { "current": { - "selected": false, - "text": "100", - "value": "100" + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] }, "datasource": { "type": "prometheus", @@ -143,9 +147,9 @@ }, "definition": "label_values(label_instanced_challenges_ctfpilot_com_owner)", "hide": 0, - "includeAll": false, + "includeAll": true, "label": "Team id", - "multi": false, + "multi": true, "name": "teamid", "options": [], "query": { From 7d828ae6559efd1d3af6346e785c240018dcce2a Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Wed, 7 Jan 2026 15:05:48 +0100 Subject: [PATCH 145/148] fix(monitoring): update node usage dashboard configuration for improved metrics display --- .../grafana/dashboards/ctf/node-usage.json | 356 +++++++++++++++++- 1 file changed, 336 insertions(+), 20 deletions(-) diff --git a/ops/prometheus/grafana/dashboards/ctf/node-usage.json b/ops/prometheus/grafana/dashboards/ctf/node-usage.json index 751e8fc..2151df2 100644 --- a/ops/prometheus/grafana/dashboards/ctf/node-usage.json +++ b/ops/prometheus/grafana/dashboards/ctf/node-usage.json @@ -18,7 +18,7 @@ "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, - "id": 53, + "id": 13, "links": [], "panels": [ { @@ -39,6 +39,7 @@ "type": "prometheus", "uid": "prometheus" }, + "description": "System uptime", "fieldConfig": { "defaults": { "color": { @@ -96,7 +97,7 @@ }, "gridPos": { "h": 13, - "w": 8, + "w": 12, "x": 0, "y": 1 }, @@ -124,9 +125,22 @@ "legendFormat": "Memory Usage", "range": true, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (node_memory_MemTotal_bytes{instance=~\"$node\"} - node_memory_MemAvailable_bytes{instance=~\"$node\"}) / (1024*1024*1024)", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" } ], - "title": "Total Memory Usage (GB)", + "title": "Uptime", "type": "timeseries" }, { @@ -191,8 +205,8 @@ }, "gridPos": { "h": 13, - "w": 9, - "x": 8, + "w": 12, + "x": 12, "y": 1 }, "id": 1, @@ -219,6 +233,19 @@ "legendFormat": "CPU Usage", "range": true, "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (rate(node_cpu_seconds_total{mode!=\"idle\", instance=~\"$node\"}[5m]))", + "hide": false, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "B" } ], "title": "Total CPU Usage (Cores)", @@ -236,13 +263,302 @@ "panels": [], "repeat": "node", "repeatDirection": "h", - "title": "Per-Node Breakdown", + "title": "Node - $node", "type": "row" }, { "datasource": { "type": "prometheus", - "uid": "YOUR_DS_UID" + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 15 + }, + "id": 25, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^nodename$/", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "max by (nodename) (node_uname_info{instance=~\"$node\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Node name", + "transformations": [ + { + "id": "labelsToFields", + "options": {} + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "k3s-agents-1-dxg": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 4, + "y": 15 + }, + "id": 26, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "count by (cluster, node) (node_cpu_seconds_total{instance=~\"$node\",mode=\"idle\"} * on (cluster, namespace, pod) group_left (node) topk by (cluster, namespace, pod) (1, node_namespace_pod:kube_pod_info:))", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total CPU", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 7, + "y": 15 + }, + "id": 27, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by (cluster) (node_memory_MemTotal_bytes{instance=~\"$node\"})", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Total RAM", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "System uptime", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 10, + "y": 15 + }, + "id": 64, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.1.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_time_seconds{instance=~\"$node\"} - node_boot_time_seconds{instance=~\"$node\"}", + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -300,10 +616,10 @@ "overrides": [] }, "gridPos": { - "h": 3, - "w": 6, + "h": 8, + "w": 12, "x": 0, - "y": 15 + "y": 19 }, "id": 3, "options": { @@ -331,7 +647,7 @@ { "datasource": { "type": "prometheus", - "uid": "YOUR_DS_UID" + "uid": "prometheus" }, "fieldConfig": { "defaults": { @@ -389,10 +705,10 @@ "overrides": [] }, "gridPos": { - "h": 3, - "w": 6, - "x": 0, - "y": 18 + "h": 8, + "w": 12, + "x": 12, + "y": 19 }, "id": 4, "options": { @@ -428,12 +744,12 @@ "list": [ { "current": { - "selected": true, + "selected": false, "text": [ - "10.0.0.101:9100" + "All" ], "value": [ - "10.0.0.101:9100" + "$__all" ] }, "datasource": { @@ -442,7 +758,7 @@ }, "definition": "label_values(node_cpu_seconds_total,instance)", "hide": 0, - "includeAll": false, + "includeAll": true, "multi": true, "name": "node", "options": [], @@ -467,6 +783,6 @@ "timezone": "", "title": "Kubernetes Nodes - CPU & Memory (by Label)", "uid": "k8s-nodes-extended2", - "version": 4, + "version": 12, "weekStart": "" } \ No newline at end of file From cba216ea5d5d57d39d8c252c863d449da661f375 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Wed, 7 Jan 2026 15:07:46 +0100 Subject: [PATCH 146/148] refactor: correct spelling of 'Traefik' in ingress.tf comments --- ops/ingress.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ops/ingress.tf b/ops/ingress.tf index a9754d7..a37f886 100644 --- a/ops/ingress.tf +++ b/ops/ingress.tf @@ -22,7 +22,7 @@ resource "kubernetes_secret" "traefik_basic_auth" { ] } -# Traefic basic auth middleware +# Traefik basic auth middleware resource "kubernetes_manifest" "traefik_basic_auth" { manifest = { apiVersion = "traefik.io/v1alpha1" From 1d531c2dec004b348565d3428ac7a498ac042990 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 26 Jan 2026 20:55:12 +0100 Subject: [PATCH 147/148] docs: add link to CTF Pilot organization page in README for ecosystem overview --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 189861d..c41eb78 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ > [!TIP] > If you are looking for **how to build challenges for CTFp**, please check out the **[CTF Pilot's Challenges Template](https://github.com/ctfpilot/challenges-template)** and **[CTF Pilot's Challenge Toolkit](https://github.com/ctfpilot/challenge-toolkit)** repositories. +> +> To learn more about the ecosystem, and view all related repositories, please visit the **[CTF Pilot organization page](https://github.com/ctfpilot)**. CTFp (CTF Pilot's CTF Platform) is a CTF platform designed to host large-scale Capture The Flag (CTF) competitions, with a focus on scalability, resilience, and ease of use. The platform uses Kubernetes as the underlying orchestration system, where the management, scoreboard, and challenge infrastructure are deployed as Kubernetes resources. It then leverages GitOps through [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) for managing the platform's configuration and deployments, including the CTF challenges. From fc7fca78601cdedf461bee015edeccfbab004437 Mon Sep 17 00:00:00 2001 From: The0Mikkel Date: Mon, 26 Jan 2026 21:28:52 +0100 Subject: [PATCH 148/148] fix(grafana): update team instances dashboard file name --- .../dashboards/ctf/{team-intsances.json => team-instances.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ops/prometheus/grafana/dashboards/ctf/{team-intsances.json => team-instances.json} (100%) diff --git a/ops/prometheus/grafana/dashboards/ctf/team-intsances.json b/ops/prometheus/grafana/dashboards/ctf/team-instances.json similarity index 100% rename from ops/prometheus/grafana/dashboards/ctf/team-intsances.json rename to ops/prometheus/grafana/dashboards/ctf/team-instances.json