From 6b0c0fcecc086eabc3cf7c3c751db866f818702b Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 4 Oct 2025 23:45:15 +0100 Subject: [PATCH 01/19] PORT: CLI arguments Mostly set up. Missing the excluded fields. Signed-off-by: Jim Fitzpatrick --- .flake8 | 5 - .gitignore | 2 + .pre-commit-config.yaml | 30 ---- README.md | 10 +- build.zig | 125 +++++++++++++++ build.zig.zon | 12 ++ kubectlgetall/__init__.py | 1 - kubectlgetall/cli.py | 309 -------------------------------------- poetry.lock | 207 ------------------------- pyproject.toml | 65 -------- src/main.zig | 117 +++++++++++++++ 11 files changed, 257 insertions(+), 626 deletions(-) delete mode 100644 .flake8 create mode 100644 build.zig create mode 100644 build.zig.zon delete mode 100644 kubectlgetall/__init__.py delete mode 100644 kubectlgetall/cli.py delete mode 100644 poetry.lock delete mode 100644 pyproject.toml create mode 100644 src/main.zig diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 6053f3b..0000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -ignore = E203, E266, E501, W503, F403, F401 -max-line-length = 120 -max-complexity = 18 -select = B,C,E,F,W,T4,B9 diff --git a/.gitignore b/.gitignore index c103bae..7a0e1b9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ __pycache__/ nohup.out /tmp *.db +zig-out/ +.zig-cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7942ce8..0f91d84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,44 +1,14 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: -- repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - language_version: python3.10 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-toml - id: detect-private-key - id: check-merge-conflict - - id: check-docstring-first - - id: check-ast - id: check-added-large-files - id: check-yaml - id: no-commit-to-branch args: - --branch=main -- repo: https://github.com/pycqa/flake8 - rev: 7.1.1 - hooks: - - id: flake8 -- repo: https://github.com/python-poetry/poetry - rev: '2.0.1' - hooks: - - id: poetry-check - - id: poetry-lock -- repo: https://github.com/PyCQA/isort - rev: '6.0.0' - hooks: - - id: isort -- repo: https://github.com/PyCQA/bandit - rev: '1.8.2' - hooks: - - id: bandit -- repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.15.0' - hooks: - - id: mypy - args: [--strict] - additional_dependencies: [rich] diff --git a/README.md b/README.md index 6261355..3da7b91 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,7 @@ List all CR's for all CRD types on a cluster in a given namespace. **Requires kubectl to be installed.** ## Installation -Installing with no external dependencies. -```shell -pipx install kubectallgetall -``` - -Install with nice formatting. -```shell -pipx install kubectallgetall[rich] -``` +ERROR: No good answer right now ## Usage diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..92cca69 --- /dev/null +++ b/build.zig @@ -0,0 +1,125 @@ +const std = @import("std"); + +// Although this function looks imperative, it does not perform the build +// directly and instead it mutates the build graph (`b`) that will be then +// executed by an external runner. The functions in `std.Build` implement a DSL +// for defining build steps and express dependencies between them, allowing the +// build runner to parallelize the build automatically (and the cache system to +// know when a step doesn't need to be re-run). +pub fn build(b: *std.Build) void { + // Standard target options allow the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + // It's also possible to define more custom flags to toggle optional features + // of this build script using `b.option()`. All defined flags (including + // target and optimize options) will be listed when running `zig build --help` + // in this directory. + // + const clap = b.dependency("clap", .{ + .target = target, + .optimize = optimize, + }); + + // Here we define an executable. An executable needs to have a root module + // which needs to expose a `main` function. While we could add a main function + // to the module defined above, it's sometimes preferable to split business + // business logic and the CLI into two separate modules. + // + // If your goal is to create a Zig library for others to use, consider if + // it might benefit from also exposing a CLI tool. A parser library for a + // data serialization format could also bundle a CLI syntax checker, for example. + // + // If instead your goal is to create an executable, consider if users might + // be interested in also being able to embed the core functionality of your + // program in their own executable in order to avoid the overhead involved in + // subprocessing your CLI tool. + // + // If neither case applies to you, feel free to delete the declaration you + // don't need and to put everything under a single module. + const exe = b.addExecutable(.{ + .name = "kubectlgetall", + .root_module = b.createModule(.{ + // b.createModule defines a new module just like b.addModule but, + // unlike b.addModule, it does not expose the module to consumers of + // this package, which is why in this case we don't have to give it a name. + .root_source_file = b.path("src/main.zig"), + // Target and optimization levels must be explicitly wired in when + // defining an executable or library (in the root module), and you + // can also hardcode a specific target for an executable or library + // definition if desireable (e.g. firmware for embedded devices). + .target = target, + .optimize = optimize, + // List of modules available for import in source files part of the + // root module. + .imports = &.{}, + }), + }); + + exe.root_module.addImport("clap", clap.module("clap")); + + // This declares intent for the executable to be installed into the + // install prefix when running `zig build` (i.e. when executing the default + // step). By default the install prefix is `zig-out/` but can be overridden + // by passing `--prefix` or `-p`. + b.installArtifact(exe); + + // This creates a top level step. Top level steps have a name and can be + // invoked by name when running `zig build` (e.g. `zig build run`). + // This will evaluate the `run` step rather than the default step. + // For a top level step to actually do something, it must depend on other + // steps (e.g. a Run step, as we will see in a moment). + const run_step = b.step("run", "Run the app"); + + // This creates a RunArtifact step in the build graph. A RunArtifact step + // invokes an executable compiled by Zig. Steps will only be executed by the + // runner if invoked directly by the user (in the case of top level steps) + // or if another step depends on it, so it's up to you to define when and + // how this Run step will be executed. In our case we want to run it when + // the user runs `zig build run`, so we create a dependency link. + const run_cmd = b.addRunArtifact(exe); + run_step.dependOn(&run_cmd.step); + + // By making the run step depend on the default step, it will be run from the + // installation directory rather than directly from within the cache directory. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // Creates an executable that will run `test` blocks from the executable's + // root module. Note that test executables only test one module at a time, + // hence why we have to create two separate ones. + const exe_tests = b.addTest(.{ + .root_module = exe.root_module, + }); + + // A run step that will run the second test executable. + const run_exe_tests = b.addRunArtifact(exe_tests); + + // A top level step for running all tests. dependOn can be called multiple + // times and since the two run steps do not depend on one another, this will + // make the two of them run in parallel. + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&run_exe_tests.step); + + // Just like flags, top level steps are also listed in the `--help` menu. + // + // The Zig build system is entirely implemented in userland, which means + // that it cannot hook into private compiler APIs. All compilation work + // orchestrated by the build system will result in other Zig compiler + // subcommands being invoked with the right flags defined. You can observe + // these invocations when one fails (or you pass a flag to increase + // verbosity) to validate assumptions and diagnose problems. + // + // Lastly, the Zig build system is relatively simple and self-contained, + // and reading its source code will allow you to master it. +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..29cf8e4 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .kubectlgetal, + .version = "0.15.1", + .dependencies = .{ + .clap = .{ + .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz", + .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", + }, + }, + .paths = .{""}, + .fingerprint = 0x1b03674a4cac8e50, +} diff --git a/kubectlgetall/__init__.py b/kubectlgetall/__init__.py deleted file mode 100644 index 6a9beea..0000000 --- a/kubectlgetall/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.4.0" diff --git a/kubectlgetall/cli.py b/kubectlgetall/cli.py deleted file mode 100644 index 1f54a32..0000000 --- a/kubectlgetall/cli.py +++ /dev/null @@ -1,309 +0,0 @@ -import argparse -import asyncio -import datetime -import json -import logging -import sqlite3 -import subprocess # nosec -from typing import Any - -from kubectlgetall import __version__ - -logger: logging.Logger = logging.getLogger("kubectlgetall") - - -def db_connection(database: str) -> tuple[sqlite3.Cursor, sqlite3.Connection]: - conn = sqlite3.connect(database) - cur = conn.cursor() - cur.execute( - "CREATE TABLE IF NOT EXISTS results(apiVersion, kind, name, namespace, creationTimestamp, resourceVersion, resultTimestamp, resultLabel)" - ) - return cur, conn - - -async def results_to_db( - namespace: str | None, - crd_types: list[str], - exclude: tuple[str], - database: str, - label: str, -) -> None: - if namespace: - logger.info(f"saving results for namespace {namespace} to {database}") - else: - logger.info(f"saving results for all namespaces to {database}") - cur, conn = db_connection(database) - exclude_ = ["events.events.k8s.io", "events", ""] - if exclude is not None: - exclude_ = exclude_ + list(exclude) - - block: dict[str, Any] = {} - for crd in crd_types: - if crd not in exclude_: - await get_cr_lists_json(crd, namespace, block) - - timestamp = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") - data = [] - for crd in block: - for item in block[crd]: - data.append( - ( - item["apiVersion"], - item["kind"], - item["metadata"]["name"], - item["metadata"]["namespace"], - item["metadata"]["creationTimestamp"], - item["metadata"].get("resourceVersion", None), - timestamp, - label, - ) - ) - cur.executemany("INSERT INTO results VALUES(?, ?, ?, ?, ?, ?, ?, ?)", data) - conn.commit() - if namespace: - logger.info( - f"saving results for namespace {namespace} to {database}: completed" - ) - else: - logger.info(f"saving results for all namespaces to {database}: completed") - - -def get_crd_list() -> list[str]: - value: list[str] = [] - data = subprocess.run( # nosec - ["kubectl", "api-resources", "--verbs=list", "--namespaced", "-o", "name"], - capture_output=True, - ) - - if data.stderr == b"": - value = data.stdout.decode().split("\n") - - return value - - -async def get_result_json( - namespace: str | None, - crd_types: list[str], - sort: bool = False, - exclude: tuple[str] | None = None, -) -> None: - exclude_ = ["events.events.k8s.io", "events", ""] - - if exclude is not None: - exclude_ = exclude_ + list(exclude) - - block: dict[str, Any] = {} - for crd in crd_types: - if crd not in exclude_: - await get_cr_lists_json(crd, namespace, block) - - print(block) - - -async def get_result( - namespace: str | None, - crd_types: list[str], - sort: bool = False, - exclude: tuple[str] | None = None, -) -> None: - exclude_ = ["events.events.k8s.io", "events", ""] - - if exclude is not None: - exclude_ = exclude_ + list(exclude) - - for crd in crd_types: - if crd not in exclude_: - if sort: - asyncio.create_task(get_cr_lists(crd, namespace)) - else: - await get_cr_lists(crd, namespace) - - -async def get_cr_lists_json( - crd: str, namespace: str | None, store: dict[str, Any] -) -> None: - cmd = [ - "kubectl", - "get", - "--ignore-not-found", - crd, - "--output", - "json", - ] - if namespace: - cmd += ["--namespace", namespace] - else: - cmd.append("--all-namespaces") - logger.debug(f'cmd = "{" ".join(cmd)}"') - data = subprocess.run( # nosec - cmd, - capture_output=True, - ) - if data.returncode == 0 and data.stdout != b"": - d = json.loads(data.stdout.decode()) - for e in d["items"]: - if "spec" in e: - del e["spec"] - if "data" in e: - del e["data"] - store[crd] = d["items"] - - -def table_print(title: str, data: str) -> None: - try: - logger.debug("Loading rich pulgin") - from rich.console import Console - from rich.table import Table - - table = Table(title=title, title_style="bold green", header_style="bold") - - split_data = data.split("\n") - for v in split_data[0].split(): - table.add_column(v, no_wrap=True) - - for a in split_data[1:-1]: - table.add_row(*a.split()) - - console = Console() - console.print(table) - console.print() - - except ModuleNotFoundError as err: - logger.debug(err) - print(title) - print(data) - - -async def get_cr_lists(crd: str, namespace: str | None) -> None: - cmd = ["kubectl", "get", "--ignore-not-found", crd] - if namespace: - cmd += ["--namespace", namespace] - else: - cmd.append("--all-namespaces") - logger.debug(f'cmd = "{" ".join(cmd)}"') - data = subprocess.run( # nosec - cmd, - capture_output=True, - ) - if data.returncode == 0 and data.stdout != b"": - table_print(crd, data.stdout.decode()) - - -def configure_logger(logger: logging.Logger, debug: bool = False) -> None: - ch = logging.StreamHandler() - if debug: - logger.setLevel(logging.DEBUG) - ch.setLevel(logging.DEBUG) - else: - logger.setLevel(logging.INFO) - ch.setLevel(logging.INFO) - formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") - ch.setFormatter(formatter) - logger.addHandler(ch) - - -def cli() -> None: - """ - Returns a list of CR for the different CRDs in a given namespace - """ - - parser = argparse.ArgumentParser( - prog="kubectlgetall", - description="Returns a list of CR for the different CRDs in a given namespace", - ) - - parser.add_argument("-n", "--namespace", help="Namespace to get resources from.") - parser.add_argument( - "-A", - "--all-namespaces", - action="store_true", - help="If present, list all objects across all namespaces. Specifinig --namespace will be ignored", - ) - parser.add_argument( - "--version", - action="version", - version=f"%(prog)s, version {__version__}", - ) - parser.add_argument( - "-s", - "--sort", - action="store_true", - help="Prints the resources in an order. Initial results take longer to show. " - "Unsorted return results faster but can hit rate limits.", - ) - parser.add_argument( - "-e", - "--exclude", - nargs="*", - help='Exclude crd types. Multiple can be excluded eg: "-e "', - ) - parser.add_argument( - "-o", - "--output", - default="tty", - choices=["tty", "json", "sqlite"], - help="Changes the output format of the results (default: %(default)s)", - ) - parser.add_argument( - "-d", - "--database", - help="Path to the sqlite file to save the results. If the file does not exist it will be created.", - ) - parser.add_argument( - "-l", - "--label", - help="Set the label that will be saved with entries when using the --database option.", - ) - parser.add_argument("--debug", help="Enable debug mode.", action="store_true") - args = parser.parse_args() - configure_logger(logger, debug=args.debug) - logger.debug(f"{args=}") - - if args.output != "tty" and args.sort: - logger.error(f"Can't use --sort with --output set to {args.output}") - exit(1) - - namespace: str | None = None - if not args.all_namespaces: - if args.namespace is None: - logger.error("Namespace is required, use --namespace NAMESPACE") - exit(1) - namespace = args.namespace - - command(namespace, args.sort, args.exclude, args.output, args.database, args.label) - - -def command( - namespace: str | None, - sort: bool, - exclude: tuple[str], - output: str, - database: str, - label: str, -) -> None: - - if namespace is None: - logger.debug("Running on all namespaces") - else: - logger.debug(f"Running on namespace: {namespace}") - - crd_types = get_crd_list() - if sort: - crd_types = sorted(crd_types) - logger.debug("Running in sorted mode") - - if output == "json": - asyncio.run( - get_result_json(namespace=namespace, crd_types=crd_types, exclude=exclude) - ) - elif output == "sqlite": - if database is None: - logger.error("Require setting --database when using --output=sqlite") - exit(1) - asyncio.run(results_to_db(namespace, crd_types, exclude, database, label)) - else: - asyncio.run(get_result(namespace, crd_types, sort, exclude)) - - -if __name__ == "__main__": - cli() diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index ac19a05..0000000 --- a/poetry.lock +++ /dev/null @@ -1,207 +0,0 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"rich\"" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = true -python-versions = ">=3.7" -groups = ["main"] -markers = "extra == \"rich\"" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mypy" -version = "1.15.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, - {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, - {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, - {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, - {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, - {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, - {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, - {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, - {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, - {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, - {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, - {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, - {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, - {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, - {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, - {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, - {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, - {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, - {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, - {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, - {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, - {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, - {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "pygments" -version = "2.19.1" -description = "Pygments is a syntax highlighting package written in Python." -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"rich\"" -files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "rich" -version = "13.9.4" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = true -python-versions = ">=3.8.0" -groups = ["main"] -markers = "extra == \"rich\"" -files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version < \"3.11\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] -markers = {main = "extra == \"rich\" and python_version < \"3.11\""} - -[extras] -rich = ["rich"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.9" -content-hash = "f9a0a26b469eec9c75901ba1b04bcd375fbe3707bcc062ad3f4d8dcf817b4be1" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index de1c34a..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,65 +0,0 @@ -[tool.poetry] -name = "kubectlgetall" -version = "0.4.0" -description = "Get a list of CRs for cluster CRDs in a namespace" -authors = ["Jim Fitzpatrick "] -readme="README.md" -homepage = "https://github.com/Boomatang/kubectlgetall" -repository = "https://github.com/Boomatang/kubectlgetall" -documentation = "https://github.com/Boomatang/kubectlgetall" -keywords = ['OpenShift', 'Kubernetes', 'k8s', 'CRD', 'CR'] -classifiers = [ - "Intended Audience :: Developers", - "Topic :: Software Development" - ] -include = ["CHANGELOG.md"] - -[tool.poetry.dependencies] -python = "^3.9" -rich = {version = "^13.9.4", optional = true} - -[tool.poetry.dev-dependencies] - -[tool.poetry.extras] -rich = ["rich"] - -[tool.poetry.scripts] -kubectlgetall = 'kubectlgetall.cli:cli' - -[tool.poetry.group.dev.dependencies] -mypy = "^1.15.0" - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" - -[tool.black] -py36 = true -include = '\.pyi?$' -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - # The following are specific to Black, you probably don't want those. - | blib2to3 - | tests/data -)/ -''' - -[tool.isort] -profile = "black" - -[tool.bandit] -assert_used.skips = ['*_test.py', '*/test_*.py'] - -[tool.towncrier] -filename = "CHANGELOG.md" -directory = "changes" -package = "kubectlgetall" diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..5b55573 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,117 @@ +const clap = @import("clap"); +const std = @import("std"); + +const Config = struct { + namespace: []const u8, + all: bool, + sort: bool, + exclude: ?[][]const u8 = null, //TODO: need to pull these from the args. + output: Output, + database: []const u8, + label: []const u8, + logLevel: Level, +}; + +const Output = enum { tty, json, sqlite }; +const Level = enum { info, debug }; +const Bool = enum { true, false }; + +pub fn main() !void { + var gpa = std.heap.DebugAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + // First we specify what parameters our program can take. + // We can use `parseParamsComptime` to parse a string into an array of `Param(Help)`. + const params = comptime clap.parseParamsComptime( + \\-h, --help Display this help and exit. + \\-n, --namespace Namespace to get resources from. + \\-A, --all-namespaces If present, list all objects across all namespaces. Specifing --namespace will be ignored. + \\-s, --sort Prints the resources in order. + \\-e, --exclude ... Exclude crd types. Multiple can be excluded eg: "-e -e " + \\-o, --output Changes the output format of the results. + \\-d, --database Path to the sqlite file to save the results. If the files does not exist it will be created. + \\-l, --label Set the label that will be saved with entries when using the --database option. + \\--log-level Set the log level. All logs are saved to file. + \\ + ); + + const parsers = comptime .{ + .OUTPUT = clap.parsers.enumeration(Output), + .STR = clap.parsers.string, + .PATH = clap.parsers.string, + .LEVEL = clap.parsers.enumeration(Level), + .BOOL = clap.parsers.enumeration(Bool), + }; + + // Initialize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also pass `.{}` to `clap.parse` if you don't + // care about the extra information `Diagnostic` provides. + var diag = clap.Diagnostic{}; + var res = clap.parse(clap.Help, ¶ms, parsers, .{ + .diagnostic = &diag, + .allocator = allocator, + }) catch |err| { + // Report useful error and exit. + try diag.reportToFile(.stderr(), err); + return err; + }; + defer res.deinit(); + + var namespace: []const u8 = &[_]u8{}; + var allNamespaces = false; + var sort = false; + var output = Output.tty; + var database: []const u8 = &[_]u8{}; + var label: []const u8 = &[_]u8{}; + var level = Level.info; + + if (res.args.help != 0) + return clap.helpToFile(.stdout(), clap.Help, ¶ms, .{}); + + if (res.args.namespace) |n| { + namespace = n; + } + + if (res.args.@"all-namespaces") |n| { + if (n == Bool.true) { + allNamespaces = true; + } + } + + if (res.args.sort) |s| { + if (s == Bool.true) { + sort = true; + } + } + + if (res.args.output) |o| { + output = o; + } + + if (res.args.database) |d| { + database = d; + } + + if (res.args.label) |l| { + label = l; + } + + if (res.args.@"log-level") |l| { + level = l; + } + + const config = Config{ + .namespace = namespace, + .all = allNamespaces, + .sort = sort, + .output = output, + .database = database, + .label = label, + .logLevel = level, + }; + + if (config.logLevel == .debug) { + std.debug.print("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}\n", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); + } +} From 30f24f0849f448c30522cb5f1d9e27e581acd00a Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 5 Oct 2025 01:20:48 +0100 Subject: [PATCH 02/19] PORT: CRD types Get the CRD types from a cluster, and put them into an array for later processing. Signed-off-by: Jim Fitzpatrick --- src/main.zig | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/main.zig b/src/main.zig index 5b55573..ae3d54c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -114,4 +114,49 @@ pub fn main() !void { if (config.logLevel == .debug) { std.debug.print("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}\n", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); } + + var crdTypes = try getCrdList(allocator); + defer { + for (crdTypes.items) |item| { + allocator.free(item); + } + crdTypes.deinit(allocator); + } + + std.debug.print("Found {} lines:\n", .{crdTypes.items.len}); + for (crdTypes.items) |line| { + std.debug.print("CRD type: {s}\n", .{line}); + } +} + +fn getCrdList(allocator: std.mem.Allocator) !std.ArrayList([]const u8) { + std.debug.print("somethig is done\n", .{}); + + const cmd = [_][]const u8{ "kubectl", "api-resources", "--verbs=list", "--namespaced", "-o", "name" }; + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &cmd, + .cwd = null, + .env_map = null, + .max_output_bytes = 1024 * 1024, // 1MB max output + }) catch |err| { + std.debug.print("Failed to run kubectl: {}\n", .{err}); + return err; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + var lines: std.ArrayList([]const u8) = .empty; + errdefer lines.deinit(allocator); + + var iter = std.mem.splitScalar(u8, result.stdout, '\n'); + while (iter.next()) |line| { + if (line.len > 0) { + const owned = try allocator.dupe(u8, line); + errdefer allocator.free(owned); + try lines.append(allocator, owned); + } + } + + return lines; } From 8643b30b767052f9353f9af3fbc5cf6356d6b421 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Tue, 14 Oct 2025 22:19:26 +0100 Subject: [PATCH 03/19] Progress: Some out put There is now some output going to the screen. It is still all way form where it needs to be. Signed-off-by: Jim Fitzpatrick --- src/main.zig | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index ae3d54c..23b4ca1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -12,12 +12,90 @@ const Config = struct { logLevel: Level, }; +const Metadata = struct { + name: []const u8, + namespace: []const u8, + creationTimestamp: []const u8, + resourceVersion: ?[]const u8, + + pub fn clone(self: Metadata, allocator: std.mem.Allocator) !Metadata { + return .{ + .name = try allocator.dupe(u8, self.name), + .namespace = try allocator.dupe(u8, self.namespace), + .creationTimestamp = try allocator.dupe(u8, self.creationTimestamp), + .resourceVersion = if (self.resourceVersion) |r| + try allocator.dupe(u8, r) + else + null, + }; + } + + pub fn deinit(self: Metadata, allocator: std.mem.Allocator) void { + allocator.free(self.namespace); + allocator.free(self.name); + allocator.free(self.creationTimestamp); + if (self.resourceVersion) |r| allocator.free(r); + } +}; + +const Resource = struct { + kind: []const u8, + apiVersion: []const u8, + metadata: Metadata, + + pub fn clone(self: Resource, allocator: std.mem.Allocator) !Resource { + return .{ + .kind = try allocator.dupe(u8, self.kind), + .apiVersion = try allocator.dupe(u8, self.apiVersion), + .metadata = try self.metadata.clone(allocator), + }; + } + + pub fn deinit(self: Resource, allocator: std.mem.Allocator) void { + allocator.free(self.apiVersion); + allocator.free(self.kind); + self.metadata.deinit(allocator); + } +}; + +const ResourceList = struct { + items: []Resource, + + pub fn clone(self: ResourceList, allocator: std.mem.Allocator) !ResourceList { + var new_items = try allocator.alloc(Resource, self.items.len); + + // On error, deinit any items that were already initialized and free the array. + var initialized: usize = 0; + errdefer { + // deinitialize only the items that were constructed so far + for (new_items[0..initialized]) |it| it.deinit(allocator); + allocator.free(new_items); + } + + // Clone each item; increment `initialized` after a successful clone. + for (self.items, 0..) |item, i| { + new_items[i] = try item.clone(allocator); + initialized += 1; + } + + // Success: cancel the errdefer cleanup by returning normally. + return ResourceList{ .items = new_items }; + } + + pub fn deinit(self: ResourceList, allocator: std.mem.Allocator) void { + for (self.items) |item| { + item.deinit(allocator); + } + allocator.free(self.items); + } +}; + const Output = enum { tty, json, sqlite }; const Level = enum { info, debug }; const Bool = enum { true, false }; pub fn main() !void { - var gpa = std.heap.DebugAllocator(.{}){}; + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); @@ -115,7 +193,13 @@ pub fn main() !void { std.debug.print("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}\n", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); } - var crdTypes = try getCrdList(allocator); + var crdTypes = getCrdList(allocator) catch |err| switch (err) { + error.BadExit => { + std.debug.print("Kubectl returned a none zero exit code\n", .{}); + std.process.exit(1); + }, + else => return err, + }; defer { for (crdTypes.items) |item| { allocator.free(item); @@ -123,12 +207,95 @@ pub fn main() !void { crdTypes.deinit(allocator); } + if (config.sort) { + std.mem.sort([]const u8, crdTypes.items, {}, compareStrings); + } + std.debug.print("Found {} lines:\n", .{crdTypes.items.len}); for (crdTypes.items) |line| { std.debug.print("CRD type: {s}\n", .{line}); + const a = getCRJson(allocator, config, line) catch |err| switch (err) { + error.NoData => { + std.debug.print("No Data retruned for: {s}", .{line}); + continue; + }, + else => return err, + }; + std.debug.print("Kind: {s}\n", .{a.items[0].kind}); + for (a.items, 1..) |item, i| { + std.debug.print("{d}: name = {s}, namespace = {s}\n", .{ i, item.metadata.name, item.metadata.namespace }); + } + std.debug.print("\n", .{}); + defer { + a.deinit(allocator); + } } } +fn getCRJson(allocator: std.mem.Allocator, config: Config, crd: []const u8) !ResourceList { + const initialcmd = &[_][]const u8{ "kubectl", "get", "--ignore-not-found", crd, "--output", "json" }; + + var cmd: std.ArrayList([]const u8) = .empty; + defer cmd.deinit(allocator); + + try cmd.appendSlice(allocator, initialcmd); + if (config.all) { + try cmd.append(allocator, "--all-namespaces"); + } else { + try cmd.append(allocator, "--namespace"); + try cmd.append(allocator, config.namespace); + } + + std.debug.print("cmd: ", .{}); + for (cmd.items) |c| { + std.debug.print("{s} ", .{c}); + } + std.debug.print("\n", .{}); + const ownedCmd = try cmd.toOwnedSlice(allocator); + defer allocator.free(ownedCmd); + + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = ownedCmd, + .cwd = null, + .env_map = null, + .max_output_bytes = 1024 * 1024, // 1MB max output + }) catch |err| { + std.debug.print("Failed to run kubectl: {}\n", .{err}); + return err; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + if (result.term.Exited != 0) { + return error.BadExit; + } + + if (result.stdout.len == 0) { + return error.NoData; + } + + const parsed: std.json.Parsed(ResourceList) = std.json.parseFromSlice(ResourceList, allocator, result.stdout, .{ .ignore_unknown_fields = true }) catch |err| switch (err) { + std.json.ParseFromValueError.MissingField => return error.NotFound, + else => return err, + }; + + defer { + parsed.deinit(); + } + + for (parsed.value.items) |item| { + std.debug.print("item name: {s}\n", .{item.metadata.name}); + } + std.debug.print("Number of items: {d}\n", .{parsed.value.items.len}); + + return parsed.value.clone(allocator); +} + +fn compareStrings(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.lessThan(u8, lhs, rhs); +} + fn getCrdList(allocator: std.mem.Allocator) !std.ArrayList([]const u8) { std.debug.print("somethig is done\n", .{}); @@ -146,8 +313,17 @@ fn getCrdList(allocator: std.mem.Allocator) !std.ArrayList([]const u8) { defer allocator.free(result.stdout); defer allocator.free(result.stderr); + if (result.term.Exited != 0) { + std.debug.print("stdout: {s}. stderr: {s}\n", .{ result.stdout, result.stdout }); + return error.BadExit; + } var lines: std.ArrayList([]const u8) = .empty; - errdefer lines.deinit(allocator); + errdefer { + for (lines.items) |item| { + allocator.free(item); + } + lines.deinit(allocator); + } var iter = std.mem.splitScalar(u8, result.stdout, '\n'); while (iter.next()) |line| { From 5bdef850da642591888b74eeff0357c7ca9f3aca Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 24 Jan 2026 18:34:44 +0000 Subject: [PATCH 04/19] FIX: configure bool flags correctly Signed-off-by: Jim Fitzpatrick --- src/main.zig | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main.zig b/src/main.zig index 23b4ca1..4a7ee4a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -104,8 +104,8 @@ pub fn main() !void { const params = comptime clap.parseParamsComptime( \\-h, --help Display this help and exit. \\-n, --namespace Namespace to get resources from. - \\-A, --all-namespaces If present, list all objects across all namespaces. Specifing --namespace will be ignored. - \\-s, --sort Prints the resources in order. + \\-A, --all-namespaces If present, list all objects across all namespaces. Specifing --namespace will be ignored. + \\-s, --sort Prints the resources in order. \\-e, --exclude ... Exclude crd types. Multiple can be excluded eg: "-e -e " \\-o, --output Changes the output format of the results. \\-d, --database Path to the sqlite file to save the results. If the files does not exist it will be created. @@ -119,7 +119,6 @@ pub fn main() !void { .STR = clap.parsers.string, .PATH = clap.parsers.string, .LEVEL = clap.parsers.enumeration(Level), - .BOOL = clap.parsers.enumeration(Bool), }; // Initialize our diagnostics, which can be used for reporting useful errors. @@ -151,16 +150,12 @@ pub fn main() !void { namespace = n; } - if (res.args.@"all-namespaces") |n| { - if (n == Bool.true) { - allNamespaces = true; - } + if (res.args.@"all-namespaces" == 1) { + allNamespaces = true; } - if (res.args.sort) |s| { - if (s == Bool.true) { - sort = true; - } + if (res.args.sort == 1) { + sort = true; } if (res.args.output) |o| { From 3717dfd11476dd42c47b3e5dfc75972ef9b93938 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 24 Jan 2026 18:46:48 +0000 Subject: [PATCH 05/19] UPDATE: small refactor Signed-off-by: Jim Fitzpatrick --- src/main.zig | 107 ++++---------------------------------------------- src/types.zig | 94 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 99 deletions(-) create mode 100644 src/types.zig diff --git a/src/main.zig b/src/main.zig index 4a7ee4a..af7bee9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,98 +1,7 @@ const clap = @import("clap"); const std = @import("std"); -const Config = struct { - namespace: []const u8, - all: bool, - sort: bool, - exclude: ?[][]const u8 = null, //TODO: need to pull these from the args. - output: Output, - database: []const u8, - label: []const u8, - logLevel: Level, -}; - -const Metadata = struct { - name: []const u8, - namespace: []const u8, - creationTimestamp: []const u8, - resourceVersion: ?[]const u8, - - pub fn clone(self: Metadata, allocator: std.mem.Allocator) !Metadata { - return .{ - .name = try allocator.dupe(u8, self.name), - .namespace = try allocator.dupe(u8, self.namespace), - .creationTimestamp = try allocator.dupe(u8, self.creationTimestamp), - .resourceVersion = if (self.resourceVersion) |r| - try allocator.dupe(u8, r) - else - null, - }; - } - - pub fn deinit(self: Metadata, allocator: std.mem.Allocator) void { - allocator.free(self.namespace); - allocator.free(self.name); - allocator.free(self.creationTimestamp); - if (self.resourceVersion) |r| allocator.free(r); - } -}; - -const Resource = struct { - kind: []const u8, - apiVersion: []const u8, - metadata: Metadata, - - pub fn clone(self: Resource, allocator: std.mem.Allocator) !Resource { - return .{ - .kind = try allocator.dupe(u8, self.kind), - .apiVersion = try allocator.dupe(u8, self.apiVersion), - .metadata = try self.metadata.clone(allocator), - }; - } - - pub fn deinit(self: Resource, allocator: std.mem.Allocator) void { - allocator.free(self.apiVersion); - allocator.free(self.kind); - self.metadata.deinit(allocator); - } -}; - -const ResourceList = struct { - items: []Resource, - - pub fn clone(self: ResourceList, allocator: std.mem.Allocator) !ResourceList { - var new_items = try allocator.alloc(Resource, self.items.len); - - // On error, deinit any items that were already initialized and free the array. - var initialized: usize = 0; - errdefer { - // deinitialize only the items that were constructed so far - for (new_items[0..initialized]) |it| it.deinit(allocator); - allocator.free(new_items); - } - - // Clone each item; increment `initialized` after a successful clone. - for (self.items, 0..) |item, i| { - new_items[i] = try item.clone(allocator); - initialized += 1; - } - - // Success: cancel the errdefer cleanup by returning normally. - return ResourceList{ .items = new_items }; - } - - pub fn deinit(self: ResourceList, allocator: std.mem.Allocator) void { - for (self.items) |item| { - item.deinit(allocator); - } - allocator.free(self.items); - } -}; - -const Output = enum { tty, json, sqlite }; -const Level = enum { info, debug }; -const Bool = enum { true, false }; +const types = @import("types.zig"); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; @@ -115,10 +24,10 @@ pub fn main() !void { ); const parsers = comptime .{ - .OUTPUT = clap.parsers.enumeration(Output), + .OUTPUT = clap.parsers.enumeration(types.Output), .STR = clap.parsers.string, .PATH = clap.parsers.string, - .LEVEL = clap.parsers.enumeration(Level), + .LEVEL = clap.parsers.enumeration(types.Level), }; // Initialize our diagnostics, which can be used for reporting useful errors. @@ -138,10 +47,10 @@ pub fn main() !void { var namespace: []const u8 = &[_]u8{}; var allNamespaces = false; var sort = false; - var output = Output.tty; + var output = types.Output.tty; var database: []const u8 = &[_]u8{}; var label: []const u8 = &[_]u8{}; - var level = Level.info; + var level = types.Level.info; if (res.args.help != 0) return clap.helpToFile(.stdout(), clap.Help, ¶ms, .{}); @@ -174,7 +83,7 @@ pub fn main() !void { level = l; } - const config = Config{ + const config = types.Config{ .namespace = namespace, .all = allNamespaces, .sort = sort, @@ -227,7 +136,7 @@ pub fn main() !void { } } -fn getCRJson(allocator: std.mem.Allocator, config: Config, crd: []const u8) !ResourceList { +fn getCRJson(allocator: std.mem.Allocator, config: types.Config, crd: []const u8) !types.ResourceList { const initialcmd = &[_][]const u8{ "kubectl", "get", "--ignore-not-found", crd, "--output", "json" }; var cmd: std.ArrayList([]const u8) = .empty; @@ -270,7 +179,7 @@ fn getCRJson(allocator: std.mem.Allocator, config: Config, crd: []const u8) !Res return error.NoData; } - const parsed: std.json.Parsed(ResourceList) = std.json.parseFromSlice(ResourceList, allocator, result.stdout, .{ .ignore_unknown_fields = true }) catch |err| switch (err) { + const parsed: std.json.Parsed(types.ResourceList) = std.json.parseFromSlice(types.ResourceList, allocator, result.stdout, .{ .ignore_unknown_fields = true }) catch |err| switch (err) { std.json.ParseFromValueError.MissingField => return error.NotFound, else => return err, }; diff --git a/src/types.zig b/src/types.zig new file mode 100644 index 0000000..6573816 --- /dev/null +++ b/src/types.zig @@ -0,0 +1,94 @@ +const std = @import("std"); + +pub const Output = enum { tty, json, sqlite }; +pub const Level = enum { info, debug }; +pub const Bool = enum { true, false }; + +pub const Config = struct { + namespace: []const u8, + all: bool, + sort: bool, + exclude: ?[][]const u8 = null, //TODO: need to pull these from the args. + output: Output, + database: []const u8, + label: []const u8, + logLevel: Level, +}; + +pub const Metadata = struct { + name: []const u8, + namespace: []const u8, + creationTimestamp: []const u8, + resourceVersion: ?[]const u8, + + pub fn clone(self: Metadata, allocator: std.mem.Allocator) !Metadata { + return .{ + .name = try allocator.dupe(u8, self.name), + .namespace = try allocator.dupe(u8, self.namespace), + .creationTimestamp = try allocator.dupe(u8, self.creationTimestamp), + .resourceVersion = if (self.resourceVersion) |r| + try allocator.dupe(u8, r) + else + null, + }; + } + + pub fn deinit(self: Metadata, allocator: std.mem.Allocator) void { + allocator.free(self.namespace); + allocator.free(self.name); + allocator.free(self.creationTimestamp); + if (self.resourceVersion) |r| allocator.free(r); + } +}; + +pub const Resource = struct { + kind: []const u8, + apiVersion: []const u8, + metadata: Metadata, + + pub fn clone(self: Resource, allocator: std.mem.Allocator) !Resource { + return .{ + .kind = try allocator.dupe(u8, self.kind), + .apiVersion = try allocator.dupe(u8, self.apiVersion), + .metadata = try self.metadata.clone(allocator), + }; + } + + pub fn deinit(self: Resource, allocator: std.mem.Allocator) void { + allocator.free(self.apiVersion); + allocator.free(self.kind); + self.metadata.deinit(allocator); + } +}; + +pub const ResourceList = struct { + items: []Resource, + + pub fn clone(self: ResourceList, allocator: std.mem.Allocator) !ResourceList { + var new_items = try allocator.alloc(Resource, self.items.len); + + // On error, deinit any items that were already initialized and free the array. + var initialized: usize = 0; + errdefer { + // deinitialize only the items that were constructed so far + for (new_items[0..initialized]) |it| it.deinit(allocator); + allocator.free(new_items); + } + + // Clone each item; increment `initialized` after a successful clone. + for (self.items, 0..) |item, i| { + new_items[i] = try item.clone(allocator); + initialized += 1; + } + + // Success: cancel the errdefer cleanup by returning normally. + return ResourceList{ .items = new_items }; + } + + pub fn deinit(self: ResourceList, allocator: std.mem.Allocator) void { + for (self.items) |item| { + item.deinit(allocator); + } + allocator.free(self.items); + } +}; From b05b37402c2209de8c63b9d418f8db722c023c3b Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 25 Jan 2026 16:40:56 +0000 Subject: [PATCH 06/19] Table view is working Signed-off-by: Jim Fitzpatrick --- src/main.zig | 207 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 187 insertions(+), 20 deletions(-) diff --git a/src/main.zig b/src/main.zig index af7bee9..51a57fe 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,10 @@ const std = @import("std"); const types = @import("types.zig"); +var stdout_buf: [1024]u8 = undefined; +var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); +const stdout: *std.io.Writer = &stdout_writer.interface; + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -115,25 +119,200 @@ pub fn main() !void { std.mem.sort([]const u8, crdTypes.items, {}, compareStrings); } - std.debug.print("Found {} lines:\n", .{crdTypes.items.len}); + std.debug.print("Total number of CRD types found {}.\n", .{crdTypes.items.len}); for (crdTypes.items) |line| { - std.debug.print("CRD type: {s}\n", .{line}); const a = getCRJson(allocator, config, line) catch |err| switch (err) { error.NoData => { - std.debug.print("No Data retruned for: {s}", .{line}); continue; }, else => return err, }; - std.debug.print("Kind: {s}\n", .{a.items[0].kind}); - for (a.items, 1..) |item, i| { - std.debug.print("{d}: name = {s}, namespace = {s}\n", .{ i, item.metadata.name, item.metadata.namespace }); - } - std.debug.print("\n", .{}); defer { a.deinit(allocator); } + try print_table(a); + } + + const todo = + \\To do after + \\- get sort function to work. + \\ + ; + std.debug.print("{s}\n", .{todo}); +} + +fn print_table(data: types.ResourceList) !void { + // TODO: This code is horrible, needs a large refactor + const title_name = "NAME"; + const title_namespace = "NAMESPACE"; + const title_creationTimestamp = "CREATION TIMESTAMP"; + + var max_name_length: usize = title_name.len; + var max_namespace_length: usize = title_namespace.len; + var max_creationTimestamp_length: usize = title_creationTimestamp.len; + const kind = data.items[0].kind; + for (data.items) |i| { + if (i.metadata.name.len > max_name_length) max_name_length = i.metadata.name.len; + if (i.metadata.namespace.len > max_namespace_length) max_namespace_length = i.metadata.namespace.len; + if (i.metadata.creationTimestamp.len > max_creationTimestamp_length) max_creationTimestamp_length = i.metadata.creationTimestamp.len; + } + + const headers: [3][]const u8 = .{ title_namespace, title_name, title_creationTimestamp }; + const spacing: [3]usize = .{ max_namespace_length, max_name_length, max_creationTimestamp_length }; + var spacing_required: usize = 0; + for (spacing) |s| spacing_required += s; + + // add 2 for table ends + // add 2 for each field printed (name, namespace) = 4 + // add field count - 1 for vertical divides + // needs a - 1 for some reason + const line_length = spacing_required + 2 + (2 * headers.len) + (headers.len - 1) - 1; + var divider_idx: usize = 0; + var divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + + const green_start = "\x1b[1;32m"; + const reset = "\x1b[0m"; + const top_left = "\u{250C}"; + const top_right = "\u{2510}"; + const bottom_left = "\u{2514}"; + const bottom_right = "\u{2518}"; + const horizontal = "\u{2500}"; + const top_junction = "\u{252C}"; + const bottom_junction = "\u{2534}"; + const intersection = "\u{253C}"; + const vertical = "\u{2502}"; + const left_junction = "\u{251C}"; + const right_junction = "\u{2524}"; + + const padding = (line_length - kind.len) / 2; + for (0..padding) |_| { + try stdout.print(" ", .{}); + } + try stdout.print("{s}{s}{s}\n", .{ green_start, kind, reset }); + // header line + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{top_left}); + } else if (i == line_length) { + try stdout.print("{s}\n", .{top_right}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{top_junction}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } + } + + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + + var pos: usize = 0; + while (pos < line_length + 1) : (pos += 1) { + if (pos == 0) { + try stdout.print("{s}", .{vertical}); + } else if (pos == divider and divider_idx == headers.len) { + try stdout.print("{s}\n", .{vertical}); + break; + } else if (pos == 2) { + try stdout.print("{s}", .{headers[0]}); + pos += headers[0].len - 1; + } else if (pos == divider and divider_idx < headers.len) { + try stdout.print("{s} {s}", .{ vertical, headers[divider_idx] }); + pos += headers[divider_idx].len - 1; + divider += spacing[divider_idx] + 1; + divider_idx += 1; + } else { + try stdout.print(" ", .{}); + } + } + + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{left_junction}); + } else if (i == line_length) { + try stdout.print("{s}\n", .{right_junction}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{intersection}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } } + + for (data.items, 1..) |item, idx| { + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + pos = 0; + while (pos < line_length + 1) : (pos += 1) { + if (pos == 0) { + try stdout.print("{s}", .{vertical}); + } else if (pos == line_length) { + try stdout.print("{s}\n", .{vertical}); + } else if (pos == 2) { + try stdout.print("{s}", .{item.metadata.namespace}); + pos += item.metadata.namespace.len - 1; + } else if (pos == divider) { + if (divider_idx == 1) { + try stdout.print("{s} {s}", .{ vertical, item.metadata.name }); + pos += item.metadata.name.len + 1; + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s} {s}", .{ vertical, item.metadata.creationTimestamp }); + pos += item.metadata.creationTimestamp.len + 1; + divider += spacing[divider_idx]; + divider_idx += 1; + } + } else { + try stdout.print(" ", .{}); + } + } + + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + if (idx != data.items.len) { + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{left_junction}); + } else if (i == line_length) { + try stdout.print("{s}\n", .{right_junction}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{intersection}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } + } + } else { + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{bottom_left}); + } else if (i == line_length) { + try stdout.print("{s}\n\n", .{bottom_right}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{bottom_junction}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } + } + } + } + try stdout.flush(); } fn getCRJson(allocator: std.mem.Allocator, config: types.Config, crd: []const u8) !types.ResourceList { @@ -150,11 +329,6 @@ fn getCRJson(allocator: std.mem.Allocator, config: types.Config, crd: []const u8 try cmd.append(allocator, config.namespace); } - std.debug.print("cmd: ", .{}); - for (cmd.items) |c| { - std.debug.print("{s} ", .{c}); - } - std.debug.print("\n", .{}); const ownedCmd = try cmd.toOwnedSlice(allocator); defer allocator.free(ownedCmd); @@ -188,11 +362,6 @@ fn getCRJson(allocator: std.mem.Allocator, config: types.Config, crd: []const u8 parsed.deinit(); } - for (parsed.value.items) |item| { - std.debug.print("item name: {s}\n", .{item.metadata.name}); - } - std.debug.print("Number of items: {d}\n", .{parsed.value.items.len}); - return parsed.value.clone(allocator); } @@ -201,8 +370,6 @@ fn compareStrings(_: void, lhs: []const u8, rhs: []const u8) bool { } fn getCrdList(allocator: std.mem.Allocator) !std.ArrayList([]const u8) { - std.debug.print("somethig is done\n", .{}); - const cmd = [_][]const u8{ "kubectl", "api-resources", "--verbs=list", "--namespaced", "-o", "name" }; const result = std.process.Child.run(.{ .allocator = allocator, From 8abea91c7d0d641d7f71714cf74e0c08601a5059 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 25 Jan 2026 17:22:55 +0000 Subject: [PATCH 07/19] ADD: basic logger configuration Signed-off-by: Jim Fitzpatrick --- src/main.zig | 54 ++++++++++++++++++++++++++++++++++++++++----------- src/types.zig | 2 +- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/main.zig b/src/main.zig index 51a57fe..acf3a48 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,6 +7,34 @@ var stdout_buf: [1024]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); const stdout: *std.io.Writer = &stdout_writer.interface; +pub const std_options: std.Options = .{ + // Keep compile-time logging permissive; runtime filter in `log`. + .log_level = .debug, + .logFn = log, +}; + +pub var log_level: std.log.Level = .info; + +pub fn log( + comptime level: std.log.Level, + comptime scope: @Type(.enum_literal), + comptime format: []const u8, + args: anytype, +) void { + const prefix = comptime blk: { + if (scope == .default) + break :blk "[" ++ level.asText() ++ "] "; + break :blk "[" ++ level.asText() ++ "][" ++ @tagName(scope) ++ "] "; + }; + if (@intFromEnum(level) <= @intFromEnum(log_level)) { + // Print the message to stderr, silently ignoring any errors + std.debug.lockStdErr(); + defer std.debug.unlockStdErr(); + const stderr = std.fs.File.stderr().deprecatedWriter(); + nosuspend stderr.print(prefix ++ format ++ "\n", args) catch return; + } +} + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); @@ -23,7 +51,7 @@ pub fn main() !void { \\-o, --output Changes the output format of the results. \\-d, --database Path to the sqlite file to save the results. If the files does not exist it will be created. \\-l, --label Set the label that will be saved with entries when using the --database option. - \\--log-level Set the log level. All logs are saved to file. + \\--log-level Set the log level. All logs are saved to file. Possible values are (debug, info, warn, error). Defualt level is warn. \\ ); @@ -54,7 +82,7 @@ pub fn main() !void { var output = types.Output.tty; var database: []const u8 = &[_]u8{}; var label: []const u8 = &[_]u8{}; - var level = types.Level.info; + var level = types.Level.warn; if (res.args.help != 0) return clap.helpToFile(.stdout(), clap.Help, ¶ms, .{}); @@ -87,6 +115,12 @@ pub fn main() !void { level = l; } + switch (level) { + .debug => log_level = std.log.Level.debug, + .@"error" => log_level = std.log.Level.err, + .info => log_level = std.log.Level.info, + .warn => log_level = std.log.Level.warn, + } const config = types.Config{ .namespace = namespace, .all = allNamespaces, @@ -97,13 +131,11 @@ pub fn main() !void { .logLevel = level, }; - if (config.logLevel == .debug) { - std.debug.print("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}\n", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); - } + std.log.debug("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}\n", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); var crdTypes = getCrdList(allocator) catch |err| switch (err) { error.BadExit => { - std.debug.print("Kubectl returned a none zero exit code\n", .{}); + std.log.err("Kubectl returned a none zero exit code\n", .{}); std.process.exit(1); }, else => return err, @@ -119,7 +151,7 @@ pub fn main() !void { std.mem.sort([]const u8, crdTypes.items, {}, compareStrings); } - std.debug.print("Total number of CRD types found {}.\n", .{crdTypes.items.len}); + std.log.info("Total number of CRD types found {}.\n", .{crdTypes.items.len}); for (crdTypes.items) |line| { const a = getCRJson(allocator, config, line) catch |err| switch (err) { error.NoData => { @@ -138,7 +170,7 @@ pub fn main() !void { \\- get sort function to work. \\ ; - std.debug.print("{s}\n", .{todo}); + std.log.debug("{s}\n", .{todo}); } fn print_table(data: types.ResourceList) !void { @@ -339,7 +371,7 @@ fn getCRJson(allocator: std.mem.Allocator, config: types.Config, crd: []const u8 .env_map = null, .max_output_bytes = 1024 * 1024, // 1MB max output }) catch |err| { - std.debug.print("Failed to run kubectl: {}\n", .{err}); + std.log.err("Failed to run kubectl: {}\n", .{err}); return err; }; defer allocator.free(result.stdout); @@ -378,14 +410,14 @@ fn getCrdList(allocator: std.mem.Allocator) !std.ArrayList([]const u8) { .env_map = null, .max_output_bytes = 1024 * 1024, // 1MB max output }) catch |err| { - std.debug.print("Failed to run kubectl: {}\n", .{err}); + std.log.err("Failed to run kubectl: {}\n", .{err}); return err; }; defer allocator.free(result.stdout); defer allocator.free(result.stderr); if (result.term.Exited != 0) { - std.debug.print("stdout: {s}. stderr: {s}\n", .{ result.stdout, result.stdout }); + std.log.debug("stdout: {s}. stderr: {s}\n", .{ result.stdout, result.stdout }); return error.BadExit; } var lines: std.ArrayList([]const u8) = .empty; diff --git a/src/types.zig b/src/types.zig index 6573816..479e9fc 100644 --- a/src/types.zig +++ b/src/types.zig @@ -1,7 +1,7 @@ const std = @import("std"); pub const Output = enum { tty, json, sqlite }; -pub const Level = enum { info, debug }; +pub const Level = enum { info, debug, @"error", warn }; pub const Bool = enum { true, false }; pub const Config = struct { From 0f073558530897e3e0882de7dd215833ad2cb0e3 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 25 Jan 2026 20:34:28 +0000 Subject: [PATCH 08/19] ADD: json formater Signed-off-by: Jim Fitzpatrick --- src/main.zig | 77 ++++++++++++++++++++++++++++++++++++++++++--------- src/types.zig | 37 +++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/main.zig b/src/main.zig index acf3a48..b0bd9fd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -131,11 +131,11 @@ pub fn main() !void { .logLevel = level, }; - std.log.debug("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}\n", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); + std.log.debug("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); var crdTypes = getCrdList(allocator) catch |err| switch (err) { error.BadExit => { - std.log.err("Kubectl returned a none zero exit code\n", .{}); + std.log.err("Kubectl returned a none zero exit code", .{}); std.process.exit(1); }, else => return err, @@ -151,26 +151,77 @@ pub fn main() !void { std.mem.sort([]const u8, crdTypes.items, {}, compareStrings); } - std.log.info("Total number of CRD types found {}.\n", .{crdTypes.items.len}); + std.log.info("Total number of CRD types found {}.", .{crdTypes.items.len}); + var map: ?std.StringHashMap(types.ResourceList) = null; + defer if (map) |*m| { + var it = m.keyIterator(); + while (it.next()) |key| { + const value = m.get(key.*).?; + value.deinit(allocator); + } + m.deinit(); + }; + if (config.output == .json) { + map = std.StringHashMap(types.ResourceList).init(allocator); + const v: u32 = if (crdTypes.items.len <= std.math.maxInt(u32)) @intCast(crdTypes.items.len) else std.math.maxInt(u32); + if (map) |*m| try m.ensureTotalCapacity(v); + } for (crdTypes.items) |line| { - const a = getCRJson(allocator, config, line) catch |err| switch (err) { + const resource = getCRJson(allocator, config, line) catch |err| switch (err) { error.NoData => { continue; }, else => return err, }; - defer { - a.deinit(allocator); + switch (config.output) { + .tty => { + defer { + resource.deinit(allocator); + } + try print_table(resource); + }, + .sqlite => { + defer { + resource.deinit(allocator); + } + try print_table(resource); + }, + .json => { + if (map) |*m| { + try m.put(line, resource); + } + }, } - try print_table(a); + } + + if (map) |*m| { + std.log.debug("map length: {}", .{m.count()}); + try stdout.print("{{", .{}); + + var it = m.iterator(); + var count: usize = 1; + while (it.next()) |key| { + const item = key.value_ptr; + const text = try item.toJson(allocator); + defer allocator.free(text); + try stdout.print("\"{s}\": {s}", .{ key.key_ptr.*, text }); + if (count < m.count()) { + try stdout.print(",", .{}); + count += 1; + } + } + + try stdout.print("}}\n", .{}); + try stdout.flush(); } const todo = - \\To do after - \\- get sort function to work. + \\To do: + \\- database configuration + \\- exclude filter \\ ; - std.log.debug("{s}\n", .{todo}); + std.log.debug("{s}", .{todo}); } fn print_table(data: types.ResourceList) !void { @@ -371,7 +422,7 @@ fn getCRJson(allocator: std.mem.Allocator, config: types.Config, crd: []const u8 .env_map = null, .max_output_bytes = 1024 * 1024, // 1MB max output }) catch |err| { - std.log.err("Failed to run kubectl: {}\n", .{err}); + std.log.err("Failed to run kubectl: {}", .{err}); return err; }; defer allocator.free(result.stdout); @@ -410,14 +461,14 @@ fn getCrdList(allocator: std.mem.Allocator) !std.ArrayList([]const u8) { .env_map = null, .max_output_bytes = 1024 * 1024, // 1MB max output }) catch |err| { - std.log.err("Failed to run kubectl: {}\n", .{err}); + std.log.err("Failed to run kubectl: {}", .{err}); return err; }; defer allocator.free(result.stdout); defer allocator.free(result.stderr); if (result.term.Exited != 0) { - std.log.debug("stdout: {s}. stderr: {s}\n", .{ result.stdout, result.stdout }); + std.log.debug("stdout: {s}. stderr: {s}", .{ result.stdout, result.stdout }); return error.BadExit; } var lines: std.ArrayList([]const u8) = .empty; diff --git a/src/types.zig b/src/types.zig index 479e9fc..d327bc1 100644 --- a/src/types.zig +++ b/src/types.zig @@ -46,6 +46,25 @@ pub const Resource = struct { apiVersion: []const u8, metadata: Metadata, + pub fn toJson(self: @This(), allocator: std.mem.Allocator) ![]const u8 { + var buffer = try std.ArrayList(u8).initCapacity(allocator, 256); + defer buffer.deinit(allocator); + + try std.fmt.format(buffer.writer(allocator), "{{\"name\": \"{s}\", \"namespace\": \"{s}\", \"createTimestamp\": \"{s}\"", .{ + self.metadata.name, + self.metadata.namespace, + self.metadata.creationTimestamp, + }); + + if (self.metadata.resourceVersion) |version| { + try std.fmt.format(buffer.writer(allocator), ", \"resourceVersion\": {s}}}", .{version}); + } else { + try std.fmt.format(buffer.writer(allocator), "}}", .{}); + } + + return try allocator.dupe(u8, buffer.items); + } + pub fn clone(self: Resource, allocator: std.mem.Allocator) !Resource { return .{ .kind = try allocator.dupe(u8, self.kind), @@ -64,6 +83,24 @@ pub const Resource = struct { pub const ResourceList = struct { items: []Resource, + pub fn toJson(self: @This(), allocator: std.mem.Allocator) ![]const u8 { + var buffer = try std.ArrayList(u8).initCapacity(allocator, 256); + defer buffer.deinit(allocator); + + try std.fmt.format(buffer.writer(allocator), "[", .{}); + for (self.items, 0..) |item, i| { + const text = try item.toJson(allocator); + defer allocator.free(text); + try std.fmt.format(buffer.writer(allocator), "{s}", .{text}); + if (i < self.items.len - 1) { + try std.fmt.format(buffer.writer(allocator), ",", .{}); + } + } + try std.fmt.format(buffer.writer(allocator), "]", .{}); + + return try allocator.dupe(u8, buffer.items); + } + pub fn clone(self: ResourceList, allocator: std.mem.Allocator) !ResourceList { var new_items = try allocator.alloc(Resource, self.items.len); From c9267599d64bd59b0e0d9aff29280989d8cd4cba Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 25 Jan 2026 20:56:07 +0000 Subject: [PATCH 09/19] FIX: set the correct version Signed-off-by: Jim Fitzpatrick --- build.zig.zon | 6 +++--- towncrier.toml | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 towncrier.toml diff --git a/build.zig.zon b/build.zig.zon index 29cf8e4..9457717 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ - .name = .kubectlgetal, - .version = "0.15.1", + .name = .kubectlgetall, + .version = "0.5.0-dev", .dependencies = .{ .clap = .{ .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz", @@ -8,5 +8,5 @@ }, }, .paths = .{""}, - .fingerprint = 0x1b03674a4cac8e50, + .fingerprint = 0x146917ff4713c3, } diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 0000000..ccc265e --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,5 @@ +[tool.towncrier] +name = "kubectlgetall" + +filename = "CHANGELOG.md" +directory = "changes" From bd6540c1db675c2734d27085b4c733f18bb87682 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 25 Jan 2026 21:26:08 +0000 Subject: [PATCH 10/19] REFACTOR: change to use @This() Signed-off-by: Jim Fitzpatrick --- src/types.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types.zig b/src/types.zig index d327bc1..d7fa7d8 100644 --- a/src/types.zig +++ b/src/types.zig @@ -21,7 +21,7 @@ pub const Metadata = struct { creationTimestamp: []const u8, resourceVersion: ?[]const u8, - pub fn clone(self: Metadata, allocator: std.mem.Allocator) !Metadata { + pub fn clone(self: @This(), allocator: std.mem.Allocator) !Metadata { return .{ .name = try allocator.dupe(u8, self.name), .namespace = try allocator.dupe(u8, self.namespace), @@ -33,7 +33,7 @@ pub const Metadata = struct { }; } - pub fn deinit(self: Metadata, allocator: std.mem.Allocator) void { + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { allocator.free(self.namespace); allocator.free(self.name); allocator.free(self.creationTimestamp); @@ -65,7 +65,7 @@ pub const Resource = struct { return try allocator.dupe(u8, buffer.items); } - pub fn clone(self: Resource, allocator: std.mem.Allocator) !Resource { + pub fn clone(self: @This(), allocator: std.mem.Allocator) !Resource { return .{ .kind = try allocator.dupe(u8, self.kind), .apiVersion = try allocator.dupe(u8, self.apiVersion), @@ -73,7 +73,7 @@ pub const Resource = struct { }; } - pub fn deinit(self: Resource, allocator: std.mem.Allocator) void { + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { allocator.free(self.apiVersion); allocator.free(self.kind); self.metadata.deinit(allocator); @@ -101,7 +101,7 @@ pub const ResourceList = struct { return try allocator.dupe(u8, buffer.items); } - pub fn clone(self: ResourceList, allocator: std.mem.Allocator) !ResourceList { + pub fn clone(self: @This(), allocator: std.mem.Allocator) !ResourceList { var new_items = try allocator.alloc(Resource, self.items.len); // On error, deinit any items that were already initialized and free the array. @@ -122,7 +122,7 @@ pub const ResourceList = struct { return ResourceList{ .items = new_items }; } - pub fn deinit(self: ResourceList, allocator: std.mem.Allocator) void { + pub fn deinit(self: @This(), allocator: std.mem.Allocator) void { for (self.items) |item| { item.deinit(allocator); } From 8c8128147d0fd4c2d71b45bba17693ff570d5838 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 25 Jan 2026 22:28:58 +0000 Subject: [PATCH 11/19] ADD: exclude now works ish Signed-off-by: Jim Fitzpatrick --- src/main.zig | 25 +++++++++++++++++++++++-- src/types.zig | 27 ++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/main.zig b/src/main.zig index b0bd9fd..8022a04 100644 --- a/src/main.zig +++ b/src/main.zig @@ -83,6 +83,7 @@ pub fn main() !void { var database: []const u8 = &[_]u8{}; var label: []const u8 = &[_]u8{}; var level = types.Level.warn; + var exclude: ?[]const []const u8 = null; if (res.args.help != 0) return clap.helpToFile(.stdout(), clap.Help, ¶ms, .{}); @@ -115,6 +116,10 @@ pub fn main() !void { level = l; } + if (res.args.exclude.len > 0) { + exclude = res.args.exclude; + } + switch (level) { .debug => log_level = std.log.Level.debug, .@"error" => log_level = std.log.Level.err, @@ -125,13 +130,14 @@ pub fn main() !void { .namespace = namespace, .all = allNamespaces, .sort = sort, + .exclude = exclude, .output = output, .database = database, .label = label, .logLevel = level, }; - std.log.debug("Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\toutput: {s}\n\tbasebase: {s}\n\tlabel: {s}\n\tlog level: {s}", .{ config.namespace, config.all, config.sort, @tagName(config.output), config.database, config.label, @tagName(config.logLevel) }); + std.log.debug("{f}", .{config}); var crdTypes = getCrdList(allocator) catch |err| switch (err) { error.BadExit => { @@ -167,6 +173,12 @@ pub fn main() !void { if (map) |*m| try m.ensureTotalCapacity(v); } for (crdTypes.items) |line| { + if (contains(config.exclude, line)) { + std.log.debug("filter out: {s}", .{line}); + + continue; + } + const resource = getCRJson(allocator, config, line) catch |err| switch (err) { error.NoData => { continue; @@ -218,12 +230,21 @@ pub fn main() !void { const todo = \\To do: \\- database configuration - \\- exclude filter \\ ; std.log.debug("{s}", .{todo}); } +fn contains(haystack: ?[]const []const u8, needle: []const u8) bool { + if (haystack) |stack| { + for (stack) |s| { + if (std.mem.eql(u8, s, needle)) return true; + } + } + + return false; +} + fn print_table(data: types.ResourceList) !void { // TODO: This code is horrible, needs a large refactor const title_name = "NAME"; diff --git a/src/types.zig b/src/types.zig index d7fa7d8..ce2b9cf 100644 --- a/src/types.zig +++ b/src/types.zig @@ -8,11 +8,36 @@ pub const Config = struct { namespace: []const u8, all: bool, sort: bool, - exclude: ?[][]const u8 = null, //TODO: need to pull these from the args. + exclude: ?[]const []const u8 = null, output: Output, database: []const u8, label: []const u8, logLevel: Level, + + pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void { + try writer.print( + "Configuration:\n\tnamespace: {s}\n\tall namespaces: {}\n\tsort: {}\n\texclude: ", + .{ self.namespace, self.all, self.sort }, + ); + + if (self.exclude) |items| { + try writer.writeAll("["); + for (items, 0..) |item, i| { + if (i > 0) try writer.writeAll(", "); + try writer.print("{s}", .{item}); + } + try writer.writeAll("]\n"); + } else { + try writer.writeAll("null\n"); + } + + try writer.print( + "\toutput: {s}\n\tdatabase: {s}\n\tlabel: {s}\n\tlog level: {s}", + .{ @tagName(self.output), self.database, self.label, @tagName(self.logLevel) }, + ); + + try writer.flush(); + } }; pub const Metadata = struct { From e3656a67faa48e38937f105d42e1cc8315634730 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 31 Jan 2026 17:23:26 +0000 Subject: [PATCH 12/19] UPDATE: changelog creation Signed-off-by: Jim Fitzpatrick --- .gitignore | 1 + build.zig | 21 +++++++++++++++++++++ changelog.d/+fd133a0b.misc.md | 1 + {changes => changelog.d}/.gitkeep | 0 towncrier.toml | 2 +- 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 changelog.d/+fd133a0b.misc.md rename {changes => changelog.d}/.gitkeep (100%) diff --git a/.gitignore b/.gitignore index 7a0e1b9..29b6812 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ nohup.out *.db zig-out/ .zig-cache/ +docs/ diff --git a/build.zig b/build.zig index 92cca69..fd20e6e 100644 --- a/build.zig +++ b/build.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const zon = @import("build.zig.zon"); // Although this function looks imperative, it does not perform the build // directly and instead it mutates the build graph (`b`) that will be then @@ -122,4 +123,24 @@ pub fn build(b: *std.Build) void { // // Lastly, the Zig build system is relatively simple and self-contained, // and reading its source code will allow you to master it. + + // Change log configuration + const changelog_cmd = b.addSystemCommand(&.{ + "towncrier", + "build", + "--draft", + "--version", + zon.version, + }); + const changelog_step = b.step("changelog_draft", "Build changelog draft"); + changelog_step.dependOn(&changelog_cmd.step); + + const changelog_release_cmd = b.addSystemCommand(&.{ + "towncrier", + "build", + "--version", + zon.version, + }); + const changelog_release_step = b.step("changelog_release", "Build changelog draft"); + changelog_release_step.dependOn(&changelog_release_cmd.step); } diff --git a/changelog.d/+fd133a0b.misc.md b/changelog.d/+fd133a0b.misc.md new file mode 100644 index 0000000..1f90598 --- /dev/null +++ b/changelog.d/+fd133a0b.misc.md @@ -0,0 +1 @@ +Configure changelog to be a zig build command diff --git a/changes/.gitkeep b/changelog.d/.gitkeep similarity index 100% rename from changes/.gitkeep rename to changelog.d/.gitkeep diff --git a/towncrier.toml b/towncrier.toml index ccc265e..aca1c4f 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -2,4 +2,4 @@ name = "kubectlgetall" filename = "CHANGELOG.md" -directory = "changes" +directory = "changelog.d" From e3d8b3030ad6cc97c90df44e5d502663c67f4bfd Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 31 Jan 2026 18:32:17 +0000 Subject: [PATCH 13/19] BUILD: release Signed-off-by: Jim Fitzpatrick --- build.zig | 147 ++++++++++++++++++++++++++++++++++ changelog.d/+7bbe14bb.misc.md | 1 + 2 files changed, 148 insertions(+) create mode 100644 changelog.d/+7bbe14bb.misc.md diff --git a/build.zig b/build.zig index fd20e6e..1207591 100644 --- a/build.zig +++ b/build.zig @@ -143,4 +143,151 @@ pub fn build(b: *std.Build) void { }); const changelog_release_step = b.step("changelog_release", "Build changelog draft"); changelog_release_step.dependOn(&changelog_release_cmd.step); + + // Build release command + const release_step = b.step("release", "Build release archives"); + const release_checks = ReleaseChecksStep.create(b); + + const release_targets = [_]ReleaseTarget{ + .{ .os_tag = .linux, .arch = .x86_64, .os_name = "linux", .arch_name = "amd64" }, + .{ .os_tag = .linux, .arch = .aarch64, .os_name = "linux", .arch_name = "arm64" }, + .{ .os_tag = .macos, .arch = .x86_64, .os_name = "darwin", .arch_name = "amd64" }, + .{ .os_tag = .macos, .arch = .aarch64, .os_name = "darwin", .arch_name = "arm64" }, + }; + + for (release_targets) |release_target| { + const resolved_target = b.resolveTargetQuery(.{ + .cpu_arch = release_target.arch, + .os_tag = release_target.os_tag, + }); + const release_exe = addReleaseExecutable( + b, + resolved_target, + optimize, + clap, + ); + + const archive_name = b.fmt("{s}_{s}_{s}_{s}.tar.gz", .{ + @tagName(zon.name), + zon.version, + release_target.os_name, + release_target.arch_name, + }); + const dist_dir = "dist"; + const staging_dir = b.fmt("{s}/stage_{s}_{s}", .{ + dist_dir, + release_target.os_name, + release_target.arch_name, + }); + + const clean_staging = b.addRemoveDirTree(b.path(staging_dir)); + const make_staging = b.addSystemCommand(&.{ "mkdir", "-p", staging_dir }); + make_staging.step.dependOn(&clean_staging.step); + + const copy_bin = b.addSystemCommand(&.{"cp"}); + copy_bin.addFileArg(release_exe.getEmittedBin()); + copy_bin.addArg(staging_dir); + + const copy_docs = b.addSystemCommand(&.{ + "cp", + "README.md", + "CHANGELOG.md", + staging_dir, + }); + + const tar_cmd = b.addSystemCommand(&.{ + "tar", + "-czf", + b.fmt("{s}/{s}", .{ dist_dir, archive_name }), + "-C", + staging_dir, + ".", + }); + + const clean_after = b.addRemoveDirTree(b.path(staging_dir)); + + copy_bin.step.dependOn(&release_exe.step); + copy_bin.step.dependOn(&make_staging.step); + copy_bin.step.dependOn(&release_checks.step); + copy_docs.step.dependOn(&make_staging.step); + copy_docs.step.dependOn(&release_checks.step); + tar_cmd.step.dependOn(&make_staging.step); + tar_cmd.step.dependOn(©_docs.step); + tar_cmd.step.dependOn(©_bin.step); + tar_cmd.step.dependOn(&release_checks.step); + clean_after.step.dependOn(&tar_cmd.step); + release_step.dependOn(&clean_after.step); + } +} + +const ReleaseTarget = struct { + os_tag: std.Target.Os.Tag, + arch: std.Target.Cpu.Arch, + os_name: []const u8, + arch_name: []const u8, +}; + +const ReleaseChecksStep = struct { + step: std.Build.Step, + version: []const u8, + + pub fn create(b: *std.Build) *ReleaseChecksStep { + const checks = b.allocator.create(ReleaseChecksStep) catch @panic("OOM"); + checks.* = .{ + .step = std.Build.Step.init(.{ + .id = .custom, + .name = "release_checks", + .owner = b, + .makeFn = make, + }), + .version = b.dupe(zon.version), + }; + + return checks; + } + + fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) anyerror!void { + _ = options; + const checks: *ReleaseChecksStep = @fieldParentPtr("step", step); + + var dir = try std.fs.cwd().openDir("changelog.d", .{ .iterate = true }); + defer dir.close(); + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind != .file) continue; + if (std.mem.eql(u8, entry.name, ".gitkeep")) continue; + return step.fail("changelog.d contains fragment: {s}", .{entry.name}); + } + + const changelog = std.fs.cwd().readFileAlloc(step.owner.allocator, "CHANGELOG.md", 1024 * 1024) catch |err| { + return step.fail("failed to read CHANGELOG.md: {s}", .{@errorName(err)}); + }; + defer step.owner.allocator.free(changelog); + if (std.mem.indexOf(u8, changelog, checks.version) == null) { + return step.fail("CHANGELOG.md missing version {s}", .{checks.version}); + } + } +}; + +fn addReleaseExecutable( + b: *std.Build, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + clap: *std.Build.Dependency, +) *std.Build.Step.Compile { + _ = optimize; + const release_optimize: std.builtin.OptimizeMode = .ReleaseSmall; + const exe = b.addExecutable(.{ + .name = @tagName(zon.name), + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = release_optimize, + }), + }); + + exe.root_module.addImport("clap", clap.module("clap")); + + return exe; } diff --git a/changelog.d/+7bbe14bb.misc.md b/changelog.d/+7bbe14bb.misc.md new file mode 100644 index 0000000..fba191f --- /dev/null +++ b/changelog.d/+7bbe14bb.misc.md @@ -0,0 +1 @@ +Add release scripts to build the different platform releases. From 208d9734ed9a5c3096fd5d720fee08b3d7bbe099 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 31 Jan 2026 22:50:28 +0000 Subject: [PATCH 14/19] ADD: sqlite This sqlite integration is using a c library. Signed-off-by: Jim Fitzpatrick --- build.zig | 11 ++++++++++ build.zig.zon | 4 ++++ src/database.zig | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 18 +++++++++-------- src/types.zig | 1 + 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 src/database.zig diff --git a/build.zig b/build.zig index 1207591..355ef9e 100644 --- a/build.zig +++ b/build.zig @@ -27,6 +27,11 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }); + const sqlite = b.dependency("sqlite", .{ + .target = target, + .optimize = optimize, + }); + // Here we define an executable. An executable needs to have a root module // which needs to expose a `main` function. While we could add a main function // to the module defined above, it's sometimes preferable to split business @@ -63,6 +68,8 @@ pub fn build(b: *std.Build) void { }); exe.root_module.addImport("clap", clap.module("clap")); + exe.root_module.addImport("sqlite", sqlite.module("sqlite")); + exe.linkLibC(); // This declares intent for the executable to be installed into the // install prefix when running `zig build` (i.e. when executing the default @@ -165,6 +172,7 @@ pub fn build(b: *std.Build) void { resolved_target, optimize, clap, + sqlite, ); const archive_name = b.fmt("{s}_{s}_{s}_{s}.tar.gz", .{ @@ -275,6 +283,7 @@ fn addReleaseExecutable( target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, clap: *std.Build.Dependency, + sqlite: *std.Build.Dependency, ) *std.Build.Step.Compile { _ = optimize; const release_optimize: std.builtin.OptimizeMode = .ReleaseSmall; @@ -288,6 +297,8 @@ fn addReleaseExecutable( }); exe.root_module.addImport("clap", clap.module("clap")); + exe.root_module.addImport("sqlite", sqlite.module("sqlite")); + exe.linkLibC(); return exe; } diff --git a/build.zig.zon b/build.zig.zon index 9457717..c74c1d8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -6,6 +6,10 @@ .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz", .hash = "clap-0.11.0-oBajB-HnAQDPCKYzwF7rO3qDFwRcD39Q0DALlTSz5H7e", }, + .sqlite = .{ + .url = "https://github.com/nDimensional/zig-sqlite/archive/refs/tags/v0.3.2-3510100.tar.gz", + .hash = "sqlite-0.3.2-3510100-djiKTqmAAADFNYxhtQSupdE7DURa1hGE8tL7aQzUFMdK", + }, }, .paths = .{""}, .fingerprint = 0x146917ff4713c3, diff --git a/src/database.zig b/src/database.zig new file mode 100644 index 0000000..b24a2e3 --- /dev/null +++ b/src/database.zig @@ -0,0 +1,52 @@ +const std = @import("std"); +const sqlite = @import("sqlite"); +const types = @import("types.zig"); + +var db: sqlite.Database = undefined; + +pub fn init(database: []const u8) !void { + std.log.debug("configure database: {s}", .{database}); + + const c_string: [*:0]const u8 = @ptrCast(database); + db = try sqlite.Database.open(.{ + .path = c_string, + }); + try db.exec("CREATE TABLE IF NOT EXISTS results(id INTEGER PRIMARY KEY AUTOINCREMENT, apiVersion, kind, name, namespace, creationTimestamp, resourceVersion, resultTimestamp, resultLabel)", .{}); +} + +pub fn add(enties: types.ResourceList, label: ?[]const u8, timestamp: i64) !void { + var _label: ?sqlite.Text = null; + if (label) |l| _label = sqlite.text(l); + + const insert = try db.prepare(Entry, void, "INSERT INTO results VALUES (NULL, :apiVersion, :kind, :name, :namespace, :creationTimestamp, :resourceVersion, :resultTimestamp, :resultLabel)"); + defer insert.finalize(); + for (enties.items) |entry| { + std.log.debug("adding {s}/{s}/{s} to database", .{ + entry.kind, + entry.metadata.namespace, + entry.metadata.name, + }); + + try insert.exec(.{ + .apiVersion = sqlite.text(entry.apiVersion), + .kind = sqlite.text(entry.kind), + .name = sqlite.text(entry.metadata.name), + .namespace = sqlite.text(entry.metadata.namespace), + .creationTimestamp = sqlite.text(entry.metadata.creationTimestamp), + .resourceVersion = sqlite.text(entry.metadata.resourceVersion.?), + .resultTimestamp = timestamp, + .resultLabel = _label, + }); + } +} + +const Entry = struct { + apiVersion: sqlite.Text, + kind: sqlite.Text, + name: sqlite.Text, + namespace: sqlite.Text, + creationTimestamp: sqlite.Text, + resourceVersion: sqlite.Text, + resultTimestamp: i64, + resultLabel: ?sqlite.Text, +}; diff --git a/src/main.zig b/src/main.zig index 8022a04..bd72d77 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,7 @@ const clap = @import("clap"); const std = @import("std"); +const db = @import("database.zig"); const types = @import("types.zig"); var stdout_buf: [1024]u8 = undefined; @@ -135,6 +136,7 @@ pub fn main() !void { .database = database, .label = label, .logLevel = level, + .timestamp = std.time.timestamp(), }; std.log.debug("{f}", .{config}); @@ -157,6 +159,13 @@ pub fn main() !void { std.mem.sort([]const u8, crdTypes.items, {}, compareStrings); } + if (config.output == .sqlite) { + if (config.database.len == 0) { + std.log.err("--database must be set to use output type of sqlite", .{}); + std.process.exit(1); + } + try db.init(config.database); + } std.log.info("Total number of CRD types found {}.", .{crdTypes.items.len}); var map: ?std.StringHashMap(types.ResourceList) = null; defer if (map) |*m| { @@ -196,7 +205,7 @@ pub fn main() !void { defer { resource.deinit(allocator); } - try print_table(resource); + try db.add(resource, config.label, config.timestamp); }, .json => { if (map) |*m| { @@ -226,13 +235,6 @@ pub fn main() !void { try stdout.print("}}\n", .{}); try stdout.flush(); } - - const todo = - \\To do: - \\- database configuration - \\ - ; - std.log.debug("{s}", .{todo}); } fn contains(haystack: ?[]const []const u8, needle: []const u8) bool { diff --git a/src/types.zig b/src/types.zig index ce2b9cf..ce5b2cf 100644 --- a/src/types.zig +++ b/src/types.zig @@ -13,6 +13,7 @@ pub const Config = struct { database: []const u8, label: []const u8, logLevel: Level, + timestamp: i64, pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print( From e487bebf9fde389ace377964f05f833b74bac8f7 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sat, 31 Jan 2026 23:48:22 +0000 Subject: [PATCH 15/19] REFACTOR: move table to own file Signed-off-by: Jim Fitzpatrick --- src/main.zig | 177 +------------------------------------------------ src/table.zig | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+), 175 deletions(-) create mode 100644 src/table.zig diff --git a/src/main.zig b/src/main.zig index bd72d77..d6f0608 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,6 +3,7 @@ const std = @import("std"); const db = @import("database.zig"); const types = @import("types.zig"); +const table = @import("table.zig"); var stdout_buf: [1024]u8 = undefined; var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); @@ -199,7 +200,7 @@ pub fn main() !void { defer { resource.deinit(allocator); } - try print_table(resource); + try table.print(resource); }, .sqlite => { defer { @@ -247,180 +248,6 @@ fn contains(haystack: ?[]const []const u8, needle: []const u8) bool { return false; } -fn print_table(data: types.ResourceList) !void { - // TODO: This code is horrible, needs a large refactor - const title_name = "NAME"; - const title_namespace = "NAMESPACE"; - const title_creationTimestamp = "CREATION TIMESTAMP"; - - var max_name_length: usize = title_name.len; - var max_namespace_length: usize = title_namespace.len; - var max_creationTimestamp_length: usize = title_creationTimestamp.len; - const kind = data.items[0].kind; - for (data.items) |i| { - if (i.metadata.name.len > max_name_length) max_name_length = i.metadata.name.len; - if (i.metadata.namespace.len > max_namespace_length) max_namespace_length = i.metadata.namespace.len; - if (i.metadata.creationTimestamp.len > max_creationTimestamp_length) max_creationTimestamp_length = i.metadata.creationTimestamp.len; - } - - const headers: [3][]const u8 = .{ title_namespace, title_name, title_creationTimestamp }; - const spacing: [3]usize = .{ max_namespace_length, max_name_length, max_creationTimestamp_length }; - var spacing_required: usize = 0; - for (spacing) |s| spacing_required += s; - - // add 2 for table ends - // add 2 for each field printed (name, namespace) = 4 - // add field count - 1 for vertical divides - // needs a - 1 for some reason - const line_length = spacing_required + 2 + (2 * headers.len) + (headers.len - 1) - 1; - var divider_idx: usize = 0; - var divider = 2 + spacing[divider_idx] + 1; - divider_idx += 1; - - const green_start = "\x1b[1;32m"; - const reset = "\x1b[0m"; - const top_left = "\u{250C}"; - const top_right = "\u{2510}"; - const bottom_left = "\u{2514}"; - const bottom_right = "\u{2518}"; - const horizontal = "\u{2500}"; - const top_junction = "\u{252C}"; - const bottom_junction = "\u{2534}"; - const intersection = "\u{253C}"; - const vertical = "\u{2502}"; - const left_junction = "\u{251C}"; - const right_junction = "\u{2524}"; - - const padding = (line_length - kind.len) / 2; - for (0..padding) |_| { - try stdout.print(" ", .{}); - } - try stdout.print("{s}{s}{s}\n", .{ green_start, kind, reset }); - // header line - for (0..line_length + 1) |i| { - if (i == 0) { - try stdout.print("{s}", .{top_left}); - } else if (i == line_length) { - try stdout.print("{s}\n", .{top_right}); - } else if (i == divider and divider_idx < spacing.len) { - try stdout.print("{s}", .{top_junction}); - divider += spacing[divider_idx] + 3; - divider_idx += 1; - } else { - try stdout.print("{s}", .{horizontal}); - } - } - - divider_idx = 0; - divider = 2 + spacing[divider_idx] + 1; - divider_idx += 1; - - var pos: usize = 0; - while (pos < line_length + 1) : (pos += 1) { - if (pos == 0) { - try stdout.print("{s}", .{vertical}); - } else if (pos == divider and divider_idx == headers.len) { - try stdout.print("{s}\n", .{vertical}); - break; - } else if (pos == 2) { - try stdout.print("{s}", .{headers[0]}); - pos += headers[0].len - 1; - } else if (pos == divider and divider_idx < headers.len) { - try stdout.print("{s} {s}", .{ vertical, headers[divider_idx] }); - pos += headers[divider_idx].len - 1; - divider += spacing[divider_idx] + 1; - divider_idx += 1; - } else { - try stdout.print(" ", .{}); - } - } - - divider_idx = 0; - divider = 2 + spacing[divider_idx] + 1; - divider_idx += 1; - for (0..line_length + 1) |i| { - if (i == 0) { - try stdout.print("{s}", .{left_junction}); - } else if (i == line_length) { - try stdout.print("{s}\n", .{right_junction}); - } else if (i == divider and divider_idx < spacing.len) { - try stdout.print("{s}", .{intersection}); - divider += spacing[divider_idx] + 3; - divider_idx += 1; - } else { - try stdout.print("{s}", .{horizontal}); - } - } - - for (data.items, 1..) |item, idx| { - divider_idx = 0; - divider = 2 + spacing[divider_idx] + 1; - divider_idx += 1; - pos = 0; - while (pos < line_length + 1) : (pos += 1) { - if (pos == 0) { - try stdout.print("{s}", .{vertical}); - } else if (pos == line_length) { - try stdout.print("{s}\n", .{vertical}); - } else if (pos == 2) { - try stdout.print("{s}", .{item.metadata.namespace}); - pos += item.metadata.namespace.len - 1; - } else if (pos == divider) { - if (divider_idx == 1) { - try stdout.print("{s} {s}", .{ vertical, item.metadata.name }); - pos += item.metadata.name.len + 1; - divider += spacing[divider_idx] + 3; - divider_idx += 1; - } else { - try stdout.print("{s} {s}", .{ vertical, item.metadata.creationTimestamp }); - pos += item.metadata.creationTimestamp.len + 1; - divider += spacing[divider_idx]; - divider_idx += 1; - } - } else { - try stdout.print(" ", .{}); - } - } - - divider_idx = 0; - divider = 2 + spacing[divider_idx] + 1; - divider_idx += 1; - if (idx != data.items.len) { - divider_idx = 0; - divider = 2 + spacing[divider_idx] + 1; - divider_idx += 1; - for (0..line_length + 1) |i| { - if (i == 0) { - try stdout.print("{s}", .{left_junction}); - } else if (i == line_length) { - try stdout.print("{s}\n", .{right_junction}); - } else if (i == divider and divider_idx < spacing.len) { - try stdout.print("{s}", .{intersection}); - divider += spacing[divider_idx] + 3; - divider_idx += 1; - } else { - try stdout.print("{s}", .{horizontal}); - } - } - } else { - for (0..line_length + 1) |i| { - if (i == 0) { - try stdout.print("{s}", .{bottom_left}); - } else if (i == line_length) { - try stdout.print("{s}\n\n", .{bottom_right}); - } else if (i == divider and divider_idx < spacing.len) { - try stdout.print("{s}", .{bottom_junction}); - divider += spacing[divider_idx] + 3; - divider_idx += 1; - } else { - try stdout.print("{s}", .{horizontal}); - } - } - } - } - try stdout.flush(); -} - fn getCRJson(allocator: std.mem.Allocator, config: types.Config, crd: []const u8) !types.ResourceList { const initialcmd = &[_][]const u8{ "kubectl", "get", "--ignore-not-found", crd, "--output", "json" }; diff --git a/src/table.zig b/src/table.zig new file mode 100644 index 0000000..837a932 --- /dev/null +++ b/src/table.zig @@ -0,0 +1,180 @@ +const std = @import("std"); +const types = @import("types.zig"); + +var stdout_buf: [1024]u8 = undefined; +var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); +const stdout: *std.io.Writer = &stdout_writer.interface; + +pub fn print(data: types.ResourceList) !void { + // TODO: This code is horrible, needs a large refactor + const title_name = "NAME"; + const title_namespace = "NAMESPACE"; + const title_creationTimestamp = "CREATION TIMESTAMP"; + + var max_name_length: usize = title_name.len; + var max_namespace_length: usize = title_namespace.len; + var max_creationTimestamp_length: usize = title_creationTimestamp.len; + const kind = data.items[0].kind; + for (data.items) |i| { + if (i.metadata.name.len > max_name_length) max_name_length = i.metadata.name.len; + if (i.metadata.namespace.len > max_namespace_length) max_namespace_length = i.metadata.namespace.len; + if (i.metadata.creationTimestamp.len > max_creationTimestamp_length) max_creationTimestamp_length = i.metadata.creationTimestamp.len; + } + + const headers: [3][]const u8 = .{ title_namespace, title_name, title_creationTimestamp }; + const spacing: [3]usize = .{ max_namespace_length, max_name_length, max_creationTimestamp_length }; + var spacing_required: usize = 0; + for (spacing) |s| spacing_required += s; + + // add 2 for table ends + // add 2 for each field printed (name, namespace) = 4 + // add field count - 1 for vertical divides + // needs a - 1 for some reason + const line_length = spacing_required + 2 + (2 * headers.len) + (headers.len - 1) - 1; + var divider_idx: usize = 0; + var divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + + const green_start = "\x1b[1;32m"; + const reset = "\x1b[0m"; + const top_left = "\u{250C}"; + const top_right = "\u{2510}"; + const bottom_left = "\u{2514}"; + const bottom_right = "\u{2518}"; + const horizontal = "\u{2500}"; + const top_junction = "\u{252C}"; + const bottom_junction = "\u{2534}"; + const intersection = "\u{253C}"; + const vertical = "\u{2502}"; + const left_junction = "\u{251C}"; + const right_junction = "\u{2524}"; + + const padding = (line_length - kind.len) / 2; + for (0..padding) |_| { + try stdout.print(" ", .{}); + } + try stdout.print("{s}{s}{s}\n", .{ green_start, kind, reset }); + // header line + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{top_left}); + } else if (i == line_length) { + try stdout.print("{s}\n", .{top_right}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{top_junction}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } + } + + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + + var pos: usize = 0; + while (pos < line_length + 1) : (pos += 1) { + if (pos == 0) { + try stdout.print("{s}", .{vertical}); + } else if (pos == divider and divider_idx == headers.len) { + try stdout.print("{s}\n", .{vertical}); + break; + } else if (pos == 2) { + try stdout.print("{s}", .{headers[0]}); + pos += headers[0].len - 1; + } else if (pos == divider and divider_idx < headers.len) { + try stdout.print("{s} {s}", .{ vertical, headers[divider_idx] }); + pos += headers[divider_idx].len - 1; + divider += spacing[divider_idx] + 1; + divider_idx += 1; + } else { + try stdout.print(" ", .{}); + } + } + + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{left_junction}); + } else if (i == line_length) { + try stdout.print("{s}\n", .{right_junction}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{intersection}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } + } + + for (data.items, 1..) |item, idx| { + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + pos = 0; + while (pos < line_length + 1) : (pos += 1) { + if (pos == 0) { + try stdout.print("{s}", .{vertical}); + } else if (pos == line_length) { + try stdout.print("{s}\n", .{vertical}); + } else if (pos == 2) { + try stdout.print("{s}", .{item.metadata.namespace}); + pos += item.metadata.namespace.len - 1; + } else if (pos == divider) { + if (divider_idx == 1) { + try stdout.print("{s} {s}", .{ vertical, item.metadata.name }); + pos += item.metadata.name.len + 1; + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s} {s}", .{ vertical, item.metadata.creationTimestamp }); + pos += item.metadata.creationTimestamp.len + 1; + divider += spacing[divider_idx]; + divider_idx += 1; + } + } else { + try stdout.print(" ", .{}); + } + } + + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + if (idx != data.items.len) { + divider_idx = 0; + divider = 2 + spacing[divider_idx] + 1; + divider_idx += 1; + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{left_junction}); + } else if (i == line_length) { + try stdout.print("{s}\n", .{right_junction}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{intersection}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } + } + } else { + for (0..line_length + 1) |i| { + if (i == 0) { + try stdout.print("{s}", .{bottom_left}); + } else if (i == line_length) { + try stdout.print("{s}\n\n", .{bottom_right}); + } else if (i == divider and divider_idx < spacing.len) { + try stdout.print("{s}", .{bottom_junction}); + divider += spacing[divider_idx] + 3; + divider_idx += 1; + } else { + try stdout.print("{s}", .{horizontal}); + } + } + } + } + try stdout.flush(); +} From 41f5fa3714ddca706f9c6a98cb14fd199044ed05 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 1 Feb 2026 00:06:47 +0000 Subject: [PATCH 16/19] PORT: update readme Signed-off-by: Jim Fitzpatrick --- README.md | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3da7b91..4509ccf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,24 @@ List all CR's for all CRD types on a cluster in a given namespace. **Requires kubectl to be installed.** ## Installation -ERROR: No good answer right now + Install it to a user-writable directory like `~/.local/bin` and ensure it is on your `PATH`. + +### Prebuilt release +Download the matching archive from the GitHub Releases page, then extract and +install it: + +```shell +tar -xzf kubectlgetall___.tar.gz +mkdir -p ~/.local/bin +cp kubectlgetall ~/.local/bin/ +``` + +### Build from source +```shell +zig build -Doptimize=ReleaseSmall -p ~/.local +``` + +Make sure `~/.local/bin` is on your `PATH`. ## Usage @@ -50,8 +67,16 @@ options: ## Dev ### Creating the changelog -On new changes a news fragment is required. -This can be created by and news fragments to the `changes` directory. -These files are should have the following naming schema `.`. -Using `towncrier create -c "change message" ` will also create the file for you in the correct location. +For new changes a news fragment is required. Add a fragment to +`changelog.d/` with the naming schema +`.`. +Using `towncrier create -c "change message" ` will create the file +for you in the correct location. + +### Release process +1. Ensure all changes have news fragments in `changelog.d/`. +2. Update the version in `build.zig.zon`. +3. Run `zig build changelog_release` to update `CHANGELOG.md`. +4. Run `zig build release` to generate release archives in `dist/`. +5. Tag and publish the release with the generated archives and changelog. From bc760e3c6e1d9d776094f0aad5082fd8288cbb92 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 1 Feb 2026 12:40:10 +0000 Subject: [PATCH 17/19] UPDATE: readme Signed-off-by: Jim Fitzpatrick --- README.md | 56 +++++++++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 4509ccf..0bd3033 100644 --- a/README.md +++ b/README.md @@ -27,40 +27,38 @@ Make sure `~/.local/bin` is on your `PATH`. ## Usage ```shell -kubectlgetall +kubectlgetall -n ``` There are some flags that can be passed. ```shell kubectlgetall --help -usage: kubectlgetall [-h] [-n NAMESPACE] [-A] [--version] [-s] - [-e [EXCLUDE ...]] [-o {tty,json,sqlite}] [-d DATABASE] - [-l LABEL] [--debug] - -Returns a list of CR for the different CRDs in a given namespace - -options: - -h, --help show this help message and exit - -n, --namespace NAMESPACE - Namespace to get resources from. - -A, --all-namespaces If present, list all objects across all namespaces. - Specifinig --namespace will be ignored - --version show program's version number and exit - -s, --sort Prints the resources in an order. Initial results take - longer to show. Unsorted return results faster but can - hit rate limits. - -e, --exclude [EXCLUDE ...] - Exclude crd types. Multiple can be excluded eg: "-e - " - -o, --output {tty,json,sqlite} - Changes the output format of the results (default: - tty) - -d, --database DATABASE - Path to the sqlite file to save the results. If the - file does not exist it will be created. - -l, --label LABEL Set the label that will be saved with entries when - using the --database option. - --debug Enable debug mode. + -h, --help + Display this help and exit. + + -n, --namespace + Namespace to get resources from. + + -A, --all-namespaces + If present, list all objects across all namespaces. Specifing --namespace will be ignored. + + -s, --sort + Prints the resources in order. + + -e, --exclude ... + Exclude crd types. Multiple can be excluded eg: "-e -e " + + -o, --output + Changes the output format of the results. + + -d, --database + Path to the sqlite file to save the results. If the files does not exist it will be created. + + -l, --label + Set the label that will be saved with entries when using the --database option. + + --log-level + Set the log level. All logs are saved to file. Possible values are (debug, info, warn, error). Defualt level is warn. ``` From af257f4d1c3d1a644e1b509593e9bf270d039a87 Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 1 Feb 2026 12:59:12 +0000 Subject: [PATCH 18/19] ADD: version flag Signed-off-by: Jim Fitzpatrick --- README.md | 3 +++ build.zig | 12 ++++++++++++ changelog.d/+60826432.feature.md | 1 + src/main.zig | 7 +++++++ 4 files changed, 23 insertions(+) create mode 100644 changelog.d/+60826432.feature.md diff --git a/README.md b/README.md index 0bd3033..f2b173f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,9 @@ kubectlgetall --help --log-level Set the log level. All logs are saved to file. Possible values are (debug, info, warn, error). Defualt level is warn. + --version + Display verson, and exit. + ``` ## Dev diff --git a/build.zig b/build.zig index 355ef9e..f5d026e 100644 --- a/build.zig +++ b/build.zig @@ -67,6 +67,12 @@ pub fn build(b: *std.Build) void { }), }); + const options = b.addOptions(); + options.addOption([]const u8, "version", zon.version); + options.addOption([]const u8, "name", @tagName(zon.name)); + + exe.root_module.addOptions("build_options", options); + exe.root_module.addImport("clap", clap.module("clap")); exe.root_module.addImport("sqlite", sqlite.module("sqlite")); exe.linkLibC(); @@ -296,6 +302,12 @@ fn addReleaseExecutable( }), }); + const options = b.addOptions(); + options.addOption([]const u8, "version", zon.version); + options.addOption([]const u8, "name", @tagName(zon.name)); + + exe.root_module.addOptions("build_options", options); + exe.root_module.addImport("clap", clap.module("clap")); exe.root_module.addImport("sqlite", sqlite.module("sqlite")); exe.linkLibC(); diff --git a/changelog.d/+60826432.feature.md b/changelog.d/+60826432.feature.md new file mode 100644 index 0000000..9ffed9d --- /dev/null +++ b/changelog.d/+60826432.feature.md @@ -0,0 +1 @@ +Version flag, `--version` flag can be used to get the current version of the application. diff --git a/src/main.zig b/src/main.zig index d6f0608..8ccfd5a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,6 @@ const clap = @import("clap"); const std = @import("std"); +const build_options = @import("build_options"); const db = @import("database.zig"); const types = @import("types.zig"); @@ -54,6 +55,7 @@ pub fn main() !void { \\-d, --database Path to the sqlite file to save the results. If the files does not exist it will be created. \\-l, --label Set the label that will be saved with entries when using the --database option. \\--log-level Set the log level. All logs are saved to file. Possible values are (debug, info, warn, error). Defualt level is warn. + \\--version Display verson, and exit. \\ ); @@ -90,6 +92,11 @@ pub fn main() !void { if (res.args.help != 0) return clap.helpToFile(.stdout(), clap.Help, ¶ms, .{}); + if (res.args.version != 0) { + std.log.info("{s}, {s}", .{ build_options.name, build_options.version }); + std.process.exit(0); + } + if (res.args.namespace) |n| { namespace = n; } From 064aacf4cb8c2b2f31ae1cf108de5436d50d083b Mon Sep 17 00:00:00 2001 From: Jim Fitzpatrick Date: Sun, 1 Feb 2026 13:07:31 +0000 Subject: [PATCH 19/19] RELEASE: 0.5.0 Signed-off-by: Jim Fitzpatrick --- CHANGELOG.md | 15 +++++++++++++++ build.zig.zon | 2 +- changelog.d/+60826432.feature.md | 1 - changelog.d/+7bbe14bb.misc.md | 1 - changelog.d/+fd133a0b.misc.md | 1 - 5 files changed, 16 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/+60826432.feature.md delete mode 100644 changelog.d/+7bbe14bb.misc.md delete mode 100644 changelog.d/+fd133a0b.misc.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ddefe1..df6aafe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# kubectlgetall 0.5.0 (2026-02-01) + +## Features + +- Version flag, `--version` flag can be used to get the current version of the application. +- Zig port. This release has moved the application to zig 0.15.2. + This has the added advantage of shipping binaries for cross-platform. + Only Linux x86_64 has being tested locally. + +## Misc + +- Add release scripts to build the different platform releases. +- Configure changelog to be a zig build command + + # Kubectlgetall 0.4.0 (2025-02-09) ### Features diff --git a/build.zig.zon b/build.zig.zon index c74c1d8..65d044f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .kubectlgetall, - .version = "0.5.0-dev", + .version = "0.5.0", .dependencies = .{ .clap = .{ .url = "https://github.com/Hejsil/zig-clap/archive/refs/tags/0.11.0.tar.gz", diff --git a/changelog.d/+60826432.feature.md b/changelog.d/+60826432.feature.md deleted file mode 100644 index 9ffed9d..0000000 --- a/changelog.d/+60826432.feature.md +++ /dev/null @@ -1 +0,0 @@ -Version flag, `--version` flag can be used to get the current version of the application. diff --git a/changelog.d/+7bbe14bb.misc.md b/changelog.d/+7bbe14bb.misc.md deleted file mode 100644 index fba191f..0000000 --- a/changelog.d/+7bbe14bb.misc.md +++ /dev/null @@ -1 +0,0 @@ -Add release scripts to build the different platform releases. diff --git a/changelog.d/+fd133a0b.misc.md b/changelog.d/+fd133a0b.misc.md deleted file mode 100644 index 1f90598..0000000 --- a/changelog.d/+fd133a0b.misc.md +++ /dev/null @@ -1 +0,0 @@ -Configure changelog to be a zig build command