1use std::fmt;
13
14use k8s_openapi::apimachinery::pkg::apis::meta::v1::{Condition, Time};
15use k8s_openapi::jiff::Timestamp;
16use kube::CustomResource;
17use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20pub mod v1 {
21 use super::*;
22
23 #[derive(CustomResource, Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
25 #[serde(rename_all = "camelCase")]
26 #[kube(
27 group = "materialize.cloud",
28 version = "v1",
29 kind = "VpcEndpoint",
30 singular = "vpcendpoint",
31 plural = "vpcendpoints",
32 shortname = "vpce",
33 namespaced,
34 status = "VpcEndpointStatus",
35 printcolumn = r#"{"name": "AwsServiceName", "type": "string", "description": "Name of the VPC Endpoint Service to connect to.", "jsonPath": ".spec.awsServiceName", "priority": 1}"#,
36 printcolumn = r#"{"name": "AvailabilityZoneIDs", "type": "string", "description": "Availability Zone IDs", "jsonPath": ".spec.availabilityZoneIds", "priority": 1}"#
37 )]
38 pub struct VpcEndpointSpec {
42 pub aws_service_name: String,
44 pub availability_zone_ids: Vec<String>,
46 pub role_suffix: String,
48 }
49
50 #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
51 #[serde(rename_all = "camelCase")]
52 pub struct VpcEndpointStatus {
53 pub vpc_endpoint_id: Option<String>,
56 pub state: Option<VpcEndpointState>,
57 pub conditions: Option<Vec<Condition>>,
58 pub auto_assigned_azs: Option<Vec<String>>,
59 }
60
61 impl Default for VpcEndpointStatus {
62 fn default() -> Self {
63 Self {
64 vpc_endpoint_id: None,
65 state: Some(VpcEndpointState::Unknown),
66 conditions: Some(Self::default_conditions()),
67 auto_assigned_azs: None,
68 }
69 }
70 }
71
72 impl VpcEndpointStatus {
73 pub fn default_conditions() -> Vec<Condition> {
74 vec![Condition {
75 type_: "Available".into(),
76 status: "Unknown".to_string(),
77 last_transition_time: Time(Timestamp::now()),
78 message: v1::VpcEndpointState::Unknown.message().into(),
79 observed_generation: None,
80 reason: "".into(),
81 }]
82 }
83 }
84
85 #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
87 #[serde(rename_all = "camelCase")]
88 pub enum VpcEndpointState {
89 PendingServiceDiscovery,
91 CreatingEndpoint,
92 RecreatingEndpoint,
93 UpdatingEndpoint,
94
95 Available,
98 Deleted,
99 Deleting,
100 Expired,
101 Failed,
102 Pending,
104 PendingAcceptance,
106 Rejected,
107 Unknown,
108 MissingAvailabilityZones,
110 }
111
112 impl VpcEndpointState {
113 pub fn message(&self) -> &str {
117 match self {
118 VpcEndpointState::PendingServiceDiscovery => {
119 "Endpoint cannot be discovered, ensure the Vpc Endpoint Service is allowing discovery"
120 }
121 VpcEndpointState::CreatingEndpoint => "Endpoint is being created",
122 VpcEndpointState::RecreatingEndpoint => "Endpoint is being re-created",
123 VpcEndpointState::UpdatingEndpoint => "Endpoint is being updated",
124 VpcEndpointState::Available => "Endpoint is available",
125 VpcEndpointState::Deleted => "Endpoint has been deleted",
126 VpcEndpointState::Deleting => "Endpoint is being deleted",
127 VpcEndpointState::Expired => {
128 "The Endpoint acceptance period has lapsed, you can still manually accept the Endpoint"
129 }
130 VpcEndpointState::Failed => "Endpoint creation has failed",
131 VpcEndpointState::Pending => {
132 "Endpoint creation is pending, this should resolve shortly"
133 }
134 VpcEndpointState::PendingAcceptance => {
135 "The Endpoint connection to the Endpoint Service is pending acceptance"
136 }
137 VpcEndpointState::Rejected => {
138 "The Endpoint connection to the Endpoint Service has been rejected"
139 }
140 VpcEndpointState::Unknown => {
141 "The Endpoint is in an unknown state, this should resolve momentarily"
142 }
143 VpcEndpointState::MissingAvailabilityZones => {
144 "The Endpoint cannot be created due to missing availability zones"
145 }
146 }
147 }
148 }
149
150 impl fmt::Display for VpcEndpointState {
151 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
153 let repr = match self {
154 VpcEndpointState::PendingServiceDiscovery => "pendingServiceDiscovery",
155 VpcEndpointState::CreatingEndpoint => "creatingEndpoint",
156 VpcEndpointState::RecreatingEndpoint => "recreatingEndpoint",
157 VpcEndpointState::UpdatingEndpoint => "updatingEndpoint",
158 VpcEndpointState::Available => "available",
159 VpcEndpointState::Deleted => "deleted",
160 VpcEndpointState::Deleting => "deleting",
161 VpcEndpointState::Expired => "expired",
162 VpcEndpointState::Failed => "failed",
163 VpcEndpointState::Pending => "pending",
164 VpcEndpointState::PendingAcceptance => "pendingAcceptance",
165 VpcEndpointState::Rejected => "rejected",
166 VpcEndpointState::Unknown => "unknown",
167 VpcEndpointState::MissingAvailabilityZones => "missingAvailabilityZones",
168 };
169 write!(f, "{}", repr)
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use std::fs;
177
178 use kube::CustomResourceExt;
179 use kube::core::crd::merge_crds;
180
181 #[mz_ore::test]
182 fn test_vpc_endpoint_crd_matches() {
183 let crd = merge_crds(vec![super::v1::VpcEndpoint::crd()], "v1").unwrap();
184 let crd_json = serde_json::to_string(&serde_json::json!(&crd)).unwrap();
185 let exported_crd_json = fs::read_to_string("src/crd/generated/vpcendpoints.json").unwrap();
186 let exported_crd_json = exported_crd_json.trim();
187 assert_eq!(
188 &crd_json, exported_crd_json,
189 "VpcEndpoint CRD json does not match exported json.\n\nCRD:\n{}\n\nExported CRD:\n{}",
190 &crd_json, exported_crd_json,
191 );
192 }
193}