diff --git a/.changes/unreleased/Changed-20260628-133837.yaml b/.changes/unreleased/Changed-20260628-133837.yaml new file mode 100644 index 0000000..855deaa --- /dev/null +++ b/.changes/unreleased/Changed-20260628-133837.yaml @@ -0,0 +1,3 @@ +kind: Changed +body: Improve CLI help output. +time: 2026-06-28T13:38:37.410920814+01:00 diff --git a/.changes/unreleased/Fixed-20260627-150100.yaml b/.changes/unreleased/Fixed-20260627-150100.yaml new file mode 100644 index 0000000..3c8660a --- /dev/null +++ b/.changes/unreleased/Fixed-20260627-150100.yaml @@ -0,0 +1,3 @@ +kind: Fixed +body: 'The json output was not valid, items would have an extra }. ' +time: 2026-06-27T15:01:00.473603684+01:00 diff --git a/README.md b/README.md index 96c502d..feb4a05 100644 --- a/README.md +++ b/README.md @@ -32,37 +32,19 @@ kubectlgetall -n There are some flags that can be passed. ```shell -kubectlgetall --help - -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. - +USAGE: + kubectlgetall [FLAGS] + +COMMANDS: +get Get list of resource on cluster. +diff Show the resources Added, Updated and Removed + from a cluster by comparing two labels defined in the database. + +GLOBAL FLAGS: +-h, --help Display this help and exit. +--version Display version, and exit. +--log-level Set the log level. All logs are saved to file. + Possible values are (debug, info, warn, error). Default level is warn. ``` ## Dev diff --git a/src/diff.zig b/src/diff.zig index 4b484ce..ce726d6 100644 --- a/src/diff.zig +++ b/src/diff.zig @@ -4,9 +4,9 @@ const logging = @import("log.zig"); const db = @import("database.zig"); const table = @import("table.zig"); const types = @import("types.zig"); +const help = @import("help.zig"); pub fn diffMain(io: std.Io, gpa: std.mem.Allocator, iter: *std.process.Args.Iterator) !void { - const params = comptime clap.parseParamsComptime( \\-h, --help Display this help and exit. \\-d, --database Path to SQLite database to load data from. @@ -37,8 +37,10 @@ pub fn diffMain(io: std.Io, gpa: std.mem.Allocator, iter: *std.process.Args.Iter }; defer res.deinit(); - if (res.args.help != 0) - return clap.helpToFile(io, .stdout(), clap.Help, ¶ms, .{}); + if (res.args.help != 0) { + try help.diff(io, .stdout()); + std.process.exit(0); + } if (res.args.@"log-level") |l| { logging.setLogLevel(l); diff --git a/src/get.zig b/src/get.zig index a2baf59..19fabc0 100644 --- a/src/get.zig +++ b/src/get.zig @@ -7,9 +7,9 @@ const logging = @import("log.zig"); const types = @import("types.zig"); const table = @import("table.zig"); const utils = @import("utils.zig"); +const help = @import("help.zig"); pub fn getMain(io: std.Io, gpa: std.mem.Allocator, iter: *std.process.Args.Iterator) !void { - var stdout_buf: [4096]u8 = undefined; var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buf); const stdout = &stdout_writer.interface; @@ -57,8 +57,10 @@ pub fn getMain(io: std.Io, gpa: std.mem.Allocator, iter: *std.process.Args.Itera var label: []const u8 = &[_]u8{}; var exclude: ?[]const []const u8 = null; - if (res.args.help != 0) - return clap.helpToFile(io, .stdout(), clap.Help, ¶ms, .{}); + if (res.args.help != 0) { + try help.get(io, .stdout()); + std.process.exit(0); + } if (res.args.namespace) |n| { namespace = n; @@ -256,16 +258,18 @@ fn compareStrings(_: void, lhs: []const u8, rhs: []const u8) bool { return std.mem.lessThan(u8, lhs, rhs); } -fn getCrdList(io: std.Io, allocator: std.mem.Allocator) !std.ArrayList([]const u8) { +fn getCrdList(io: std.Io, gpa: std.mem.Allocator) !std.ArrayList([]const u8) { const cmd = [_][]const u8{ "kubectl", "api-resources", "--verbs=list", "--namespaced", "-o", "name" }; - const result = std.process.run(allocator, io, .{ + const result = std.process.run(gpa, io, .{ .argv = &cmd, }) catch |err| { std.log.err("Failed to run kubectl: {}", .{err}); return err; }; - defer allocator.free(result.stdout); - defer allocator.free(result.stderr); + defer { + gpa.free(result.stdout); + gpa.free(result.stderr); + } if (result.term.exited != 0) { std.log.debug("stdout: {s}. stderr: {s}", .{ result.stdout, result.stderr }); @@ -274,17 +278,17 @@ fn getCrdList(io: std.Io, allocator: std.mem.Allocator) !std.ArrayList([]const u var lines: std.ArrayList([]const u8) = .empty; errdefer { for (lines.items) |item| { - allocator.free(item); + gpa.free(item); } - lines.deinit(allocator); + lines.deinit(gpa); } 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); + const owned = try gpa.dupe(u8, line); + errdefer gpa.free(owned); + try lines.append(gpa, owned); } } diff --git a/src/help.zig b/src/help.zig new file mode 100644 index 0000000..b7e645f --- /dev/null +++ b/src/help.zig @@ -0,0 +1,94 @@ +//! Provides help strings for the commands defined in the application. +//! The help documentation is manually formatted. + +const std = @import("std"); + +const main_msg = + \\USAGE: + \\ kubectlgetall [FLAGS] + \\ + \\COMMANDS: + \\get Get list of resource on cluster. + \\diff Show the resources Added, Updated and Removed + \\ from a cluster by comparing two labels defined in the database. + \\ + \\GLOBAL FLAGS: + \\-h, --help Display this help and exit. + \\--version Display version, and exit. + \\--log-level Set the log level. All logs are saved to file. + \\ Possible values are (debug, info, warn, error). Default level is warn. + \\ +; + +const get_msg = + \\USAGE: + \\ kubectlgetall get [FLAGS] + \\ + \\Get list of resource on a cluster. + \\ + \\REQUIREMENTS: + \\kubectl must be installed and available on PATH. + \\An active connection to a Kubernetes cluster is required. + \\ + \\FLAGS: + \\-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. + \\ Specifying --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. [default: tty, tty|json|sqlite] + \\-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. + \\ +; + +const diff_msg = + \\USAGE: + \\ kubectlgetall diff [FLAGS] + \\ + \\Show the resources Added, Updated and Removed + \\from a cluster by comparing two labels defined in the database. + \\ + \\REQUIREMENTS: + \\A SQLite database populated by the get command. See: kubectlgetall get --help + \\ + \\ARGUMENTS: + \\BASE The older label. Serves as the baseline for comparison. + \\HEAD The newer label. Compared against BASE to identify what has changed. + \\ + \\Items in HEAD but not BASE are reported as new. + \\Items in BASE but not HEAD are reported as removed. + \\Items in both but differing in value are reported as updated. + \\ + \\FLAGS: + \\-h, --help Display this help and exit. + \\-d, --database Path to SQLite database to load data from. + \\-e, --exclude ... Exclude resource types. Multiple can be excluded eg: "-e -e " + \\-o, --output Changes the output format of the results. [default: tty, tty|json] + \\ +; + +fn print( + io: std.Io, + file: std.Io.File, + msg: []const u8, +) !void { + var buf: [1024]u8 = undefined; + var writer = file.writer(io, &buf); + try writer.interface.writeAll(msg); + return writer.interface.flush(); +} + +pub fn main(io: std.Io, file: std.Io.File) !void { + return print(io, file, main_msg); +} + +pub fn get(io: std.Io, file: std.Io.File) !void { + return print(io, file, get_msg); +} + +pub fn diff(io: std.Io, file: std.Io.File) !void { + return print(io, file, diff_msg); +} diff --git a/src/main.zig b/src/main.zig index dfda6ae..bfc3ee6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,7 @@ const arg = @import("args.zig"); const diff = @import("diff.zig"); const get = @import("get.zig"); const logging = @import("log.zig"); +const help = @import("help.zig"); pub const std_options: std.Options = .{ .log_level = .debug, @@ -32,11 +33,18 @@ pub fn main(init: std.process.Init) !void { }; defer res.deinit(); - if (res.args.help != 0) - return clap.helpToFile(init.io, .stdout(), clap.Help, &arg.main_params, .{}); + if (res.args.help != 0) { + try help.main(init.io, .stdout()); + std.process.exit(0); + } if (res.args.version != 0) { - std.log.info("{s}, {s}", .{ build_options.name, build_options.version }); + const version = try std.fmt.allocPrint(init.gpa, "{s}, {s}\n", .{ build_options.name, build_options.version }); + defer init.gpa.free(version); + var buf: [1024]u8 = undefined; + var writer = std.Io.File.stdout().writer(init.io, &buf); + try writer.interface.writeAll(version); + try writer.interface.flush(); std.process.exit(0); } @@ -45,7 +53,7 @@ pub fn main(init: std.process.Init) !void { } const command = res.positionals[0] orelse { - try clap.helpToFile(init.io, .stdout(), clap.Help, &arg.main_params, .{}); + try help.main(init.io, .stdout()); return error.MissingCommand; }; switch (command) { diff --git a/src/types.zig b/src/types.zig index 957fdc9..f56555f 100644 --- a/src/types.zig +++ b/src/types.zig @@ -74,21 +74,21 @@ pub const Metadata = struct { resourceVersion: ?[]const u8 = null, generation: ?u64 = null, - pub fn clone(self: @This(), allocator: std.mem.Allocator) !Metadata { + pub fn clone(self: @This(), gpa: 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, + .name = try gpa.dupe(u8, self.name), + .namespace = try gpa.dupe(u8, self.namespace), + .creationTimestamp = try gpa.dupe(u8, self.creationTimestamp), + .resourceVersion = if (self.resourceVersion) |r| try gpa.dupe(u8, r) else null, .generation = self.generation, }; } - 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 fn deinit(self: @This(), gpa: std.mem.Allocator) void { + gpa.free(self.namespace); + gpa.free(self.name); + gpa.free(self.creationTimestamp); + if (self.resourceVersion) |r| gpa.free(r); } }; @@ -97,35 +97,35 @@ 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); + pub fn toJson(self: @This(), gpa: std.mem.Allocator) ![]const u8 { + var buffer = try std.ArrayList(u8).initCapacity(gpa, 256); + defer buffer.deinit(gpa); - const initial_string = try std.fmt.allocPrint(allocator, "{{\"kind\": \"{s}\", \"apiVersion\": \"{s}\", \"name\": \"{s}\", \"namespace\": \"{s}\", \"creationTimestamp\": \"{s}\"", .{ + const initial_string = try std.fmt.allocPrint(gpa, "{{\"kind\": \"{s}\", \"apiVersion\": \"{s}\", \"name\": \"{s}\", \"namespace\": \"{s}\", \"creationTimestamp\": \"{s}\"", .{ self.kind, self.apiVersion, self.metadata.name, self.metadata.namespace, self.metadata.creationTimestamp, }); - try buffer.appendSlice(allocator, initial_string); - defer allocator.free(initial_string); + try buffer.appendSlice(gpa, initial_string); + defer gpa.free(initial_string); if (self.metadata.resourceVersion) |version| { - const resource_version = try std.fmt.allocPrint(allocator, ", \"resourceVersion\": \"{s}\"", .{version}); - defer allocator.free(resource_version); - try buffer.appendSlice(allocator, resource_version); + const resource_version = try std.fmt.allocPrint(gpa, ", \"resourceVersion\": \"{s}\"", .{version}); + defer gpa.free(resource_version); + try buffer.appendSlice(gpa, resource_version); } if (self.metadata.generation) |generation| { - const generation_str = try std.fmt.allocPrint(allocator, ", \"generation\": {}", .{generation}); - defer allocator.free(generation_str); - try buffer.appendSlice(allocator, generation_str); + const generation_str = try std.fmt.allocPrint(gpa, ", \"generation\": {}", .{generation}); + defer gpa.free(generation_str); + try buffer.appendSlice(gpa, generation_str); } - try buffer.appendSlice(allocator, "}}"); + try buffer.append(gpa, '}'); - return try allocator.dupe(u8, buffer.items); + return try gpa.dupe(u8, buffer.items); } pub fn clone(self: @This(), allocator: std.mem.Allocator) !Resource { @@ -146,39 +146,39 @@ 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); + pub fn toJson(self: @This(), gpa: std.mem.Allocator) ![]const u8 { + var buffer = try std.ArrayList(u8).initCapacity(gpa, 256); + defer buffer.deinit(gpa); - try buffer.append(allocator, '['); + try buffer.append(gpa, '['); for (self.items, 0..) |item, i| { - const text = try item.toJson(allocator); - defer allocator.free(text); + const text = try item.toJson(gpa); + defer gpa.free(text); - try buffer.appendSlice(allocator, text); + try buffer.appendSlice(gpa, text); if (i < self.items.len - 1) { - try buffer.append(allocator, ','); + try buffer.append(gpa, ','); } } - try buffer.append(allocator, ']'); + try buffer.append(gpa, ']'); - return try allocator.dupe(u8, buffer.items); + return try gpa.dupe(u8, buffer.items); } - pub fn clone(self: @This(), allocator: std.mem.Allocator) !ResourceList { - var new_items = try allocator.alloc(Resource, self.items.len); + pub fn clone(self: @This(), gpa: std.mem.Allocator) !ResourceList { + var new_items = try gpa.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); + for (new_items[0..initialized]) |it| it.deinit(gpa); + gpa.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); + new_items[i] = try item.clone(gpa); initialized += 1; }