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