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 kube::CustomResource;
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18
19pub mod v1 {
20    use super::*;
21
22    /// Describes an AWS VPC endpoint to create.
23    #[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    // If making changes to this spec,
38    // you must also update src/cloud-resources/gen/vpcendpoints.crd.json
39    // so that cloudtest can register the CRD.
40    pub struct VpcEndpointSpec {
41        /// The name of the service to connect to.
42        pub aws_service_name: String,
43        /// The IDs of the availability zones in which the service is available.
44        pub availability_zone_ids: Vec<String>,
45        /// A suffix to use in the name of the IAM role that is created.
46        pub role_suffix: String,
47    }
48
49    #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
50    #[serde(rename_all = "camelCase")]
51    pub struct VpcEndpointStatus {
52        // This will be None if the customer hasn't allowed our principal, got the name of their
53        // VPC Endpoint Service wrong, or we've otherwise failed to create the VPC Endpoint.
54        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    /// The AWS SDK State enum is not serializable, so we have to make our own.
85    #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
86    #[serde(rename_all = "camelCase")]
87    pub enum VpcEndpointState {
88        // Internal States
89        PendingServiceDiscovery,
90        CreatingEndpoint,
91        RecreatingEndpoint,
92        UpdatingEndpoint,
93
94        // AWS States
95        // Connection established to the customer's VPC Endpoint Service.
96        Available,
97        Deleted,
98        Deleting,
99        Expired,
100        Failed,
101        // Customer has approved the connection. It should eventually move to Available.
102        Pending,
103        // Waiting on the customer to approve the connection.
104        PendingAcceptance,
105        Rejected,
106        Unknown,
107        // Could not place the endpoint in a subnet with the provided AZs
108        MissingAvailabilityZones,
109    }
110
111    impl VpcEndpointState {
112        // These are high level messages that can be used in conditions and
113        // may be forwarded to end users. It is important to make them concise
114        // and at a level which can be understand by end users of materialize
115        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        // Internal States
151        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}