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