1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
use crate::workload_api::error::WorkloadApiError;
use std::fmt;
use thiserror::Error;
/// Errors returned by `X509Source`.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum X509SourceError {
/// Failed to retrieve or refresh X.509 material from the source.
///
/// During the initial synchronization performed by [`X509Source::new`](crate::X509Source::new)/
/// [`X509SourceBuilder::build`](crate::X509SourceBuilder::build), most underlying
/// [`WorkloadApiError`]s (e.g. transient connectivity failures, `NoIdentityIssued`)
/// are retried with backoff rather than surfaced immediately. Fixed endpoint syntax
/// is validated before synchronization. gRPC `INVALID_ARGUMENT` fails initial sync
/// immediately; an unsupported `FetchX509SVID` returns [`Self::UnsupportedRpc`].
/// Steady-state reconnects continue retrying after successful initial sync.
#[error("x509 source error: {0}")]
Source(#[from] WorkloadApiError),
/// The required `FetchX509SVID` RPC returned gRPC `UNIMPLEMENTED` during initial sync.
///
/// Includes failures in response headers or trailers before the first accepted
/// item. The original Workload API error, status code, and diagnostic cause are
/// retained. Correct the server/profile configuration and build a new source.
/// After successful initialization, this status continues to be retried.
#[error("required Workload API RPC FetchX509SVID is unsupported during initial synchronization: {0}")]
UnsupportedRpc(#[source] WorkloadApiError),
/// No SVID could be selected from the received context.
///
/// This can occur when:
/// - The picker rejects all available SVIDs
/// - No default SVID is available and no picker is configured
/// - The selected SVID is expired or the local clock is unavailable
///
/// Initial synchronization requires a usable identity. Later complete updates
/// still replace trust bundles even when this error prevents identity acquisition.
/// If expiry is unexpected, check the host's clock for skew.
#[error("no suitable svid found")]
NoSuitableSvid,
/// The monitored material RPC denied permission. Recovery requires a valid complete response.
#[error("material withdrawn: {0}")]
Withdrawn(#[source] std::sync::Arc<WorkloadApiError>),
/// The source was closed.
#[error("source is closed")]
Closed,
/// Initial synchronization timeout exceeded.
///
/// This error occurs when `X509SourceBuilder::initial_sync_timeout()` is configured
/// and the initial synchronization performed during source construction does not
/// complete within the specified timeout.
#[error("initial synchronization timed out")]
InitialSyncTimeout,
/// The workload API stream ended.
///
/// This error occurs when the gRPC stream from the Workload API terminates
/// normally (returns `None`). This is distinct from `Source(WorkloadApiError)`,
/// which represents actual errors during stream operations.
///
/// **When this occurs:**
/// - The stream reaches end-of-stream (normal termination)
/// - The connection is closed by the server
///
/// **Not to be confused with:**
/// - `Source(WorkloadApiError::EmptyResponse)` - occurs when the API returns
/// empty data, not when the stream ends
/// - `Source(WorkloadApiError::Transport(...))` - occurs for transport-level errors
#[error("workload api stream ended")]
StreamEnded,
/// Resource limit exceeded.
///
/// This error indicates that a received X.509 context exceeds one of the configured
/// resource limits. The error includes the kind of limit, the configured limit, and
/// the actual value that exceeded it.
#[error("resource limit exceeded: {kind} (limit={limit}, actual={actual})")]
ResourceLimitExceeded {
/// The kind of limit that was exceeded.
kind: LimitKind,
/// The configured limit value.
limit: usize,
/// The actual value that exceeded the limit.
actual: usize,
},
/// Shutdown timeout exceeded.
///
/// This error occurs when `shutdown_with_timeout()` is called and the background
/// tasks do not complete within the specified timeout.
#[error("shutdown timeout exceeded")]
ShutdownTimeout,
}
impl X509SourceError {
pub(super) fn initial_rpc_error(error: WorkloadApiError) -> Self {
if error.is_unimplemented() {
Self::UnsupportedRpc(error)
} else {
Self::Source(error)
}
}
}
/// The kind of resource limit that was exceeded.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum LimitKind {
/// Maximum number of SVIDs exceeded.
MaxSvids,
/// Maximum number of bundles exceeded.
MaxBundles,
/// Maximum bundle DER bytes exceeded.
MaxBundleDerBytes,
}
impl LimitKind {
/// Returns a stable string representation of the limit kind.
///
/// This is useful for error messages, metrics labels, and logging.
pub const fn as_str(self) -> &'static str {
match self {
Self::MaxSvids => "max_svids",
Self::MaxBundles => "max_bundles",
Self::MaxBundleDerBytes => "max_bundle_der_bytes",
}
}
}
impl fmt::Display for LimitKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
/// Error kinds for structured metrics reporting.
///
/// Use these stable, low-cardinality labels when recording metrics.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum MetricsErrorKind {
/// Failed to create a Workload API client.
ClientCreation,
/// Failed to connect to the Workload API stream.
StreamConnect,
/// Error occurred while reading from the stream.
StreamError,
/// The Workload API stream ended unexpectedly.
StreamEnded,
/// Initial synchronization with the Workload API failed.
InitialSyncFailed,
/// No suitable SVID could be selected from the context.
NoSuitableSvid,
/// Resource limit exceeded: maximum SVID count.
LimitMaxSvids,
/// Resource limit exceeded: maximum bundle count.
LimitMaxBundles,
/// Resource limit exceeded: maximum bundle DER bytes.
LimitMaxBundleDerBytes,
/// An X.509 context update was rejected (validation failed).
UpdateRejected,
/// Failed to join supervisor task during shutdown.
SupervisorJoinFailed,
}
impl MetricsErrorKind {
/// Returns a string representation of the error kind.
///
/// This is useful for metrics systems that require string labels.
pub const fn as_str(self) -> &'static str {
match self {
Self::ClientCreation => "client_creation",
Self::StreamConnect => "stream_connect",
Self::StreamError => "stream_error",
Self::StreamEnded => "stream_ended",
Self::InitialSyncFailed => "initial_sync_failed",
Self::NoSuitableSvid => "no_suitable_svid",
Self::LimitMaxSvids => "limit_max_svids",
Self::LimitMaxBundles => "limit_max_bundles",
Self::LimitMaxBundleDerBytes => "limit_max_bundle_der_bytes",
Self::UpdateRejected => "update_rejected",
Self::SupervisorJoinFailed => "supervisor_join_failed",
}
}
}
impl fmt::Display for MetricsErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}