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.
@@ -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
@@ -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)
@@ -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