walheim 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/whctl +8 -695
- data/lib/walheim/cli/base_command.rb +171 -0
- data/lib/walheim/cli/helpers.rb +113 -0
- data/lib/walheim/cli/legacy_context.rb +206 -0
- data/lib/walheim/cli/resource_command.rb +53 -0
- data/lib/walheim/cli.rb +63 -0
- data/lib/walheim/cluster_resource.rb +5 -5
- data/lib/walheim/config.rb +36 -34
- data/lib/walheim/handler_registry.rb +26 -0
- data/lib/walheim/namespaced_resource.rb +49 -13
- data/lib/walheim/resource.rb +16 -11
- data/lib/walheim/resources/apps.rb +132 -113
- data/lib/walheim/resources/configmaps.rb +7 -7
- data/lib/walheim/resources/namespaces.rb +67 -10
- data/lib/walheim/resources/secrets.rb +9 -9
- data/lib/walheim/sync.rb +5 -5
- data/lib/walheim/version.rb +1 -1
- data/lib/walheim.rb +14 -11
- metadata +21 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "helpers"
|
|
4
|
+
|
|
5
|
+
module Walheim
|
|
6
|
+
module BaseCommand
|
|
7
|
+
def self.execute(operation:, kind:, name:, options:, parent_options:)
|
|
8
|
+
# 1. Resolve handler
|
|
9
|
+
handler_info = Walheim::HandlerRegistry.get(kind)
|
|
10
|
+
unless handler_info
|
|
11
|
+
warn "Error: unknown kind '#{kind}'"
|
|
12
|
+
warn ""
|
|
13
|
+
warn "Available kinds:"
|
|
14
|
+
Walheim::HandlerRegistry.all_visible.each { |h| warn " #{h[:name]}" }
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# 2. Check operation support
|
|
19
|
+
unless Walheim::HandlerRegistry.supports_operation?(kind, operation)
|
|
20
|
+
warn "Error: #{operation} not supported for #{kind}"
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 3. Resolve data directory from context
|
|
25
|
+
data_dir = resolve_data_dir(options, parent_options)
|
|
26
|
+
|
|
27
|
+
# 4. Initialize handler
|
|
28
|
+
handler = handler_info[:handler].new(data_dir: data_dir)
|
|
29
|
+
|
|
30
|
+
# 5. Validate namespace requirements
|
|
31
|
+
validate_namespace_options!(operation, kind, name, options) if handler.is_a?(Walheim::NamespacedResource)
|
|
32
|
+
|
|
33
|
+
# 6. Dispatch to handler
|
|
34
|
+
dispatch_to_handler(handler, operation, name, options, handler_info)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.resolve_data_dir(options, parent_options)
|
|
38
|
+
# Merge options
|
|
39
|
+
all_options = parent_options.merge(options)
|
|
40
|
+
|
|
41
|
+
# Try to load config
|
|
42
|
+
begin
|
|
43
|
+
config = Walheim::Config.new(config_path: all_options[:whconfig])
|
|
44
|
+
|
|
45
|
+
context_name = all_options[:context] || config.current_context
|
|
46
|
+
|
|
47
|
+
if context_name
|
|
48
|
+
config.data_dir(context_name)
|
|
49
|
+
elsif all_options[:data_dir]
|
|
50
|
+
warn "Warning: --data-dir is deprecated. Use contexts."
|
|
51
|
+
all_options[:data_dir]
|
|
52
|
+
else
|
|
53
|
+
warn "Error: No Walheim configuration found."
|
|
54
|
+
warn ""
|
|
55
|
+
warn "Create your first context:"
|
|
56
|
+
warn " whctl context new <name> --data-dir <path>"
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
rescue Walheim::Config::ConfigError, Walheim::Config::ValidationError
|
|
60
|
+
if all_options[:data_dir]
|
|
61
|
+
warn "Warning: --data-dir is deprecated."
|
|
62
|
+
all_options[:data_dir]
|
|
63
|
+
else
|
|
64
|
+
warn "Error: No Walheim configuration found."
|
|
65
|
+
warn ""
|
|
66
|
+
warn "Create your first context:"
|
|
67
|
+
warn " whctl context new <name> --data-dir <path>"
|
|
68
|
+
exit 1
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.validate_namespace_options!(operation, kind, _name, options)
|
|
74
|
+
# Operations that require namespace or --all
|
|
75
|
+
requires_namespace = %i[get apply delete start pause stop logs import]
|
|
76
|
+
return unless requires_namespace.include?(operation)
|
|
77
|
+
|
|
78
|
+
# get can use --all
|
|
79
|
+
if operation == :get
|
|
80
|
+
return if options[:all] || options[:namespace]
|
|
81
|
+
|
|
82
|
+
warn "Error: either -n {namespace} or --all/-A flag is required"
|
|
83
|
+
warn "Usage: whctl get #{kind} -n {namespace}"
|
|
84
|
+
warn "Usage: whctl get #{kind} --all"
|
|
85
|
+
exit 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Other operations require namespace
|
|
89
|
+
return if options[:namespace]
|
|
90
|
+
|
|
91
|
+
warn "Error: -n {namespace} is required"
|
|
92
|
+
warn "Usage: whctl #{operation} #{kind} {name} -n {namespace}"
|
|
93
|
+
exit 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.dispatch_to_handler(handler, operation, name, options, handler_info)
|
|
97
|
+
case operation
|
|
98
|
+
when :get
|
|
99
|
+
dispatch_get(handler, name, options, handler_info)
|
|
100
|
+
when :apply
|
|
101
|
+
dispatch_apply(handler, name, options, handler_info)
|
|
102
|
+
when :delete
|
|
103
|
+
handler.delete(namespace: options[:namespace], name: name)
|
|
104
|
+
when :create
|
|
105
|
+
# Special case: create namespace
|
|
106
|
+
handler.create(name: name, username: options[:username], hostname: options[:hostname])
|
|
107
|
+
when :import
|
|
108
|
+
# Special case: import app
|
|
109
|
+
compose_manifest = Walheim::Helpers.read_yaml_input(options[:file])
|
|
110
|
+
handler.import(namespace: options[:namespace], name: name, compose_manifest: compose_manifest)
|
|
111
|
+
when :start, :pause, :stop
|
|
112
|
+
handler.send(operation, namespace: options[:namespace], name: name)
|
|
113
|
+
when :logs
|
|
114
|
+
log_opts = {}
|
|
115
|
+
log_opts[:follow] = options[:follow] if options[:follow]
|
|
116
|
+
log_opts[:tail] = options[:tail] if options[:tail]
|
|
117
|
+
log_opts[:timestamps] = options[:timestamps] if options[:timestamps]
|
|
118
|
+
handler.logs(namespace: options[:namespace], name: name, **log_opts)
|
|
119
|
+
else
|
|
120
|
+
warn "Error: operation #{operation} not implemented"
|
|
121
|
+
exit 1
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def self.dispatch_get(handler, name, options, handler_info)
|
|
126
|
+
if handler.is_a?(Walheim::ClusterResource)
|
|
127
|
+
result = handler.get(name: name)
|
|
128
|
+
Walheim::Helpers.print_cluster_resources_table(result, handler_info[:name])
|
|
129
|
+
else
|
|
130
|
+
result = if options[:all]
|
|
131
|
+
handler.get(namespace: nil, name: nil)
|
|
132
|
+
else
|
|
133
|
+
handler.get(namespace: options[:namespace], name: name)
|
|
134
|
+
end
|
|
135
|
+
Walheim::Helpers.print_resources_table(result, options[:all], handler_info[:name])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.dispatch_apply(handler, name, options, _handler_info)
|
|
140
|
+
# Extract from manifest if -f provided
|
|
141
|
+
if options[:file]
|
|
142
|
+
manifest_data = Walheim::Helpers.read_yaml_input(options[:file])
|
|
143
|
+
|
|
144
|
+
if handler.is_a?(Walheim::NamespacedResource)
|
|
145
|
+
namespace = manifest_data["metadata"]["namespace"]
|
|
146
|
+
name = manifest_data["metadata"]["name"]
|
|
147
|
+
|
|
148
|
+
unless namespace && name
|
|
149
|
+
warn "Error: Manifest must contain metadata.namespace and metadata.name"
|
|
150
|
+
exit 1
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
handler.apply(namespace: namespace, name: name, manifest_source: options[:file])
|
|
154
|
+
else
|
|
155
|
+
# Cluster resource
|
|
156
|
+
name = manifest_data["metadata"]["name"]
|
|
157
|
+
unless name
|
|
158
|
+
warn "Error: Manifest must contain metadata.name"
|
|
159
|
+
exit 1
|
|
160
|
+
end
|
|
161
|
+
handler.apply(name: name, manifest_source: options[:file])
|
|
162
|
+
end
|
|
163
|
+
elsif handler.is_a?(Walheim::NamespacedResource)
|
|
164
|
+
# Apply from existing manifest in data dir
|
|
165
|
+
handler.apply(namespace: options[:namespace], name: name)
|
|
166
|
+
else
|
|
167
|
+
handler.apply(name: name)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "terminal-table"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Walheim
|
|
7
|
+
module Helpers
|
|
8
|
+
# Print resources table for namespaced resources
|
|
9
|
+
def self.print_resources_table(result, all_namespaces, kind_name)
|
|
10
|
+
if result.is_a?(Hash)
|
|
11
|
+
# Single resource - print YAML
|
|
12
|
+
puts YAML.dump(result[:manifest])
|
|
13
|
+
return
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if result.empty?
|
|
17
|
+
puts all_namespaces ? "No #{kind_name} found" : "No #{kind_name} found in namespace"
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Extract summary field names from first result
|
|
22
|
+
summary_fields = result.first[:summary].keys
|
|
23
|
+
|
|
24
|
+
# Build header row
|
|
25
|
+
headers = if all_namespaces
|
|
26
|
+
%w[NAMESPACE NAME] + summary_fields.map(&:to_s).map(&:upcase)
|
|
27
|
+
else
|
|
28
|
+
[ "NAME" ] + summary_fields.map(&:to_s).map(&:upcase)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Build data rows
|
|
32
|
+
rows = result.map do |resource|
|
|
33
|
+
row = if all_namespaces
|
|
34
|
+
[ resource[:namespace], resource[:name] ]
|
|
35
|
+
else
|
|
36
|
+
[ resource[:name] ]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Add summary field values
|
|
40
|
+
summary_values = summary_fields.map { |field| resource[:summary][field] || "N/A" }
|
|
41
|
+
row + summary_values
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
all_rows = [ headers ] + rows
|
|
45
|
+
|
|
46
|
+
table = Terminal::Table.new do |t|
|
|
47
|
+
t.rows = all_rows
|
|
48
|
+
t.style = {
|
|
49
|
+
border_x: "", border_y: "", border_i: "",
|
|
50
|
+
padding_left: 0, padding_right: 3,
|
|
51
|
+
border_top: false, border_bottom: false,
|
|
52
|
+
all_separators: false
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
puts table
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Print resources table for cluster resources
|
|
60
|
+
def self.print_cluster_resources_table(result, kind_name)
|
|
61
|
+
if result.is_a?(Hash)
|
|
62
|
+
# Single resource - print YAML
|
|
63
|
+
puts YAML.dump(result[:manifest])
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if result.empty?
|
|
68
|
+
puts "No #{kind_name} found"
|
|
69
|
+
return
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Extract summary field names from first result
|
|
73
|
+
summary_fields = result.first[:summary].keys
|
|
74
|
+
|
|
75
|
+
# Build header row (no NAMESPACE column for cluster resources)
|
|
76
|
+
headers = [ "NAME" ] + summary_fields.map(&:to_s).map(&:upcase)
|
|
77
|
+
|
|
78
|
+
# Build data rows
|
|
79
|
+
rows = result.map do |resource|
|
|
80
|
+
row = [ resource[:name] ]
|
|
81
|
+
|
|
82
|
+
# Add summary field values
|
|
83
|
+
summary_values = summary_fields.map { |field| resource[:summary][field] || "N/A" }
|
|
84
|
+
row + summary_values
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
all_rows = [ headers ] + rows
|
|
88
|
+
|
|
89
|
+
table = Terminal::Table.new do |t|
|
|
90
|
+
t.rows = all_rows
|
|
91
|
+
t.style = {
|
|
92
|
+
border_x: "", border_y: "", border_i: "",
|
|
93
|
+
padding_left: 0, padding_right: 3,
|
|
94
|
+
border_top: false, border_bottom: false,
|
|
95
|
+
all_separators: false
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
puts table
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Read YAML from file or stdin
|
|
103
|
+
def self.read_yaml_input(file_path)
|
|
104
|
+
if file_path == "-"
|
|
105
|
+
# Read from stdin
|
|
106
|
+
YAML.safe_load($stdin.read)
|
|
107
|
+
else
|
|
108
|
+
# Read from file
|
|
109
|
+
YAML.load_file(file_path)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "terminal-table"
|
|
5
|
+
|
|
6
|
+
module Walheim
|
|
7
|
+
module LegacyContext
|
|
8
|
+
def self.execute(argv)
|
|
9
|
+
options = {}
|
|
10
|
+
|
|
11
|
+
# Remove 'context' from argv since it's already been identified
|
|
12
|
+
argv.shift if argv[0] == "context"
|
|
13
|
+
|
|
14
|
+
# Parse global flags
|
|
15
|
+
global_parser = OptionParser.new do |opts|
|
|
16
|
+
opts.on("--whconfig PATH", "Alternate config file path") { |v| options[:whconfig] = v }
|
|
17
|
+
opts.on("-d DIR", "--data-dir DIR", "Data directory") { |v| options[:data_dir] = v }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Parse global flags (use parse! to consume all flags)
|
|
21
|
+
begin
|
|
22
|
+
global_parser.parse!(argv)
|
|
23
|
+
rescue OptionParser::InvalidOption => e
|
|
24
|
+
warn "Error: #{e.message}"
|
|
25
|
+
exit 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get subcommand and context name (now argv only contains non-flag arguments)
|
|
29
|
+
subcommand = argv[0]
|
|
30
|
+
context_name = argv[1]
|
|
31
|
+
|
|
32
|
+
case subcommand
|
|
33
|
+
when "new"
|
|
34
|
+
# whctl context new {name} --data-dir {path}
|
|
35
|
+
unless context_name
|
|
36
|
+
warn "Error: context name is required"
|
|
37
|
+
warn "Usage: whctl context new {name} --data-dir {path}"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
unless options[:data_dir]
|
|
42
|
+
warn "Error: --data-dir flag is required"
|
|
43
|
+
warn "Usage: whctl context new {name} --data-dir {path}"
|
|
44
|
+
exit 1
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Validate data_dir exists
|
|
48
|
+
expanded_data_dir = File.expand_path(options[:data_dir])
|
|
49
|
+
unless Dir.exist?(expanded_data_dir)
|
|
50
|
+
warn "Error: data directory '#{expanded_data_dir}' does not exist"
|
|
51
|
+
warn ""
|
|
52
|
+
warn "Please create the directory first or provide a valid path"
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check for namespaces subdirectory
|
|
57
|
+
namespaces_path = File.join(expanded_data_dir, "namespaces")
|
|
58
|
+
unless Dir.exist?(namespaces_path)
|
|
59
|
+
warn "Warning: data directory does not contain 'namespaces' subdirectory"
|
|
60
|
+
warn "Expected path: #{namespaces_path}"
|
|
61
|
+
warn ""
|
|
62
|
+
warn "This directory should contain your homelab configuration."
|
|
63
|
+
warn "Creating namespaces directory..."
|
|
64
|
+
Dir.mkdir(namespaces_path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Load or create config
|
|
68
|
+
config = Walheim::Config.new(config_path: options[:whconfig])
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
config.add_context(context_name, expanded_data_dir, activate: true)
|
|
72
|
+
config.save_config
|
|
73
|
+
puts "Created context '#{context_name}'"
|
|
74
|
+
puts " Data directory: #{expanded_data_dir}"
|
|
75
|
+
puts " Status: Active (automatically activated)"
|
|
76
|
+
rescue Walheim::Config::ValidationError => e
|
|
77
|
+
warn "Error: #{e.message}"
|
|
78
|
+
exit 1
|
|
79
|
+
rescue Walheim::Config::ConfigError => e
|
|
80
|
+
warn "Error: #{e.message}"
|
|
81
|
+
exit 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
when "list"
|
|
85
|
+
# whctl context list
|
|
86
|
+
config = Walheim::Config.new(config_path: options[:whconfig])
|
|
87
|
+
|
|
88
|
+
unless Walheim::Config.exists?(config_path: options[:whconfig])
|
|
89
|
+
warn "No Walheim configuration found."
|
|
90
|
+
warn ""
|
|
91
|
+
warn "Create your first context:"
|
|
92
|
+
warn " whctl context new <name> --data-dir <path>"
|
|
93
|
+
exit 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
contexts = config.list_contexts
|
|
97
|
+
if contexts.empty?
|
|
98
|
+
puts "No contexts configured."
|
|
99
|
+
exit 0
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
rows = contexts.map do |ctx|
|
|
103
|
+
active_marker = ctx["active"] ? "*" : ""
|
|
104
|
+
[ active_marker, ctx["name"], ctx["dataDir"] ]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
table = Terminal::Table.new do |t|
|
|
108
|
+
t.headings = [ "CURRENT", "NAME", "DATA DIRECTORY" ]
|
|
109
|
+
t.rows = rows
|
|
110
|
+
t.style = {
|
|
111
|
+
border_x: "", border_y: "", border_i: "",
|
|
112
|
+
padding_left: 0, padding_right: 3,
|
|
113
|
+
border_top: false, border_bottom: false,
|
|
114
|
+
all_separators: false
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
puts table
|
|
119
|
+
|
|
120
|
+
when "use"
|
|
121
|
+
# whctl context use {name}
|
|
122
|
+
unless context_name
|
|
123
|
+
warn "Error: context name is required"
|
|
124
|
+
warn "Usage: whctl context use {name}"
|
|
125
|
+
exit 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
config = Walheim::Config.new(config_path: options[:whconfig])
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
config.use_context(context_name)
|
|
132
|
+
config.save_config
|
|
133
|
+
puts "Switched to context '#{context_name}'"
|
|
134
|
+
rescue Walheim::Config::ConfigError => e
|
|
135
|
+
warn "Error: #{e.message}"
|
|
136
|
+
exit 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
when "current"
|
|
140
|
+
# whctl context current
|
|
141
|
+
config = Walheim::Config.new(config_path: options[:whconfig])
|
|
142
|
+
|
|
143
|
+
unless Walheim::Config.exists?(config_path: options[:whconfig])
|
|
144
|
+
warn "No Walheim configuration found."
|
|
145
|
+
warn ""
|
|
146
|
+
warn "Create your first context:"
|
|
147
|
+
warn " whctl context new <name> --data-dir <path>"
|
|
148
|
+
exit 1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if config.current_context
|
|
152
|
+
puts "Current context: #{config.current_context}"
|
|
153
|
+
puts "Data directory: #{config.data_dir}"
|
|
154
|
+
else
|
|
155
|
+
puts "No active context selected."
|
|
156
|
+
puts ""
|
|
157
|
+
puts "Available contexts:"
|
|
158
|
+
config.list_contexts.each do |ctx|
|
|
159
|
+
puts " - #{ctx['name']}"
|
|
160
|
+
end
|
|
161
|
+
puts ""
|
|
162
|
+
puts "Select a context:"
|
|
163
|
+
puts " whctl context use <context-name>"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
when "delete"
|
|
167
|
+
# whctl context delete {name}
|
|
168
|
+
unless context_name
|
|
169
|
+
warn "Error: context name is required"
|
|
170
|
+
warn "Usage: whctl context delete {name}"
|
|
171
|
+
exit 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
config = Walheim::Config.new(config_path: options[:whconfig])
|
|
175
|
+
|
|
176
|
+
begin
|
|
177
|
+
config.delete_context(context_name)
|
|
178
|
+
config.save_config
|
|
179
|
+
puts "Deleted context '#{context_name}'"
|
|
180
|
+
|
|
181
|
+
# Show warning if this was the active context
|
|
182
|
+
unless config.current_context
|
|
183
|
+
puts ""
|
|
184
|
+
puts "Note: This was your active context."
|
|
185
|
+
puts "Select a new context with:"
|
|
186
|
+
puts " whctl context use <context-name>"
|
|
187
|
+
end
|
|
188
|
+
rescue Walheim::Config::ConfigError => e
|
|
189
|
+
warn "Error: #{e.message}"
|
|
190
|
+
exit 1
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
else
|
|
194
|
+
warn "Error: unknown context subcommand '#{subcommand}'"
|
|
195
|
+
warn ""
|
|
196
|
+
warn "Available context commands:"
|
|
197
|
+
warn " whctl context new {name} --data-dir {path}"
|
|
198
|
+
warn " whctl context list"
|
|
199
|
+
warn " whctl context use {name}"
|
|
200
|
+
warn " whctl context current"
|
|
201
|
+
warn " whctl context delete {name}"
|
|
202
|
+
exit 1
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_command"
|
|
4
|
+
|
|
5
|
+
module Walheim
|
|
6
|
+
module ResourceCommand
|
|
7
|
+
def self.register_operation(cli_class, operation)
|
|
8
|
+
# Get handlers supporting this operation
|
|
9
|
+
handlers = Walheim::HandlerRegistry.handlers_for_operation(operation)
|
|
10
|
+
return if handlers.empty?
|
|
11
|
+
|
|
12
|
+
# Build command description
|
|
13
|
+
descriptions = handlers.map { |h| h[:name] }.join(", ")
|
|
14
|
+
desc_text = "#{operation.to_s.capitalize} resources (#{descriptions})"
|
|
15
|
+
|
|
16
|
+
# Define Thor command
|
|
17
|
+
cli_class.desc "#{operation} KIND [NAME]", desc_text
|
|
18
|
+
|
|
19
|
+
# Add operation-specific options from handler metadata
|
|
20
|
+
# Collect all unique options across handlers for this operation
|
|
21
|
+
all_options = {}
|
|
22
|
+
handlers.each do |handler_info|
|
|
23
|
+
handler_class = handler_info[:handler]
|
|
24
|
+
next unless handler_class.respond_to?(:operation_info)
|
|
25
|
+
|
|
26
|
+
op_metadata = handler_class.operation_info[operation]
|
|
27
|
+
all_options.merge!(op_metadata[:options]) if op_metadata && op_metadata[:options]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Register options with Thor
|
|
31
|
+
all_options.each do |opt_name, opt_config|
|
|
32
|
+
# Thor expects method_option calls
|
|
33
|
+
# We'll need to call this dynamically
|
|
34
|
+
cli_class.method_option opt_name, opt_config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Define command method
|
|
38
|
+
cli_class.define_method(operation) do |kind, name = nil|
|
|
39
|
+
BaseCommand.execute(
|
|
40
|
+
operation: operation,
|
|
41
|
+
kind: kind,
|
|
42
|
+
name: name,
|
|
43
|
+
options: options,
|
|
44
|
+
parent_options: self.class.class_options.transform_keys(&:to_sym).transform_values do |v|
|
|
45
|
+
options[v.name]
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/walheim/cli.rb
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "cli/helpers"
|
|
5
|
+
require_relative "cli/base_command"
|
|
6
|
+
require_relative "cli/resource_command"
|
|
7
|
+
|
|
8
|
+
module Walheim
|
|
9
|
+
class CLI < Thor
|
|
10
|
+
# Global flags
|
|
11
|
+
class_option :context,
|
|
12
|
+
type: :string,
|
|
13
|
+
desc: "Override active context"
|
|
14
|
+
|
|
15
|
+
class_option :whconfig,
|
|
16
|
+
type: :string,
|
|
17
|
+
desc: "Alternate config file path"
|
|
18
|
+
|
|
19
|
+
class_option :data_dir,
|
|
20
|
+
type: :string,
|
|
21
|
+
aliases: [ :d ],
|
|
22
|
+
desc: "Data directory (deprecated: use contexts)"
|
|
23
|
+
|
|
24
|
+
# Dynamically register operations
|
|
25
|
+
def self.register_operations
|
|
26
|
+
Walheim::HandlerRegistry.all_operations.each do |operation|
|
|
27
|
+
ResourceCommand.register_operation(self, operation)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Version command
|
|
32
|
+
desc "version", "Show version"
|
|
33
|
+
def version
|
|
34
|
+
puts "whctl version #{Walheim::VERSION}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Override help to maintain kubectl-style help
|
|
38
|
+
def self.help(shell, subcommand = false)
|
|
39
|
+
list = printable_commands(true, subcommand)
|
|
40
|
+
Thor::Util.thor_classes_in(self).each do |klass|
|
|
41
|
+
list += klass.printable_commands(false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Group commands by category
|
|
45
|
+
shell.say "Usage: whctl [global flags] <command> [arguments]"
|
|
46
|
+
shell.say ""
|
|
47
|
+
shell.say "Global flags:"
|
|
48
|
+
shell.say " --context CONTEXT Override active context for this command"
|
|
49
|
+
shell.say " --whconfig PATH Use alternate config file (default: ~/.walheim/config)"
|
|
50
|
+
shell.say " -d, --data-dir DIR Data directory containing namespaces (deprecated: use contexts)"
|
|
51
|
+
shell.say ""
|
|
52
|
+
shell.say "Available commands:"
|
|
53
|
+
shell.say ""
|
|
54
|
+
|
|
55
|
+
# Print commands
|
|
56
|
+
list.each do |command|
|
|
57
|
+
next if command[0] == "help"
|
|
58
|
+
|
|
59
|
+
shell.say " #{command[0].ljust(30)} #{command[1]}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "resource"
|
|
4
4
|
|
|
5
5
|
module Walheim
|
|
6
6
|
# ClusterResource represents resources that are cluster-scoped (not namespaced)
|
|
@@ -69,9 +69,9 @@ module Walheim
|
|
|
69
69
|
# Apply operation - create or update
|
|
70
70
|
def apply(name:, manifest_source: nil)
|
|
71
71
|
manifest_data = if manifest_source
|
|
72
|
-
|
|
72
|
+
File.read(manifest_source)
|
|
73
73
|
else
|
|
74
|
-
|
|
74
|
+
read_manifest_from_db(name)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
if resource_exists?(name)
|
|
@@ -91,7 +91,7 @@ module Walheim
|
|
|
91
91
|
|
|
92
92
|
manifest_path = File.join(resource_dir(name), manifest_filename)
|
|
93
93
|
manifest_content = File.read(manifest_path)
|
|
94
|
-
manifest_data = YAML.
|
|
94
|
+
manifest_data = YAML.safe_load(manifest_content)
|
|
95
95
|
|
|
96
96
|
# Compute summary fields
|
|
97
97
|
summary = {}
|
|
@@ -123,7 +123,7 @@ module Walheim
|
|
|
123
123
|
def find_resource_names
|
|
124
124
|
base_dir = File.join(@data_dir, self.class.kind_info[:plural])
|
|
125
125
|
Dir.entries(base_dir)
|
|
126
|
-
.select { |entry| File.directory?(File.join(base_dir, entry)) && !entry.start_with?(
|
|
126
|
+
.select { |entry| File.directory?(File.join(base_dir, entry)) && !entry.start_with?(".") }
|
|
127
127
|
.select { |entry| File.exist?(File.join(base_dir, entry, manifest_filename)) }
|
|
128
128
|
.sort
|
|
129
129
|
end
|