Skip to main content

mz_cloud_resources/crd/
vpc_endpoint.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! VpcEndpoint custom resource, to be reconciled into an AWS VPC Endpoint by the
11//! environment-controller.
12use 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    /// Describes an AWS VPC endpoint to create.
24    #[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    // If making changes to this spec,
47    // you must also update src/cloud-resources/gen/vpcendpoints.crd.json
48    // so that cloudtest can register the CRD.
49    pub struct VpcEndpointSpec {
50        /// The name of the service to connect to.
51        pub aws_service_name: String,
52        /// The IDs of the availability zones in which the service is available.
53        pub availability_zone_ids: Vec<String>,
54        /// A suffix to use in the name of the IAM role that is created.
55        pub role_suffix: String,
56    }
57
58    #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
59    #[serde(rename_all = "camelCase")]
60    pub struct VpcEndpointStatus {
61        // This will be None if the customer hasn't allowed our principal, got the name of their
62        // VPC Endpoint Service wrong, or we've otherwise failed to create the VPC Endpoint.
63        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    /// The AWS SDK State enum is not serializable, so we have to make our own.
94    #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
95    #[serde(rename_all = "camelCase")]
96    pub enum VpcEndpointState {
97        // Internal States
98        PendingServiceDiscovery,
99        CreatingEndpoint,
100        RecreatingEndpoint,
101        UpdatingEndpoint,
102
103        // AWS States
104        // Connection established to the customer's VPC Endpoint Service.
105        Available,
106        Deleted,
107        Deleting,
108        Expired,
109        Failed,
110        // Customer has approved the connection. It should eventually move to Available.
111        Pending,
112        // Waiting on the customer to approve the connection.
113        PendingAcceptance,
114        Rejected,
115        Unknown,
116        // Could not place the endpoint in a subnet with the provided AZs
117        MissingAvailabilityZones,
118    }
119
120    impl VpcEndpointState {
121        // These are high level messages that can be used in conditions and
122        // may be forwarded to end users. It is important to make them concise
123        // and at a level which can be understand by end users of materialize
124        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        // Internal States
160        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}