rusty_cdk_core/cloudfront/
builder.rs

1use crate::cloudfront::{CacheBehavior, CachePolicy, CachePolicyConfig, CachePolicyProperties, CachePolicyRef, CookiesConfig, CustomOriginConfig, DefaultCacheBehavior, Distribution, DistributionConfig, DistributionProperties, DistributionRef, HeadersConfig, Origin, OriginAccessControl, OriginAccessControlConfig, OriginAccessControlRef, OriginControlProperties, OriginCustomHeader, ParametersInCacheKeyAndForwardedToOrigin, QueryStringsConfig, S3OriginConfig, ViewerCertificate, VpcOriginConfig};
2use crate::iam::Principal::Service;
3use crate::iam::{Effect, PolicyDocumentBuilder, ServicePrincipal, StatementBuilder};
4use crate::intrinsic::{get_att, get_ref, join, AWS_ACCOUNT_PSEUDO_PARAM};
5use crate::s3::BucketPolicyBuilder;
6use crate::s3::BucketRef;
7use crate::shared::http::HttpMethod::{Delete, Get, Head, Options, Patch, Post, Put};
8use crate::shared::Id;
9use crate::stack::{Resource, StackBuilder};
10use crate::wrappers::{CfConnectionTimeout, ConnectionAttempts, DefaultRootObject, IamAction, OriginPath, S3OriginReadTimeout};
11use serde_json::{json, Value};
12use std::marker::PhantomData;
13use crate::type_state;
14
15pub enum SslSupportedMethod {
16    SniOnly,
17    Vip,
18    StaticIp,
19}
20
21impl From<SslSupportedMethod> for String {
22    fn from(value: SslSupportedMethod) -> Self {
23        match value {
24            SslSupportedMethod::SniOnly => "sni-only".to_string(),
25            SslSupportedMethod::Vip => "vip".to_string(),
26            SslSupportedMethod::StaticIp => "static-ip".to_string(),
27        }
28    }
29}
30
31pub enum MinProtocolVersion {
32    SSLV3,
33    TLSv1,
34    TLSv1_1,
35    TLSv1_2_2018,
36    TLSv1_2_2019,
37    TLSv1_2_2021,
38    TLSv1_2_2025,
39    TLSv1_3,
40}
41
42impl From<MinProtocolVersion> for String {
43    fn from(value: MinProtocolVersion) -> Self {
44        match value {
45            MinProtocolVersion::SSLV3 => "SSLv3".to_string(),
46            MinProtocolVersion::TLSv1 => "TLSv1".to_string(),
47            MinProtocolVersion::TLSv1_1 => "TLSv1.1_2016".to_string(),
48            MinProtocolVersion::TLSv1_2_2018 => "TLSv1.2_2018".to_string(),
49            MinProtocolVersion::TLSv1_2_2019 => "TLSv1.2_2019".to_string(),
50            MinProtocolVersion::TLSv1_2_2021 => "TLSv1.2_2021".to_string(),
51            MinProtocolVersion::TLSv1_2_2025 => "TLSv1.2_2025".to_string(),
52            MinProtocolVersion::TLSv1_3 => "TLSv1.3_2025".to_string(),
53        }
54    }
55}
56
57type_state!(
58    ViewerCertificateState,
59    ViewerCertificateStateStartState,
60    ViewerCertificateStateAcmOrIamState,
61    ViewerCertificateStateEndState,
62);
63
64/// Builder for CloudFront viewer certificates.
65pub struct ViewerCertificateBuilder<T: ViewerCertificateState> {
66    phantom_data: PhantomData<T>,
67    cloudfront_default_cert: Option<bool>,
68    acm_cert_arn: Option<String>,
69    iam_cert_id: Option<String>,
70    min_protocol_version: Option<String>,
71    ssl_support_method: Option<String>,
72}
73
74impl Default for ViewerCertificateBuilder<ViewerCertificateStateStartState> {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl ViewerCertificateBuilder<ViewerCertificateStateStartState> {
81    pub fn new() -> ViewerCertificateBuilder<ViewerCertificateStateStartState> {
82        ViewerCertificateBuilder {
83            phantom_data: Default::default(),
84            acm_cert_arn: None,
85            cloudfront_default_cert: None,
86            iam_cert_id: None,
87            min_protocol_version: None,
88            ssl_support_method: None,
89        }
90    }
91
92    pub fn cloudfront_default_cert(self) -> ViewerCertificateBuilder<ViewerCertificateStateEndState> {
93        ViewerCertificateBuilder {
94            phantom_data: Default::default(),
95            cloudfront_default_cert: Some(true),
96            acm_cert_arn: self.acm_cert_arn,
97            iam_cert_id: self.iam_cert_id,
98            min_protocol_version: self.min_protocol_version,
99            ssl_support_method: self.ssl_support_method,
100        }
101    }
102
103    pub fn iam_cert_id(self, id: String) -> ViewerCertificateBuilder<ViewerCertificateStateAcmOrIamState> {
104        ViewerCertificateBuilder {
105            phantom_data: Default::default(),
106            cloudfront_default_cert: Some(true),
107            acm_cert_arn: self.acm_cert_arn,
108            iam_cert_id: Some(id),
109            min_protocol_version: self.min_protocol_version,
110            ssl_support_method: self.ssl_support_method,
111        }
112    }
113
114    pub fn acm_cert_arn(self, id: String) -> ViewerCertificateBuilder<ViewerCertificateStateAcmOrIamState> {
115        ViewerCertificateBuilder {
116            phantom_data: Default::default(),
117            cloudfront_default_cert: Some(true),
118            acm_cert_arn: Some(id),
119            iam_cert_id: self.iam_cert_id,
120            min_protocol_version: self.min_protocol_version,
121            ssl_support_method: self.ssl_support_method,
122        }
123    }
124}
125
126impl<T: ViewerCertificateState> ViewerCertificateBuilder<T> {
127    fn build_internal(self) -> ViewerCertificate {
128        ViewerCertificate {
129            cloudfront_default_cert: self.cloudfront_default_cert,
130            acm_cert_arn: self.acm_cert_arn,
131            iam_cert_id: self.iam_cert_id,
132            min_protocol_version: self.min_protocol_version,
133            ssl_support_method: self.ssl_support_method,
134        }
135    }
136}
137
138impl ViewerCertificateBuilder<ViewerCertificateStateAcmOrIamState> {
139    pub fn min_protocol_version(self, protocol_version: MinProtocolVersion) -> Self {
140        Self {
141            phantom_data: Default::default(),
142            min_protocol_version: Some(protocol_version.into()),
143            cloudfront_default_cert: self.cloudfront_default_cert,
144            acm_cert_arn: self.acm_cert_arn,
145            iam_cert_id: self.iam_cert_id,
146            ssl_support_method: self.ssl_support_method,
147        }
148    }
149
150    pub fn ssl_support_method(self, ssl_support: SslSupportedMethod) -> Self {
151        Self {
152            phantom_data: Default::default(),
153            ssl_support_method: Some(ssl_support.into()),
154            min_protocol_version: self.min_protocol_version,
155            cloudfront_default_cert: self.cloudfront_default_cert,
156            acm_cert_arn: self.acm_cert_arn,
157            iam_cert_id: self.iam_cert_id,
158        }
159    }
160
161    #[must_use]
162    pub fn build(self) -> ViewerCertificate {
163        self.build_internal()
164    }
165}
166
167impl ViewerCertificateBuilder<ViewerCertificateStateEndState> {
168    #[must_use]
169    pub fn build(self) -> ViewerCertificate {
170        self.build_internal()
171    }
172}
173
174pub enum Cookies {
175    None,
176    Whitelist(Vec<String>),
177    AllExcept(Vec<String>),
178    All,
179}
180pub enum QueryString {
181    None,
182    Whitelist(Vec<String>),
183    AllExcept(Vec<String>),
184    All,
185}
186pub enum Headers {
187    None,
188    Whitelist(Vec<String>),
189}
190
191/// Builder for cache key and forwarding parameters.
192///
193/// Configures which request parameters (cookies, headers, query strings) are included in the cache key and forwarded to the origin.
194pub struct ParametersInCacheKeyAndForwardedToOriginBuilder {
195    cookies_config: CookiesConfig,
196    headers_config: HeadersConfig,
197    query_strings_config: QueryStringsConfig,
198    accept_encoding_gzip: bool,
199    accept_encoding_brotli: Option<bool>,
200}
201
202impl ParametersInCacheKeyAndForwardedToOriginBuilder {
203    pub fn new(accept_encoding_gzip: bool, cookies: Cookies, query_string: QueryString, headers: Headers) -> Self {
204        let cookies_config = match cookies {
205            Cookies::None => CookiesConfig {
206                cookie_behavior: "none".to_string(),
207                cookies: None,
208            },
209            Cookies::Whitelist(list) => CookiesConfig {
210                cookie_behavior: "whitelist".to_string(),
211                cookies: Some(list),
212            },
213            Cookies::AllExcept(list) => CookiesConfig {
214                cookie_behavior: "allExcept".to_string(),
215                cookies: Some(list),
216            },
217            Cookies::All => CookiesConfig {
218                cookie_behavior: "all".to_string(),
219                cookies: None,
220            },
221        };
222        let query_strings_config = match query_string {
223            QueryString::None => QueryStringsConfig {
224                query_strings_behavior: "none".to_string(),
225                query_strings: None,
226            },
227            QueryString::Whitelist(list) => QueryStringsConfig {
228                query_strings_behavior: "whitelist".to_string(),
229                query_strings: Some(list),
230            },
231            QueryString::AllExcept(list) => QueryStringsConfig {
232                query_strings_behavior: "allExcept".to_string(),
233                query_strings: Some(list),
234            },
235            QueryString::All => QueryStringsConfig {
236                query_strings_behavior: "all".to_string(),
237                query_strings: None,
238            },
239        };
240        let headers_config = match headers {
241            Headers::None => HeadersConfig {
242                headers_behavior: "none".to_string(),
243                headers: None,
244            },
245            Headers::Whitelist(list) => HeadersConfig {
246                headers_behavior: "whitelist".to_string(),
247                headers: Some(list),
248            },
249        };
250
251        Self {
252            cookies_config,
253            headers_config,
254            query_strings_config,
255            accept_encoding_gzip,
256            accept_encoding_brotli: None,
257        }
258    }
259
260    pub fn accept_encoding_brotli(self, accept: bool) -> Self {
261        Self {
262            accept_encoding_brotli: Some(accept),
263            ..self
264        }
265    }
266
267    #[must_use]
268    pub fn build(self) -> ParametersInCacheKeyAndForwardedToOrigin {
269        ParametersInCacheKeyAndForwardedToOrigin {
270            cookies_config: self.cookies_config,
271            accept_encoding_brotli: self.accept_encoding_brotli,
272            accept_encoding_gzip: self.accept_encoding_gzip,
273            headers_config: self.headers_config,
274            query_strings_config: self.query_strings_config,
275        }
276    }
277}
278
279/// Builder for CloudFront cache policies.
280pub struct CachePolicyBuilder {
281    id: Id,
282    name: String,
283    default_ttl: u32,
284    min_ttl: u32,
285    max_ttl: u32,
286    cache_params: ParametersInCacheKeyAndForwardedToOrigin,
287}
288
289impl CachePolicyBuilder {
290    /// Creates a new CloudFront cache policy builder.
291    ///
292    /// # Arguments
293    /// * `id` - Unique identifier for the cache policy
294    /// * `unique_name` - Name for the cache policy (must be unique)
295    /// * `default_ttl` - Default time to live in seconds
296    /// * `min_ttl` - Minimum time to live in seconds
297    /// * `max_ttl` - Maximum time to live in seconds
298    /// * `cache_params` - Parameters for cache key and origin forwarding
299    pub fn new(
300        id: &str,
301        unique_name: &str,
302        default_ttl: u32,
303        min_ttl: u32,
304        max_ttl: u32,
305        cache_params: ParametersInCacheKeyAndForwardedToOrigin,
306    ) -> Self {
307        Self {
308            id: Id(id.to_string()),
309            name: unique_name.to_string(),
310            default_ttl,
311            min_ttl,
312            max_ttl,
313            cache_params,
314        }
315    }
316
317    pub fn build(self, stack_builder: &mut StackBuilder) -> CachePolicyRef {
318        let resource_id = Resource::generate_id("CachePolicy");
319        stack_builder.add_resource(CachePolicy {
320            id: self.id,
321            resource_id: resource_id.clone(),
322            r#type: "AWS::CloudFront::CachePolicy".to_string(),
323            properties: CachePolicyProperties {
324                config: CachePolicyConfig {
325                    default_ttl: self.default_ttl,
326                    min_ttl: self.min_ttl,
327                    max_ttl: self.max_ttl,
328                    name: self.name,
329                    params_in_cache_key_and_forwarded: self.cache_params,
330                },
331            },
332        });
333        CachePolicyRef::new(resource_id)
334    }
335}
336
337pub enum HttpVersion {
338    Http1,
339    Http2,
340    Http3,
341    Http2And3,
342}
343
344impl From<HttpVersion> for String {
345    fn from(value: HttpVersion) -> Self {
346        match value {
347            HttpVersion::Http1 => "http1.1".to_string(),
348            HttpVersion::Http2 => "http2".to_string(),
349            HttpVersion::Http3 => "http3".to_string(),
350            HttpVersion::Http2And3 => "http2and3".to_string(),
351        }
352    }
353}
354
355pub enum PriceClass {
356    PriceClass100,
357    PriceClass200,
358    PriceClassAll,
359    None,
360}
361
362impl From<PriceClass> for String {
363    fn from(value: PriceClass) -> Self {
364        match value {
365            PriceClass::PriceClass100 => "PriceClass_100".to_string(),
366            PriceClass::PriceClass200 => "PriceClass_200".to_string(),
367            PriceClass::PriceClassAll => "PriceClass_All".to_string(),
368            PriceClass::None => "None".to_string(),
369        }
370    }
371}
372
373pub enum OriginProtocolPolicy {
374    HttpOnly,
375    MatchViewer,
376    HttpsOnly,
377}
378
379impl From<OriginProtocolPolicy> for String {
380    fn from(value: OriginProtocolPolicy) -> Self {
381        match value {
382            OriginProtocolPolicy::HttpOnly => "http-only".to_string(),
383            OriginProtocolPolicy::MatchViewer => "match-viewer".to_string(),
384            OriginProtocolPolicy::HttpsOnly => "https-only".to_string(),
385        }
386    }
387}
388
389pub enum IpAddressType {
390    IPv4,
391    IPv6,
392    Dualstack,
393}
394
395impl From<IpAddressType> for String {
396    fn from(value: IpAddressType) -> Self {
397        match value {
398            IpAddressType::IPv4 => "ipv4".to_string(),
399            IpAddressType::IPv6 => "ipv6".to_string(),
400            IpAddressType::Dualstack => "dualstack".to_string(),
401        }
402    }
403}
404
405type_state!(
406    OriginState,
407    OriginStartState,
408    OriginS3OriginState,
409    OriginCustomOriginState,
410);
411
412// TODO more origins
413
414/// Builder for CloudFront distribution origins.
415pub struct OriginBuilder<T: OriginState> {
416    phantom_data: PhantomData<T>,
417    id: String,
418    bucket_arn: Option<Value>,
419    bucket_ref: Option<Value>,
420    domain_name: Option<Value>,
421    connection_attempts: Option<u8>,
422    connection_timeout: Option<u16>,
423    response_completion_timeout: Option<u16>,
424    origin_access_control_id: Option<Value>,
425    origin_path: Option<String>,
426    s3origin_config: Option<S3OriginConfig>,
427    origin_custom_headers: Option<Vec<OriginCustomHeader>>,
428    vpc_origin_config: Option<VpcOriginConfig>,
429    custom_origin_config: Option<CustomOriginConfig>,
430}
431
432impl OriginBuilder<OriginStartState> {
433    pub fn new(origin_id: &str) -> Self {
434        Self {
435            phantom_data: Default::default(),
436            id: origin_id.to_string(),
437            bucket_arn: None,
438            bucket_ref: None,
439            domain_name: None,
440            connection_attempts: None,
441            connection_timeout: None,
442            origin_access_control_id: None,
443            origin_path: None,
444            response_completion_timeout: None,
445            s3origin_config: None,
446            origin_custom_headers: None,
447            vpc_origin_config: None,
448            custom_origin_config: None,
449        }
450    }
451
452    /// Configures an S3 bucket as the origin.
453    ///
454    /// Automatically creates a bucket policy allowing CloudFront access via Origin Access Control.
455    pub fn s3_origin(
456        self,
457        bucket: &BucketRef,
458        oac: &OriginAccessControlRef,
459        origin_read_timeout: Option<S3OriginReadTimeout>,
460    ) -> OriginBuilder<OriginS3OriginState> {
461        let s3origin_config = S3OriginConfig {
462            origin_read_timeout: origin_read_timeout.map(|v| v.0),
463        };
464
465        let domain = bucket.get_att("RegionalDomainName");
466
467        OriginBuilder {
468            phantom_data: Default::default(),
469            id: self.id.to_string(),
470            bucket_arn: Some(bucket.get_arn()),
471            bucket_ref: Some(bucket.get_ref()),
472            domain_name: Some(domain),
473            connection_attempts: self.connection_attempts,
474            connection_timeout: self.connection_timeout,
475            origin_access_control_id: Some(oac.get_att("Id")),
476            origin_path: self.origin_path,
477            response_completion_timeout: self.response_completion_timeout,
478            origin_custom_headers: self.origin_custom_headers,
479            s3origin_config: Some(s3origin_config),
480            vpc_origin_config: None,
481            custom_origin_config: None,
482        }
483    }
484    
485    // TODO add test
486    //  and could also add additional methods for ELB etc. that pass in the ELB, to have extra safety
487
488    /// Configures a custom origin.
489    pub fn custom_origin(self, domain: &str, policy: OriginProtocolPolicy) -> OriginBuilder<OriginCustomOriginState> {
490        let custom_origin_config = CustomOriginConfig {
491            origin_protocol_policy: policy.into(),
492            http_port: None,
493            https_port: None,
494            ip_address_type: None,
495            origin_keep_alive_timeout: None,
496            origin_read_timeout: None,
497            origin_ssl_protocols: None,
498        };
499
500        OriginBuilder {
501            phantom_data: Default::default(),
502            id: self.id.to_string(),
503            domain_name: Some(Value::String(domain.to_string())),
504            connection_attempts: self.connection_attempts,
505            connection_timeout: self.connection_timeout,
506            origin_path: self.origin_path,
507            response_completion_timeout: self.response_completion_timeout,
508            origin_custom_headers: self.origin_custom_headers,
509            custom_origin_config: Some(custom_origin_config),
510            vpc_origin_config: None,
511            origin_access_control_id: None,
512            s3origin_config: None,
513            bucket_arn: None,
514            bucket_ref: None,
515        }
516    }
517}
518
519impl<T: OriginState> OriginBuilder<T> {
520    pub fn connection_attempts(self, attempts: ConnectionAttempts) -> Self {
521        Self {
522            connection_attempts: Some(attempts.0),
523            ..self
524        }
525    }
526
527    pub fn timeouts(self, timeouts: CfConnectionTimeout) -> Self {
528        Self {
529            connection_timeout: timeouts.0,
530            response_completion_timeout: timeouts.1,
531            ..self
532        }
533    }
534
535    pub fn origin_path(self, path: OriginPath) -> Self {
536        Self {
537            origin_path: Some(path.0),
538            ..self
539        }
540    }
541
542    fn build_internal(self) -> Origin {
543        Origin {
544            id: self.id,
545            s3_bucket_policy: None,
546            domain_name: self.domain_name.expect("domain name should be present for cloudfront distribution"),
547            connection_attempts: self.connection_attempts,
548            connection_timeout: self.connection_timeout,
549            origin_access_control_id: self.origin_access_control_id,
550            origin_path: self.origin_path,
551            response_completion_timeout: self.response_completion_timeout,
552            s3origin_config: self.s3origin_config,
553            origin_custom_headers: self.origin_custom_headers,
554            vpc_origin_config: self.vpc_origin_config,
555            custom_origin_config: self.custom_origin_config,
556        }
557    }
558}
559
560impl OriginBuilder<OriginCustomOriginState> {
561    pub fn ip_address_type(self, address_type: IpAddressType) -> Self {
562        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
563        config.ip_address_type = Some(address_type.into());
564
565        OriginBuilder {
566            custom_origin_config: Some(config),
567            ..self
568        }
569    }
570    pub fn http_port(self, port: u16) -> Self {
571        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
572        config.http_port = Some(port);
573
574        OriginBuilder {
575            custom_origin_config: Some(config),
576            ..self
577        }
578    }
579
580    pub fn https_port(self, port: u16) -> Self {
581        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
582        config.https_port = Some(port);
583
584        OriginBuilder {
585            custom_origin_config: Some(config),
586            ..self
587        }
588    }
589
590    pub fn origin_keep_alive_timeout(self, timeout: u8) -> Self {
591        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
592        config.origin_keep_alive_timeout = Some(timeout);
593
594        OriginBuilder {
595            custom_origin_config: Some(config),
596            ..self
597        }
598    }
599
600    pub fn origin_read_timeout(self, timeout: u8) -> Self {
601        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
602        config.origin_read_timeout = Some(timeout);
603
604        OriginBuilder {
605            custom_origin_config: Some(config),
606            ..self
607        }
608    }
609
610    pub fn add_origin_ssl_protocol(self, protocol: String) -> Self {
611        let mut config = self.custom_origin_config.expect("custom config to be present in Custom Origin State");
612
613        let protocols = if let Some(mut protocols) = config.origin_ssl_protocols {
614            protocols.push(protocol);
615            protocols
616        } else {
617            vec![protocol]
618        };
619
620        config.origin_ssl_protocols = Some(protocols);
621
622        OriginBuilder {
623            custom_origin_config: Some(config),
624            ..self
625        }
626    }
627
628    pub fn build(self) -> Origin {
629        self.build_internal()
630    }
631}
632
633impl OriginBuilder<OriginS3OriginState> {
634    pub fn build(mut self) -> Origin {
635        let bucket_ref = self.bucket_ref.take().expect("bucket ref to be present in S3 origin state");
636        let bucket_arn = self.bucket_arn.take().expect("bucket arn to be present in S3 origin state");
637
638        let bucket_items = vec![join("", vec![bucket_arn, Value::String("/*".to_string())])];
639        let statement = StatementBuilder::new(vec![IamAction("s3:GetObject".to_string())], Effect::Allow)
640            .resources(bucket_items)
641            .principal(Service(ServicePrincipal {
642                service: "cloudfront.amazonaws.com".to_string(),
643            }))
644            .build();
645        let doc = PolicyDocumentBuilder::new(vec![statement]).build();
646        let bucket_policy_id = format!("{}-website-s3-policy", self.id);
647        let (_, s3_policy) = BucketPolicyBuilder::new_with_bucket_ref(bucket_policy_id.as_str(), bucket_ref, doc).raw_build();
648
649        let mut origin = self.build_internal();
650        origin.s3_bucket_policy = Some(s3_policy);
651
652        origin
653    }
654}
655
656pub enum DefaultCacheAllowedMethods {
657    GetHead,
658    GetHeadOptions,
659    All,
660}
661
662impl From<DefaultCacheAllowedMethods> for Vec<String> {
663    fn from(value: DefaultCacheAllowedMethods) -> Self {
664        match value {
665            DefaultCacheAllowedMethods::GetHead => vec![Get.into(), Head.into()],
666            DefaultCacheAllowedMethods::GetHeadOptions => vec![Get.into(), Head.into(), Options.into()],
667            DefaultCacheAllowedMethods::All => vec![
668                Get.into(),
669                Head.into(),
670                Options.into(),
671                Put.into(),
672                Patch.into(),
673                Post.into(),
674                Delete.into(),
675            ],
676        }
677    }
678}
679
680pub enum DefaultCacheCachedMethods {
681    GetHead,
682    GetHeadOptions,
683}
684
685impl From<DefaultCacheCachedMethods> for Vec<String> {
686    fn from(value: DefaultCacheCachedMethods) -> Self {
687        match value {
688            DefaultCacheCachedMethods::GetHead => vec![Get.into(), Head.into()],
689            DefaultCacheCachedMethods::GetHeadOptions => vec![Get.into(), Head.into(), Options.into()],
690        }
691    }
692}
693
694pub enum ViewerProtocolPolicy {
695    AllowAll,
696    RedirectToHttps,
697    HttpsOnly,
698}
699
700impl From<ViewerProtocolPolicy> for String {
701    fn from(value: ViewerProtocolPolicy) -> Self {
702        match value {
703            ViewerProtocolPolicy::AllowAll => "allow-all".to_string(),
704            ViewerProtocolPolicy::RedirectToHttps => "redirect-to-https".to_string(),
705            ViewerProtocolPolicy::HttpsOnly => "https-only".to_string(),
706        }
707    }
708}
709
710/// Builder for CloudFront default cache behavior.
711pub struct DefaultCacheBehaviorBuilder {
712    target_origin_id: String,
713    cache_policy_id: Value,
714    viewer_protocol_policy: String,
715    allowed_methods: Option<Vec<String>>,
716    cached_methods: Option<Vec<String>>,
717    compress: Option<bool>,
718}
719
720impl DefaultCacheBehaviorBuilder {
721    pub fn new(origin: &Origin, policy: &CachePolicyRef, viewer_protocol_policy: ViewerProtocolPolicy) -> Self {
722        Self {
723            target_origin_id: origin.get_origin_id().to_string(),
724            cache_policy_id: policy.get_att("Id"),
725            viewer_protocol_policy: viewer_protocol_policy.into(),
726            allowed_methods: None,
727            cached_methods: None,
728            compress: None,
729        }
730    }
731
732    pub fn allowed_methods(self, methods: DefaultCacheAllowedMethods) -> Self {
733        Self {
734            allowed_methods: Some(methods.into()),
735            target_origin_id: self.target_origin_id,
736            cache_policy_id: self.cache_policy_id,
737            viewer_protocol_policy: self.viewer_protocol_policy,
738            cached_methods: self.cached_methods,
739            compress: self.compress,
740        }
741    }
742
743    pub fn cached_methods(self, methods: DefaultCacheCachedMethods) -> Self {
744        Self {
745            cached_methods: Some(methods.into()),
746            target_origin_id: self.target_origin_id,
747            cache_policy_id: self.cache_policy_id,
748            viewer_protocol_policy: self.viewer_protocol_policy,
749            allowed_methods: self.allowed_methods,
750            compress: self.compress,
751        }
752    }
753
754    pub fn compress(self, compress: bool) -> Self {
755        Self {
756            compress: Some(compress),
757            target_origin_id: self.target_origin_id,
758            cache_policy_id: self.cache_policy_id,
759            viewer_protocol_policy: self.viewer_protocol_policy,
760            allowed_methods: self.allowed_methods,
761            cached_methods: self.cached_methods,
762        }
763    }
764
765    pub fn build(self) -> DefaultCacheBehavior {
766        DefaultCacheBehavior {
767            target_origin_id: self.target_origin_id,
768            cache_policy_id: self.cache_policy_id,
769            viewer_protocol_policy: self.viewer_protocol_policy,
770            allowed_methods: self.allowed_methods,
771            cached_methods: self.cached_methods,
772            compress: self.compress,
773        }
774    }
775}
776
777type_state!(
778    DistributionState,
779    DistributionStartState,
780    DistributionOriginState,
781);
782
783pub enum SigningBehavior {
784    Never,
785    NoOverride,
786    Always,
787}
788
789impl From<SigningBehavior> for String {
790    fn from(value: SigningBehavior) -> Self {
791        match value {
792            SigningBehavior::Never => "never".to_string(),
793            SigningBehavior::NoOverride => "no-override".to_string(),
794            SigningBehavior::Always => "always".to_string(),
795        }
796    }
797}
798
799pub enum SigningProtocol {
800    SigV4,
801}
802
803impl From<SigningProtocol> for String {
804    fn from(value: SigningProtocol) -> Self {
805        match value {
806            SigningProtocol::SigV4 => "sigv4".to_string(),
807        }
808    }
809}
810
811pub enum OriginAccessControlType {
812    S3,
813    MediaStore,
814    Lambda,
815    MediaPackageV2,
816}
817
818impl From<OriginAccessControlType> for String {
819    fn from(value: OriginAccessControlType) -> Self {
820        match value {
821            OriginAccessControlType::S3 => "s3".to_string(),
822            OriginAccessControlType::MediaStore => "mediastore".to_string(),
823            OriginAccessControlType::Lambda => "lambda".to_string(),
824            OriginAccessControlType::MediaPackageV2 => "mediapackagev2".to_string(),
825        }
826    }
827}
828
829/// Builder for CloudFront Origin Access Control.
830///
831/// Controls access from CloudFront to origins like S3 buckets.
832pub struct OriginAccessControlBuilder {
833    id: Id,
834    name: String,
835    origin_access_control_type: OriginAccessControlType,
836    signing_behavior: SigningBehavior,
837    signing_protocol: SigningProtocol,
838}
839
840impl OriginAccessControlBuilder {
841    /// Creates a new CloudFront Origin Access Control builder.
842    ///
843    /// # Arguments
844    /// * `id` - Unique identifier for the origin access control
845    /// * `name` - Name of the origin access control
846    /// * `origin_access_control_type` - Type of origin (S3, MediaStore, Lambda, etc.)
847    /// * `signing_behavior` - When to sign requests (Never, NoOverride, Always)
848    /// * `signing_protocol` - Protocol for signing requests
849    pub fn new(
850        id: &str,
851        name: &str,
852        origin_access_control_type: OriginAccessControlType,
853        signing_behavior: SigningBehavior,
854        signing_protocol: SigningProtocol,
855    ) -> Self {
856        Self {
857            id: Id(id.to_string()),
858            name: name.to_string(),
859            origin_access_control_type,
860            signing_behavior,
861            signing_protocol,
862        }
863    }
864
865    pub fn build(self, stack_builder: &mut StackBuilder) -> OriginAccessControlRef {
866        let resource_id = Resource::generate_id("OAC");
867        stack_builder.add_resource(OriginAccessControl {
868            id: self.id,
869            resource_id: resource_id.clone(),
870            r#type: "AWS::CloudFront::OriginAccessControl".to_string(),
871            properties: OriginControlProperties {
872                config: OriginAccessControlConfig {
873                    name: self.name,
874                    origin_access_control_type: self.origin_access_control_type.into(),
875                    signing_behavior: self.signing_behavior.into(),
876                    signing_protocol: self.signing_protocol.into(),
877                },
878            },
879        });
880        OriginAccessControlRef::new(resource_id)
881    }
882}
883
884/// Builder for CloudFront distributions.
885///
886/// Creates a CloudFront distribution with origins, cache behaviors, and other configuration.
887///
888/// # Example
889///
890/// ```rust,no_run
891/// use rusty_cdk_core::stack::StackBuilder;
892/// use rusty_cdk_core::cloudfront::{DistributionBuilder, OriginBuilder, DefaultCacheBehaviorBuilder};
893/// use rusty_cdk_core::s3::BucketBuilder;
894/// use rusty_cdk_core::wrappers::*;
895///
896/// let mut stack_builder = StackBuilder::new();
897///
898/// let bucket = unimplemented!("create a bucket");
899/// let oac = unimplemented!("create an origin access control");
900/// let policy = unimplemented!("create an origin");
901/// let viewer_protocol_policy = unimplemented!("create a viewer protocol");
902///
903/// let origin = OriginBuilder::new("my-origin").s3_origin(&bucket, &oac, None).build();
904/// let cache_behavior = DefaultCacheBehaviorBuilder::new(&origin, &policy, viewer_protocol_policy).build();
905///
906/// let distribution = DistributionBuilder::new("my-distribution", cache_behavior)
907///     .origins(vec![origin])
908///     .build(&mut stack_builder);
909/// ```
910pub struct DistributionBuilder<T: DistributionState> {
911    phantom_data: PhantomData<T>,
912    id: Id,
913    enabled: bool,
914    default_cache_behavior: DefaultCacheBehavior,
915    price_class: Option<String>,
916    http_version: Option<String>,
917    aliases: Option<Vec<String>>,
918    cnames: Option<Vec<String>>,
919    ipv6_enabled: Option<bool>,
920    viewer_certificate: Option<ViewerCertificate>,
921    cache_behaviors: Option<Vec<CacheBehavior>>,
922    default_root_object: Option<String>,
923    // TODO add. and either this or the next is required!
924    // origin_groups: Option<OriginGroups>,
925    origins: Option<Vec<Origin>>,
926}
927
928impl DistributionBuilder<DistributionStartState> {
929    /// Creates a new CloudFront distribution builder.
930    ///
931    /// # Arguments
932    /// * `id` - Unique identifier for the distribution
933    /// * `default_cache_behavior` - Default cache behavior for all requests
934    pub fn new(id: &str, default_cache_behavior: DefaultCacheBehavior) -> Self {
935        Self {
936            phantom_data: Default::default(),
937            id: Id(id.to_string()),
938            enabled: true,
939            default_cache_behavior,
940            aliases: None,
941            cache_behaviors: None,
942            cnames: None,
943            default_root_object: None,
944            http_version: None,
945            ipv6_enabled: None,
946            origins: None,
947            price_class: None,
948            viewer_certificate: None,
949        }
950    }
951
952    pub fn origins(self, origins: Vec<Origin>) -> DistributionBuilder<DistributionOriginState> {
953        DistributionBuilder {
954            phantom_data: Default::default(),
955            origins: Some(origins),
956            id: self.id,
957            enabled: self.enabled,
958            default_cache_behavior: self.default_cache_behavior,
959            price_class: self.price_class,
960            http_version: self.http_version,
961            aliases: self.aliases,
962            cnames: self.cnames,
963            ipv6_enabled: self.ipv6_enabled,
964            viewer_certificate: self.viewer_certificate,
965            cache_behaviors: self.cache_behaviors,
966            default_root_object: self.default_root_object,
967        }
968    }
969}
970
971impl DistributionBuilder<DistributionOriginState> {
972    pub fn build(mut self, stack_builder: &mut StackBuilder) -> DistributionRef {
973        let mut origins = self.origins.take().expect("origins to be present in distribution origin state");
974        let resource_id = Resource::generate_id("CloudFrontDistribution");
975
976        origins
977            .iter_mut()
978            .filter(|o| o.s3_bucket_policy.is_some())
979            .map(|s3| {
980                let mut policy = s3
981                    .s3_bucket_policy
982                    .take()
983                    .expect("just checked that this was present, only need to use it this one time");
984                let distro_id = get_att(&resource_id, "Id");
985                let source_arn_value = join(
986                    "",
987                    vec![
988                        Value::String("arn:aws:cloudfront::".to_string()),
989                        get_ref(AWS_ACCOUNT_PSEUDO_PARAM),
990                        Value::String(":distribution/".to_string()),
991                        distro_id,
992                    ],
993                );
994                let distro_condition = json!({
995                    "StringEquals": {
996                        "AWS:SourceArn": source_arn_value
997                    }
998                });
999                policy
1000                    .properties
1001                    .policy_document
1002                    .statements
1003                    .iter_mut()
1004                    .for_each(|v| v.condition = Some(distro_condition.clone()));
1005                policy
1006            })
1007            .for_each(|p| {
1008                stack_builder.add_resource(p);
1009            });
1010
1011        self.origins = Some(origins);
1012
1013        self.build_internal(resource_id, stack_builder)
1014    }
1015}
1016
1017impl<T: DistributionState> DistributionBuilder<T> {
1018    pub fn add_cache_behavior(mut self, behavior: CacheBehavior) -> Self {
1019        if let Some(mut behaviors) = self.cache_behaviors {
1020            behaviors.push(behavior);
1021            self.cache_behaviors = Some(behaviors);
1022        } else {
1023            self.cache_behaviors = Some(vec![behavior])
1024        }
1025        self
1026    }
1027
1028    pub fn aliases(self, aliases: Vec<String>) -> Self {
1029        Self {
1030            aliases: Some(aliases),
1031            ..self
1032        }
1033    }
1034
1035    // could have a regex for this?
1036    pub fn cnames(self, cnames: Vec<String>) -> Self {
1037        Self {
1038            cnames: Some(cnames),
1039            ..self
1040        }
1041    }
1042
1043    pub fn price_class(self, price_class: PriceClass) -> Self {
1044        Self {
1045            price_class: Some(price_class.into()),
1046            ..self
1047        }
1048    }
1049
1050    pub fn http_version(self, http_version: HttpVersion) -> Self {
1051        Self {
1052            http_version: Some(http_version.into()),
1053            ..self
1054        }
1055    }
1056
1057    pub fn ipv6_enabled(self, enabled: bool) -> Self {
1058        Self {
1059            ipv6_enabled: Some(enabled),
1060            ..self
1061        }
1062    }
1063
1064    pub fn viewer_certificate(self, viewer_certificate: ViewerCertificate) -> Self {
1065        Self {
1066            viewer_certificate: Some(viewer_certificate),
1067            ..self
1068        }
1069    }
1070
1071    pub fn enabled(self, enabled: bool) -> Self {
1072        Self { enabled, ..self }
1073    }
1074
1075    pub fn default_root_object(self, default: DefaultRootObject) -> Self {
1076        Self {
1077            default_root_object: Some(default.0),
1078            ..self
1079        }
1080    }
1081
1082    fn build_internal(self, resource_id: String, stack_builder: &mut StackBuilder) -> DistributionRef {
1083        let config = DistributionConfig {
1084            enabled: self.enabled,
1085            default_cache_behavior: self.default_cache_behavior,
1086            aliases: self.aliases,
1087            cache_behaviors: self.cache_behaviors,
1088            cnames: self.cnames,
1089            default_root_object: self.default_root_object.unwrap_or_default(),
1090            http_version: self.http_version,
1091            ipv6_enabled: self.ipv6_enabled,
1092            price_class: self.price_class,
1093            viewer_certificate: self.viewer_certificate,
1094            origins: self.origins,
1095            origin_groups: None,
1096        };
1097        stack_builder.add_resource(Distribution {
1098            id: self.id,
1099            resource_id: resource_id.clone(),
1100            r#type: "AWS::CloudFront::Distribution".to_string(),
1101            properties: DistributionProperties { config },
1102        });
1103
1104        DistributionRef::new(resource_id)
1105    }
1106}