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..29b6812 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ __pycache__/ nohup.out /tmp *.db +zig-out/ +.zig-cache/ +docs/ 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/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/README.md b/README.md index 6261355..f2b173f 100644 --- a/README.md +++ b/README.md @@ -5,61 +5,79 @@ 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. + 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 -pipx install kubectallgetall +tar -xzf kubectlgetall___.tar.gz +mkdir -p ~/.local/bin +cp kubectlgetall ~/.local/bin/ ``` -Install with nice formatting. +### Build from source ```shell -pipx install kubectallgetall[rich] +zig build -Doptimize=ReleaseSmall -p ~/.local ``` +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. + + --version + Display verson, and exit. ``` ## 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. diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..f5d026e --- /dev/null +++ b/build.zig @@ -0,0 +1,316 @@ +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 +// 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, + }); + + 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 + // 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 = &.{}, + }), + }); + + 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(); + + // 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. + + // 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); + + // 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, + sqlite, + ); + + 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, + sqlite: *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, + }), + }); + + 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(); + + return exe; +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..65d044f --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,16 @@ +.{ + .name = .kubectlgetall, + .version = "0.5.0", + .dependencies = .{ + .clap = .{ + .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/changes/.gitkeep b/changelog.d/.gitkeep similarity index 100% rename from changes/.gitkeep rename to changelog.d/.gitkeep 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/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 new file mode 100644 index 0000000..8ccfd5a --- /dev/null +++ b/src/main.zig @@ -0,0 +1,349 @@ +const clap = @import("clap"); +const std = @import("std"); +const build_options = @import("build_options"); + +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); +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(); + 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. Possible values are (debug, info, warn, error). Defualt level is warn. + \\--version Display verson, and exit. + \\ + ); + + const parsers = comptime .{ + .OUTPUT = clap.parsers.enumeration(types.Output), + .STR = clap.parsers.string, + .PATH = clap.parsers.string, + .LEVEL = clap.parsers.enumeration(types.Level), + }; + + // 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 = types.Output.tty; + 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, .{}); + + 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; + } + + if (res.args.@"all-namespaces" == 1) { + allNamespaces = true; + } + + if (res.args.sort == 1) { + 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; + } + + 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, + .info => log_level = std.log.Level.info, + .warn => log_level = std.log.Level.warn, + } + const config = types.Config{ + .namespace = namespace, + .all = allNamespaces, + .sort = sort, + .exclude = exclude, + .output = output, + .database = database, + .label = label, + .logLevel = level, + .timestamp = std.time.timestamp(), + }; + + std.log.debug("{f}", .{config}); + + var crdTypes = getCrdList(allocator) catch |err| switch (err) { + error.BadExit => { + std.log.err("Kubectl returned a none zero exit code", .{}); + std.process.exit(1); + }, + else => return err, + }; + defer { + for (crdTypes.items) |item| { + allocator.free(item); + } + crdTypes.deinit(allocator); + } + + if (config.sort) { + 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| { + 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| { + 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; + }, + else => return err, + }; + switch (config.output) { + .tty => { + defer { + resource.deinit(allocator); + } + try table.print(resource); + }, + .sqlite => { + defer { + resource.deinit(allocator); + } + try db.add(resource, config.label, config.timestamp); + }, + .json => { + if (map) |*m| { + try m.put(line, resource); + } + }, + } + } + + 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(); + } +} + +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 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; + 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); + } + + 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.log.err("Failed to run kubectl: {}", .{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(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, + }; + + defer { + parsed.deinit(); + } + + 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) { + 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.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}", .{ result.stdout, result.stdout }); + return error.BadExit; + } + var lines: std.ArrayList([]const u8) = .empty; + errdefer { + for (lines.items) |item| { + allocator.free(item); + } + 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; +} 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(); +} diff --git a/src/types.zig b/src/types.zig new file mode 100644 index 0000000..ce5b2cf --- /dev/null +++ b/src/types.zig @@ -0,0 +1,157 @@ +const std = @import("std"); + +pub const Output = enum { tty, json, sqlite }; +pub const Level = enum { info, debug, @"error", warn }; +pub const Bool = enum { true, false }; + +pub const Config = struct { + namespace: []const u8, + all: bool, + sort: bool, + exclude: ?[]const []const u8 = null, + output: Output, + 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( + "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 { + name: []const u8, + namespace: []const u8, + creationTimestamp: []const u8, + resourceVersion: ?[]const u8, + + 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), + .creationTimestamp = try allocator.dupe(u8, self.creationTimestamp), + .resourceVersion = if (self.resourceVersion) |r| + try allocator.dupe(u8, r) + else + null, + }; + } + + pub fn deinit(self: @This(), 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 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: @This(), 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: @This(), 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 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: @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. + 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: @This(), allocator: std.mem.Allocator) void { + for (self.items) |item| { + item.deinit(allocator); + } + allocator.free(self.items); + } +}; diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 0000000..aca1c4f --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,5 @@ +[tool.towncrier] +name = "kubectlgetall" + +filename = "CHANGELOG.md" +directory = "changelog.d"