Class: OpenC3::SettingModel

Inherits:
Model show all
Defined in:
lib/openc3/models/setting_model.rb

Constant Summary collapse

PRIMARY_KEY =
'openc3__settings'
SETTING_ENV_PREFIX =

One environment variable per setting: OPENC3_SETTING_TIME_ZONE=UTC sets the 'time_zone' setting. A prefix scan rather than an enumerated list, so a setting added by a later release needs no change here. Deliberately not a single JSON blob variable: embedding JSON in a compose environment list, a Helm value and an ECS task definition each need different escaping, and one variable per setting reads the same in all three.

'OPENC3_SETTING_'
OVERWRITE_ENV_VAR =

Set to true to write the values on every init rather than only when the setting is missing. Use when the environment is the source of truth and Admin Console edits should not survive a restart.

Unlike the OPENC3_NO_* install flags, this is NOT enabled by presence: it discards whatever an operator configured in the Admin Console, so OPENC3_SETTINGS_OVERWRITE=false and =0 mean off, as they read. An unrecognized value is an error rather than a silent guess.

Note this does not start with SETTING_ENV_PREFIX, so the prefix scan can't mistake it for a setting named 'overwrite'.

'OPENC3_SETTINGS_OVERWRITE'
ALLOW_UNKNOWN_ENV_VAR =

Set to true to allow setting names that are not in KNOWN_SETTINGS. Needed for a setting added by a newer tool than this library knows about; without it a misspelled name is reported and skipped.

'OPENC3_SETTINGS_ALLOW_UNKNOWN'
STRICT_ENV_VAR =

Set to true to make a rejected setting fail init instead of being reported and skipped. Off by default: the init container restarts on failure, so a typo in a cosmetic setting would otherwise crash loop COSMOS with the cause buried in restarting container logs, and the operator ends up with no COSMOS rather than COSMOS with one default time zone. Turn it on for a deployment that would rather not come up than come up misconfigured.

initsettings --dry-run fails regardless - a preflight check exists to.

'OPENC3_SETTINGS_STRICT'
KNOWN_SETTINGS =

Every setting that can be seeded from the environment.

An unknown name is reported and skipped rather than written: nothing reads it, so the result of a typo is a dead Redis key plus a setting the operator believes they configured and did not. Init still continues - ALLOW_UNKNOWN_ENV_VAR opts out of the check entirely.

TO ADD A SETTING, add a row here. The name is the string the Admin Console component passes to loadSetting/saveSetting - find it in openc3-cosmos-init/plugins/packages/openc3-vue-common/src/tools/admin/ tabs/settings/Settings.vue, e.g. TimeZoneSettings.vue has const settingName = 'time_zone'. Then:

type:   what the reader SAVES and expects back, not what a control
      displays. Storing the wrong shape is silent - the reader either
      throws or ignores the value:
        :string     plain text (a URL, a subtitle)
        :boolean    real true/false; the frontend treats the string
                    "false" as truthy, so these must not be text
        :json_text  JSON kept as a String. A component that calls
                    JSON.stringify before saveSetting and JSON.parse
                    in parseSetting is this - handing it a parsed
                    object makes its own JSON.parse throw
        :json       JSON parsed into an object before storing, for a
                    reader that checks the shape. AiChatConfig.load
                    does `raw['data'].is_a?(Hash) ? ... : {}`, so text
                    would be silently discarded
values: the allowed values, or nil for free text.
      Copy them from the component's v-select items.
require_keys: for :json/:json_text, top-level keys the blob must have.
      Use when a partial blob would break the reader rather than just
      fall back to a default. See 'system_health' below.
example: for :json/:json_text, a valid value. There is no other way for
      an operator to discover the shape of a blob short of reading the
      component source, so every JSON setting needs one. Copy it from
      the JSON.stringify({...}) in the component's save method.

Example, for a hypothetical LogLevelSettings.vue holding 'log_level':

'log_level' => { type: :string, values: ['DEBUG', 'INFO', 'WARN'] },

No other change is needed - the env var (OPENC3_SETTING_LOG_LEVEL), the validation and the cli initsettings --help listing all follow.

{
  # Booleans. The frontend treats the string "false" as truthy, so these
  # have to reach Redis as real booleans.
  'ai_chat' => { type: :boolean, values: nil },
  'news_feed' => { type: :boolean, values: nil },
  'script_runner_locking' => { type: :boolean, values: nil },
  'script_runner_lifecycle' => { type: :boolean, values: nil }, # Enterprise only
  # Fixed choice strings
  'time_zone' => { type: :string, values: ['local', 'UTC'] },
  'time_format' => { type: :string, values: ['ampm', '24hr'] },
  'theme' => { type: :string, values: ['cosmosDark', 'cosmosDarkCobalt', 'cosmosDarkIndigo',
                                       'cosmosDarkSlate', 'cosmosDarkEmerald'] },
  # Free text
  'subtitle' => { type: :string, values: nil },
  'source_url' => { type: :string, values: nil },
  'rubygems_url' => { type: :string, values: nil },
  'pypi_url' => { type: :string, values: nil },
  # JSON *text*: these components JSON.stringify before saving and
  # JSON.parse on load, so the stored value is a String, not an object
  'astro' => { type: :json_text, values: nil,
               example: '{"hideClock":false}' },
  # require_keys because the Admin Console writes all five together and both
  # readers assume that: a missing height reaches the stylesheet as
  # "height: undefinedpx", which is invalid CSS, so the banner renders at
  # auto height top AND bottom instead of staying hidden.
  'classification_banner' => { type: :json_text, values: nil,
                               require_keys: ['text', 'fontColor', 'backgroundColor',
                                              'topHeight', 'bottomHeight'],
                               example: '{"text":"UNCLASSIFIED","fontColor":"#ffffff",' \
                                        '"backgroundColor":"#00cc00","topHeight":20,"bottomHeight":0}' },
  'context_tag' => { type: :json_text, values: nil,
                     example: '{"text":"DEV","fontColor":"#ffffff","backgroundColor":"#ff0000"}' },
  # Settings with no Admin Console tab of their own - see NO_ADMIN_TAB below
  #
  # Written as JSON text by ScopeModel#seed_database and re-read by the
  # Enterprise metrics microservices. require_keys because a partial blob
  # doesn't degrade gracefully: log_thresholds does
  # data['global']['enableAlerts'] and passes data[metric_name] straight to
  # check_persistent_threshold, so a blob missing either key raises rather
  # than falling back, silently ending CPU/memory/disk alerting.
  'system_health' => { type: :json_text, values: nil,
                       require_keys: ['cpu', 'memory', 'disk', 'global'],
                       example: '{"cpu":{"redThreshold":90.0,"yellowThreshold":80.0,"snoozeMinutes":15,' \
                                '"sustainedSeconds":15},"memory":{"redThreshold":90.0,' \
                                '"yellowThreshold":80.0,"snoozeMinutes":15,"sustainedSeconds":15},' \
                                '"disk":{"redThreshold":90.0,"yellowThreshold":80.0,' \
                                '"snoozeMinutes":720,"sustainedSeconds":60},' \
                                '"global":{"enableAlerts":true}}' },
  # Enterprise AI chat provider/model config. :json, not :json_text -
  # AiChatConfig.load ignores a String and falls back to {}
  'ai_chat_config' => { type: :json, values: nil,
                        example: '{"provider":"anthropic","model":"claude-opus-4-7","base_url":""}' },
}
NO_ADMIN_TAB =

Settings that KNOWN_SETTINGS lists on purpose despite having no *Settings.vue tab in this repo. The drift specs compare KNOWN_SETTINGS against those components, so without this list adding either of these would fail the "doesn't list a setting the Admin Console no longer has" check. Both are real settings that code reads.

['system_health', 'ai_chat_config']
SEEDED_PRIMARY_KEY =

Provenance for the values this seeder wrote, so a later init can tell an untouched setting from one an operator changed in the Admin Console. Without it the seeder has two bad options: never update (so editing the env var does nothing) or always update (so Admin Console edits silently revert on every restart).

'openc3__settings_seeded'
MAX_VALUE_BYTES =

Largest value we will write. Settings are read into every browser tab, so this is a guard against an env var that is a file by mistake rather than a real limit - the biggest real setting is a few hundred bytes.

64 * 1024

Instance Attribute Summary

Attributes inherited from Model

#name, #plugin, #scope, #updated_at

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model

#check_disable_erb, #create, #deploy, #destroy, #destroyed?, #diff, filter, find_all_by_plugin, from_json, get_all_models, get_model, handle_config, set, store, store_queued, #undeploy, #update

Constructor Details

#initialize(name:, scope: nil, data:) ⇒ SettingModel

Returns a new instance of SettingModel.



582
583
584
585
# File 'lib/openc3/models/setting_model.rb', line 582

def initialize(name:, scope: nil, data:)
  super(PRIMARY_KEY, name: name, scope: scope)
  @data = data
end

Class Method Details

.all(scope: nil) ⇒ Object

NOSONAR - scope: is part of the caller-facing signature



177
178
179
# File 'lib/openc3/models/setting_model.rb', line 177

def self.all(scope: nil) # NOSONAR - scope: is part of the caller-facing signature
  super(PRIMARY_KEY)
end

.apply_defaults(env: ENV, overwrite: nil, dry_run: false, strict: nil) ⇒ Array<String>

Seed settings from OPENC3_SETTING_ environment variables. Called by openc3cli initsettings during init container startup.

By default a setting is only written when it does not already exist, so a value changed in the Admin Console survives a container restart. Set OPENC3_SETTINGS_OVERWRITE to write on every run instead.

Nothing here aborts init by default. A bad setting name, a bad value, or a malformed control variable is reported on stdout and that one setting is skipped, leaving COSMOS on its built-in default. Set STRICT_ENV_VAR to fail init instead; --dry-run always fails.

Parameters:

  • (defaults to: ENV)

    environment to read from, defaults to ENV

  • (defaults to: nil)

    nil reads OVERWRITE_ENV_VAR from env

  • (defaults to: nil)

    nil reads STRICT_ENV_VAR from env

  • (defaults to: false)

    report what would happen and write nothing. Reports every problem rather than aborting on the first, and works before Redis is up so it can be run ahead of starting COSMOS.

Returns:

  • names of the settings that were written



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/openc3/models/setting_model.rb', line 201

def self.apply_defaults(env: ENV, overwrite: nil, dry_run: false, strict: nil)
  problems = []
  strict = read_control_flag(env, STRICT_ENV_VAR, problems) if strict.nil?
  overwrite = read_control_flag(env, OVERWRITE_ENV_VAR, problems) if overwrite.nil?
  allow_unknown = read_control_flag(env, ALLOW_UNKNOWN_ENV_VAR, problems)
  # Collects a value that can't be coerced, so OPENC3_SETTING_AI_CHAT=nope is
  # reported like any other bad value rather than escaping as an exception
  settings = parse_defaults_env(env, problems)

  prefix = dry_run ? '[dry run] ' : ''
  written = []
  if settings.empty?
    # Only when nothing matched the prefix. If every matching variable failed
    # to coerce, settings is empty too, and claiming none were set is a lie -
    # the errors reported below are the explanation
    if problems.empty?
      puts "#{prefix}No #{SETTING_ENV_PREFIX}* environment variables set - nothing to seed"
    end
  else
    # A dry run is most useful before `openc3.sh start`, when there is no
    # Redis to compare against. Names and values can still be checked.
    comparable = dry_run ? redis_available? : true
    puts "#{prefix}Redis is not reachable - checking names and values only" unless comparable

    settings.each do |name, value|
      begin
        validate_setting!(name, value, allow_unknown: allow_unknown)
      rescue StandardError => error
        # Collect rather than abort. The init container restarts on failure
        # (compose restart: on-failure, Kubernetes restartPolicy OnFailure),
        # so raising here puts COSMOS in a crash loop over a cosmetic
        # setting, with the cause buried in restarting container logs.
        # Skipping leaves the setting at its default, which is the same
        # outcome as not setting it, and the error is reported below.
        problems << error.message
        next
      end

      existing = comparable ? get(name: name) : nil
      action, message = plan_setting(name, value, existing, overwrite)
      message += ' (current value unknown)' unless comparable
      puts "#{prefix}#{message}"
      next if action == :skip

      # :record leaves the setting alone and only refreshes provenance, so it
      # is not reported as written
      written << name if action == :write
      next if dry_run
      set({ name: name, data: value }, scope: nil) if action == :write
      record_seeded(name, value)
    end
  end

  report_problems(problems, prefix: prefix, dry_run: dry_run, strict: strict)
  written
end

.coerce(name, value) ⇒ Object

Environment variables and local mode files are always strings, but a setting holds whatever the Admin Console stores - a boolean for 'ai_chat', a String for everything else, including the settings whose String happens to contain JSON.

Driven by the declared type rather than by attempting JSON.parse on everything: parse-and-see turns 'classification_banner' into a Hash and the component's own JSON.parse then throws on it, and it would turn a subtitle of "2024" into a number.

Parameters:

  • setting name

  • raw value

Returns:

  • value in the form the frontend expects



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/openc3/models/setting_model.rb', line 404

def self.coerce(name, value)
  return value unless value.is_a?(String)
  case KNOWN_SETTINGS.dig(name, :type)
  when :boolean
    ConfigParser.handle_true_false_strict(value, description: "setting '#{name}'")
  when :json
    # Parsed, because this setting's reader checks for an object and
    # discards text. Validated here so a malformed blob is reported at seed
    # time rather than read back as a default nobody asked for.
    parse_json!(name, value)
  when :json_text
    # Kept as text, but parsed anyway to prove it is valid - the component
    # that JSON.parses it has no way to report a failure
    parse_json!(name, value)
    value
  else
    value
  end
end

.describe_json_settingsArray<Array(String, String)>

The JSON settings with a valid example value, for --help. Kept separate from describe_settings because a blob is far too long for that one-line "name: allowed values" format.

Returns:

  • name and example, in table order



540
541
542
543
544
545
# File 'lib/openc3/models/setting_model.rb', line 540

def self.describe_json_settings
  KNOWN_SETTINGS.filter_map do |name, details|
    next unless [:json, :json_text].include?(details[:type])
    [name, details[:example]]
  end
end

.describe_settingsArray<String>

Every setting that can be seeded, with its allowed values, for the cli initsettings --help listing.

Returns:

  • one 'name: allowed values' line per setting



518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
# File 'lib/openc3/models/setting_model.rb', line 518

def self.describe_settings
  KNOWN_SETTINGS.map do |name, details|
    allowed = if details[:values]
                details[:values].join(', ')
              else
                case details[:type]
                when :boolean then 'true, false (1, 0 also work)'
                when :json, :json_text
                  keys = details[:require_keys]
                  keys ? "JSON object with keys: #{keys.join(', ')}" : 'JSON object'
                else 'any text'
                end
              end
    "#{name}: #{allowed}"
  end
end

.export_linesArray<String>

The stored settings as paste-ready OPENC3_SETTING_* lines, for cli initsettings --export.

This is the answer to "what do I put in a JSON setting" - configure it in the Admin Console, run --export, paste the line. Reading the shape out of a running system can't drift from the code the way a documented example can, and it captures the values an operator already tuned by hand.

Parameters:

  • skip settings that have no value stored

Returns:

  • one compose "environment:" list item per setting



496
497
498
499
500
501
502
# File 'lib/openc3/models/setting_model.rb', line 496

def self.export_lines
  KNOWN_SETTINGS.keys.filter_map do |name|
    setting = get(name: name)
    next if setting.nil?
    "- #{yaml_env_item(name, setting['data'])}"
  end
end

.get(name:, scope: nil) ⇒ Object

NOTE: The following three class methods are used by the ModelController and are reimplemented to enable various Model class methods to work



169
170
171
# File 'lib/openc3/models/setting_model.rb', line 169

def self.get(name:, scope: nil) # NOSONAR - scope: is part of the caller-facing signature
  super(PRIMARY_KEY, name: name)
end

.names(scope: nil) ⇒ Object

NOSONAR - scope: is part of the caller-facing signature



173
174
175
# File 'lib/openc3/models/setting_model.rb', line 173

def self.names(scope: nil) # NOSONAR - scope: is part of the caller-facing signature
  super(PRIMARY_KEY)
end

.near_match?(known, name) ⇒ Boolean

Cheap typo detection: same length with one character different, or one character inserted / deleted. Enough to catch 'time_zones' and 'timezone' without pulling in a Levenshtein dependency.

Returns:



550
551
552
553
554
555
556
557
558
559
560
561
# File 'lib/openc3/models/setting_model.rb', line 550

def self.near_match?(known, name)
  return false if (known.length - name.length).abs > 1
  long, short = known.length >= name.length ? [known, name] : [name, known]
  i = 0
  i += 1 while i < short.length and long[i] == short[i]
  return true if i == short.length and long.length - short.length <= 1
  if long.length == short.length
    long[(i + 1)..-1] == short[(i + 1)..-1]
  else
    long[(i + 1)..-1] == short[i..-1]
  end
end

.parse_defaults_env(env, problems = []) ⇒ Hash

Collect every OPENC3_SETTING_ variable into a name => value hash.

Parameters:

  • environment to read from

  • (defaults to: [])

    collects a value that can't be coerced, so OPENC3_SETTING_AI_CHAT=nope is reported and skipped alongside every other bad value rather than escaping as an exception and aborting init

Returns:

  • setting name => coerced value



375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/openc3/models/setting_model.rb', line 375

def self.parse_defaults_env(env, problems = [])
  settings = {}
  env.each do |key, value|
    key = to_str(key)
    next unless key.start_with?(SETTING_ENV_PREFIX)
    name = key[SETTING_ENV_PREFIX.length..-1].downcase
    next if name.empty?
    begin
      settings[name] = coerce(name, to_str(value))
    rescue StandardError => error
      problems << error.message
    end
  end
  settings
end

.parse_json!(name, value) ⇒ Object

Returns the parsed blob.

Returns:

  • the parsed blob

Raises:

  • when the value isn't a JSON object with the keys the setting's reader requires



427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/openc3/models/setting_model.rb', line 427

def self.parse_json!(name, value)
  parsed = JSON.parse(value)
  unless parsed.is_a?(Hash)
    raise "Value for setting '#{name}' must be a JSON object, got #{parsed.class}"
  end
  required = KNOWN_SETTINGS.dig(name, :require_keys)
  if required
    missing = required - parsed.keys
    unless missing.empty?
      raise "Value for setting '#{name}' is missing required key(s): #{missing.join(', ')}. " \
            "Seed the whole object - a partial one breaks the code that reads it"
    end
  end
  parsed
rescue JSON::ParserError => error
  raise "Value for setting '#{name}' is not valid JSON: #{error.message}"
end

.plan_setting(name, value, existing, overwrite) ⇒ Array(Symbol, String)

What apply_defaults will do with one setting. Single-sourced so a dry run cannot report one thing and the real run do another.

Returns:

  • :write, :record or :skip, and the line to log. :record means the stored value is already correct but provenance still needs writing, so the setting itself is left untouched.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/openc3/models/setting_model.rb', line 301

def self.plan_setting(name, value, existing, overwrite)
  if existing.nil?
    [:write, "Set default setting '#{name}' to: #{value.inspect}"]
  elsif overwrite
    if existing['data'] == value
      # Rewriting the same value would bump updated_at on every init and
      # report a write that changed nothing. Provenance is still recorded,
      # so the next run without OVERWRITE can tell this value came from the
      # environment rather than from an Admin Console edit.
      [:record, "Setting '#{name}' already matches #{value.inspect}"]
    else
      # Overwrite discards an Admin Console edit, so say what was lost -
      # otherwise the log reads identically to a first-time seed
      [:write, "Overwriting setting '#{name}': #{existing['data'].inspect} -> " \
               "#{value.inspect} (#{OVERWRITE_ENV_VAR} is set)"]
    end
  elsif !seeded_value?(name, existing['data'])
    # Says what is known - that this value didn't come from here - rather
    # than claiming it was edited. An unrecorded setting is the normal case
    # on a deployment upgrading into this feature: nobody changed it, it was
    # written by seed_database or by a release before provenance existed.
    [:skip, "Setting '#{name}' holds a value initsettings didn't write - leaving as " \
            "#{existing['data'].inspect} (set #{OVERWRITE_ENV_VAR} to replace it)"]
  elsif existing['data'] == value
    [:skip, "Setting '#{name}' already matches #{value.inspect} - leaving unchanged"]
  else
    [:write, "Updating unedited setting '#{name}': #{existing['data'].inspect} -> #{value.inspect}"]
  end
end

.read_control_flag(env, name, problems) ⇒ Boolean

Read a boolean control variable, reporting an unparsable value rather than letting it abort init. Off is the safe reading of all three: OVERWRITE off doesn't discard an Admin Console edit, ALLOW_UNKNOWN off doesn't write a name nothing reads, and STRICT off doesn't fail init.

Parameters:

  • collects the message when the value is bad

Returns:



288
289
290
291
292
293
# File 'lib/openc3/models/setting_model.rb', line 288

def self.read_control_flag(env, name, problems)
  truthy_env?(env, name)
rescue StandardError => error
  problems << "#{error.message} - treating #{name} as off"
  false
end

.record_seeded(name, value) ⇒ Object



364
365
366
# File 'lib/openc3/models/setting_model.rb', line 364

def self.record_seeded(name, value)
  Store.hset(SEEDED_PRIMARY_KEY, name, JSON.generate({ 'data' => value.as_json(allow_nan: true) }))
end

.redis_available?Boolean

Returns:



331
332
333
334
335
336
# File 'lib/openc3/models/setting_model.rb', line 331

def self.redis_available?
  names()
  true
rescue StandardError
  false
end

.report_problems(problems, prefix:, dry_run:, strict:) ⇒ Object

Print every problem and decide whether it should end the process.

A typo in a cosmetic setting must not put the init container in a restart loop with the cause buried in restarting container logs, so the default is report-and-continue. Two things opt into failing: --dry-run, which exists to be a preflight gate, and STRICT_ENV_VAR, for a deployment that would rather not come up at all than come up misconfigured.



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/openc3/models/setting_model.rb', line 265

def self.report_problems(problems, prefix:, dry_run:, strict:)
  return if problems.empty?
  problems.each { |problem| puts "#{prefix}ERROR: #{problem}" }
  # "problem" rather than "setting" - a malformed control variable is
  # reported here too, and it isn't a setting that got skipped
  summary = "#{problems.length} #{SETTING_ENV_PREFIX}* configuration problem(s)"
  if dry_run or strict
    puts "#{prefix}#{summary}"
  else
    puts "#{prefix}#{summary} - the affected setting(s) were skipped and COSMOS will use the default"
    puts "#{prefix}Set #{STRICT_ENV_VAR} to fail init on these instead of continuing"
  end
  $stdout.flush
  raise "#{summary}: #{problems.join('; ')}" if dry_run or strict
end

.seeded_value?(name, current) ⇒ Boolean

An unrecorded setting is NOT treated as seeded. It was set by the Admin Console, by seed_database, or by a release before this tracking existed, and overwriting it is the silent clobber this mechanism exists to prevent. The cost is that a deployment upgrading into this feature needs one run with OVERWRITE_ENV_VAR before env changes take effect again. That cost is documented for operators too - compose.override.yaml under OPENC3_SETTINGS_OVERWRITE and cli initsettings --help both say it - so keep those in sync with this behavior.

Returns:

  • true when the current value is the one we last seeded, meaning nobody has changed it since.



356
357
358
359
360
361
362
# File 'lib/openc3/models/setting_model.rb', line 356

def self.seeded_value?(name, current)
  recorded = Store.hget(SEEDED_PRIMARY_KEY, name)
  return false if recorded.nil?
  JSON.parse(recorded, allow_nan: true, create_additions: true)['data'] == current
rescue JSON::ParserError
  false
end

.to_str(value) ⇒ Object

ENV values are frozen Strings but a Hash passed in tests may hold symbols



578
579
580
# File 'lib/openc3/models/setting_model.rb', line 578

def self.to_str(value)
  value.nil? ? nil : value.to_s
end

.truthy_env?(env, name) ⇒ Boolean

Read a boolean control variable. Unset means false.

The OPENC3_NO_* install flags are enabled by presence, so 'VAR=0' turns them ON. That is fine for "skip installing a tool" and wrong here: OVERWRITE_ENV_VAR discards what an operator configured in the Admin Console, so '0' and 'false' have to mean off, as they read.

Parameters:

  • environment to read from

  • variable name

Returns:



573
574
575
# File 'lib/openc3/models/setting_model.rb', line 573

def self.truthy_env?(env, name)
  ConfigParser.handle_true_false_strict(to_str(env[name]), description: name)
end

.validate_setting!(name, value, allow_unknown: false) ⇒ Object

Raise on anything we can prove is wrong, so the operator is told rather than left with a tool silently falling back to its built-in default.

Raising here does NOT abort init: apply_defaults collects the message, skips that one setting and keeps going, because crash-looping the init container over a cosmetic setting would be worse than using the default. initsettings --dry-run is the mode that exits non-zero.



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/openc3/models/setting_model.rb', line 457

def self.validate_setting!(name, value, allow_unknown: false)
  unless name =~ /\A[a-z0-9_]+\z/
    raise "Invalid setting name #{name.inspect}. Names must be lowercase letters, numbers and underscores"
  end

  unless KNOWN_SETTINGS.key?(name)
    unless allow_unknown
      # Period here, not on the suggestion - without a near match the
      # sentence used to run straight into "Set OPENC3_SETTINGS_ALLOW_UNKNOWN"
      message = "'#{name}' is not a known COSMOS setting."
      suggestion = KNOWN_SETTINGS.keys.find { |known| near_match?(known, name) }
      message += " Did you mean '#{suggestion}'?" if suggestion
      message += " Set #{ALLOW_UNKNOWN_ENV_VAR} to apply it anyway."
      raise message
    end
    puts "WARNING: '#{name}' is not a known COSMOS setting - #{ALLOW_UNKNOWN_ENV_VAR} is set, applying anyway"
  end

  size = value.is_a?(String) ? value.bytesize : JSON.generate(value).bytesize
  if size > MAX_VALUE_BYTES
    raise "Value for setting '#{name}' is #{size} bytes, exceeds the #{MAX_VALUE_BYTES} byte limit"
  end

  allowed = KNOWN_SETTINGS.dig(name, :values)
  return if allowed.nil? or allowed.include?(value)
  # Report the coerced value so 'Local' vs 'local' is visible in the error
  raise "Invalid value #{value.inspect} for setting '#{name}'. Must be one of: #{allowed.map(&:inspect).join(', ')}"
end

.yaml_env_item(name, data) ⇒ Object

One "KEY=value" env entry, quoted when YAML would otherwise mangle it. JSON always contains '":', which YAML reads as a mapping, so those always need quoting - the exact trap the compose.override.yaml comments warn about.



507
508
509
510
511
512
# File 'lib/openc3/models/setting_model.rb', line 507

def self.yaml_env_item(name, data)
  value = data.is_a?(String) ? data : JSON.generate(data)
  entry = "#{SETTING_ENV_PREFIX}#{name.upcase}=#{value}"
  return entry unless entry =~ /[:#"'\\]|\A\s|\s\z/
  %("#{entry.gsub('\\', '\\\\\\\\').gsub('"', '\\\\"')}")
end

Instance Method Details

#as_json(*a) ⇒ Hash

Returns JSON encoding of this model.

Returns:

  • JSON encoding of this model



588
589
590
591
592
593
594
# File 'lib/openc3/models/setting_model.rb', line 588

def as_json(*a)
  {
    'name' => @name,
    'data' => @data.as_json(*a),
    'updated_at' => @updated_at
  }
end