add secrets manager

Signed-off-by: vladmandic <mandic00@live.com>
pull/4690/head
vladmandic 2026-03-17 13:56:55 +01:00
parent d588ced2ef
commit 4e1c1a6844
6 changed files with 93 additions and 55 deletions

@ -1 +1 @@
Subproject commit bdc35c4731ee37e2191d52d3cf3ba6ccd37eecf8
Subproject commit f287be148c8345913cba177982c4fb6e18ad70dc

View File

@ -1619,6 +1619,15 @@ def read_options():
opts = json.loads(opts)
except Exception as e:
log.error(f'Error reading options file: {file} {e}')
if os.path.isfile(args.secrets):
with open(args.secrets, encoding="utf8") as file:
try:
secrets = json.load(file)
if type(secrets) is str:
secrets = json.loads(secrets)
opts = opts | secrets
except Exception as e:
log.error(f"Error reading secrets file: {file} {e}")
ts('options', t_start)

View File

@ -272,7 +272,7 @@ def post_settings(request: dict):
shared.opts.data['civitai_save_subfolder'] = save_subfolder
if discard_hash_mismatch is not None:
shared.opts.data['civitai_discard_hash_mismatch'] = discard_hash_mismatch
shared.opts.save(shared.config_filename)
shared.opts.save()
return get_settings()
@ -606,5 +606,3 @@ def register_api():
# Legacy endpoints (backward compatibility)
api.add_api_route("/sdapi/v1/civitai", legacy_get_civitai, methods=["GET"], response_model=list, tags=["Models"])
api.add_api_route("/sdapi/v1/civitai", legacy_post_civitai, methods=["POST"], response_model=list, tags=["Models"])
log.debug('CivitAI API: registered endpoints')

View File

@ -27,6 +27,7 @@ def add_core_args(p):
def add_config_arg(p, data_dir):
p.add_argument("--config", type=str, default=os.environ.get("SD_CONFIG", os.path.join(data_dir, 'config.json')), help="Use specific server configuration file, default: %(default)s")
p.add_argument("--secrets", type=str, default=os.environ.get("SD_SECRETS", os.path.join(data_dir, 'secrets.json')), help="Use specific server secrets file, default: %(default)s")
def add_compute_args(p):
@ -42,9 +43,11 @@ def add_compute_args(p):
p.add_argument("--no-half", default=env_flag("SD_NOHALF", False), action='store_true', help="Do not switch the model to 16-bit float, default: %(default)s")
p.add_argument("--no-half-vae", default=env_flag("SD_NOHALFVAE", False), action='store_true', help="Do not switch VAE model to 16-bit float, default: %(default)s")
def add_ui_args(p):
p.add_argument('--theme', type=str, default=os.environ.get("SD_THEME", None), help='Override UI theme')
p.add_argument('--locale', type=str, default=os.environ.get("SD_LOCALE", None), help='Override UI locale')
p.add_argument("--enso", default=env_flag("SD_ENSO", False), action="store_true", help="Enable Enso frontend, default: %(default)s")
def add_http_args(p):

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import os
import sys
import json
import threading
from typing import TYPE_CHECKING
@ -16,55 +17,79 @@ if TYPE_CHECKING:
cmd_opts = cmd_args.parse_args()
compatibility_opts = ['clip_skip', 'uni_pc_lower_order_final', 'uni_pc_order']
secrets_pattern = ['_version', '_token', '_key', '_secret', '_password']
class Options:
data_labels: dict[str, OptionInfo | LegacyOption]
data: dict[str, Any]
secrets: dict[str, Any]
typemap = {int: float}
debug = os.environ.get('SD_CONFIG_DEBUG', None) is not None
secrets_debug = os.environ.get("SD_SECRETS_DEBUG", None) is not None
def __init__(self, options_templates: dict[str, OptionInfo | LegacyOption] = None, restricted_opts: set[str] | None = None, *, filename = ''):
def __init__(self, options_templates: dict[str, OptionInfo | LegacyOption] = None, restricted: set[str] | None = None, *, filename = '', secrets = ''):
if options_templates is None:
options_templates = {}
if restricted_opts is None:
restricted_opts = set()
if restricted is None:
restricted = set()
super().__setattr__('data_labels', options_templates)
super().__setattr__('data', {k: v.default for k, v in options_templates.items()})
super().__setattr__('secrets', {})
self.filename: str = filename or cmd_opts.config
self.restricted_opts = restricted_opts
self.secretsfn: str = secrets or cmd_opts.secrets
self.restricted: set[str] = restricted
self.legacy = [k for k, v in options_templates.items() if isinstance(v, LegacyOption)]
self.load()
def __setattr__(self, key, value): # pylint: disable=inconsistent-return-statements
if key in self.data or key in self.data_labels:
if cmd_opts.freeze:
log.warning(f'Settings are frozen: {key}')
return
if cmd_opts.hide_ui_dir_config and key in self.restricted_opts:
log.warning(f'Settings key is restricted: {key}')
return
if self.debug:
log.trace(f'Settings set: {key}={value}')
if key in self.legacy:
log.warning(f'Settings set: {key}={value} legacy')
self.data[key] = value
return
return super().__setattr__(key, value) # pylint: disable=super-with-arguments
def __getattr__(self, item):
if item == 'secrets':
return super().__getattribute__('secrets')
if item == 'data':
return super().__getattribute__('data')
if item in self.secrets:
if self.secrets_debug:
fn = f"{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}" # pylint: disable=protected-access
log.trace(f"Secret: get={item} fn={fn}")
return self.secrets[item]
if item in self.data:
return self.data[item]
if item in self.data_labels:
return self.data_labels[item].default
return super().__getattribute__(item) # pylint: disable=super-with-arguments
def get(self, item):
if item in self.secrets:
if self.secrets_debug:
fn = f'{sys._getframe(2).f_code.co_name}:{sys._getframe(1).f_code.co_name}' # pylint: disable=protected-access
log.trace(f"Secret: get={item} fn={fn}")
return self.secrets[item]
if item in self.data:
return self.data[item]
if item in self.data_labels:
return self.data_labels[item].default
return super().__getattribute__(item) # pylint: disable=super-with-arguments
return super().__getattribute__(item) # pylint: disable=super-with-arguments
def __getattr__(self, item):
if item in self.data:
return self.data[item]
if item in self.data_labels:
return self.data_labels[item].default
return super().__getattribute__(item) # pylint: disable=super-with-arguments
def __setattr__(self, key, value): # pylint: disable=inconsistent-return-statements
if (key in self.data_labels) or (key in self.data) or (key in self.secrets):
if cmd_opts.freeze:
log.warning(f"Settings are frozen: {key}")
return
if cmd_opts.hide_ui_dir_config and key in self.restricted:
log.warning(f"Settings key is restricted: {key}")
return
if self.debug:
log.trace(f"Settings set: {key}={value}")
if key in self.legacy:
log.warning(f"Settings set: {key}={value} legacy")
if any(key.endswith(pattern) for pattern in secrets_pattern):
if self.secrets_debug:
log.trace(f"Secret: set={key}")
self.secrets[key] = value
else:
self.data[key] = value
return
return super().__setattr__(key, value) # pylint: disable=super-with-arguments
def set(self, key, value):
"""sets an option and calls its onchange callback, returning True if the option changed and False otherwise"""
@ -102,28 +127,22 @@ class Options:
components = [k for k, v in self.data_labels.items() if v.visible]
return components
def save_atomic(self, filename=None, silent=False):
def save_atomic(self, silent=False):
if self.debug:
log.debug(f'Settings: save settings="{self.filename}" override="{filename}" cmd="{cmd_opts.config}" cwd="{os.getcwd()}"')
if filename is None:
filename = self.filename
filename = os.path.abspath(filename)
log.debug(f'Settings: save settings="{self.filename}" secrets="{self.secretsfn}" cmd="{cmd_opts.config}" cwd="{os.getcwd()}"')
filename = os.path.abspath(self.filename)
secretsfn = os.path.abspath(self.secretsfn)
if cmd_opts.freeze:
log.warning(f'Setting: fn="{filename}" save disabled')
return
try:
diff = {}
unused_settings = []
# if self.debug:
# log.debug('Settings: user')
# for k, v in self.data.items():
# log.trace(f' Config: item={k} value={v} default={self.data_labels[k].default if k in self.data_labels else None}')
if self.debug:
log.debug(f'Settings: total={len(self.data.keys())} known={len(self.data_labels.keys())}')
log.debug(f'Settings: total={len(self.data.keys())} secrets={len(self.secrets.keys())} known={len(self.data_labels.keys())}')
for k, v in self.data.items():
all_options = self.data | self.secrets
diff = {}
for k, v in all_options.items():
if k in self.data_labels:
default = self.data_labels[k].default
if isinstance(v, list):
@ -142,16 +161,24 @@ class Options:
unused_settings.append(k)
if self.debug:
log.trace(f'Settings unknown: {k}={v}')
writefile(diff, filename, silent=silent)
options = {}
secrets = {}
for k, v in diff.items():
if any(k.endswith(pattern) for pattern in secrets_pattern):
secrets[k] = v
else:
options[k] = v
writefile(options, filename, silent=silent)
writefile(secrets, secretsfn, silent=silent)
if self.debug:
log.trace(f'Settings save: count={len(diff.keys())} {diff}')
if len(unused_settings) > 0:
log.debug(f"Settings: unused={unused_settings}")
except Exception as err:
log.error(f'Settings: fn="{filename}" {err}')
log.error(f'Settings: config="{filename}" secrets="{secretsfn}" {err}')
def save(self, filename=None, silent=False):
threading.Thread(target=self.save_atomic, args=(filename, silent)).start()
def save(self, silent=False):
threading.Thread(target=self.save_atomic, args=(silent,)).start()
def same_type(self, x, y):
if x is None or y is None:
@ -160,15 +187,15 @@ class Options:
type_y = self.typemap.get(type(y), type(y))
return type_x == type_y
def load(self, filename=None):
if filename is None:
filename = self.filename
filename = os.path.abspath(filename)
def load(self):
filename = os.path.abspath(self.filename)
secretsfn = os.path.abspath(self.secretsfn)
if not os.path.isfile(filename):
log.debug(f'Settings: fn="{filename}" created')
self.save(filename)
log.debug(f'Settings: config="{filename}" secrets="{secretsfn}" created')
self.save()
return
self.data = readfile(filename, lock=True, as_type="dict")
self.secrets = readfile(secretsfn, lock=True, as_type="dict")
if self.data.get('quicksettings') is not None and self.data.get('quicksettings_list') is None:
self.data['quicksettings_list'] = [i.strip() for i in self.data.get('quicksettings', '').split(',')]
unknown_settings = []

View File

@ -162,7 +162,8 @@ from modules.shared_legacy import get_legacy_options
options_templates.update(get_legacy_options())
from modules.options_handler import Options
config_filename = cmd_opts.config
opts = Options(options_templates, restricted_opts, filename=config_filename)
secrets_filename = cmd_opts.secrets
opts = Options(options_templates, restricted=restricted_opts, filename=config_filename, secrets=secrets_filename)
cmd_opts = cmd_args.settings_args(opts, cmd_opts)
cmd_opts = cmd_args.override_args(opts, cmd_opts)
if cmd_opts.locale is not None: