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