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
data/lib/walheim/config.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
5
|
|
|
6
6
|
module Walheim
|
|
7
7
|
# Configuration management for Walheim contexts
|
|
@@ -10,9 +10,9 @@ module Walheim
|
|
|
10
10
|
class ConfigError < StandardError; end
|
|
11
11
|
class ValidationError < ConfigError; end
|
|
12
12
|
|
|
13
|
-
DEFAULT_CONFIG_PATH = File.expand_path(
|
|
14
|
-
API_VERSION =
|
|
15
|
-
KIND =
|
|
13
|
+
DEFAULT_CONFIG_PATH = File.expand_path("~/.walheim/config")
|
|
14
|
+
API_VERSION = "walheim.io/v1"
|
|
15
|
+
KIND = "Config"
|
|
16
16
|
|
|
17
17
|
attr_reader :current_context, :contexts, :config_path
|
|
18
18
|
|
|
@@ -35,18 +35,18 @@ module Walheim
|
|
|
35
35
|
data = YAML.load_file(@config_path)
|
|
36
36
|
validate_schema!(data)
|
|
37
37
|
|
|
38
|
-
@current_context = data[
|
|
39
|
-
@contexts = data[
|
|
38
|
+
@current_context = data["currentContext"]
|
|
39
|
+
@contexts = data["contexts"].map do |ctx|
|
|
40
40
|
{
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
"name" => ctx["name"],
|
|
42
|
+
"dataDir" => expand_path(ctx["dataDir"])
|
|
43
43
|
}
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
validate_current_context!
|
|
47
47
|
rescue Psych::SyntaxError => e
|
|
48
48
|
raise ConfigError, "Invalid YAML in config file: #{e.message}"
|
|
49
|
-
rescue => e
|
|
49
|
+
rescue StandardError => e
|
|
50
50
|
raise ConfigError, "Failed to load config: #{e.message}"
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -56,13 +56,13 @@ module Walheim
|
|
|
56
56
|
# @raise [ConfigError] if file cannot be written
|
|
57
57
|
def save_config
|
|
58
58
|
data = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
"apiVersion" => API_VERSION,
|
|
60
|
+
"kind" => KIND,
|
|
61
|
+
"currentContext" => @current_context,
|
|
62
|
+
"contexts" => @contexts.map do |ctx|
|
|
63
63
|
{
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
"name" => ctx["name"],
|
|
65
|
+
"dataDir" => ctx["dataDir"]
|
|
66
66
|
}
|
|
67
67
|
end
|
|
68
68
|
}
|
|
@@ -74,7 +74,7 @@ module Walheim
|
|
|
74
74
|
temp_file = "#{@config_path}.tmp.#{Process.pid}"
|
|
75
75
|
File.write(temp_file, YAML.dump(data))
|
|
76
76
|
File.rename(temp_file, @config_path)
|
|
77
|
-
rescue => e
|
|
77
|
+
rescue StandardError => e
|
|
78
78
|
File.delete(temp_file) if temp_file && File.exist?(temp_file)
|
|
79
79
|
raise ConfigError, "Failed to save config: #{e.message}"
|
|
80
80
|
end
|
|
@@ -86,12 +86,12 @@ module Walheim
|
|
|
86
86
|
# @raise [ConfigError] if context not found or no current context
|
|
87
87
|
def data_dir(context_name = nil)
|
|
88
88
|
name = context_name || @current_context
|
|
89
|
-
raise ConfigError,
|
|
89
|
+
raise ConfigError, "No active context selected" if name.nil?
|
|
90
90
|
|
|
91
91
|
context = find_context(name)
|
|
92
92
|
raise ConfigError, "Context '#{name}' not found" if context.nil?
|
|
93
93
|
|
|
94
|
-
context[
|
|
94
|
+
context["dataDir"]
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# Add a new context
|
|
@@ -105,8 +105,8 @@ module Walheim
|
|
|
105
105
|
raise ValidationError, "Context '#{name}' already exists" if find_context(name)
|
|
106
106
|
|
|
107
107
|
@contexts << {
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
"name" => name,
|
|
109
|
+
"dataDir" => expand_path(data_dir)
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
@current_context = name if activate
|
|
@@ -134,6 +134,7 @@ module Walheim
|
|
|
134
134
|
# @raise [ConfigError] if context not found
|
|
135
135
|
def use_context(name)
|
|
136
136
|
raise ConfigError, "Context '#{name}' not found" unless find_context(name)
|
|
137
|
+
|
|
137
138
|
@current_context = name
|
|
138
139
|
end
|
|
139
140
|
|
|
@@ -142,7 +143,7 @@ module Walheim
|
|
|
142
143
|
# @return [Array<Hash>] Array of context hashes with 'name', 'dataDir', and 'active' keys
|
|
143
144
|
def list_contexts
|
|
144
145
|
@contexts.map do |ctx|
|
|
145
|
-
ctx.merge(
|
|
146
|
+
ctx.merge("active" => ctx["name"] == @current_context)
|
|
146
147
|
end
|
|
147
148
|
end
|
|
148
149
|
|
|
@@ -159,7 +160,8 @@ module Walheim
|
|
|
159
160
|
# Resolve the config file path with precedence: param > $WHCONFIG > default
|
|
160
161
|
def resolve_config_path(config_path)
|
|
161
162
|
return expand_path(config_path) if config_path
|
|
162
|
-
return expand_path(ENV[
|
|
163
|
+
return expand_path(ENV["WHCONFIG"]) if ENV["WHCONFIG"]
|
|
164
|
+
|
|
163
165
|
DEFAULT_CONFIG_PATH
|
|
164
166
|
end
|
|
165
167
|
|
|
@@ -170,27 +172,27 @@ module Walheim
|
|
|
170
172
|
|
|
171
173
|
# Find a context by name
|
|
172
174
|
def find_context(name)
|
|
173
|
-
@contexts.find { |ctx| ctx[
|
|
175
|
+
@contexts.find { |ctx| ctx["name"] == name }
|
|
174
176
|
end
|
|
175
177
|
|
|
176
178
|
# Validate config schema
|
|
177
179
|
def validate_schema!(data)
|
|
178
|
-
raise ValidationError,
|
|
179
|
-
raise ValidationError, "Invalid apiVersion: expected '#{API_VERSION}'" unless data[
|
|
180
|
-
raise ValidationError, "Invalid kind: expected '#{KIND}'" unless data[
|
|
181
|
-
raise ValidationError,
|
|
182
|
-
raise ValidationError,
|
|
183
|
-
raise ValidationError,
|
|
180
|
+
raise ValidationError, "Config must be a Hash" unless data.is_a?(Hash)
|
|
181
|
+
raise ValidationError, "Invalid apiVersion: expected '#{API_VERSION}'" unless data["apiVersion"] == API_VERSION
|
|
182
|
+
raise ValidationError, "Invalid kind: expected '#{KIND}'" unless data["kind"] == KIND
|
|
183
|
+
raise ValidationError, "Missing required field: contexts" unless data["contexts"]
|
|
184
|
+
raise ValidationError, "contexts must be an Array" unless data["contexts"].is_a?(Array)
|
|
185
|
+
raise ValidationError, "contexts array cannot be empty" if data["contexts"].empty?
|
|
184
186
|
|
|
185
187
|
# Validate each context
|
|
186
|
-
data[
|
|
188
|
+
data["contexts"].each_with_index do |ctx, index|
|
|
187
189
|
raise ValidationError, "Context at index #{index} must be a Hash" unless ctx.is_a?(Hash)
|
|
188
|
-
raise ValidationError, "Context at index #{index} missing 'name'" unless ctx[
|
|
189
|
-
raise ValidationError, "Context at index #{index} missing 'dataDir'" unless ctx[
|
|
190
|
+
raise ValidationError, "Context at index #{index} missing 'name'" unless ctx["name"]
|
|
191
|
+
raise ValidationError, "Context at index #{index} missing 'dataDir'" unless ctx["dataDir"]
|
|
190
192
|
end
|
|
191
193
|
|
|
192
194
|
# Check for duplicate context names
|
|
193
|
-
names = data[
|
|
195
|
+
names = data["contexts"].map { |ctx| ctx["name"] }
|
|
194
196
|
duplicates = names.select { |name| names.count(name) > 1 }.uniq
|
|
195
197
|
raise ValidationError, "Duplicate context names: #{duplicates.join(', ')}" unless duplicates.empty?
|
|
196
198
|
end
|
|
@@ -50,6 +50,32 @@ 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
|
+
handler_class.operation_info.each_key { |op| ops[op] = true } if handler_class.respond_to?(:operation_info)
|
|
61
|
+
end
|
|
62
|
+
ops.keys.sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get handlers supporting a specific operation
|
|
66
|
+
def handlers_for_operation(operation)
|
|
67
|
+
all_visible.select do |handler_info|
|
|
68
|
+
supports_operation?(handler_info[:name], operation)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if handler is cluster or namespaced resource
|
|
73
|
+
def cluster_resource?(kind)
|
|
74
|
+
handler_info = get(kind)
|
|
75
|
+
return false unless handler_info
|
|
76
|
+
|
|
77
|
+
handler_info[:handler] < Walheim::ClusterResource
|
|
78
|
+
end
|
|
53
79
|
end
|
|
54
80
|
end
|
|
55
81
|
end
|
|
@@ -1,19 +1,46 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "resource"
|
|
4
4
|
|
|
5
5
|
module Walheim
|
|
6
6
|
# NamespacedResource represents resources that are scoped to a namespace
|
|
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)
|
|
13
40
|
manifest_data = if manifest_source
|
|
14
|
-
|
|
41
|
+
File.read(manifest_source)
|
|
15
42
|
else
|
|
16
|
-
|
|
43
|
+
read_manifest_from_db(namespace, name)
|
|
17
44
|
end
|
|
18
45
|
|
|
19
46
|
if resource_exists?(namespace, name)
|
|
@@ -78,7 +105,7 @@ module Walheim
|
|
|
78
105
|
# Get single resource
|
|
79
106
|
get_single_resource(namespace, name)
|
|
80
107
|
else
|
|
81
|
-
raise ArgumentError,
|
|
108
|
+
raise ArgumentError, "namespace is required when name is specified"
|
|
82
109
|
end
|
|
83
110
|
end
|
|
84
111
|
|
|
@@ -92,7 +119,7 @@ module Walheim
|
|
|
92
119
|
|
|
93
120
|
manifest_path = File.join(resource_dir(namespace, name), manifest_filename)
|
|
94
121
|
manifest_content = File.read(manifest_path)
|
|
95
|
-
manifest_data = YAML.
|
|
122
|
+
manifest_data = YAML.safe_load(manifest_content)
|
|
96
123
|
|
|
97
124
|
# Compute summary fields
|
|
98
125
|
summary = {}
|
|
@@ -110,7 +137,7 @@ module Walheim
|
|
|
110
137
|
|
|
111
138
|
# Path to resource directory: {data_dir}/namespaces/{namespace}/{kind_plural}/{name}/
|
|
112
139
|
def resource_dir(namespace, name)
|
|
113
|
-
File.join(@data_dir,
|
|
140
|
+
File.join(@data_dir, "namespaces", namespace, self.class.kind_info[:plural], name)
|
|
114
141
|
end
|
|
115
142
|
|
|
116
143
|
def resource_exists?(namespace, name)
|
|
@@ -142,7 +169,7 @@ module Walheim
|
|
|
142
169
|
end
|
|
143
170
|
|
|
144
171
|
def list_single_namespace(namespace)
|
|
145
|
-
namespaces_dir = File.join(@data_dir,
|
|
172
|
+
namespaces_dir = File.join(@data_dir, "namespaces")
|
|
146
173
|
namespace_path = File.join(namespaces_dir, namespace)
|
|
147
174
|
|
|
148
175
|
unless Dir.exist?(namespace_path)
|
|
@@ -155,7 +182,10 @@ module Walheim
|
|
|
155
182
|
|
|
156
183
|
# Find all resource directories
|
|
157
184
|
resource_names = Dir.entries(resources_path)
|
|
158
|
-
.select
|
|
185
|
+
.select do |entry|
|
|
186
|
+
File.directory?(File.join(resources_path,
|
|
187
|
+
entry)) && !entry.start_with?(".")
|
|
188
|
+
end
|
|
159
189
|
.sort
|
|
160
190
|
|
|
161
191
|
# Return array of manifest hashes
|
|
@@ -165,13 +195,16 @@ module Walheim
|
|
|
165
195
|
end
|
|
166
196
|
|
|
167
197
|
def list_all_namespaces
|
|
168
|
-
namespaces_dir = File.join(@data_dir,
|
|
198
|
+
namespaces_dir = File.join(@data_dir, "namespaces")
|
|
169
199
|
return [] unless Dir.exist?(namespaces_dir)
|
|
170
200
|
|
|
171
201
|
# Find all namespaces
|
|
172
202
|
namespace_names = Dir.entries(namespaces_dir)
|
|
173
|
-
.select
|
|
174
|
-
|
|
203
|
+
.select do |entry|
|
|
204
|
+
File.directory?(File.join(namespaces_dir,
|
|
205
|
+
entry)) && !entry.start_with?(".")
|
|
206
|
+
end
|
|
207
|
+
.select { |entry| File.exist?(File.join(namespaces_dir, entry, ".namespace.yaml")) }
|
|
175
208
|
.sort
|
|
176
209
|
|
|
177
210
|
# Collect all resources from all namespaces
|
|
@@ -181,7 +214,10 @@ module Walheim
|
|
|
181
214
|
next unless Dir.exist?(resources_path)
|
|
182
215
|
|
|
183
216
|
resource_names = Dir.entries(resources_path)
|
|
184
|
-
.select
|
|
217
|
+
.select do |entry|
|
|
218
|
+
File.directory?(File.join(resources_path,
|
|
219
|
+
entry)) && !entry.start_with?(".")
|
|
220
|
+
end
|
|
185
221
|
.sort
|
|
186
222
|
|
|
187
223
|
resource_names.each do |resource_name|
|
|
@@ -189,7 +225,7 @@ module Walheim
|
|
|
189
225
|
end
|
|
190
226
|
end
|
|
191
227
|
|
|
192
|
-
all_resources.sort_by { |resource| [resource[:namespace], resource[:name]] }
|
|
228
|
+
all_resources.sort_by { |resource| [ resource[:namespace], resource[:name] ] }
|
|
193
229
|
end
|
|
194
230
|
end
|
|
195
231
|
end
|
data/lib/walheim/resource.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "terminal-table"
|
|
6
6
|
|
|
7
7
|
module Walheim
|
|
8
8
|
# Base Resource class containing common functionality for all resource types
|
|
@@ -18,7 +18,7 @@ module Walheim
|
|
|
18
18
|
|
|
19
19
|
# Metadata - must be overridden by subclasses
|
|
20
20
|
def self.kind_info
|
|
21
|
-
raise NotImplementedError,
|
|
21
|
+
raise NotImplementedError, "Subclass must implement kind_info"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Lifecycle hooks - can be overridden by subclasses
|
|
@@ -32,7 +32,7 @@ module Walheim
|
|
|
32
32
|
|
|
33
33
|
# Summary fields for get output - can be overridden by subclasses
|
|
34
34
|
def self.summary_fields
|
|
35
|
-
{}
|
|
35
|
+
{} # Default: no summary fields
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Operation metadata - defines how operations appear in help
|
|
@@ -40,20 +40,25 @@ module Walheim
|
|
|
40
40
|
def self.operation_info
|
|
41
41
|
{
|
|
42
42
|
get: {
|
|
43
|
-
description:
|
|
43
|
+
description: "List or retrieve resources",
|
|
44
44
|
usage: [
|
|
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
|
-
description:
|
|
52
|
-
usage: ["apply #{kind_info[:singular]} {name} -n {namespace}"]
|
|
52
|
+
description: "Create or update a resource",
|
|
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
|
-
description:
|
|
56
|
-
usage: ["delete #{kind_info[:singular]} {name} -n {namespace}"]
|
|
59
|
+
description: "Delete a resource",
|
|
60
|
+
usage: [ "delete #{kind_info[:singular]} {name} -n {namespace}" ],
|
|
61
|
+
options: {} # Subclasses will override
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
end
|