1use std::path::PathBuf;
2
3#[derive(Debug, thiserror::Error)]
4pub enum Error {
5 #[error("config not found at {0}")]
6 ConfigNotFound(PathBuf),
7
8 #[error("failed to read {path}: {source}")]
9 FileRead {
10 path: PathBuf,
11 source: std::io::Error,
12 },
13
14 #[error("failed to write {path}: {source}")]
15 FileWrite {
16 path: PathBuf,
17 source: std::io::Error,
18 },
19
20 #[error("failed to parse {path}: {source}")]
21 TomlParse {
22 path: PathBuf,
23 source: toml::de::Error,
24 },
25
26 #[error("failed to serialize config: {0}")]
27 TomlSerialize(#[from] toml::ser::Error),
28
29 #[error(
30 "service {name} not found in any registry{}",
31 crate::registry::format_service_suggestions(suggestions)
32 )]
33 ServiceNotFound {
34 name: String,
35 suggestions: Vec<String>,
36 },
37
38 #[error("service {0} is already installed")]
39 ServiceAlreadyInstalled(String),
40
41 #[error("service {0} is not installed")]
42 ServiceNotInstalled(String),
43
44 #[error(
45 "service {0} has leftover state from a prior install (incomplete install or preserved remove)"
46 )]
47 ServiceIncomplete(String),
48
49 #[error("{service} requires the following services to be installed first: {}", missing.join(", "))]
50 MissingRequiredServices {
51 service: String,
52 missing: Vec<String>,
53 },
54
55 #[error("registry {0} not found")]
56 RegistryNotFound(String),
57
58 #[error("no ports available in range {start}–{end}")]
59 PortsExhausted { start: u16, end: u16 },
60
61 #[error("port {port} is already in use")]
62 PortConflict { port: u16 },
63
64 #[error("git command failed: {0}")]
65 Git(String),
66
67 #[error("systemctl command failed: {0}")]
68 Systemctl(String),
69
70 #[error("tailscale: {0}")]
71 Tailscale(String),
72
73 #[error("directory creation failed for {path}: {source}")]
74 DirCreate {
75 path: PathBuf,
76 source: std::io::Error,
77 },
78
79 #[error("template rendering failed: {0}")]
80 Template(String),
81
82 #[error(
83 "auth integration requires an auth provider to be configured first — run `ryra add authelia`"
84 )]
85 AuthNotConfigured,
86
87 #[error("service {0} does not support native OIDC auth")]
88 NoOidcSupport(String),
89
90 #[error(
91 "authelia is local-only at {auth_url}, but {service} will be reachable at \
92 {service_url}. Off-host clients (e.g., other devices on your tailnet) can't \
93 resolve `*.internal` hostnames, so the OIDC redirect from {service} back \
94 to authelia would fail.\n\n\
95 Fix: re-install authelia at the same exposure as {service}:\n \
96 ryra remove authelia --purge\n \
97 ryra add authelia --tailscale (or --url <public-https-url>)"
98 )]
99 AuthExposureMismatch {
100 auth_url: String,
101 service: String,
102 service_url: String,
103 },
104
105 #[error(
106 "{service} uses --auth and authelia is exposed at {auth_url}, but no reverse \
107 proxy is installed. The OIDC back-channel between {service} and authelia runs \
108 through Caddy as the internal TLS terminator, so Caddy must be installed first.\n\n\
109 Fix:\n \
110 ryra add caddy\n \
111 then re-run your `ryra add {service} --auth ...` command"
112 )]
113 AuthRequiresReverseProxy { service: String, auth_url: String },
114
115 #[error("{0}")]
116 UnsupportedArchitecture(String),
117
118 #[error("service '{service}' has no env_group named '{group}'{hint}")]
119 UnknownEnvGroup {
120 service: String,
121 group: String,
122 hint: String,
123 },
124
125 #[error("`ryra configure` can't change {field} for service '{service}'. {workaround}")]
126 ConfigureUnsupported {
127 service: String,
128 field: String,
129 workaround: String,
130 },
131
132 #[error("could not determine home directory: set $HOME")]
133 HomeDirNotFound,
134
135 #[error("invalid service reference: {0}")]
136 InvalidServiceRef(String),
137
138 #[error("registry configuration error: {0}")]
139 RegistryConfig(String),
140
141 #[error("quadlet bundle error: {0}")]
142 Bundle(String),
143
144 #[error("auth context error: {0}")]
145 AuthContext(String),
146
147 #[error("config validation failed: {0}")]
148 ConfigValidation(String),
149
150 #[error(
151 "{service}: {} hand-edited file(s) would be overwritten — re-run with --force to overwrite, or back up your changes first:\n {}",
152 paths.len(),
153 paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join("\n ")
154 )]
155 HandEditedFiles {
156 service: String,
157 paths: Vec<PathBuf>,
158 },
159
160 #[error("no backups found for service '{0}' — `ryra upgrade` creates them, run that first")]
161 NoBackup(String),
162
163 #[error(
164 "service '{service}' has no backup at timestamp {stamp} — run `ryra revert {service}` to use the most recent"
165 )]
166 BackupNotFound { service: String, stamp: String },
167
168 #[error(
169 "service '{0}' does not declare backup support — the service author must set `backup = true` under [integrations] in its service.toml first"
170 )]
171 BackupNotSupported(String),
172
173 #[error("backup repository is not configured — run `ryra backup configure` first")]
174 BackupRepoNotConfigured,
175
176 #[error("backup is not enabled for service '{0}' — re-install with `ryra add {0} --backup`")]
177 BackupNotEnabled(String),
178
179 #[error(
180 "no snapshots found for service '{0}' in the backup repository — has `ryra backup run` ever succeeded?"
181 )]
182 BackupNoSnapshots(String),
183
184 #[error("restic command failed: {0}")]
185 Restic(String),
186
187 #[error("backup hook '{hook}' for service '{service}' failed: {message}")]
188 BackupHookFailed {
189 service: String,
190 hook: String,
191 message: String,
192 },
193
194 #[error(
195 "service '{service}' was backed up at manifest hash {backed_up} but current install is at {current} — pass --force to restore anyway, or --migrate-to=<dir> to extract without touching the live install"
196 )]
197 BackupVersionMismatch {
198 service: String,
199 backed_up: String,
200 current: String,
201 },
202}
203
204pub type Result<T> = std::result::Result<T, Error>;