From 67ef963667d31c752ac950e71f5d73c2de9227d1 Mon Sep 17 00:00:00 2001 From: Stavros Kois <47820033+stavros-k@users.noreply.github.com> Date: Thu, 15 Aug 2024 13:57:17 +0300 Subject: [PATCH] Add minio enterprise (#8) * test ci * make sure if docker daemon does not start any containers, we fail * fix image * add minio enterprise * test perms * update lib funcs * named arsg * fix indents * imports * fix identation * check if db is needed * add todo * one omre * add dep * fix * add macros * auto perms * remove dummy file * fix values * updates * minio perms * abstract away * cleaner * safer,cleaner * new storage funcs * storage options * use built ins * rename * spelling * rename * add vol suport * test vol * manually add container name * volumes * cleaner vols * do some renames * renames * squash * add todo * move few things to the lib, currently broken * more dynamic container names * fix message * cleanup * update app * spelling * remove port * more fixes * fix pg test * hm * thats better * fmt * back to jinja * duh * wait for it * typo * typo * ui * fix checks * todo * items is a builtin * fixes * resources * resources * -.- * ... * use lib * add rough migration paths * Update docker-compose.yaml * Update docker-compose.yaml * move usages under value * update usages * cleanup * fix url * order * another url * fix * fix hc too * update app.yaml * update lib * update lib * update lib * update lib * update lib * update lib * update lib * update lib * lint * update lib * fix ixvol * update lib * update lib * update lib * remove logsearch/postgres * fmt * replace set _ * update lib * adapt to lib changes * update lib * update lib * update lib * update lib * update lib * update lib * sync library * updates * update lib * update compose * typo * update ci checks * now actually fix app * add migration * fix migration and update error message * order * remove test data * update readme * fix ui --- .github/scripts/ci.py | 78 +++- README.md | 2 +- cspell.config.yaml | 8 +- ix-dev/enterprise/minio/README.md | 5 + ix-dev/enterprise/minio/app.yaml | 34 ++ ix-dev/enterprise/minio/item.yaml | 11 + ix-dev/enterprise/minio/ix_values.yaml | 8 + .../minio/migrations/migrate_from_kubernetes | 58 +++ .../migrations/migration_helpers/__init__.py | 0 .../minio/migrations/migration_helpers/cpu.py | 27 ++ .../migration_helpers/dns_config.py | 9 + .../migration_helpers/kubernetes_secrets.py | 15 + .../migrations/migration_helpers/memory.py | 49 +++ .../migrations/migration_helpers/resources.py | 59 +++ .../migrations/migration_helpers/storage.py | 115 ++++++ ix-dev/enterprise/minio/questions.yaml | 286 ++++++++++++++ .../minio/templates/docker-compose.yaml | 109 ++++++ .../templates/library/base_v1_0_0/__init__.py | 0 .../library/base_v1_0_0/environment.py | 90 +++++ .../library/base_v1_0_0/healthchecks.py | 110 ++++++ .../templates/library/base_v1_0_0/metadata.py | 71 ++++ .../templates/library/base_v1_0_0/network.py | 21 + .../templates/library/base_v1_0_0/ports.py | 42 ++ .../templates/library/base_v1_0_0/postgres.py | 77 ++++ .../templates/library/base_v1_0_0/redis.py | 49 +++ .../library/base_v1_0_0/resources.py | 87 +++++ .../templates/library/base_v1_0_0/security.py | 27 ++ .../templates/library/base_v1_0_0/storage.py | 363 ++++++++++++++++++ .../templates/library/base_v1_0_0/utils.py | 83 ++++ .../enterprise/minio/v1_0_0/__init__.py | 0 .../library/enterprise/minio/v1_0_0/data.py | 63 +++ .../macros/global/perms/container.yaml.jinja | 48 +++ .../macros/global/perms/script.sh.jinja | 75 ++++ .../test_values/basic-multi-mode-values.yaml | 58 +++ .../templates/test_values/basic-values.yaml | 33 ++ .../templates/test_values/https-values.yaml | 120 ++++++ 36 files changed, 2276 insertions(+), 14 deletions(-) create mode 100644 ix-dev/enterprise/minio/README.md create mode 100644 ix-dev/enterprise/minio/app.yaml create mode 100644 ix-dev/enterprise/minio/item.yaml create mode 100644 ix-dev/enterprise/minio/ix_values.yaml create mode 100755 ix-dev/enterprise/minio/migrations/migrate_from_kubernetes create mode 100644 ix-dev/enterprise/minio/migrations/migration_helpers/__init__.py create mode 100644 ix-dev/enterprise/minio/migrations/migration_helpers/cpu.py create mode 100644 ix-dev/enterprise/minio/migrations/migration_helpers/dns_config.py create mode 100644 ix-dev/enterprise/minio/migrations/migration_helpers/kubernetes_secrets.py create mode 100644 ix-dev/enterprise/minio/migrations/migration_helpers/memory.py create mode 100644 ix-dev/enterprise/minio/migrations/migration_helpers/resources.py create mode 100644 ix-dev/enterprise/minio/migrations/migration_helpers/storage.py create mode 100644 ix-dev/enterprise/minio/questions.yaml create mode 100644 ix-dev/enterprise/minio/templates/docker-compose.yaml create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/__init__.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/environment.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/healthchecks.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/metadata.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/network.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/ports.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/postgres.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/redis.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/resources.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/security.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/storage.py create mode 100644 ix-dev/enterprise/minio/templates/library/base_v1_0_0/utils.py create mode 100644 ix-dev/enterprise/minio/templates/library/enterprise/minio/v1_0_0/__init__.py create mode 100644 ix-dev/enterprise/minio/templates/library/enterprise/minio/v1_0_0/data.py create mode 100644 ix-dev/enterprise/minio/templates/macros/global/perms/container.yaml.jinja create mode 100644 ix-dev/enterprise/minio/templates/macros/global/perms/script.sh.jinja create mode 100644 ix-dev/enterprise/minio/templates/test_values/basic-multi-mode-values.yaml create mode 100644 ix-dev/enterprise/minio/templates/test_values/basic-values.yaml create mode 100644 ix-dev/enterprise/minio/templates/test_values/https-values.yaml diff --git a/.github/scripts/ci.py b/.github/scripts/ci.py index 54ab029a46..2691c9a027 100755 --- a/.github/scripts/ci.py +++ b/.github/scripts/ci.py @@ -229,6 +229,69 @@ def get_parsed_containers(): return parsed_containers +def status_indicates_healthcheck_existence(container): + """Assumes healthcheck exists if status contains "health" """ + # eg "health: starting". This happens right after a container is started or restarted + return "health" in container.get("Status", "") + + +def state_indicates_restarting(container): + """Assumes restarting if state is "restarting" """ + return container.get("State", "") == "restarting" + + +def exit_code_indicates_normal_exit(container): + """Assumes normal exit if there is no exit code or if it is 0""" + return container.get("ExitCode", 0) == 0 + + +def health_indicates_healthy(container): + """Assumes healthy if there is no health status or if it is "healthy" """ + health = container.get("Health", "") + if health in ["healthy", ""]: + return True + return False + + +def is_considered_healthy(container): + message = [ + f"Skipping container [{container['Name']}({container['ID']})] with status [{container.get('State')}]" + + " for the following reasons:" + ] + reasons = [] + + if health_indicates_healthy(container): + reasons.append("\t- Container is healthy") + + if exit_code_indicates_normal_exit(container): + reasons.append(f"\t- Exit code is [{container.get('ExitCode', 0)}]") + + if not state_indicates_restarting(container): + reasons.append("\t- Container is not restarting") + + if not status_indicates_healthcheck_existence(container): + reasons.append("\t- Status does not indicate a healthcheck exists") + + # Mark it as healthy if ALL of the following are true: + # 1. It is healthy + # 2. Its exit code is normal + # 3. Its not restarting + # 4. It does not indicate a healthcheck exists + + # For #4, there was some cases where the container was restarting and at the time of check, + # the "Health" was empty and "State" was "running" (similar to init containers). This check + # added to try to catch those cases, by inspecting the "Status" field which if there is a healthcheck + # it will contain the word "health". + result = ( + health_indicates_healthy(container) + and exit_code_indicates_normal_exit(container) + and not state_indicates_restarting(container) + and not status_indicates_healthcheck_existence(container) + ) + + return {"result": result, "reasons": "\n".join(message + reasons)} + + def get_failed_containers(): parsed_containers = get_parsed_containers() @@ -236,18 +299,9 @@ def get_failed_containers(): for container in parsed_containers: # Skip containers that are exited with 0 (eg init containers), # but not restarting (during a restart exit code is 0) - if ( - ( - container.get("Health", "") == "" - or container.get("Health", "") == "healthy" - ) - and container.get("ExitCode", 0) == 0 - and (not container.get("State", "") == "restarting") - ): - print_stderr( - f"Skipping container [{container['Name']}({container['ID']})] with status [{container.get('State')}]" - + " because it exited with 0 and has no health status" - ) + is_healthy = is_considered_healthy(container) + if is_healthy["result"]: + print_stderr(is_healthy["reasons"]) continue failed.append(container) diff --git a/README.md b/README.md index bd6f7f5651..f75c1ced03 100644 --- a/README.md +++ b/README.md @@ -120,5 +120,5 @@ Thank you for your understanding. | whoogle | community | ✅ | - | | wordpress | community | - | - | | zerotier | community | ✅ | - | -| minio | enterprise | - | - | +| minio | enterprise | ✅ | ✅ | | syncthing | enterprise | - | - | diff --git a/cspell.config.yaml b/cspell.config.yaml index f1a83ac240..d6b469fdfc 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -19,6 +19,7 @@ words: - consts - cooldown - cpus + - creds - cuda - ddns - ddnss @@ -48,6 +49,8 @@ words: - goip - goipde - graalvm + - healthcheck + - healthchecks - hetzner - hexparrot - homarr @@ -59,6 +62,7 @@ words: - inwx - ionos - ipaddr + - ipam - ipfs - ipify - ipinfo @@ -73,7 +77,6 @@ words: - komga - libsmbclient - lidarr - - logsearch - logseq - luadns - mangas @@ -122,6 +125,9 @@ words: - rclone - rcon - readarr + - rprivate + - rshared + - rslave - scandir - seeip - selfhost diff --git a/ix-dev/enterprise/minio/README.md b/ix-dev/enterprise/minio/README.md new file mode 100644 index 0000000000..af8044d092 --- /dev/null +++ b/ix-dev/enterprise/minio/README.md @@ -0,0 +1,5 @@ +# MinIO + +[MinIO](https://min.io) is a High Performance Object Storage released under Apache License v2.0. +It is API compatible with Amazon S3 cloud storage service. Use MinIO to build high performance infrastructure +for machine learning, analytics and application data workloads. diff --git a/ix-dev/enterprise/minio/app.yaml b/ix-dev/enterprise/minio/app.yaml new file mode 100644 index 0000000000..7506aa4908 --- /dev/null +++ b/ix-dev/enterprise/minio/app.yaml @@ -0,0 +1,34 @@ +app_version: '2023-12-07' +capabilities: [] +categories: +- networking +description: High Performance, Kubernetes Native Object Storage +home: https://min.io +host_mounts: [] +icon: https://media.sys.truenas.net/apps/minio/icons/icon.png +keywords: +- object storage +- minio +- cloud +- s3 +lib_version: 1.0.0 +lib_version_hash: 317726af1e56541666942aeebcc7543e6f0946f96c322d35b612c0f2f7189a88 +maintainers: +- email: dev@ixsystems.com + name: truenas + url: https://www.truenas.com/ +name: minio +run_as_context: +- description: MinIO runs as any non-root user. + gid: 568 + group_name: minio + uid: 568 + user_name: minio +screenshots: +- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot1.png +- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot2.png +sources: +- https://github.com/minio/minio +title: MinIO +train: enterprise +version: 1.0.0 diff --git a/ix-dev/enterprise/minio/item.yaml b/ix-dev/enterprise/minio/item.yaml new file mode 100644 index 0000000000..e290c4ba0b --- /dev/null +++ b/ix-dev/enterprise/minio/item.yaml @@ -0,0 +1,11 @@ +categories: +- networking +icon_url: https://media.sys.truenas.net/apps/minio/icons/icon.png +screenshots: +- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot1.png +- https://media.sys.truenas.net/apps/adguard-home/screenshots/screenshot2.png +tags: +- object storage +- minio +- cloud +- s3 diff --git a/ix-dev/enterprise/minio/ix_values.yaml b/ix-dev/enterprise/minio/ix_values.yaml new file mode 100644 index 0000000000..baedbd9ca5 --- /dev/null +++ b/ix-dev/enterprise/minio/ix_values.yaml @@ -0,0 +1,8 @@ +images: + image: + repository: minio/minio + tag: RELEASE.2023-12-07T04-16-00Z + +consts: + minio_container_name: minio + perms_container_name: permissions diff --git a/ix-dev/enterprise/minio/migrations/migrate_from_kubernetes b/ix-dev/enterprise/minio/migrations/migrate_from_kubernetes new file mode 100755 index 0000000000..bcbfcd9ad9 --- /dev/null +++ b/ix-dev/enterprise/minio/migrations/migrate_from_kubernetes @@ -0,0 +1,58 @@ +#!/usr/bin/python3 + +import os +import sys +import yaml + +from migration_helpers.resources import migrate_resources +from migration_helpers.storage import migrate_storage_item + + +def migrate(values): + config = values.get("helm_secret", {}).get("config", {}) + if not config: + raise ValueError("No config found in values") + + new_values = { + "minio": { + "credentials": { + "access_key": config["minioCreds"]["rootUser"], + "secret_key": config["minioCreds"]["rootPass"], + }, + "logging": { + "quiet": config["minioLogging"]["quiet"], + "anonymous": config["minioLogging"]["anonymous"], + }, + "multi_mode": { + "enabled": config["enableMultiMode"], + "entries": config.get("minioMultiMode", []), + }, + }, + "run_as": { + "user": config["minioRunAs"].get("user", 568), + "group": config["minioRunAs"].get("group", 568), + }, + "network": { + "api_port": config["minioNetwork"]["apiPort"], + "console_port": config["minioNetwork"]["webPort"], + "host_network": config["minioNetwork"]["hostNetwork"], + "certificate_id": config["minioNetwork"].get("certificateID", None), + "server_url": config["minioNetwork"]["serverUrl"], + "console_url": config["minioNetwork"]["consoleUrl"], + }, + "storage": { + "data_dirs": [migrate_storage_item(item) for item in config["minioStorage"]] + }, + "resources": migrate_resources(config["resources"]), + } + + return new_values + + +if __name__ == "__main__": + if len(sys.argv) != 2: + exit(1) + + if os.path.exists(sys.argv[1]): + with open(sys.argv[1], "r") as f: + print(yaml.dump(migrate(yaml.safe_load(f.read())))) diff --git a/ix-dev/enterprise/minio/migrations/migration_helpers/__init__.py b/ix-dev/enterprise/minio/migrations/migration_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ix-dev/enterprise/minio/migrations/migration_helpers/cpu.py b/ix-dev/enterprise/minio/migrations/migration_helpers/cpu.py new file mode 100644 index 0000000000..4ec9ad7140 --- /dev/null +++ b/ix-dev/enterprise/minio/migrations/migration_helpers/cpu.py @@ -0,0 +1,27 @@ +import math +import re +import os + +CPU_COUNT = os.cpu_count() + +NUMBER_REGEX = re.compile(r"^[1-9][0-9]$") +FLOAT_REGEX = re.compile(r"^[0-9]+\.[0-9]+$") +MILI_CPU_REGEX = re.compile(r"^[0-9]+m$") + + +def transform_cpu(cpu) -> int: + result = 2 + if NUMBER_REGEX.match(cpu): + result = int(cpu) + elif FLOAT_REGEX.match(cpu): + result = int(math.ceil(float(cpu))) + elif MILI_CPU_REGEX.match(cpu): + num = int(cpu[:-1]) + num = num / 1000 + result = int(math.ceil(num)) + + if CPU_COUNT is not None: + # Do not exceed the actual CPU count + result = min(result, CPU_COUNT) + + return result diff --git a/ix-dev/enterprise/minio/migrations/migration_helpers/dns_config.py b/ix-dev/enterprise/minio/migrations/migration_helpers/dns_config.py new file mode 100644 index 0000000000..d0bf8f2734 --- /dev/null +++ b/ix-dev/enterprise/minio/migrations/migration_helpers/dns_config.py @@ -0,0 +1,9 @@ +def migrate_dns_config(dns_config): + if not dns_config: + return [] + + dns_opts = [] + for opt in dns_config.get("options", []): + dns_opts.append(f"{opt['name']}:{opt['value']}") + + return dns_opts diff --git a/ix-dev/enterprise/minio/migrations/migration_helpers/kubernetes_secrets.py b/ix-dev/enterprise/minio/migrations/migration_helpers/kubernetes_secrets.py new file mode 100644 index 0000000000..7a78f1f619 --- /dev/null +++ b/ix-dev/enterprise/minio/migrations/migration_helpers/kubernetes_secrets.py @@ -0,0 +1,15 @@ +def get_value_from_secret(secrets={}, secret_name="", key=""): + if not secrets or not secret_name or not key: + raise ValueError("Expected [secrets], [secret_name] and [key] to be set") + for secret in secrets.items(): + curr_secret_name = secret[0] + curr_data = secret[1] + + if curr_secret_name.endswith(secret_name): + if not curr_data.get(key, None): + raise ValueError( + f"Expected [{key}] to be set in secret [{curr_secret_name}]" + ) + return curr_data[key] + + raise ValueError(f"Secret [{secret_name}] not found") diff --git a/ix-dev/enterprise/minio/migrations/migration_helpers/memory.py b/ix-dev/enterprise/minio/migrations/migration_helpers/memory.py new file mode 100644 index 0000000000..cbbb4adc49 --- /dev/null +++ b/ix-dev/enterprise/minio/migrations/migration_helpers/memory.py @@ -0,0 +1,49 @@ +import re +import math +import psutil + +TOTAL_MEM = psutil.virtual_memory().total + +SINGLE_SUFFIX_REGEX = re.compile(r"^[1-9][0-9]*([EPTGMK])$") +DOUBLE_SUFFIX_REGEX = re.compile(r"^[1-9][0-9]*([EPTGMK])i$") +BYTES_INTEGER_REGEX = re.compile(r"^[1-9][0-9]*$") +EXPONENT_REGEX = re.compile(r"^[1-9][0-9]*e[0-9]+$") + +SUFFIX_MULTIPLIERS = { + "K": 10**3, + "M": 10**6, + "G": 10**9, + "T": 10**12, + "P": 10**15, + "E": 10**18, +} + +DOUBLE_SUFFIX_MULTIPLIERS = { + "Ki": 2**10, + "Mi": 2**20, + "Gi": 2**30, + "Ti": 2**40, + "Pi": 2**50, + "Ei": 2**60, +} + + +def transform_memory(memory): + result = 4096 # Default to 4GB + + if re.match(SINGLE_SUFFIX_REGEX, memory): + suffix = memory[-1] + result = int(memory[:-1]) * SUFFIX_MULTIPLIERS[suffix] + elif re.match(DOUBLE_SUFFIX_REGEX, memory): + suffix = memory[-2:] + result = int(memory[:-2]) * DOUBLE_SUFFIX_MULTIPLIERS[suffix] + elif re.match(BYTES_INTEGER_REGEX, memory): + result = int(memory) + elif re.match(EXPONENT_REGEX, memory): + result = int(float(memory)) + + result = math.ceil(result) + result = min(result, TOTAL_MEM) + # Convert to Megabytes + result = result / 1024 / 1024 + return int(result) diff --git a/ix-dev/enterprise/minio/migrations/migration_helpers/resources.py b/ix-dev/enterprise/minio/migrations/migration_helpers/resources.py new file mode 100644 index 0000000000..7885c889ac --- /dev/null +++ b/ix-dev/enterprise/minio/migrations/migration_helpers/resources.py @@ -0,0 +1,59 @@ +from .memory import transform_memory, TOTAL_MEM +from .cpu import transform_cpu, CPU_COUNT + + +def migrate_resources(resources, gpus=None, system_gpus=None): + gpus = gpus or {} + system_gpus = system_gpus or [] + + result = { + "limits": { + "cpus": (CPU_COUNT or 2) / 2, + "memory": {TOTAL_MEM / 1024 / 1024}, + } + } + + if resources.get("limits", {}).get("cpu", ""): + result["limits"].update( + {"cpus": transform_cpu(resources.get("limits", {}).get("cpu", ""))} + ) + if resources.get("limits", {}).get("memory", ""): + result["limits"].update( + {"memory": transform_memory(resources.get("limits", {}).get("memory", ""))} + ) + + gpus_result = {} + for gpu in gpus.items() if gpus else []: + kind = gpu[0].lower() # Kind of gpu (amd, nvidia, intel) + count = gpu[1] # Number of gpus user requested + + if count == 0: + continue + + if "amd" in kind or "intel" in kind: + gpus_result.update({"use_all_gpus": True}) + elif "nvidia" in kind: + sys_gpus = [ + gpu_item + for gpu_item in system_gpus + if gpu_item.get("error") is None + and gpu_item.get("vendor", None) is not None + and gpu_item.get("vendor", "").upper() == "NVIDIA" + ] + for sys_gpu in sys_gpus: + if count == 0: # We passed # of gpus that user previously requested + break + guid = sys_gpu.get("vendor_specific_config", {}).get("uuid", "") + pci_slot = sys_gpu.get("pci_slot", "") + if not guid or not pci_slot: + continue + + gpus_result.update( + {"nvidia_gpu_selection": {pci_slot: {"uuid": guid, "use_gpu": True}}} + ) + count -= 1 + + if gpus_result: + result.update({"gpus": gpus_result}) + + return result diff --git a/ix-dev/enterprise/minio/migrations/migration_helpers/storage.py b/ix-dev/enterprise/minio/migrations/migration_helpers/storage.py new file mode 100644 index 0000000000..7d0072d77b --- /dev/null +++ b/ix-dev/enterprise/minio/migrations/migration_helpers/storage.py @@ -0,0 +1,115 @@ +def migrate_storage_item(storage_item, include_read_only=False): + if not storage_item: + raise ValueError("Expected [storage_item] to be set") + + result = {} + if storage_item["type"] == "ixVolume": + result = migrate_ix_volume_type(storage_item) + elif storage_item["type"] == "hostPath": + result = migrate_host_path_type(storage_item) + elif storage_item["type"] == "emptyDir": + result = migrate_empty_dir_type(storage_item) + elif storage_item["type"] == "smb-pv-pvc": + result = migrate_smb_pv_pvc_type(storage_item) + + mount_path = storage_item.get("mountPath", "") + if mount_path: + result.update({"mount_path": mount_path}) + + if include_read_only: + result.update({"read_only": storage_item.get("readOnly", False)}) + return result + + +def migrate_smb_pv_pvc_type(smb_pv_pvc): + smb_config = smb_pv_pvc.get("smbConfig", {}) + if not smb_config: + raise ValueError("Expected [smb_pv_pvc] to have [smbConfig] set") + + return { + "type": "cifs", + "cifs_config": { + "server": smb_config["server"], + "share": smb_config["share"], + "domain": smb_config.get("domain", ""), + "username": smb_config["username"], + "password": smb_config["password"], + }, + } + + +def migrate_empty_dir_type(empty_dir): + empty_dir_config = empty_dir.get("emptyDirConfig", {}) + if not empty_dir_config: + raise ValueError("Expected [empty_dir] to have [emptyDirConfig] set") + + if empty_dir_config.get("medium", "") == "Memory": + # Convert Gi to Mi + size = empty_dir_config.get("size", 0.5) * 1024 + return { + "type": "tmpfs", + "tmpfs_config": {"size": size}, + } + + return {"type": "temporary"} + + +def migrate_ix_volume_type(ix_volume): + vol_config = ix_volume.get("ixVolumeConfig", {}) + if not vol_config: + raise ValueError("Expected [ix_volume] to have [ixVolumeConfig] set") + + result = { + "type": "ix_volume", + "ix_volume_config": { + "acl_enable": vol_config.get("aclEnable", False), + "dataset_name": vol_config.get("datasetName", ""), + }, + } + + if vol_config.get("aclEnable", False): + result["ix_volume_config"].update( + {"acl_entries": migrate_acl_entries(vol_config["aclEntries"])} + ) + + return result + + +def migrate_host_path_type(host_path): + path_config = host_path.get("hostPathConfig", {}) + if not path_config: + raise ValueError("Expected [host_path] to have [hostPathConfig] set") + + result = { + "type": "host_path", + "host_path_config": { + "acl_enable": path_config.get("aclEnable", False), + }, + } + + if path_config.get("aclEnable", False): + result["host_path_config"].update( + {"acl": migrate_acl_entries(path_config.get("acl", {}))} + ) + else: + result["host_path_config"].update({"path": path_config["hostPath"]}) + + return result + + +def migrate_acl_entries(acl_entries: dict) -> dict: + entries = [] + for entry in acl_entries.get("entries", []): + entries.append( + { + "access": entry["access"], + "id": entry["id"], + "id_type": entry["id_type"], + } + ) + + return { + "entries": entries, + "options": {"force": acl_entries.get("force", False)}, + "path": acl_entries["path"], + } diff --git a/ix-dev/enterprise/minio/questions.yaml b/ix-dev/enterprise/minio/questions.yaml new file mode 100644 index 0000000000..8bdac22d6c --- /dev/null +++ b/ix-dev/enterprise/minio/questions.yaml @@ -0,0 +1,286 @@ +groups: + - name: MinIO Configuration + description: Configure MinIO + - name: User and Group Configuration + description: Configure User and Group for MinIO + - name: Network Configuration + description: Configure Network for MinIO + - name: Storage Configuration + description: Configure Storage for MinIO + - name: Resources Configuration + description: Configure Resources for MinIO + +questions: + - variable: minio + label: "" + group: MinIO Configuration + schema: + type: dict + attrs: + - variable: credentials + label: Credentials + description: The credentials for the root user. + schema: + type: dict + attrs: + - variable: access_key + label: Access Key + description: The access key for the root user. + schema: + type: string + min_length: 5 + required: true + private: true + - variable: secret_key + label: Secret Key + description: The secret key for the root user. + schema: + type: string + min_length: 8 + required: true + private: true + - variable: multi_mode + label: Multi Mode (SNMD or MNMD) Configuration + description: | + For Single Node Multi Drive (SNMD), the entry will look like this:
+ Example Entry - /data{1...4}

+ For Multi Node Multi Drive (MNMD), the entry will look like this:
+ Example Entry - https://minio{1...3}.example.com:30000/data{1...4}

+ Note that each host must use the same port number and the same number of storage items.
+ In both cases /data{1...4} is the directories to be used for MinIO. + You have to add additional storage for each data entry. + schema: + type: dict + attrs: + - variable: enabled + label: Enabled + description: Enable Multi Mode + schema: + type: boolean + default: false + - variable: entries + label: Multi Mode (SNMD or MNMD) Entries + schema: + type: list + show_if: [["enabled", "=", true]] + default: [] + items: + - variable: item + label: "" + schema: + type: string + required: true + - variable: logging + label: "" + description: Logging configuration + schema: + type: dict + attrs: + - variable: quiet + label: Quiet + description: Disables startup information. + schema: + type: boolean + default: false + - variable: anonymous + label: Anonymous + description: Hides sensitive information from logging. + schema: + type: boolean + default: false + - variable: additional_envs + label: Additional Environment Variables + description: Configure additional environment variables for MinIO. + schema: + type: list + default: [] + items: + - variable: env + label: Environment Variable + schema: + type: dict + attrs: + - variable: name + label: Name + schema: + type: string + required: true + - variable: value + label: Value + schema: + type: string + required: true + + - variable: run_as + label: "" + group: User and Group Configuration + schema: + type: dict + attrs: + - variable: user + label: User ID + description: The user id that MinIO will run as. + schema: + type: int + min: 568 + default: 568 + required: true + - variable: group + label: Group ID + description: The group id that MinIO will run as. + schema: + type: int + min: 568 + default: 568 + required: true + + - variable: network + label: "" + group: Network Configuration + schema: + type: dict + attrs: + - variable: api_port + label: API Port + description: The port for the MinIO API. + schema: + type: int + default: 30000 + required: true + $ref: + - "definitions/port" + - variable: console_port + label: Console Port (Web UI) + description: The port for the MinIO Web UI. + schema: + type: int + default: 30001 + required: true + $ref: + - "definitions/port" + - variable: server_url + label: Server URL + description: | + The URL that console will use to reach API
+ For example https://minio1.example.com.

+ schema: + type: string + required: true + - variable: console_url + label: Console URL + description: | + The URL that console will provide as a redirect URL
+ For example https://console.example.com.

+ schema: + type: string + required: true + - variable: host_network + label: Host Network + description: | + Bind to the host network. It's recommended to keep this disabled. + schema: + type: boolean + default: false + - variable: certificate_id + label: Certificate + description: The certificate to use for MinIO + schema: + type: int + "null": true + $ref: + - "definitions/certificate" + + - variable: storage + label: "" + group: Storage Configuration + schema: + type: dict + attrs: + - variable: data_dirs + label: Data Directories + schema: + type: list + default: + - type: ix_volume + mount_path: /data1 + dataset_name: data1 + items: + - variable: item + label: "" + schema: + type: dict + attrs: + - variable: type + label: Type + description: | + ixVolume: Is dataset created automatically by the system.
+ Host Path: Is a path that already exists on the system. + schema: + type: string + required: true + default: host_path + enum: + - value: host_path + description: Host Path (Path that already exists on the system) + - value: ix_volume + description: ixVolume (Dataset created automatically by the system) + - variable: mount_path + label: Mount Path + description: The path inside the container to mount the storage. + schema: + type: path + required: true + immutable: true + default: /data1 + - variable: host_path_config + label: Host Path Configuration + schema: + type: dict + show_if: [["type", "=", "host_path"]] + attrs: + - variable: path + label: Path + description: Path on the host + schema: + type: hostpath + required: true + - variable: ix_volume_config + label: iX Volume Configuration + schema: + type: dict + show_if: [["type", "=", "ix_volume"]] + attrs: + - variable: dataset_name + label: Dataset Name + schema: + type: string + required: true + immutable: true + default: "data1" + $ref: + - "normalize/ix_volume" + - variable: resources + label: "" + group: Resources Configuration + schema: + type: dict + attrs: + - variable: limits + label: Limits + schema: + type: dict + attrs: + - variable: cpus + label: CPUs + description: CPUs limit for MinIO. + schema: + type: int + default: 2 + required: true + - variable: memory + label: Memory (in MB) + description: Memory limit for MinIO. + schema: + type: int + default: 4096 + required: true diff --git a/ix-dev/enterprise/minio/templates/docker-compose.yaml b/ix-dev/enterprise/minio/templates/docker-compose.yaml new file mode 100644 index 0000000000..da48a57fe3 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/docker-compose.yaml @@ -0,0 +1,109 @@ +{% from "macros/global/perms/container.yaml.jinja" import perms_container %} + +{% do ix_lib.enterprise.minio.data.validate(data=values) %} + +{# Stores minio "volumes" that will be passed in the MINIO_VOLUMES env var #} +{% set minio_config_items = namespace(items=[]) %} + +{# Stores minio storage items that contains info for volumes, vol mounts, perms dirs and perms mounts #} +{% set storage_items = namespace(items=[]) %} +{# Stores the minio container volume mounts #} +{% set volume_mounts = namespace(items=[]) %} +{# Stores the top level volumes #} +{% set volumes = namespace(items={}) %} +{# Stores the perms container volume mounts #} +{% set perms_mounts = namespace(items=[]) %} +{# Stores the perms container dirs #} +{% set perms_dirs = namespace(items=[]) %} + +{% do storage_items.items.append(ix_lib.base.storage.storage_item(data={"type":"anonymous", "mount_path": "/tmp"})) %} +{% for store in values.storage.data_dirs %} + {% set item = ix_lib.base.storage.storage_item(data=store, values=values, + perm_opts={"mount_path": "/mnt/minio/dir_%s"|format(loop.index0), "mode": "check", "uid": values.run_as.user, "gid": values.run_as.group}) + %} + {% do storage_items.items.append(item) %} + {% do minio_config_items.items.append(item.vol_mount.target) %} +{% endfor %} + +{# Add each item to the above lists #} +{% for item in storage_items.items %} + {% if item.vol and volumes.items.update(item.vol) %}{% endif %} + {% if item.vol_mount and volume_mounts.items.append(item.vol_mount) %}{% endif %} + {% if item.perms_item and (perms_dirs.items.append(item.perms_item.perm_dir), perms_mounts.items.append(item.perms_item.vol_mount)) %}{% endif %} +{% endfor %} + +{# Configs #} +{% if values.network.certificate_id %} +configs: + private: + content: {{ values.ix_certificates[values.network.certificate_id].privatekey | tojson }} + public: + content: {{ values.ix_certificates[values.network.certificate_id].certificate | tojson }} +{% endif %} + +{# Containers #} +services: + {{ values.consts.minio_container_name }}: + image: {{ ix_lib.base.utils.get_image(images=values.images, name="image") }} + user: {{ "%s:%s" | format(values.run_as.user, values.run_as.group) }} + restart: unless-stopped + deploy: + resources: {{ ix_lib.base.resources.resources(values.resources) | tojson }} + devices: {{ ix_lib.base.resources.get_devices(values.resources) | tojson }} + cap_drop: {{ ix_lib.base.security.get_caps().drop | tojson }} + security_opt: {{ ix_lib.base.security.get_sec_opts() | tojson }} + {% if values.network.host_network %} + network_mode: host + {% endif %} + {% if values.network.dns_opts %} + dns_opt: {{ ix_lib.base.network.dns_opts(values.network.dns_opts) | tojson }} + {% endif %} + {% if values.network.certificate_id %} + configs: + - source: private + target: /.minio/certs/private.key + - source: public + target: /.minio/certs/public.crt + {% endif %} + command: {{ ix_lib.enterprise.minio.data.get_commands(values=values) | tojson }} + {% if perms_dirs.items %} + depends_on: + {{ values.consts.perms_container_name }}: + condition: service_completed_successfully + {% endif %} + healthcheck: {{ ix_lib.base.healthchecks.check_health( + "mc ready --insecure --cluster-read health" + ) | tojson }} + volumes: {{ volume_mounts.items | tojson }} + {% set proto = "https" if values.network.certificate_id else "http" %} + {% set app_env = ix_lib.base.utils.merge_dicts({ + "MC_HOST_health": "%s://localhost:%d" | format(proto, values.network.api_port), + "MINIO_ROOT_USER": values.minio.credentials.access_key, + "MINIO_ROOT_PASSWORD": values.minio.credentials.secret_key, + "MINIO_VOLUMES": (values.minio.multi_mode.entries if values.minio.multi_mode.entries else minio_config_items.items) | join(" "), + }, + {"MINIO_SERVER_URL": values.network.server_url} if values.network.server_url else {}, + {"MINIO_BROWSER_REDIRECT_URL": values.network.console_url} if values.network.console_url else {} + ) %} + environment: {{ ix_lib.base.environment.envs(app=app_env, user=values.minio.additional_envs, values=values) | tojson }} + {% if not values.network.host_network %} + ports: + - {{ ix_lib.base.ports.get_port(port={"target": values.network.console_port, "published": values.network.console_port}) | tojson }} + - {{ ix_lib.base.ports.get_port(port={"target": values.network.api_port, "published": values.network.api_port}) | tojson }} + {% endif %} + {# Permissions Container #} + {% if perms_dirs.items %} + {{ values.consts.perms_container_name }}: + {{ perms_container(items=perms_dirs.items) | indent(4) }} + volumes: + {% for item in perms_mounts.items %} + - {{ item | tojson }} + {% endfor %} + {% endif %} + +{% if volumes.items %} +volumes: {{ volumes.items | tojson }} +{% endif %} + +x-portals: {{ ix_lib.base.metadata.get_portals([{"port": values.network.console_port, "scheme": proto}]) | tojson }} +x-notes: {{ ix_lib.base.metadata.get_notes("MinIO") | tojson }} diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/__init__.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/environment.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/environment.py new file mode 100644 index 0000000000..10dc12ac4d --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/environment.py @@ -0,0 +1,90 @@ +from . import utils +from .resources import get_nvidia_gpus_reservations + + +def envs(app: dict | None = None, user: list | None = None, values: dict | None = None): + app = app or {} + user = user or [] + values = values or {} + result = {} + + if not values: + utils.throw_error("Values cannot be empty in environment.py") + + if not isinstance(user, list): + utils.throw_error( + f"Unsupported type for user environment variables [{type(user)}]" + ) + + # Always set TZ + result.update({"TZ": values.get("TZ", "Etc/UTC")}) + + # Update envs with nvidia variables + if values.get("resources", {}).get("gpus", {}): + result.update(get_nvidia_env(values.get("resources", {}).get("gpus", {}))) + + # Update envs with run_as variables + if values.get("run_as"): + result.update(get_run_as_envs(values.get("run_as", {}))) + + # Make sure we don't manually set any of the above + for item in app.items(): + if not item[0]: + utils.throw_error("Environment variable name cannot be empty.") + if item[0] in result: + utils.throw_error( + f"Environment variable [{item[0]}] is already defined automatically from the library." + ) + result[item[0]] = item[1] + + for item in user: + if not item.get("name"): + utils.throw_error("Environment variable name cannot be empty.") + if item.get("name") in result: + utils.throw_error( + f"Environment variable [{item['name']}] is already defined from the application developer." + ) + result[item["name"]] = item.get("value") + + return result + + +# Sets some common variables that most applications use +def get_run_as_envs(run_as: dict) -> dict: + result = {} + user = run_as.get("user") + group = run_as.get("group") + if user: + result.update( + { + "PUID": user, + "UID": user, + "USER_ID": user, + } + ) + if group: + result.update( + { + "PGID": group, + "GID": group, + "GROUP_ID": group, + } + ) + return result + + +def get_nvidia_env(gpus: dict) -> dict: + reservations = get_nvidia_gpus_reservations(gpus) + if not reservations.get("device_ids"): + return { + "NVIDIA_VISIBLE_DEVICES": "void", + } + + return { + "NVIDIA_VISIBLE_DEVICES": ( + ",".join(reservations["device_ids"]) + if reservations.get("device_ids") + else "void" + ), + "NVIDIA_DRIVER_CAPABILITIES": "all", + } diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/healthchecks.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/healthchecks.py new file mode 100644 index 0000000000..afe9bfded1 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/healthchecks.py @@ -0,0 +1,110 @@ +from . import utils + + +def check_health(test, interval=10, timeout=10, retries=5, start_period=30): + if not test: + utils.throw_error("Expected [test] to be set") + + return { + "test": test, + "interval": f"{interval}s", + "timeout": f"{timeout}s", + "retries": retries, + "start_period": f"{start_period}s", + } + + +def pg_test(user, db, config=None): + config = config or {} + if not user or not db: + utils.throw_error("Postgres container: [user] and [db] must be set") + + host = config.get("host", "127.0.0.1") + port = config.get("port", 5432) + + return f"pg_isready -h {host} -p {port} -d {db} -U {user}" + + +def redis_test(config=None): + config = config or {} + + host = config.get("host", "127.0.0.1") + port = config.get("port", 6379) + password = "$$REDIS_PASSWORD" + + return f"redis-cli -h {host} -p {port} -a {password} ping | grep -q PONG" + + +def curl_test(port, path, config=None): + config = config or {} + if not port or not path: + utils.throw_error("Expected [port] and [path] to be set") + + scheme = config.get("scheme", "http") + host = config.get("host", "127.0.0.1") + headers = config.get("headers", []) + + opts = [] + if scheme == "https": + opts.append("--insecure") + + for header in headers: + if not header[0] or not header[1]: + utils.throw_error("Expected [header] to be a list of two items") + opts.append(f'--header "{header[0]}: {header[1]}"') + + return f"curl --silent --output /dev/null --show-error --fail {' '.join(opts)} {scheme}://{host}:{port}{path}" + + +def wget_test(port, path, config=None): + config = config or {} + if not port or not path: + utils.throw_error("Expected [port] and [path] to be set") + + scheme = config.get("scheme", "http") + host = config.get("host", "127.0.0.1") + headers = config.get("headers", []) + + opts = [] + if scheme == "https": + opts.append("--no-check-certificate") + + for header in headers: + if not header[0] or not header[1]: + utils.throw_error("Expected [header] to be a list of two items") + opts.append(f'--header "{header[0]}: {header[1]}"') + + return f"wget --spider --quiet {' '.join(opts)} {scheme}://{host}:{port}{path}" + + +def http_test(port, path, config=None): + config = config or {} + if not port or not path: + utils.throw_error("Expected [port] and [path] to be set") + + host = config.get("host", "127.0.0.1") + + return ( + f"/bin/bash -c 'exec {{health_check_fd}}<>/dev/tcp/{host}/{port} && echo -e \"GET {path} HTTP/1.1\\r\\nHost: " + + f"{host}\\r\\nConnection: close\\r\\n\\r\\n\" >&$${{health_check_fd}} && cat <&$${{health_check_fd}}'" + ) + + +def netcat_test(port, config=None): + config = config or {} + if not port: + utils.throw_error("Expected [port] to be set") + + host = config.get("host", "127.0.0.1") + + return f"nc -z -w 1 {host} {port}" + + +def tcp_test(port, config=None): + config = config or {} + if not port: + utils.throw_error("Expected [port] to be set") + + host = config.get("host", "127.0.0.1") + + return f"timeout 1 bash -c 'cat < /dev/null > /dev/tcp/{host}/{port}'" diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/metadata.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/metadata.py new file mode 100644 index 0000000000..c0a59f8979 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/metadata.py @@ -0,0 +1,71 @@ +from . import utils + + +def get_header(app_name: str): + return f"""# Welcome to TrueNAS SCALE + +Thank you for installing {app_name}! +""" + + +def get_footer(app_name: str): + return f"""## Documentation + +Documentation for {app_name} can be found at https://www.truenas.com/docs. + +## Bug reports + +If you find a bug in this app, please file an issue at +https://ixsystems.atlassian.net or https://github.com/truenas/apps + +## Feature requests or improvements + +If you find a feature request for this app, please file an issue at +https://ixsystems.atlassian.net or https://github.com/truenas/apps +""" + + +def get_notes(app_name: str, body: str = ""): + if not app_name: + utils.throw_error("Expected [app_name] to be set") + + return f"{get_header(app_name)}\n\n{body}\n\n{get_footer(app_name)}" + + +def get_portals(portals: list): + valid_schemes = ["http", "https"] + result = [] + for portal in portals: + # Most apps have a single portal, lets default to a standard name + name = portal.get("name", "Web UI") + scheme = portal.get("scheme", "http") + path = portal.get("path", "/") + + if not name: + utils.throw_error("Expected [portal.name] to be set") + if name in [p["name"] for p in result]: + utils.throw_error( + f"Expected [portal.name] to be unique, got [{', '.join([p['name'] for p in result]+[name])}]" + ) + if scheme not in valid_schemes: + utils.throw_error( + f"Expected [portal.scheme] to be one of [{', '.join(valid_schemes)}], got [{portal['scheme']}]" + ) + if not portal.get("port"): + utils.throw_error("Expected [portal.port] to be set") + if not path.startswith("/"): + utils.throw_error( + f"Expected [portal.path] to start with /, got [{portal['path']}]" + ) + + result.append( + { + "name": name, + "scheme": scheme, + "host": portal.get("host", "0.0.0.0"), + "port": portal["port"], + "path": path, + } + ) + + return result diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/network.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/network.py new file mode 100644 index 0000000000..e4761fd295 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/network.py @@ -0,0 +1,21 @@ +from . import utils + + +def dns_opts(dns_options=None): + dns_options = dns_options or [] + if not dns_options: + return [] + + tracked = {} + disallowed_opts = [] + for opt in dns_options: + key = opt.split(":")[0] + if key in tracked: + utils.throw_error( + f"Expected [dns_opts] to be unique, got [{', '.join([d.split(':')[0] for d in tracked])}]" + ) + if key in disallowed_opts: + utils.throw_error(f"Expected [dns_opts] to not contain [{key}] key.") + tracked[key] = opt + + return dns_options diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/ports.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/ports.py new file mode 100644 index 0000000000..c895b47a44 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/ports.py @@ -0,0 +1,42 @@ +import ipaddress + +from . import utils + + +def must_valid_port(num: int): + if num < 1 or num > 65535: + utils.throw_error(f"Expected a valid port number, got [{num}]") + + +def must_valid_ip(ip: str): + try: + ipaddress.ip_address(ip) + except ValueError: + utils.throw_error(f"Expected a valid IP address, got [{ip}]") + + +def must_valid_protocol(protocol: str): + if protocol not in ["tcp", "udp"]: + utils.throw_error(f"Expected a valid protocol, got [{protocol}]") + + +def must_valid_mode(mode: str): + if mode not in ["ingress", "host"]: + utils.throw_error(f"Expected a valid mode, got [{mode}]") + + +def get_port(port=None): + port = port or {} + must_valid_port(port["published"]) + must_valid_port(port["target"]) + must_valid_ip(port.get("host_ip", "0.0.0.0")) + must_valid_protocol(port.get("protocol", "tcp")) + must_valid_mode(port.get("mode", "ingress")) + + return { + "target": port["target"], + "published": port["published"], + "protocol": port.get("protocol", "tcp"), + "mode": port.get("mode", "ingress"), + "host_ip": port.get("host_ip", "0.0.0.0"), + } diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/postgres.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/postgres.py new file mode 100644 index 0000000000..4dc7e32344 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/postgres.py @@ -0,0 +1,77 @@ +from . import utils +from .security import get_caps, get_sec_opts +from .network import dns_opts +from .healthchecks import pg_test, check_health +from .resources import resources + + +def pg_url(variant, host, user, password, dbname, port=5432): + if not host: + utils.throw_error("Expected [host] to be set") + if not user: + utils.throw_error("Expected [user] to be set") + if not password: + utils.throw_error("Expected [password] to be set") + if not dbname: + utils.throw_error("Expected [dbname] to be set") + + if variant == "postgresql": + return f"postgresql://{user}:{password}@{host}:{port}/{dbname}?sslmode=disable" + elif variant == "postgres": + return f"postgres://{user}:{password}@{host}:{port}/{dbname}?sslmode=disable" + else: + utils.throw_error( + f"Expected [variant] to be one of [postgresql, postgres], got [{variant}]" + ) + + +def pg_env(user, password, dbname, port=5432): + if not user: + utils.throw_error("Expected [user] to be set for postgres") + if not password: + utils.throw_error("Expected [password] to be set for postgres") + if not dbname: + utils.throw_error("Expected [dbname] to be set for postgres") + return { + "POSTGRES_USER": user, + "POSTGRES_PASSWORD": password, + "POSTGRES_DB": dbname, + "POSTGRES_PORT": port, + } + + +def pg_container(data={}): + req_keys = ["db_user", "db_password", "db_name", "volumes", "resources"] + for key in req_keys: + if not data.get(key): + utils.throw_error(f"Expected [{key}] to be set for postgres") + + pg_user = data["db_user"] + pg_password = data["db_password"] + pg_dbname = data["db_name"] + pg_port = data.get("port", 5432) + depends = data.get("depends_on", {}) + depends_on = {} + for key in depends: + depends_on[key] = { + "condition": depends[key].get("condition", "service_completed_successfully") + } + + return { + "image": f"{data.get('image', 'postgres:15')}", + "user": f"{data.get('user', '999')}:{data.get('group', '999')}", + "restart": "unless-stopped", + "cap_drop": get_caps()["drop"], + "security_opt": get_sec_opts(), + **({"dns_opts": dns_opts(data["dns_opts"])} if data.get("dns_opts") else {}), + "healthcheck": check_health(pg_test(user=pg_user, db=pg_dbname)), + "environment": pg_env( + user=pg_user, + password=pg_password, + dbname=pg_dbname, + port=pg_port, + ), + "volumes": data["volumes"], + "depends_on": depends_on, + "deploy": {"resources": resources(data["resources"])}, + } diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/redis.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/redis.py new file mode 100644 index 0000000000..2de75eda8a --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/redis.py @@ -0,0 +1,49 @@ +from . import utils +from .security import get_caps, get_sec_opts +from .network import dns_opts +from .healthchecks import redis_test, check_health +from .resources import resources + + +def redis_container(data={}): + req_keys = ["password", "volumes", "resources"] + for key in req_keys: + if not data.get(key): + utils.throw_error(f"Expected [{key}] to be set for postgres") + + redis_password = data["password"] + redis_port = data.get("port", 6379) + depends = data.get("depends_on", {}) + depends_on = {} + for key in depends: + depends_on[key] = { + "condition": depends[key].get("condition", "service_completed_successfully") + } + + return { + "image": f"{data.get('image', 'bitnami/redis:7.0.11')}", + "user": f"{data.get('user', '1001')}:{data.get('group', '0')}", + "restart": "unless-stopped", + "cap_drop": get_caps()["drop"], + "security_opt": get_sec_opts(), + **({"dns_opts": dns_opts(data["dns_opts"])} if data.get("dns_opts") else {}), + "healthcheck": check_health(redis_test(config={"port": redis_port})), + "environment": redis_env( + password=redis_password, + port=redis_port, + ), + "volumes": data["volumes"], + "depends_on": depends_on, + "deploy": {"resources": resources(data["resources"])}, + } + + +def redis_env(password, port=6379): + if not password: + utils.throw_error("Expected [password] to be set for redis") + + return { + "ALLOW_EMPTY_PASSWORD": "no", + "REDIS_PASSWORD": password, + "REDIS_PORT_NUMBER": port, + } diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/resources.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/resources.py new file mode 100644 index 0000000000..5c8ff652ba --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/resources.py @@ -0,0 +1,87 @@ +import re + +from . import utils + + +def resources(resources): + gpus = resources.get("gpus", {}) + cpus = str(resources.get("limits", {}).get("cpus", 2.0)) + memory = str(resources.get("limits", {}).get("memory", 4096)) + if not re.match(r"^[1-9][0-9]*(\.[0-9]+)?$", cpus): + utils.throw_error(f"Expected cpus to be a number or a float, got [{cpus}]") + if not re.match(r"^[1-9][0-9]*$", memory): + raise ValueError(f"Expected memory to be a number, got [{memory}]") + + result = { + "limits": {"cpus": cpus, "memory": f"{memory}M"}, + "reservations": {"devices": []}, + } + + if gpus: + gpu_result = get_nvidia_gpus_reservations(gpus) + if gpu_result: + # Appending to devices, as we can later extend this to support other types of devices. Eg. TPUs. + result["reservations"]["devices"].append(get_nvidia_gpus_reservations(gpus)) + + # Docker does not like empty "things" all around. + if not result["reservations"]["devices"]: + del result["reservations"] + + return result + + +def get_nvidia_gpus_reservations(gpus: dict) -> dict: + """ + Input: + { + "nvidia_gpu_selection": { + "pci_slot_0": {"uuid": "uuid_0", "use_gpu": True}, + "pci_slot_1": {"uuid": "uuid_1", "use_gpu": True}, + }, + } + """ + if not gpus: + return {} + + device_ids = [] + for gpu in gpus.get("nvidia_gpu_selection", {}).values(): + if gpu["use_gpu"]: + device_ids.append(gpu["uuid"]) + + if not device_ids: + return {} + + return { + "capabilities": ["gpu"], + "driver": "nvidia", + "device_ids": device_ids, + } + + +disallowed_devices = ["/dev/dri"] + + +# Returns the top level devices list +# Accepting other_devices to allow manually adding devices +# directly to the list. (Eg sound devices) +def get_devices(resources: dict, other_devices: list = []) -> list: + devices = [] + if resources.get("gpus", {}).get("use_all_gpus", False): + devices.append("/dev/dri:/dev/dri") + + added_host_devices: list = [] + for device in other_devices: + host_device = device.get("host_device", "").rstrip("/") + container_device = device.get("container_device", "") or host_device + if not host_device: + utils.throw_error(f"Expected [host_device] to be set for device [{device}]") + if not utils.valid_path(host_device): + utils.throw_error(f"Expected [host_device] to be a valid path for device [{device}]") + if host_device in disallowed_devices: + utils.throw_error(f"Device [{host_device}] is not allowed to be manually added.") + if host_device in added_host_devices: + utils.throw_error(f"Expected devices to be unique, but [{host_device}] was already added.") + devices.append(f"{host_device}:{container_device}") + added_host_devices.append(host_device) + + return devices diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/security.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/security.py new file mode 100644 index 0000000000..0bc5119cf6 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/security.py @@ -0,0 +1,27 @@ +from base64 import b64encode + + +def get_caps(add=None, drop=None): + add = add or [] + drop = drop or ["ALL"] + result = {"drop": drop} + if add: + result["add"] = add + return result + + +def get_sec_opts(add=None, remove=None): + add = add or [] + remove = remove or [] + result = ["no-new-privileges"] + for opt in add: + if opt not in result: + result.append(opt) + for opt in remove: + if opt in result: + result.remove(opt) + return result + + +def htpasswd(username, password): + return b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8") diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/storage.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/storage.py new file mode 100644 index 0000000000..c73b3e4b50 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/storage.py @@ -0,0 +1,363 @@ +import re + +from . import utils + + +BIND_TYPES = ["host_path", "ix_volume"] +VOL_TYPES = ["volume", "nfs", "cifs", "temporary"] +ALL_TYPES = BIND_TYPES + VOL_TYPES + ["tmpfs", "anonymous"] +PROPAGATION_TYPES = ["shared", "slave", "private", "rshared", "rslave", "rprivate"] + + +def _get_name_for_temporary(data): + if not data.get("mount_path"): + utils.throw_error("Expected [mount_path] to be set for temporary volume") + + return ( + data["mount_path"] + .lstrip("/") + .lower() + .replace("/", "_") + .replace(".", "_") + .replace(" ", "_") + ) + + +# Returns a volume mount object (Used in container's "volumes" level) +def vol_mount(data, values=None): + values = values or {} + ix_volumes = values.get("ix_volumes") or [] + vol_type = _get_docker_vol_type(data) + + volume = { + "type": vol_type, + "target": utils.valid_path(data.get("mount_path", "")), + "read_only": data.get("read_only", False), + } + if vol_type == "bind": # Default create_host_path is true in short-syntax + volume.update(_get_bind_vol_config(data, ix_volumes)) + elif vol_type == "volume": + volume.update(_get_volume_vol_config(data)) + elif vol_type == "tmpfs": + volume.update(_get_tmpfs_vol_config(data)) + elif vol_type == "temporary": + volume["type"] = "volume" + volume.update(_get_volume_vol_config(data)) + elif vol_type == "anonymous": + volume["type"] = "volume" + volume.update(_get_anonymous_vol_config(data)) + + return volume + + +def storage_item(data, values=None, perm_opts=None): + values = values or {} + perm_opts = perm_opts or {} + if data.get("type") == "temporary": + data.update({"volume_name": _get_name_for_temporary(data)}) + return { + "vol_mount": vol_mount(data, values), + "vol": vol(data), + "perms_item": perms_item(data, values, perm_opts) if perm_opts else {}, + } + + +def perms_item(data, values=None, opts=None): + opts = opts or {} + values = values or {} + ix_context = values.get("ix_context") or {} + vol_type = data.get("type", "") + + # Temp volumes are always auto permissions + if vol_type == "temporary": + data.update({"auto_permissions": True}) + + # If its ix_volume and we are installing, we need to set auto permissions + if vol_type == "ix_volume" and ix_context.get("is_install", False): + data.update({"auto_permissions": True}) + + if not data.get("auto_permissions"): + return {} + + if vol_type == "host_path": + if data.get("host_path_config", {}).get("acl_enable", False): + return {} + if vol_type == "ix_volume": + if data.get("ix_volume_config", {}).get("acl_enable", False): + return {} + + req_keys = ["mount_path", "mode", "uid", "gid"] + for key in req_keys: + if opts.get(key, None) is None: + utils.throw_error(f"Expected opts passed to [perms_item] to have [{key}] key") + + data.update({"mount_path": opts["mount_path"]}) + volume_mount = vol_mount(data, values) + + return { + "vol_mount": volume_mount, + "perm_dir": { + "dir": volume_mount["target"], + "mode": opts["mode"], + "uid": opts["uid"], + "gid": opts["gid"], + "chmod": opts.get("chmod", "false"), + "is_temporary": data["type"] == "temporary", + }, + } + + +def _get_bind_vol_config(data, ix_volumes=None): + ix_volumes = ix_volumes or [] + path = host_path(data, ix_volumes) + if data.get("propagation", "rprivate") not in PROPAGATION_TYPES: + utils.throw_error( + f"Expected [propagation] to be one of [{', '.join(PROPAGATION_TYPES)}], got [{data['propagation']}]" + ) + + # https://docs.docker.com/storage/bind-mounts/#configure-bind-propagation + return { + "source": path, + "bind": { + "create_host_path": data.get("host_path_config", {}).get( + "create_host_path", True + ), + "propagation": _get_valid_propagation(data), + }, + } + + +def _get_volume_vol_config(data): + if not data.get("volume_name"): + utils.throw_error("Expected [volume_name] to be set for [volume] type") + + return {"source": data["volume_name"], "volume": _process_volume_config(data)} + + +def _get_anonymous_vol_config(data): + return {"volume": _process_volume_config(data)} + + +mode_regex = re.compile(r"^0[0-7]{3}$") + + +def _get_tmpfs_vol_config(data): + tmpfs = {} + config = data.get("tmpfs_config", {}) + + if config.get("size"): + if not isinstance(config["size"], int): + utils.throw_error("Expected [size] to be an integer for [tmpfs] type") + if not config["size"] > 0: + utils.throw_error("Expected [size] to be greater than 0 for [tmpfs] type") + # Convert Mebibytes to Bytes + tmpfs.update({"size": config["size"] * 1024 * 1024}) + + if config.get("mode"): + if not mode_regex.match(str(config["mode"])): + utils.throw_error( + f"Expected [mode] to be a octal string for [tmpfs] type, got [{config['mode']}]" + ) + tmpfs.update({"mode": int(config["mode"], 8)}) + + return {"tmpfs": tmpfs} + + +# Returns a volume object (Used in top "volumes" level) +def vol(data): + if not data or _get_docker_vol_type(data) != "volume": + return {} + + if not data.get("volume_name"): + utils.throw_error("Expected [volume_name] to be set for [volume] type") + + if data["type"] == "nfs": + return {data["volume_name"]: _process_nfs(data)} + elif data["type"] == "cifs": + return {data["volume_name"]: _process_cifs(data)} + else: + return {data["volume_name"]: {}} + + +def _is_host_path(data): + return data.get("type") == "host_path" + + +def _get_valid_propagation(data): + if not data.get("propagation"): + return "rprivate" + if not data["propagation"] in PROPAGATION_TYPES: + utils.throw_error( + f"Expected [propagation] to be one of [{', '.join(PROPAGATION_TYPES)}], got [{data['propagation']}]" + ) + return data["propagation"] + + +def _is_ix_volume(data): + return data.get("type") == "ix_volume" + + +# Returns the host path for a for either a host_path or ix_volume +def host_path(data, ix_volumes=None): + ix_volumes = ix_volumes or [] + path = "" + if _is_host_path(data): + path = _process_host_path_config(data) + elif _is_ix_volume(data): + path = _process_ix_volume_config(data, ix_volumes) + else: + utils.throw_error( + f"Expected [host_path()] to be called only for types [host_path, ix_volume], got [{data['type']}]" + ) + + return utils.valid_path(path) + + +# Returns the type of storage as used in docker-compose +def _get_docker_vol_type(data): + if not data.get("type"): + utils.throw_error("Expected [type] to be set for storage") + + if data["type"] not in ALL_TYPES: + utils.throw_error( + f"Expected storage [type] to be one of {ALL_TYPES}, got [{data['type']}]" + ) + + if data["type"] in BIND_TYPES: + return "bind" + elif data["type"] in VOL_TYPES: + return "volume" + else: + return data["type"] + + +def _process_host_path_config(data): + if data.get("host_path_config", {}).get("acl_enable", False): + if not data["host_path_config"].get("acl", {}).get("path"): + utils.throw_error( + "Expected [host_path_config.acl.path] to be set for [host_path] type with ACL enabled" + ) + return data["host_path_config"]["acl"]["path"] + + if not data.get("host_path_config", {}).get("path"): + utils.throw_error( + "Expected [host_path_config.path] to be set for [host_path] type" + ) + + return data["host_path_config"]["path"] + + +def _process_volume_config(data): + return {"nocopy": data.get("volume_config", {}).get("nocopy", False)} + + +def _process_ix_volume_config(data, ix_volumes): + path = "" + if not data.get("ix_volume_config", {}).get("dataset_name"): + utils.throw_error( + "Expected [ix_volume_config.dataset_name] to be set for [ix_volume] type" + ) + + if not ix_volumes: + utils.throw_error("Expected [ix_volumes] to be set for [ix_volume] type") + + ds = data["ix_volume_config"]["dataset_name"] + path = ix_volumes.get(ds, None) + if not path: + utils.throw_error(f"Expected the key [{ds}] to be set in [ix_volumes]") + + return path + + +# Constructs a volume object for a cifs type +def _process_cifs(data): + if not data.get("cifs_config"): + utils.throw_error("Expected [cifs_config] to be set for [cifs] type") + + required_keys = ["server", "path", "username", "password"] + for key in required_keys: + if not data["cifs_config"].get(key): + utils.throw_error(f"Expected [{key}] to be set for [cifs] type") + + opts = [ + f"user={data['cifs_config']['username']}", + f"password={data['cifs_config']['password']}", + ] + if data["cifs_config"].get("domain"): + opts.append(f'domain={data["cifs_config"]["domain"]}') + + if data["cifs_config"].get("options"): + if not isinstance(data["cifs_config"]["options"], list): + utils.throw_error( + "Expected [cifs_config.options] to be a list for [cifs] type" + ) + + disallowed_opts = ["user", "password", "domain"] + for opt in data["cifs_config"]["options"]: + if not isinstance(opt, str): + utils.throw_error( + "Expected [cifs_config.options] to be a list of strings for [cifs] type" + ) + + key = opt.split("=")[0] + for disallowed in disallowed_opts: + if key == disallowed: + utils.throw_error( + f"Expected [cifs_config.options] to not start with [{disallowed}] for [cifs] type" + ) + + opts.append(opt) + + server = data["cifs_config"]["server"].lstrip("/") + path = data["cifs_config"]["path"] + volume = { + "driver_opts": { + "type": "cifs", + "device": f"//{server}/{path}", + "o": f"{','.join(opts)}", + }, + } + + return volume + + +# Constructs a volume object for a nfs type +def _process_nfs(data): + if not data.get("nfs_config"): + utils.throw_error("Expected [nfs_config] to be set for [nfs] type") + + required_keys = ["server", "path"] + for key in required_keys: + if not data["nfs_config"].get(key): + utils.throw_error(f"Expected [{key}] to be set for [nfs] type") + + opts = [f"addr={data['nfs_config']['server']}"] + if data["nfs_config"].get("options"): + if not isinstance(data["nfs_config"]["options"], list): + utils.throw_error("Expected [nfs_config.options] to be a list for [nfs] type") + + disallowed_opts = ["addr"] + for opt in data["nfs_config"]["options"]: + if not isinstance(opt, str): + utils.throw_error( + "Expected [nfs_config.options] to be a list of strings for [nfs] type" + ) + + key = opt.split("=")[0] + for disallowed in disallowed_opts: + if key == disallowed: + utils.throw_error( + f"Expected [nfs_config.options] to not start with [{disallowed}] for [nfs] type" + ) + + opts.append(opt) + + volume = { + "driver_opts": { + "type": "nfs", + "device": f":{data['nfs_config']['path']}", + "o": f"{','.join(opts)}", + }, + } + + return volume diff --git a/ix-dev/enterprise/minio/templates/library/base_v1_0_0/utils.py b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/utils.py new file mode 100644 index 0000000000..0fee6d2d65 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/base_v1_0_0/utils.py @@ -0,0 +1,83 @@ +import hashlib +import secrets +import sys + +from . import security + + +class TemplateException(Exception): + pass + + +def throw_error(message): + # When throwing a known error, hide the traceback + # This is because the error is also shown in the UI + # and having a traceback makes it hard for user to read + sys.tracebacklimit = 0 + raise TemplateException(message) + + +def secure_string(length): + return secrets.token_urlsafe(length) + + +def basic_auth_header(username, password): + return f"Basic {security.htpasswd(username, password)}" + + +def merge_dicts(*dicts): + merged_dict = {} + for dictionary in dicts: + merged_dict.update(dictionary) + return merged_dict + + +# Basic validation for a path (Expand later) +def valid_path(path=""): + if not path.startswith("/"): + throw_error(f"Expected path [{path}] to start with /") + + # There is no reason to allow / as a path, either on host or in a container + if path == "/": + throw_error(f"Expected path [{path}] to not be /") + + return path + + +def camel_case(string): + return string.title() + + +def is_boolean(string): + return string.lower() in ["true", "false"] + + +def is_number(string): + try: + float(string) + return True + except ValueError: + return False + + +def get_image(images={}, name=""): + if not images: + throw_error("Expected [images] to be set") + if name not in images: + throw_error(f"Expected [images.{name}] to be set") + if not images[name].get("repository") or not images[name].get("tag"): + throw_error( + f"Expected [images.{name}.repository] and [images.{name}.tag] to be set" + ) + + return f"{images[name]['repository']}:{images[name]['tag']}" + + +def hash_data(data=""): + if not data: + throw_error("Expected [data] to be set") + return hashlib.sha256(data.encode("utf-8")).hexdigest() + + +def get_image_with_hashed_data(images={}, name="", data=""): + return f"ix-{get_image(images, name)}-{hash_data(data)}" diff --git a/ix-dev/enterprise/minio/templates/library/enterprise/minio/v1_0_0/__init__.py b/ix-dev/enterprise/minio/templates/library/enterprise/minio/v1_0_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ix-dev/enterprise/minio/templates/library/enterprise/minio/v1_0_0/data.py b/ix-dev/enterprise/minio/templates/library/enterprise/minio/v1_0_0/data.py new file mode 100644 index 0000000000..202409de2f --- /dev/null +++ b/ix-dev/enterprise/minio/templates/library/enterprise/minio/v1_0_0/data.py @@ -0,0 +1,63 @@ +from base_v1_0_0 import utils + + +def validate(data): + multi_mode = data["minio"]["multi_mode"] + storage = data["storage"] + if len(storage["data_dirs"]) == 0: + utils.throw_error("At least 1 storage item must be set") + + if ( + len(storage["data_dirs"]) > 1 + and not multi_mode.get("enabled", False) + and not len(multi_mode.get("entries", [])) > 0 + ): + utils.throw_error( + "[Multi Mode] must be enabled and entries must be set if more than 1 storage item is set" + ) + + # make sure mount_paths in data["storage"]["data_dirs"] are unique + mount_paths = [item["mount_path"].rstrip("/") for item in storage["data_dirs"]] + if len(mount_paths) != len(set(mount_paths)): + utils.throw_error( + f"Mount paths in storage items must be unique, found duplicates: [{', '.join(mount_paths)}]" + ) + + if len(multi_mode.get("entries", [])) > 0: + disallowed_keys = ["server"] + for item in multi_mode["entries"]: + if item in disallowed_keys: + utils.throw_error( + f"MinIO: Value [{item}] is not allowed in [Multi Mode] items" + ) + + # /data{1...4} + if item.startswith("/"): + # check if these characters exist in item + if any(char in item for char in ["{", "}"]) and "..." not in item: + utils.throw_error( + f"MinIO: [Multi Mode] item [{item}] must have 3 dots when they are paths" + + " with expansion eg [/some_path{1...4}]" + ) + + +def get_commands(values): + commands = [ + "server", + "--address", + f":{values['network']['api_port']}", + "--console-address", + f":{values['network']['console_port']}", + ] + + if values["network"]["certificate_id"]: + commands.append("--certs-dir") + commands.append("/.minio/certs") + + if values["minio"]["logging"]["quiet"]: + commands.append("--quiet") + + if values["minio"]["logging"]["anonymous"]: + commands.append("--anonymous") + + return commands diff --git a/ix-dev/enterprise/minio/templates/macros/global/perms/container.yaml.jinja b/ix-dev/enterprise/minio/templates/macros/global/perms/container.yaml.jinja new file mode 100644 index 0000000000..039637bee2 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/macros/global/perms/container.yaml.jinja @@ -0,0 +1,48 @@ +{% from "macros/global/perms/script.sh.jinja" import process_dir_func %} + +{# Takes a list of items to process #} +{# Each item is a dictionary with the following keys: #} +{# - dir: directory to process #} +{# - mode: always, check. ( + always: Always changes ownership and permissions, + check: Checks the top level dir, and only applies if there is a mismatch. +) #} +{# - uid: uid to change to #} +{# - gid: gid to change to #} +{# - chmod: chmod to change to (Optional, default is no change) #} +{% macro perms_container(items=[]) %} +image: bash +user: root +deploy: + resources: + limits: + cpus: "1.0" + memory: 512m +entrypoint: + - bash + - -c +command: + - | + {{- process_dir_func() | indent(4) }} + {%- for item in items %} + process_dir {{ item.dir }} {{ item.mode }} {{ item.uid }} {{ item.gid }} {{ item.chmod }} {{ item.is_temporary|lower }} + {%- endfor %} +{% endmacro %} + +{# Examples #} +{# perms_container([ + { + "dir": "/mnt/directories/dir1", + "mode": "always", + "uid": 500, + "gid": 500, + "chmod": "755", + }, + { + "dir": "/mnt/directories/dir2", + "mode": "check", + "uid": 500, + "gid": 500, + "chmod": "755", + }, +]) #} diff --git a/ix-dev/enterprise/minio/templates/macros/global/perms/script.sh.jinja b/ix-dev/enterprise/minio/templates/macros/global/perms/script.sh.jinja new file mode 100644 index 0000000000..7b8fe31cd3 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/macros/global/perms/script.sh.jinja @@ -0,0 +1,75 @@ +{# +Don't forget to use double $ for shell variables, +otherwise docker-compose will try to expand them +#} + +{% macro process_dir_func() %} +function process_dir() { + local dir=$$1 + local mode=$$2 + local uid=$$3 + local gid=$$4 + local chmod=$$5 + local is_temporary=$$6 + + local fix_owner="false" + local fix_perms="false" + + if [ ! -d "$$dir" ]; then + echo "Path [$$dir] does is not a directory, skipping..." + exit 0 + fi + + if [ "$$is_temporary" = "true" ]; then + echo "Path [$$dir] is a temporary directory, ensuring it is empty..." + rm -rf "$$dir/{*,.*}" + fi + + echo "Current Ownership and Permissions on [$$dir]:" + echo "chown: $$(stat -c "%u %g" "$$dir")" + echo "chmod: $$(stat -c "%a" "$$dir")" + + if [ "$$mode" = "always" ]; then + fix_owner="true" + fix_perms="true" + fi + + if [ "$$mode" = "check" ]; then + if [ $$(stat -c %u "$$dir") -eq $$uid ] && [ $$(stat -c %g "$$dir") -eq $$gid ]; then + echo "Ownership is correct. Skipping..." + fix_owner="false" + else + echo "Ownership is incorrect. Fixing..." + fix_owner="true" + fi + + if [ "$$chmod" = "false" ]; then + echo "Skipping permissions check, chmod is false" + elif [ -n "$$chmod" ]; then + if [ $$(stat -c %a "$$dir") -eq $$chmod ]; then + echo "Permissions are correct. Skipping..." + fix_perms="false" + else + echo "Permissions are incorrect. Fixing..." + fix_perms="true" + fi + fi + fi + + if [ "$$fix_owner" = "true" ]; then + echo "Changing ownership to $$uid:$$gid on: [$$dir]" + chown -R "$$uid:$$gid" "$$dir" + echo "Finished changing ownership" + echo "Ownership after changes:" + stat -c "%u %g" "$$dir" + fi + + if [ -n "$$chmod" ] && [ "$$fix_perms" = "true" ]; then + echo "Changing permissions to $$chmod on: [$$dir]" + chmod -R "$$chmod" "$$dir" + echo "Finished changing permissions" + echo "Permissions after changes:" + stat -c "%a" "$$dir" + fi +} +{% endmacro %} diff --git a/ix-dev/enterprise/minio/templates/test_values/basic-multi-mode-values.yaml b/ix-dev/enterprise/minio/templates/test_values/basic-multi-mode-values.yaml new file mode 100644 index 0000000000..c11b6c93f0 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/test_values/basic-multi-mode-values.yaml @@ -0,0 +1,58 @@ +resources: + limits: + cpus: 2.0 + memory: 4096 + +minio: + credentials: + access_key: minio + secret_key: minio123 + multi_mode: + enabled: true + entries: + - /data{1...5} + logging: + quiet: false + anonymous: false + +network: + api_port: 9000 + console_port: 9001 + certificate_id: null + host_network: false + console_url: http://localhost:9001 + server_url: http://localhost:9000 + +run_as: + user: 568 + group: 568 + +storage: + data_dirs: + - type: host_path + mount_path: /data1 + auto_permissions: true + host_path_config: + path: /mnt/test/data1 + - type: host_path + mount_path: /data2 + auto_permissions: true + host_path_config: + path: /mnt/test/data2 + - type: host_path + mount_path: /data3 + auto_permissions: true + host_path_config: + path: /mnt/test/data3 + - type: host_path + mount_path: /data4 + auto_permissions: true + host_path_config: + path: /mnt/test/data4 + - type: ix_volume + mount_path: /data5 + auto_permissions: true + ix_volume_config: + dataset_name: data5 +ix_volumes: + data5: /mnt/test/data5 diff --git a/ix-dev/enterprise/minio/templates/test_values/basic-values.yaml b/ix-dev/enterprise/minio/templates/test_values/basic-values.yaml new file mode 100644 index 0000000000..c21682a550 --- /dev/null +++ b/ix-dev/enterprise/minio/templates/test_values/basic-values.yaml @@ -0,0 +1,33 @@ +resources: + limits: + cpus: 2.0 + memory: 4096 + +minio: + credentials: + access_key: minio + secret_key: minio123 + multi_mode: + entries: [] + logging: + quiet: false + anonymous: false + +network: + api_port: 9000 + console_port: 9001 + certificate_id: null + host_network: false + console_url: http://localhost:9001 + server_url: http://localhost:9000 + +run_as: + user: 568 + group: 568 + +storage: + data_dirs: + - type: volume + volume_name: data1 + mount_path: /data1 + auto_permissions: true diff --git a/ix-dev/enterprise/minio/templates/test_values/https-values.yaml b/ix-dev/enterprise/minio/templates/test_values/https-values.yaml new file mode 100644 index 0000000000..1c1db6b93a --- /dev/null +++ b/ix-dev/enterprise/minio/templates/test_values/https-values.yaml @@ -0,0 +1,120 @@ +resources: + limits: + cpus: 2.0 + memory: 4096 + +minio: + credentials: + access_key: minio + secret_key: minio123 + multi_mode: + entries: [] + logging: + quiet: false + anonymous: false + +network: + api_port: 9000 + console_port: 9001 + certificate_id: "1" + host_network: false + console_url: http://localhost:9001 + server_url: http://localhost:9000 + +storage: + data_dirs: + - type: host_path + mount_path: /data1 + auto_permissions: true + host_path_config: + path: /mnt/test/data1 + +run_as: + user: 568 + group: 568 + +ix_certificates: + "1": + certificate: | + -----BEGIN CERTIFICATE----- + MIIEdjCCA16gAwIBAgIDYFMYMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz + ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD + VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w + HhcNMjEwODMwMjMyMzU0WhcNMjMxMjAzMjMyMzU0WjBuMQswCQYDVQQDDAJhZDEL + MAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxDTALBgNVBAcMBGFzZGYxDTALBgNV + BAoMBGFkc2YxDTALBgNVBAsMBGFzZGYxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w + ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7+1xOHRQyOnQTHFcrdasX + Zl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/AbkH7oVFWC1 + P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI4vQCdYgW + 2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2dNgsxKU0H + PGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB+Zie331t + AzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7A/FuDVg3 + AgMBAAGjggEdMIIBGTAnBgNVHREEIDAehwTAqAADhwTAqAAFhwTAqAC2hwTAqACB + hwTAqACSMB0GA1UdDgQWBBQ4G2ff4tgZl4vmo4xCfqmJhdqShzAMBgNVHRMBAf8E + AjAAMIGYBgNVHSMEgZAwgY2AFLlYf9L99nxJDcpCM/LT3V5hQ/a3oXCkbjBsMQww + CgYDVQQDDANhc2QxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQH + DAJhZjENMAsGA1UECgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkB + FgdhQGEuY29tggNgUxcwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwEwDgYDVR0PAQH/ + BAQDAgWgMA0GCSqGSIb3DQEBCwUAA4IBAQA6FpOInEHB5iVk3FP67GybJ29vHZTD + KQHbQgmg8s4L7qIsA1HQ+DMCbdylpA11x+t/eL/n48BvGw2FNXpN6uykhLHJjbKR + h8yITa2KeD3LjLYhScwIigXmTVYSP3km6s8jRL6UKT9zttnIHyXVpBDya6Q4WTMx + fmfC6O7t1PjQ5ZyVtzizIUP8ah9n4TKdXU4A3QIM6WsJXpHb+vqp1WDWJ7mKFtgj + x5TKv3wcPnktx0zMPfLb5BTSE9rc9djcBG0eIAsPT4FgiatCUChe7VhuMnqskxEz + MymJLoq8+mzucRwFkOkR2EIt1x+Irl2mJVMeBow63rVZfUQBD8h++LqB + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIEhDCCA2ygAwIBAgIDYFMXMA0GCSqGSIb3DQEBCwUAMGwxDDAKBgNVBAMMA2Fz + ZDELMAkGA1UEBhMCVVMxDTALBgNVBAgMBGFzZGYxCzAJBgNVBAcMAmFmMQ0wCwYD + VQQKDARhc2RmMQwwCgYDVQQLDANhc2QxFjAUBgkqhkiG9w0BCQEWB2FAYS5jb20w + HhcNMjEwODMwMjMyMDQ1WhcNMzEwODI4MjMyMDQ1WjBsMQwwCgYDVQQDDANhc2Qx + CzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARhc2RmMQswCQYDVQQHDAJhZjENMAsGA1UE + CgwEYXNkZjEMMAoGA1UECwwDYXNkMRYwFAYJKoZIhvcNAQkBFgdhQGEuY29tMIIB + IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq//c0hEEr83CS1pMgsHX50jt + 2MqIbcf63UUNJTiYpUUvUQSFJFc7m/dr+RTZvu97eDCnD5K2qkHHvTPaPZwY+Djf + iy7N641Sz6u/y3Yo3xxs1Aermsfedh48vusJpjbkT2XS44VjbkrpKcWDNVpp3Evd + M7oJotXeUsZ+imiyVCfr4YhoY5gbGh/r+KN9Wf9YKoUyfLLZGwdZkhtX2zIbidsL + Thqi9YTaUHttGinjiBBum234u/CfvKXsfG3yP2gvBGnlvZnM9ktv+lVffYNqlf7H + VmB1bKKk84HtzuW5X76SGAgOG8eHX4x5ZLI1WQUuoQOVRl1I0UCjBtbz8XhwvQID + AQABo4IBLTCCASkwLQYDVR0RBCYwJIcEwKgABYcEwKgAA4cEwKgAkocEwKgAtYcE + wKgAgYcEwKgAtjAdBgNVHQ4EFgQUuVh/0v32fEkNykIz8tPdXmFD9rcwDwYDVR0T + AQH/BAUwAwEB/zCBmAYDVR0jBIGQMIGNgBS5WH/S/fZ8SQ3KQjPy091eYUP2t6Fw + pG4wbDEMMAoGA1UEAwwDYXNkMQswCQYDVQQGEwJVUzENMAsGA1UECAwEYXNkZjEL + MAkGA1UEBwwCYWYxDTALBgNVBAoMBGFzZGYxDDAKBgNVBAsMA2FzZDEWMBQGCSqG + SIb3DQEJARYHYUBhLmNvbYIDYFMXMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF + BQcDAjAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAKEocOmVuWlr + zegtKYMe8NhHIkFY9oVn5ym6RHNOJpPH4QF8XYC3Z5+iC5yGh4P/jVe/4I4SF6Ql + PtofU0jNq5vzapt/y+m008eXqPQFmoUOvu+JavoRVcRx2LIP5AgBA1mF56CSREsX + TkuJAA9IUQ8EjnmAoAeKINuPaKxGDuU8BGCMqr/qd564MKNf9XYL+Fb2rlkA0O2d + 2No34DQLgqSmST/LAvPM7Cbp6knYgnKmGr1nETCXasg1cueHLnWWTvps2HiPp2D/ + +Fq0uqcZLu4Mdo0CPs4e5sHRyldEnRSKh0DVLprq9zr/GMipmPLJUsT5Jed3sj0w + M7Y3vwxshpo= + -----END CERTIFICATE----- + privatekey: | + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC7+1xOHRQyOnQT + HFcrdasXZl0gzutVlA890a1wiQpdD5dOtCLo7+eqVYjqVKo9W8RUIArXWmBu/Abk + H7oVFWC1P973W1+ArF5sA70f7BZgqRKJTIisuIFIlRETgfnP2pfQmHRZtGaIJRZI + 4vQCdYgW2g0KOvvNcZJCVq1OrhKiNiY1bWCp66DGg0ic6OEkZFHTm745zUNQaf2d + NgsxKU0HPGjVLJI//yrRFAOSBUqgD4c50krnMF7fU/Fqh+UyOu8t6Y/HsySh3urB + +Zie331tAzV6QV39KKxRflNx/yuWrtIEslGTm+xHKoCYJEk/nZ3mX8Y5hG6wWAb7 + A/FuDVg3AgMBAAECggEAapt30rj9DitGTtxAt13pJMEhyYxvvD3WkvmJwguF/Bbu + eW0Ba1c668fMeRCA54FWi1sMqusPS4HUqqUvk+tmyAOsAF4qgD/A4MMSC7uJSVI5 + N/JWhJWyhCY94/FPakiO1nbPbVw41bcqtzU2qvparpME2CtxSCbDiqm7aaag3Kqe + EF0fGSUdZ+TYl9JM05+eIyiX+UY19Fg0OjTHMn8nGpxcNTfDBdQ68TKvdo/dtIKL + PLKzJUNNdM8odC4CvQtfGMqaslwZwXkiOl5VJcW21ncj/Y0ngEMKeD/i65ZoqGdR + 0FKCQYEAGtM2FvJcZQ92Wsw7yj2bK2MSegVUyLK32QKBgQDe8syVCepPzRsfjfxA + 6TZlWcGuTZLhwIx97Ktw3VcQ1f4rLoEYlv0xC2VWBORpzIsJo4I/OLmgp8a+Ga8z + FkVRnq90dV3t4NP9uJlHgcODHnOardC2UUka4olBSCG6zmK4Jxi34lOxhGRkshOo + L4IBeOIB5g+ZrEEXkzfYJHESRQKBgQDX2YhFhGIrT8BAnC5BbXbhm8h6Bhjz8DYL + d+qhVJjef7L/aJxViU0hX9Ba2O8CLK3FZeREFE3hJPiJ4TZSlN4evxs5p+bbNDcA + 0mhRI/o3X4ac6IxdRebyYnCOB/Cu94/MzppcZcotlCekKNike7eorCcX4Qavm7Pu + MUuQ+ifmSwKBgEnchoqZzlbBzMqXb4rRuIO7SL9GU/MWp3TQg7vQmJerTZlgvsQ2 + wYsOC3SECmhCq4117iCj2luvOdihCboTFsQDnn0mpQe6BIF6Ns3J38wAuqv0CcFd + DKsrge1uyD3rQilgSoAhKzkUc24o0PpXQurZ8YZPgbuXpbj5vPaOnCdBAoGACYc7 + wb3XS4wos3FxhUfcwJbM4b4VKeeHqzfu7pI6cU/3ydiHVitKcVe2bdw3qMPqI9Wc + nvi6e17Tbdq4OCsEJx1OiVwFD9YdO3cOTc6lw/3+hjypvZBRYo+/4jUthbu96E+S + dtOzehGZMmDvN0uSzupSi3ZOgkAAUFpyuIKickMCgYAId0PCRjonO2thn/R0rZ7P + //L852uyzYhXKw5/fjFGhQ6LbaLgIRFaCZ0L2809u0HFnNvJjHv4AKP6j+vFQYYY + qQ+66XnfsA9G/bu4MDS9AX83iahD9IdLXQAy8I19prAbpVumKegPbMnNYNB/TYEc + 3G15AKCXo7jjOUtHY01DCQ== + -----END PRIVATE KEY-----