Commit Graph
8 Commits
Author SHA1 Message Date
Mike DegatanoandGitHub 96798b5041 Migrate addon API error metadata to app naming (#7058) 2026-07-23 16:02:49 +02:00
c119e0cff1 Rename Docker app container and builder names from addon_ to app_ (#7024)
* Rename Docker app container and builder names from addon_ to app_

* Update container state event tests to app_ names

* Migrate legacy app container names on attach

* Fix error handling

* Fixes from feedback

* Fix pytest

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-07-23 15:43:33 +02:00
dfe727410c Fix length validation of str and password options with bounds (#7044)
* Fix length validation of str and password options with bounds

App option schemas like str(1,32) validated the string value with
vol.Range, which compares the value itself against the numeric bounds.
Comparing a str with a float raises TypeError, which voluptuous reports
as "invalid value or type (must have a partial ordering)". As a result,
saving options always failed for any add-on using a bounded str or
password schema; unbounded str/password was unaffected since vol.Range
without bounds performs no comparison.

Use vol.Length to check the string length against the bounds, which is
the documented meaning of str(min,max). Also replace the str(value)
literal (a self-equality check) with the str type so non-string values
still fail validation, now with a clearer error message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update API test for changed non-string option error message

Validating a str option against the str type instead of the str(value)
literal changed the voluptuous error for non-string values from "not a
valid value" to "expected str". Update the expected message in the API
options error test accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:31:28 +02:00
Mike DegatanoandGitHub 35542d5bcc Replace addon with app in docker container names for apps (#6997)
* Support dual app/addon log identifiers during migration

* Use single journald epoch query for dual app identifiers

* Add API-keyed container epoch error handling

* Template CONTAINER_LOG_EPOCH via extra fields
2026-07-03 15:44:22 +02:00
1857753e22 Redact app options in info for unprivileged apps (#6953)
The `/addons/{slug}/info` endpoint returned the target app's user options,
which can contain secrets such as passwords and API keys. The security
middleware grants every role (including the default role) access to any
`/.+/info` path, so an installed app with `hassio_api: true` and the default
role could read another app's options simply by requesting its info.

Redact the options field in info_data() unless the caller is entitled to see
it: Home Assistant Core (and other non-app internals), the app reading its
own info, or an app with the manager or admin role. Other apps reading a
different app's info now receive an empty options dict while all non-secret
metadata stays available for discovery. This mirrors the existing self-only
restriction on the dedicated /options/config endpoint.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:54:57 +02:00
a973d22e35 Derive App state from container state (#6890)
* Derive App state from container state

The App.state setter mixed two responsibilities: it both mutated a
private `_state` field and dispatched side effects (WebSocket events,
issue dismissal, startup_event signaling). On top of that, an installed
but never-started app stayed in AppState.UNKNOWN forever, because the
attach() image-only fallback never fires a container state-change event
and the AppState therefore kept its constructor default. Conceptually,
ContainerState.UNKNOWN ("container does not exist") and AppState.UNKNOWN
("nothing observed yet") happened to share a name but meant different
things, which made the distinction easy to lose.

Make App.state a pure derived property. The source of truth is the last
observed ContainerState (cached on the App), plus a sticky operation-
error flag for start/stop failures that the docker event stream cannot
reflect. When no container has been observed yet, the derivation falls
back to install signals: an attached instance (image present) is
STOPPED, otherwise UNKNOWN. As a side effect, an installed-but-never-
started app now correctly reports STOPPED instead of UNKNOWN.

container_state_changed updates the cached container state and routes
all side effects through a single _emit_state_change helper that diffs
old vs new derived state. The two start/stop failure paths route
through _set_operation_error. Uninstall resets the cached signals so
the derivation naturally returns UNKNOWN.

Tests use a new tests/common.force_app_state helper that pokes the
underlying signals directly; the production class no longer carries
test-only setters.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix App state drive to AppState.UNKNOWN

* Unify state mutation through _update_state

Previously, state-driving signal changes were spread across two helpers
(_set_operation_error, _emit_state_change(old_state)) and required each
caller to capture self.state before mutating a private field — leaking
implementation details to call sites and raising the "why am I emitting
the old state?" question pointed out in code review.

Replace both helpers with a single _update_state(*, container_state=,
operation_error=) entry point. Callers describe what changed via
keyword arguments (None leaves a signal untouched); the helper captures
the previous state, applies the updates, recomputes the derived state
and emits side effects if anything changed.

Diff against a tracked _last_state instead of a freshly derived
"current" state, so that an out-of-band mutation between updates does
not silently shift the comparison baseline. The concrete case is
App.uninstall: instance.remove() clears the docker meta mid-flow, which
would otherwise reshape the derivation (RUNNING with no healthcheck
becomes STARTED instead of STARTUP) and suppress the STARTUP transition
that resolves the start-wait task. As a side effect, the initial
UNKNOWN -> STOPPED transition on attach is also now reliably emitted.

Switch the uninstall path to ContainerState.UNKNOWN ("we know there is
no container") rather than the constructor sentinel None.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Cache app state instead of deriving on every read

Building on the previous commit, make App.state a plain read of a
cached _state field rather than re-deriving on every property access.
The derivation moves to _derive_state(), and _update_state() is the
sole place that recomputes and assigns _state, so the value consumers
read always matches what was last emitted to listeners.

This removes the _last_state bookkeeping introduced previously: with a
single cached value there is no longer a separate "derived now" vs
"last emitted" distinction to reconcile, and out-of-band mutations
(e.g. instance.remove() clearing _meta during uninstall) can no longer
silently shift what state returns between updates.

Call _update_state() at the end of load() so the cached state settles
once attach() has run. Image-only attaches do not fire a docker event,
so without this an installed app would stay in the constructor-default
UNKNOWN until first start; this also makes the initial transition on
attach observable to listeners.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Pass operation error to _derive_state instead of storing it

The two state-driving signals were not symmetric. _container_state is
genuinely persisted state ("the last thing docker told us") that
re-derivation legitimately reads across calls. _operation_error, on the
other hand, is a momentary "force ERROR for this transition" signal; the
persistence of an error condition already lives in the cached _state.

Storing it as an instance attribute implied a sticky cross-call behavior
that no call path actually exercised: every caller either set it
explicitly right before deriving (start/stop failures, container events)
or ran argless only at load time, where no failure has occurred.

Drop the _operation_error field and pass operation_error as a parameter
to _derive_state(), defaulting to False in _update_state(). A container
observation now supersedes a prior error implicitly via the default,
which lets the container-event and uninstall call sites drop their
explicit operation_error=False.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Settle load state synchronously from current_state

The argless _update_state() settle at the end of load() raced attach()'s
container-state event. attach() fires DOCKER_CONTAINER_STATE_CHANGE via
the bus, which schedules the container_state_changed listener as a task
rather than running it inline. In the deprecated-arch early-return path
there is no await between attach() and the settle, so the listener had
not run yet: _container_state was still None and the settle derived
STOPPED (instance attached) — emitting a transient UNKNOWN->STOPPED even
for a running container before the listener corrected it. The main path
only avoided this incidentally, by having awaits (check_image,
save_persist) in between for the listener to run.

Derive the load-time state synchronously from instance.current_state()
instead of relying on the asynchronously delivered event. current_state()
returns the real container state, or UNKNOWN when only an image is
present (which derives to STOPPED), so both paths settle correctly
without racing the event.

Add a regression test that loading a running container settles to
STARTED, and mock current_state() in the state-listener test which
relies on a clean UNKNOWN baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 19:50:06 +02:00
Stefan AgnerandGitHub 0f881d69fe Model client-state Apps* errors as APIError (#6856)
* apps: model client-state Apps* errors as APIError

Follow-up on #6739: with HassioError now logged and captured by Sentry
in api_process, a handful of Apps* exceptions raised from
AppManager.install/update/rebuild and AppModel._validate_availability
surfaced as "unexpected" 400s with a noisy log entry and a Sentry
event, even though they are all user/client-state errors (clicked
install on an already-installed add-on, "no update available", local
and store versions diverged, system architecture/machine/HA version
incompatible, etc.). The dominant offender is SUPERVISOR-1JVV
("No update available for app core_mosquitto", ~19k events / ~12k
users), but several siblings show the same shape.

Map these through properly so the API returns clean, structured 400s:

- Add modeled APIError subclasses in exceptions.py for the previously
  raw raises in apps/manager.py: AppAlreadyInstalledError,
  AppNotFoundError, AppNotInstalledError, AppNotInStoreError,
  AppNoUpdateAvailableError, AppRebuildVersionChangedError,
  AppRebuildImageBasedError. Each gets a stable error_key, a
  message_template, and an "addon" extra_field.
- Add APIError to AppNotSupportedArchitectureError,
  AppNotSupportedMachineTypeError and
  AppNotSupportedHomeAssistantVersionError so they behave the same as
  the other Apps* APIErrors instead of being treated as unexpected.
- Pass the app's display name (app.name from the add-on config)
  instead of the slug to extra_fields wherever an App or AppModel is
  available at the raise site, so users see "Mosquitto broker" rather
  than "core_mosquitto" in error messages. Slug is only used as a
  fallback when no app object exists (install of an unknown slug,
  update/rebuild of a slug that is not installed).
- Update raise sites in apps/manager.py and apps/model.py to use the
  new typed exceptions and the addon= keyword.

These are all runtime states users hit during normal interaction with
the apps UI, not Supervisor bugs worth paging on.

* apps: include slug alongside name in Apps* APIError extra_fields

Address review feedback from #6856: clients still need the slug to look
up additional add-on information (the name is for display only), and we
should be consistent about it across the Apps* errors touched by this
PR.

Every Apps* APIError raised with an App/AppModel available now carries
both `addon` (display name, used by the message_template) and `slug` in
extra_fields. Raise sites in apps/manager.py and apps/model.py pass
both. The two errors raised before an app object exists keep slug-only
extra_fields and use {slug} in their message:

- AppNotFoundError (install of an unknown slug)
- AppNotInstalledError (update/rebuild of a slug not in self.local)

Pre-existing Apps* APIErrors outside the scope of this PR
(AppUnknownError, AppConfigurationInvalidError, AppBootConfigCannot
ChangeError, AppNotRunningError, AppPortConflict, AppNotSupportedWrite
StdinError, AppBuild*) will be migrated in a follow-up.

* apps: introduce AppAPIError base for uniform addon/slug extra_fields

Address review follow-up on #6856: the addon/slug convention was
enforced only by hand-rolled __init__s, easy to drift on (forget slug,
use a different key, etc.). Promote it to a base class that owns the
shape of extra_fields for all App-related API errors.

- Add AppAPIError(AppsError, APIError). Its __init__ takes
  `app: AppModel | App | AppStore | str` and uniformly populates
  extra_fields with `addon` (display name) and `slug`. Pass a string
  when no app object exists; only `slug` is set in that case. Extra
  per-error fields flow through **extra_fields and merge with the
  defaults.
- Convert the new exceptions added in this PR
  (AppAlreadyInstalledError, AppNotFoundError, AppNotInstalledError,
  AppNotInStoreError, AppNoUpdateAvailableError,
  AppRebuildVersionChangedError) into thin subclasses that only declare
  error_key and message_template -- the __init__ is inherited.
- Migrate the AppNotSupported* errors (architecture, machine type, HA
  version) and AppRebuildImageBasedError to use AppAPIError too;
  their bespoke per-error fields go through **extra_fields. They keep
  inheriting AppNotSupportedError so `except AppNotSupportedError`
  callers (e.g., AppModel._available) still work; MRO routes __init__
  through AppAPIError.
- Update raise sites in apps/manager.py and apps/model.py to pass
  `app=<obj-or-slug>` instead of repeating `addon=...` and `slug=...`.

Pre-existing App* APIErrors outside this PR's scope
(AppUnknownError, AppConfigurationInvalidError,
AppBootConfigCannotChangeError, AppNotRunningError, AppPortConflict,
AppNotSupportedWriteStdinError, AppBuild*) will be migrated to
AppAPIError in a follow-up; the base class is in place for them.

* apps: tighten AppAPIError model per review

Address mdegat01's two follow-ups on #6856 (review approved as-is,
this is the cleanup):

- AppNotFoundError and AppNotInstalledError are raised before any
  App/AppModel object exists (unknown slug; not-installed slug). Pull
  them out of AppAPIError and inherit (AppsError, APIError) directly
  with a slug-only __init__ + {slug} message_template. Removes the
  conceptually-wrong str branch from AppAPIError.__init__: it now
  strictly requires an app-like object with .name and .slug.
- Restore per-class typed __init__ on AppNotSupportedArchitectureError,
  AppNotSupportedMachineTypeError and
  AppNotSupportedHomeAssistantVersionError so callers get an explicit
  signature for the bespoke architectures/machine_types/version params
  instead of dumping them through **extra_fields. Each override just
  delegates to AppAPIError.__init__, which keeps ownership of the
  addon/slug shape. The list-joining for architectures/machine_types
  moves back into the override (raise sites pass the raw list again).

AppRebuildImageBasedError takes no bespoke fields and stays a plain
AppAPIError subclass.
2026-05-22 16:54:59 +02:00
f8880a72be Rename addon/addons to app/apps in filenames and imports (#6837)
* Rename addon/addons to app/apps in filenames and imports

Continues the addon→app terminology migration (#6786).
Renames all source files, test files, fixture files, and
directories that contained 'addon'/'addons' in their names,
and updates all imports accordingly.

Resolution check files in supervisor/resolution/checks/ that were
renamed override the slug property to preserve the existing API
contract (slugs are exposed via the resolution info API and used
to run checks by name).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Rename add-on.json fixture

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-13 20:55:46 +02:00