Class: Pvectl::Config::Provider

Inherits:
Object
  • Object
show all
Defined in:
lib/pvectl/config/provider.rb,
sig/pvectl/config/provider.rbs

Overview

Loads and resolves configuration from multiple sources.

Provider handles loading configuration from YAML files and environment variables, then merging them with proper priority to produce a final ResolvedConfig. Priority order (highest to lowest):

  1. CLI options
  2. Environment variables
  3. Configuration file

Examples:

Loading configuration

provider = Provider.new
config = provider.resolve(
  config_path: "~/.pvectl/config",
  cli_options: { context: "production" }
)

Constant Summary collapse

ENV_VARS =

Mapping of environment variables to configuration keys

{
  "PROXMOX_HOST" => :server,
  "PROXMOX_TOKEN_ID" => :token_id,
  "PROXMOX_TOKEN_SECRET" => :token_secret,
  "PROXMOX_USER" => :username,
  "PROXMOX_PASSWORD" => :password,
  "PROXMOX_VERIFY_SSL" => :verify_ssl,
  "PVECTL_CONTEXT" => :context,
  "PVECTL_CONFIG" => :config_path,
  # Retry/timeout settings
  "PROXMOX_TIMEOUT" => :timeout,
  "PROXMOX_RETRY_COUNT" => :retry_count,
  "PROXMOX_RETRY_DELAY" => :retry_delay,
  "PROXMOX_MAX_RETRY_DELAY" => :max_retry_delay,
  "PROXMOX_RETRY_WRITES" => :retry_writes
}.freeze
INTEGER_VARS =

Keys that should be parsed as integers

i[timeout retry_count retry_delay max_retry_delay].freeze
BOOLEAN_VARS =

Keys that should be parsed as booleans

i[verify_ssl retry_writes].freeze

Instance Method Summary collapse

Instance Method Details

#build_resolved_config(context:, cluster:, user:, env_config:, cli_options:) ⇒ Models::ResolvedConfig

Builds a ResolvedConfig from merged sources.



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'lib/pvectl/config/provider.rb', line 238

def build_resolved_config(context:, cluster:, user:, env_config:, cli_options:)
  # Merge with priority: CLI > ENV > file
  server = env_config[:server] || cluster.server
  verify_ssl = env_config.key?(:verify_ssl) ? env_config[:verify_ssl] : cluster.verify_ssl

  # Determine auth type and credentials
  auth_type, token_id, token_secret, username, password = resolve_auth(user, env_config)

  # Resolve retry/timeout settings with priority: ENV > file
  timeout = env_config[:timeout] || cluster.timeout
  retry_count = env_config[:retry_count] || cluster.retry_count
  retry_delay = env_config[:retry_delay] || cluster.retry_delay
  max_retry_delay = env_config[:max_retry_delay] || cluster.max_retry_delay
  retry_writes = env_config.key?(:retry_writes) ? env_config[:retry_writes] : cluster.retry_writes

  Models::ResolvedConfig.new(
    context_name: context.name,
    server: server,
    verify_ssl: verify_ssl,
    certificate_authority: cluster.certificate_authority,
    auth_type: auth_type,
    token_id: token_id,
    token_secret: token_secret,
    username: username,
    password: password,
    default_node: context.default_node,
    timeout: timeout,
    retry_count: retry_count,
    retry_delay: retry_delay,
    max_retry_delay: max_retry_delay,
    retry_writes: retry_writes
  )
end

#file_exists?(path) ⇒ Boolean

Checks if a configuration file exists at the given path.



52
53
54
# File 'lib/pvectl/config/provider.rb', line 52

def file_exists?(path)
  File.exist?(path)
end

#find_cluster(config, name) ⇒ Models::Cluster

Finds a cluster by name in the configuration.

Raises:



206
207
208
209
210
211
212
213
# File 'lib/pvectl/config/provider.rb', line 206

def find_cluster(config, name)
  clusters = config["clusters"] || []
  cluster_hash = clusters.find { |c| c["name"] == name }

  raise ClusterNotFoundError, "Cluster '#{name}' not found in configuration" if cluster_hash.nil?

  Models::Cluster.from_hash(cluster_hash)
end

#find_context(config, name) ⇒ Models::Context

Finds a context by name in the configuration.

Raises:



188
189
190
191
192
193
194
195
196
197
198
# File 'lib/pvectl/config/provider.rb', line 188

def find_context(config, name)
  contexts = config["contexts"] || []
  context_hash = contexts.find { |c| c["name"] == name }

  if context_hash.nil?
    available = contexts.map { |c| c["name"] }.join(", ")
    raise ContextNotFoundError, "Context '#{name}' not found. Available: #{available}"
  end

  Models::Context.from_hash(context_hash)
end

#find_user(config, name) ⇒ Models::User

Finds a user by name in the configuration.

Raises:



221
222
223
224
225
226
227
228
# File 'lib/pvectl/config/provider.rb', line 221

def find_user(config, name)
  users = config["users"] || []
  user_hash = users.find { |u| u["name"] == name }

  raise UserNotFoundError, "User '#{name}' not found in configuration" if user_hash.nil?

  Models::User.from_hash(user_hash)
end

#insecure_permissions?(path) ⇒ Boolean

Checks if a file has insecure permissions (readable by group/others).



60
61
62
63
64
65
# File 'lib/pvectl/config/provider.rb', line 60

def insecure_permissions?(path)
  return false unless File.exist?(path)

  mode = File.stat(path).mode & 0o777
  (mode & 0o077) != 0
end

#load_envHash

Loads configuration from environment variables.

Raises:



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/pvectl/config/provider.rb', line 86

def load_env
  result = {}

  ENV_VARS.each do |env_var, config_key|
    value = ENV[env_var]
    next if value.nil? || value.empty?

    result[config_key] = parse_env_value(config_key, value)
  end

  result
end

#load_file(path) ⇒ Hash

Loads configuration from a YAML file.

Raises:



73
74
75
76
77
78
79
80
# File 'lib/pvectl/config/provider.rb', line 73

def load_file(path)
  raise ConfigNotFoundError, "Configuration file not found: #{path}" unless File.exist?(path)

  content = File.read(path)
  YAML.safe_load(content, permitted_classes: [Symbol])
rescue Psych::SyntaxError => e
  raise InvalidConfigError, "Invalid YAML in #{path}: #{e.message}"
end

#parse_boolean_env(value) ⇒ Boolean

Parses a boolean environment variable.



162
163
164
# File 'lib/pvectl/config/provider.rb', line 162

def parse_boolean_env(value)
  %w[true 1 yes].include?(value.to_s.downcase)
end

#parse_env_value(key, value) ⇒ Object

Parses an environment variable value to the appropriate type.

Raises:



148
149
150
151
152
153
154
155
156
# File 'lib/pvectl/config/provider.rb', line 148

def parse_env_value(key, value)
  if BOOLEAN_VARS.include?(key)
    parse_boolean_env(value)
  elsif INTEGER_VARS.include?(key)
    parse_integer_env(key, value)
  else
    value
  end
end

#parse_integer_env(key, value) ⇒ Integer

Parses an integer environment variable with validation.

Raises:



172
173
174
175
176
177
178
179
180
# File 'lib/pvectl/config/provider.rb', line 172

def parse_integer_env(key, value)
  unless value.to_s.match?(/\A\d+\z/)
    raise InvalidConfigError,
          "Invalid integer for #{key.to_s.tr('_', '-')}: '#{value}' " \
          "(must be a non-negative integer)"
  end

  value.to_i
end

#resolve(config_path:, cli_options:, cluster_override: nil) ⇒ Models::ResolvedConfig

Resolves full configuration by merging all sources.

Raises:



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/pvectl/config/provider.rb', line 120

def resolve(config_path:, cli_options:, cluster_override: nil)
  file_config = load_file(config_path)
  env_config = load_env

  context_name = resolve_context_name(cli_options: cli_options, file_config: file_config)
  context = find_context(file_config, context_name)

  cluster_name = cluster_override || context.cluster_ref
  cluster = find_cluster(file_config, cluster_name)
  user = find_user(file_config, context.user_ref)

  build_resolved_config(
    context: context,
    cluster: cluster,
    user: user,
    env_config: env_config,
    cli_options: cli_options
  )
end

#resolve_auth(user, env_config) ⇒ Array

Resolves authentication type and credentials.



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
# File 'lib/pvectl/config/provider.rb', line 277

def resolve_auth(user, env_config)
  # ENV token auth takes precedence
  if env_config[:token_id] && env_config[:token_secret]
    return [:token, env_config[:token_id], env_config[:token_secret], nil, nil]
  end

  # ENV password auth
  if env_config[:username] && env_config[:password]
    return [:password, nil, nil, env_config[:username], env_config[:password]]
  end

  # Fall back to user from config
  if user.token_auth?
    [:token, user.token_id, user.token_secret, nil, nil]
  else
    [:password, nil, nil, user.username, user.password]
  end
end

#resolve_context_name(cli_options:, file_config: {}) ⇒ String?

Resolves the context name from various sources.

Priority: CLI > ENV > file



106
107
108
# File 'lib/pvectl/config/provider.rb', line 106

def resolve_context_name(cli_options:, file_config: {})
  cli_options[:context] || ENV["PVECTL_CONTEXT"] || file_config["current-context"]
end