walheim 0.1.2 → 0.2.0
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 +176 -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 +51 -0
- data/lib/walheim/cli.rb +62 -0
- data/lib/walheim/handler_registry.rb +27 -0
- data/lib/walheim/namespaced_resource.rb +27 -0
- data/lib/walheim/resource.rb +8 -3
- data/lib/walheim/resources/apps.rb +24 -7
- data/lib/walheim/resources/namespaces.rb +57 -0
- data/lib/walheim/version.rb +1 -1
- data/lib/walheim.rb +3 -0
- metadata +20 -1
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
if handler.is_a?(Walheim::NamespacedResource)
|
|
32
|
+
validate_namespace_options!(operation, kind, name, options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# 6. Dispatch to handler
|
|
36
|
+
dispatch_to_handler(handler, operation, name, options, handler_info)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def self.resolve_data_dir(options, parent_options)
|
|
42
|
+
# Merge options
|
|
43
|
+
all_options = parent_options.merge(options)
|
|
44
|
+
|
|
45
|
+
# Try to load config
|
|
46
|
+
begin
|
|
47
|
+
config = Walheim::Config.new(config_path: all_options[:whconfig])
|
|
48
|
+
|
|
49
|
+
context_name = all_options[:context] || config.current_context
|
|
50
|
+
|
|
51
|
+
if context_name
|
|
52
|
+
config.data_dir(context_name)
|
|
53
|
+
elsif all_options[:data_dir]
|
|
54
|
+
warn 'Warning: --data-dir is deprecated. Use contexts.'
|
|
55
|
+
all_options[:data_dir]
|
|
56
|
+
else
|
|
57
|
+
warn 'Error: No Walheim configuration found.'
|
|
58
|
+
warn ''
|
|
59
|
+
warn 'Create your first context:'
|
|
60
|
+
warn ' whctl context new <name> --data-dir <path>'
|
|
61
|
+
exit 1
|
|
62
|
+
end
|
|
63
|
+
rescue Walheim::Config::ConfigError, Walheim::Config::ValidationError
|
|
64
|
+
if all_options[:data_dir]
|
|
65
|
+
warn 'Warning: --data-dir is deprecated.'
|
|
66
|
+
all_options[:data_dir]
|
|
67
|
+
else
|
|
68
|
+
warn 'Error: No Walheim configuration found.'
|
|
69
|
+
warn ''
|
|
70
|
+
warn 'Create your first context:'
|
|
71
|
+
warn ' whctl context new <name> --data-dir <path>'
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.validate_namespace_options!(operation, kind, name, options)
|
|
78
|
+
# Operations that require namespace or --all
|
|
79
|
+
requires_namespace = [:get, :apply, :delete, :start, :pause, :stop, :logs, :import]
|
|
80
|
+
return unless requires_namespace.include?(operation)
|
|
81
|
+
|
|
82
|
+
# get can use --all
|
|
83
|
+
if operation == :get
|
|
84
|
+
return if options[:all] || options[:namespace]
|
|
85
|
+
warn 'Error: either -n {namespace} or --all/-A flag is required'
|
|
86
|
+
warn "Usage: whctl get #{kind} -n {namespace}"
|
|
87
|
+
warn "Usage: whctl get #{kind} --all"
|
|
88
|
+
exit 1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Other operations require namespace
|
|
92
|
+
unless options[:namespace]
|
|
93
|
+
warn 'Error: -n {namespace} is required'
|
|
94
|
+
warn "Usage: whctl #{operation} #{kind} {name} -n {namespace}"
|
|
95
|
+
exit 1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.dispatch_to_handler(handler, operation, name, options, handler_info)
|
|
100
|
+
case operation
|
|
101
|
+
when :get
|
|
102
|
+
dispatch_get(handler, name, options, handler_info)
|
|
103
|
+
when :apply
|
|
104
|
+
dispatch_apply(handler, name, options, handler_info)
|
|
105
|
+
when :delete
|
|
106
|
+
handler.delete(namespace: options[:namespace], name: name)
|
|
107
|
+
when :create
|
|
108
|
+
# Special case: create namespace
|
|
109
|
+
handler.create(name: name, username: options[:username], hostname: options[:hostname])
|
|
110
|
+
when :import
|
|
111
|
+
# Special case: import app
|
|
112
|
+
compose_manifest = Walheim::Helpers.read_yaml_input(options[:file])
|
|
113
|
+
handler.import(namespace: options[:namespace], name: name, compose_manifest: compose_manifest)
|
|
114
|
+
when :start, :pause, :stop
|
|
115
|
+
handler.send(operation, namespace: options[:namespace], name: name)
|
|
116
|
+
when :logs
|
|
117
|
+
log_opts = {}
|
|
118
|
+
log_opts[:follow] = options[:follow] if options[:follow]
|
|
119
|
+
log_opts[:tail] = options[:tail] if options[:tail]
|
|
120
|
+
log_opts[:timestamps] = options[:timestamps] if options[:timestamps]
|
|
121
|
+
handler.logs(namespace: options[:namespace], name: name, **log_opts)
|
|
122
|
+
else
|
|
123
|
+
warn "Error: operation #{operation} not implemented"
|
|
124
|
+
exit 1
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.dispatch_get(handler, name, options, handler_info)
|
|
129
|
+
if handler.is_a?(Walheim::ClusterResource)
|
|
130
|
+
result = handler.get(name: name)
|
|
131
|
+
Walheim::Helpers.print_cluster_resources_table(result, handler_info[:name])
|
|
132
|
+
else
|
|
133
|
+
result = if options[:all]
|
|
134
|
+
handler.get(namespace: nil, name: nil)
|
|
135
|
+
else
|
|
136
|
+
handler.get(namespace: options[:namespace], name: name)
|
|
137
|
+
end
|
|
138
|
+
Walheim::Helpers.print_resources_table(result, options[:all], handler_info[:name])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.dispatch_apply(handler, name, options, handler_info)
|
|
143
|
+
# Extract from manifest if -f provided
|
|
144
|
+
if options[:file]
|
|
145
|
+
manifest_data = Walheim::Helpers.read_yaml_input(options[:file])
|
|
146
|
+
|
|
147
|
+
if handler.is_a?(Walheim::NamespacedResource)
|
|
148
|
+
namespace = manifest_data['metadata']['namespace']
|
|
149
|
+
name = manifest_data['metadata']['name']
|
|
150
|
+
|
|
151
|
+
unless namespace && name
|
|
152
|
+
warn 'Error: Manifest must contain metadata.namespace and metadata.name'
|
|
153
|
+
exit 1
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
handler.apply(namespace: namespace, name: name, manifest_source: options[:file])
|
|
157
|
+
else
|
|
158
|
+
# Cluster resource
|
|
159
|
+
name = manifest_data['metadata']['name']
|
|
160
|
+
unless name
|
|
161
|
+
warn 'Error: Manifest must contain metadata.name'
|
|
162
|
+
exit 1
|
|
163
|
+
end
|
|
164
|
+
handler.apply(name: name, manifest_source: options[:file])
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
# Apply from existing manifest in data dir
|
|
168
|
+
if handler.is_a?(Walheim::NamespacedResource)
|
|
169
|
+
handler.apply(namespace: options[:namespace], name: name)
|
|
170
|
+
else
|
|
171
|
+
handler.apply(name: name)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
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
|
+
if all_namespaces
|
|
26
|
+
headers = ['NAMESPACE', 'NAME'] + summary_fields.map(&:to_s).map(&:upcase)
|
|
27
|
+
else
|
|
28
|
+
headers = ['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.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,51 @@
|
|
|
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
|
+
if handler_class.respond_to?(:operation_info)
|
|
25
|
+
op_metadata = handler_class.operation_info[operation]
|
|
26
|
+
if op_metadata && op_metadata[:options]
|
|
27
|
+
all_options.merge!(op_metadata[:options])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Register options with Thor
|
|
33
|
+
all_options.each do |opt_name, opt_config|
|
|
34
|
+
# Thor expects method_option calls
|
|
35
|
+
# We'll need to call this dynamically
|
|
36
|
+
cli_class.method_option opt_name, opt_config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Define command method
|
|
40
|
+
cli_class.define_method(operation) do |kind, name = nil|
|
|
41
|
+
BaseCommand.execute(
|
|
42
|
+
operation: operation,
|
|
43
|
+
kind: kind,
|
|
44
|
+
name: name,
|
|
45
|
+
options: options,
|
|
46
|
+
parent_options: self.class.class_options.transform_keys(&:to_sym).transform_values { |v| options[v.name] rescue nil }
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/walheim/cli.rb
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
shell.say " #{command[0].ljust(30)} #{command[1]}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -50,6 +50,33 @@ module Walheim
|
|
|
50
50
|
handler_class = handler_info[:handler]
|
|
51
51
|
handler_class.public_instance_methods.include?(operation.to_sym)
|
|
52
52
|
end
|
|
53
|
+
|
|
54
|
+
# Get all unique operations across all handlers
|
|
55
|
+
def all_operations
|
|
56
|
+
ops = {}
|
|
57
|
+
handlers.values.uniq { |h| h[:handler] }.each do |handler_info|
|
|
58
|
+
handler_class = handler_info[:handler]
|
|
59
|
+
# Get operations from operation_info metadata
|
|
60
|
+
if handler_class.respond_to?(:operation_info)
|
|
61
|
+
handler_class.operation_info.keys.each { |op| ops[op] = true }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
ops.keys.sort
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get handlers supporting a specific operation
|
|
68
|
+
def handlers_for_operation(operation)
|
|
69
|
+
all_visible.select do |handler_info|
|
|
70
|
+
supports_operation?(handler_info[:name], operation)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if handler is cluster or namespaced resource
|
|
75
|
+
def cluster_resource?(kind)
|
|
76
|
+
handler_info = get(kind)
|
|
77
|
+
return false unless handler_info
|
|
78
|
+
handler_info[:handler] < Walheim::ClusterResource
|
|
79
|
+
end
|
|
53
80
|
end
|
|
54
81
|
end
|
|
55
82
|
end
|
|
@@ -7,6 +7,33 @@ module Walheim
|
|
|
7
7
|
# Examples: Apps, Secrets, ConfigMaps
|
|
8
8
|
# These resources live under namespaces/{namespace}/{kind}/{name}/
|
|
9
9
|
class NamespacedResource < Resource
|
|
10
|
+
# Override operation_info to add namespace flags
|
|
11
|
+
def self.operation_info
|
|
12
|
+
ops = super
|
|
13
|
+
|
|
14
|
+
# Add namespace flags to all operations
|
|
15
|
+
namespace_options = {
|
|
16
|
+
namespace: {
|
|
17
|
+
type: :string,
|
|
18
|
+
aliases: [:n],
|
|
19
|
+
desc: 'Target namespace',
|
|
20
|
+
required: false # Validated at runtime
|
|
21
|
+
},
|
|
22
|
+
all: {
|
|
23
|
+
type: :boolean,
|
|
24
|
+
aliases: [:A],
|
|
25
|
+
desc: 'All namespaces',
|
|
26
|
+
banner: '' # No value needed for boolean
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ops[:get][:options].merge!(namespace_options)
|
|
31
|
+
ops[:apply][:options].merge!(namespace: namespace_options[:namespace])
|
|
32
|
+
ops[:delete][:options].merge!(namespace: namespace_options[:namespace])
|
|
33
|
+
|
|
34
|
+
ops
|
|
35
|
+
end
|
|
36
|
+
|
|
10
37
|
# CRUD operations for namespace-scoped resources
|
|
11
38
|
|
|
12
39
|
def apply(namespace:, name:, manifest_source: nil)
|
data/lib/walheim/resource.rb
CHANGED
|
@@ -45,15 +45,20 @@ module Walheim
|
|
|
45
45
|
"get #{kind_info[:plural]} -n {namespace}",
|
|
46
46
|
"get #{kind_info[:plural]} --all/-A",
|
|
47
47
|
"get #{kind_info[:singular]} {name} -n {namespace}"
|
|
48
|
-
]
|
|
48
|
+
],
|
|
49
|
+
options: {} # Subclasses will override
|
|
49
50
|
},
|
|
50
51
|
apply: {
|
|
51
52
|
description: 'Create or update a resource',
|
|
52
|
-
usage: ["apply #{kind_info[:singular]} {name} -n {namespace}"]
|
|
53
|
+
usage: ["apply #{kind_info[:singular]} {name} -n {namespace}"],
|
|
54
|
+
options: {
|
|
55
|
+
file: { type: :string, aliases: [:f], desc: 'Manifest file (use - for stdin)' }
|
|
56
|
+
}
|
|
53
57
|
},
|
|
54
58
|
delete: {
|
|
55
59
|
description: 'Delete a resource',
|
|
56
|
-
usage: ["delete #{kind_info[:singular]} {name} -n {namespace}"]
|
|
60
|
+
usage: ["delete #{kind_info[:singular]} {name} -n {namespace}"],
|
|
61
|
+
options: {} # Subclasses will override
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
end
|