middleman-s3_sync 4.0.3 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.s3_sync.sample +20 -0
- data/README.md +216 -40
- data/lib/middleman/redirect.rb +21 -0
- data/lib/middleman/s3_sync/cloudfront.rb +192 -0
- data/lib/middleman/s3_sync/options.rb +6 -0
- data/lib/middleman/s3_sync/resource.rb +129 -24
- data/lib/middleman/s3_sync/version.rb +1 -1
- data/lib/middleman/s3_sync.rb +87 -38
- data/lib/middleman-s3_sync/commands.rb +32 -1
- data/lib/middleman-s3_sync/extension.rb +19 -3
- data/lib/middleman-s3_sync.rb +1 -0
- data/middleman-s3_sync.gemspec +9 -5
- data/spec/cloudfront_spec.rb +391 -0
- data/spec/resource_spec.rb +78 -13
- data/spec/s3_sync_integration_spec.rb +132 -0
- data/spec/spec_helper.rb +41 -1
- metadata +77 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7d5baff2d2863949eb480b7ac94d105d3fdcc2c7df67fab66c649f575dd686a4
|
4
|
+
data.tar.gz: 70c12f42e60eabb0bf063d47a3e42f2766ea883bf47c17557cf323b15a9e546a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 663ab12a585095543adb3843976e1d24bbf5d3e6bb52a5285b3ce7de5cc7796cb916b72dc330d110225e191325add7e6eed81797abccae9c4e66d0b1d84c8608
|
7
|
+
data.tar.gz: b9b720083616f701318747b661aa04d9257c9539136ae0a2d153cc314eced337baa7ca158cb6e10de1d86ef1bd29cb90cd49b7ee1160c69bdf7b9bab09d4c27f
|
data/.gitignore
CHANGED
data/.s3_sync.sample
CHANGED
@@ -1,3 +1,23 @@
|
|
1
1
|
---
|
2
2
|
aws_access_key_id: <AWS Access Key>
|
3
3
|
aws_secret_access_key: <AWS Secret Access Key>
|
4
|
+
bucket: <S3 Bucket Name>
|
5
|
+
region: us-east-1
|
6
|
+
delete: true
|
7
|
+
after_build: false
|
8
|
+
prefer_gzip: true
|
9
|
+
path_style: true
|
10
|
+
reduced_redundancy_storage: false
|
11
|
+
acl: public-read
|
12
|
+
encryption: false
|
13
|
+
prefix: ''
|
14
|
+
version_bucket: false
|
15
|
+
index_document: index.html
|
16
|
+
error_document: 404.html
|
17
|
+
|
18
|
+
# CloudFront Invalidation Settings
|
19
|
+
cloudfront_distribution_id: <CloudFront Distribution ID> # e.g., E1234567890123
|
20
|
+
cloudfront_invalidate: false # Set to true to enable
|
21
|
+
cloudfront_invalidate_all: false # Set to true to invalidate all paths (/*)
|
22
|
+
cloudfront_invalidation_batch_size: 1000 # Max paths per invalidation request
|
23
|
+
cloudfront_wait: false # Set to true to wait for invalidation to complete
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Middleman::S3Sync
|
2
2
|
|
3
|
-
[](https://gitter.im/fredjean/middleman-s3_sync?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://gitter.im/fredjean/middleman-s3_sync?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://codeclimate.com/github/fredjean/middleman-s3_sync) [](https://travis-ci.org/fredjean/middleman-s3_sync)
|
4
4
|
|
5
5
|
This gem determines which files need to be added, updated and optionally deleted
|
6
6
|
and only transfer these files up. This reduces the impact of an update
|
@@ -54,6 +54,9 @@ activate :s3_sync do |s3_sync|
|
|
54
54
|
s3_sync.version_bucket = false
|
55
55
|
s3_sync.index_document = 'index.html'
|
56
56
|
s3_sync.error_document = '404.html'
|
57
|
+
s3_sync.cloudfront_distribution_id = 'E1234567890123' # CloudFront distribution ID
|
58
|
+
s3_sync.cloudfront_invalidate = false # Enable CloudFront invalidation
|
59
|
+
s3_sync.cloudfront_invalidate_all = false # Invalidate all paths (/*) or only changed files
|
57
60
|
end
|
58
61
|
```
|
59
62
|
|
@@ -76,81 +79,254 @@ The following defaults apply to the configuration items:
|
|
76
79
|
| encryption | ```false``` |
|
77
80
|
| acl | ```'public-read'``` |
|
78
81
|
| version_bucket | ```false``` |
|
82
|
+
| cloudfront_distribution_id | - |
|
83
|
+
| cloudfront_invalidate | ```false``` |
|
84
|
+
| cloudfront_invalidate_all | ```false``` |
|
85
|
+
| cloudfront_wait | ```false``` |
|
79
86
|
|
80
87
|
## Setting AWS Credentials
|
81
88
|
|
82
|
-
There are several ways to provide
|
89
|
+
There are several secure ways to provide AWS credentials for s3_sync. Using temporary, least-privilege credentials is strongly recommended.
|
83
90
|
|
84
|
-
####
|
91
|
+
#### Best Practices for AWS Credentials (Recommended)
|
85
92
|
|
86
|
-
|
87
|
-
that is passed to the activate method.
|
93
|
+
##### 1. AWS IAM Roles (Most Secure)
|
88
94
|
|
89
|
-
|
90
|
-
|
95
|
+
###### For CI/CD and Cloud Environments
|
96
|
+
- **EC2 Instance Profiles**: If running on EC2, use IAM roles attached to your instance. Credentials are automatically rotated and managed by AWS.
|
97
|
+
- **ECS Task Roles**: For container workloads, use task roles to provide permissions to specific containers.
|
98
|
+
- **CI/CD Service Roles**: Most CI/CD services (GitHub Actions, CircleCI, etc.) offer native AWS integrations that support assuming IAM roles.
|
91
99
|
|
92
|
-
|
100
|
+
###### For Local Development
|
101
|
+
- **AWS IAM Identity Center (SSO)** and configured profiles in your AWS config file
|
102
|
+
- **AWS CLI credential process** to integrate with external identity providers
|
103
|
+
- **Role assumption** with short-lived credentials through `aws sts assume-role`
|
93
104
|
|
94
|
-
|
95
|
-
The credentials are passed in the YAML format. The keys match the
|
96
|
-
options keys.
|
97
|
-
|
98
|
-
The .s3_sync file takes precedence to the configuration passed in the
|
99
|
-
activate method.
|
100
|
-
|
101
|
-
A sample `.s3_sync` file is included at the root of this repo.
|
102
|
-
|
103
|
-
> Make sure to add .s3_sync to your ignore list if you choose this approach. Not doing so may expose
|
104
|
-
> credentials to the world.
|
105
|
+
To use these methods, you don't need to specify credentials in your Middleman configuration. The AWS SDK will automatically detect and use them.
|
105
106
|
|
106
|
-
|
107
|
+
##### 2. Environment Variables with Temporary Credentials
|
107
108
|
|
108
|
-
|
109
|
-
`--aws_access_key_id` (`-k`) and `--aws_secret_access_key` (`-s`). They should override
|
110
|
-
any other settings if specified.
|
109
|
+
Using environment variables with short-lived credentials from role assumption:
|
111
110
|
|
112
|
-
|
111
|
+
```bash
|
112
|
+
# Obtain temporary credentials via assume-role or similar
|
113
|
+
# Then set these environment variables
|
114
|
+
export AWS_ACCESS_KEY_ID="temporary-access-key"
|
115
|
+
export AWS_SECRET_ACCESS_KEY="temporary-secret-key"
|
116
|
+
export AWS_SESSION_TOKEN="temporary-session-token"
|
117
|
+
export AWS_BUCKET="your-bucket-name"
|
118
|
+
```
|
113
119
|
|
114
|
-
|
115
|
-
map to the following values:
|
120
|
+
These environment variables are used when credentials are not otherwise specified:
|
116
121
|
|
117
122
|
| Setting | Environment Variable |
|
118
123
|
| --------------------- | ---------------------------------- |
|
119
124
|
| aws_access_key_id | ```ENV['AWS_ACCESS_KEY_ID']``` |
|
120
125
|
| aws_secret_access_key | ```ENV['AWS_SECRET_ACCESS_KEY']``` |
|
126
|
+
| aws_session_token | ```ENV['AWS_SESSION_TOKEN']``` |
|
121
127
|
| bucket | ```ENV['AWS_BUCKET']``` |
|
122
128
|
|
123
|
-
|
124
|
-
method or passed through the ```.s3_sync``` configuration file.
|
129
|
+
#### Alternative Methods (Not Recommended for Production)
|
125
130
|
|
126
|
-
|
131
|
+
The following methods are less secure and should be avoided in production environments:
|
127
132
|
|
128
|
-
|
133
|
+
##### Through `.s3_sync` File
|
129
134
|
|
130
|
-
|
135
|
+
You can create a `.s3_sync` at the root of your middleman project.
|
136
|
+
The credentials are passed in the YAML format. The keys match the options keys.
|
137
|
+
|
138
|
+
A sample `.s3_sync` file is included at the root of this repo.
|
139
|
+
|
140
|
+
> **SECURITY WARNING**: If using this approach, ensure you add `.s3_sync` to your `.gitignore` to prevent
|
141
|
+
> accidentally committing credentials to your repository. Consider using this only for local development
|
142
|
+
> and only with temporary credentials.
|
143
|
+
|
144
|
+
##### Through `config.rb`
|
145
|
+
|
146
|
+
You can set the AWS credentials in the activation block, but this is strongly discouraged:
|
147
|
+
|
148
|
+
> **SECURITY WARNING**: This method could lead to credentials being committed to version control,
|
149
|
+
> potentially exposing sensitive information. Never use long-lived credentials with this method.
|
150
|
+
|
151
|
+
##### Through Command Line
|
152
|
+
|
153
|
+
Credentials can be passed via command line options, but this may expose them in shell history:
|
154
|
+
|
155
|
+
> **SECURITY WARNING**: Command line parameters may be visible in process listings or shell history.
|
156
|
+
> Consider using environment variables or IAM roles instead.
|
157
|
+
|
158
|
+
## CloudFront Invalidation
|
159
|
+
|
160
|
+
The gem can automatically invalidate CloudFront cache after a successful sync. This ensures that your CloudFront distribution serves the latest content immediately after deployment.
|
161
|
+
|
162
|
+
### Configuration
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
activate :s3_sync do |s3_sync|
|
166
|
+
# ... other configuration ...
|
167
|
+
s3_sync.cloudfront_distribution_id = 'E1234567890123' # Your CloudFront distribution ID
|
168
|
+
s3_sync.cloudfront_invalidate = true # Enable invalidation
|
169
|
+
s3_sync.cloudfront_invalidate_all = false # Invalidate only changed files
|
170
|
+
end
|
171
|
+
```
|
172
|
+
|
173
|
+
### Configuration Options
|
174
|
+
|
175
|
+
| Setting | Default | Description |
|
176
|
+
| --------------------------------- | ----------- | ----------- |
|
177
|
+
| cloudfront_distribution_id | - | CloudFront distribution ID to invalidate |
|
178
|
+
| cloudfront_invalidate | ```false``` | Enable CloudFront invalidation after sync |
|
179
|
+
| cloudfront_invalidate_all | ```false``` | Invalidate all paths (/*) instead of only changed files |
|
180
|
+
| cloudfront_invalidation_batch_size| ```1000``` | Maximum paths per invalidation request |
|
181
|
+
| cloudfront_wait | ```false``` | Wait for CloudFront invalidation to complete |
|
182
|
+
</edits>
|
183
|
+
|
184
|
+
### Command Line Options
|
185
|
+
|
186
|
+
You can also control CloudFront invalidation via command line:
|
187
|
+
|
188
|
+
```bash
|
189
|
+
# Enable CloudFront invalidation for this sync
|
190
|
+
middleman s3_sync --cloudfront-invalidate --cloudfront-distribution-id E1234567890123
|
191
|
+
|
192
|
+
# Invalidate all paths instead of just changed files
|
193
|
+
middleman s3_sync --cloudfront-invalidate-all --cloudfront-distribution-id E1234567890123
|
194
|
+
|
195
|
+
# Wait for invalidation to complete before exiting
|
196
|
+
middleman s3_sync --cloudfront-invalidate --cloudfront-wait --cloudfront-distribution-id E1234567890123
|
197
|
+
|
198
|
+
# Custom batch size for large numbers of files
|
199
|
+
middleman s3_sync --cloudfront-invalidate --cloudfront-invalidation-batch-size 500 --cloudfront-distribution-id E1234567890123
|
200
|
+
|
201
|
+
# Short aliases
|
202
|
+
middleman s3_sync -c -d E1234567890123 # Basic invalidation
|
203
|
+
middleman s3_sync -a -d E1234567890123 # Invalidate all paths
|
204
|
+
middleman s3_sync -c -w -d E1234567890123 # Invalidate and wait
|
205
|
+
middleman s3_sync -c -a -w -d E1234567890123 # Invalidate all and wait
|
206
|
+
```
|
131
207
|
|
132
|
-
|
133
|
-
contained in a bucket named "mysite.com":
|
208
|
+
#### Available CloudFront Command Line Options
|
134
209
|
|
210
|
+
| Option | Short | Description |
|
211
|
+
| ------ | ----- | ----------- |
|
212
|
+
| `--cloudfront-distribution-id` | `-d` | CloudFront distribution ID |
|
213
|
+
| `--cloudfront-invalidate` | `-c` | Enable CloudFront invalidation |
|
214
|
+
| `--cloudfront-invalidate-all` | `-a` | Invalidate all paths (/*) |
|
215
|
+
| `--cloudfront-invalidation-batch-size` | - | Max paths per request (default: 1000) |
|
216
|
+
| `--cloudfront-wait` | `-w` | Wait for invalidation to complete |
|
217
|
+
|
218
|
+
### How It Works
|
219
|
+
|
220
|
+
1. **Smart Invalidation**: By default, only files that were created, updated, or deleted during the sync are invalidated
|
221
|
+
2. **Path Optimization**: Duplicate paths are removed and redundant paths (covered by wildcards) are eliminated
|
222
|
+
3. **Batch Processing**: Large numbers of paths are split into multiple invalidation requests to respect CloudFront limits
|
223
|
+
4. **Error Handling**: Invalidation failures are logged but don't stop the sync process
|
224
|
+
5. **Dry Run Support**: Use `--dry-run` to see what would be invalidated without making actual API calls
|
225
|
+
|
226
|
+
### IAM Permissions
|
227
|
+
|
228
|
+
Your AWS credentials need CloudFront permissions in addition to S3:
|
229
|
+
|
230
|
+
```json
|
231
|
+
{
|
232
|
+
"Version": "2012-10-17",
|
233
|
+
"Statement": [
|
234
|
+
{
|
235
|
+
"Effect": "Allow",
|
236
|
+
"Action": [
|
237
|
+
"cloudfront:CreateInvalidation",
|
238
|
+
"cloudfront:GetInvalidation",
|
239
|
+
"cloudfront:ListInvalidations"
|
240
|
+
],
|
241
|
+
"Resource": "arn:aws:cloudfront::*:distribution/E1234567890123"
|
242
|
+
}
|
243
|
+
]
|
244
|
+
}
|
135
245
|
```
|
246
|
+
|
247
|
+
### Cost Considerations
|
248
|
+
|
249
|
+
- CloudFront allows 1,000 free invalidation paths per month
|
250
|
+
- Additional invalidations cost $0.005 per path
|
251
|
+
- Use `cloudfront_invalidate_all: true` for major updates to minimize costs (counts as 1 path)
|
252
|
+
- Consider the trade-off between immediate cache invalidation and cost
|
253
|
+
|
254
|
+
#### IAM Policy
|
255
|
+
|
256
|
+
Here's a sample IAM policy with least-privilege permissions that will allow syncing to a bucket named "mysite.com":
|
257
|
+
|
258
|
+
```json
|
136
259
|
{
|
137
260
|
"Version": "2012-10-17",
|
138
261
|
"Statement": [
|
139
262
|
{
|
140
263
|
"Effect": "Allow",
|
141
|
-
"Action":
|
264
|
+
"Action": [
|
265
|
+
"s3:ListBucket",
|
266
|
+
"s3:GetBucketLocation",
|
267
|
+
"s3:GetBucketVersioning"
|
268
|
+
],
|
142
269
|
"Resource": "arn:aws:s3:::mysite.com"
|
143
270
|
},
|
144
271
|
{
|
145
272
|
"Effect": "Allow",
|
146
|
-
"Action":
|
273
|
+
"Action": [
|
274
|
+
"s3:PutObject",
|
275
|
+
"s3:PutObjectAcl",
|
276
|
+
"s3:GetObject",
|
277
|
+
"s3:GetObjectAcl",
|
278
|
+
"s3:DeleteObject",
|
279
|
+
"s3:HeadObject"
|
280
|
+
],
|
147
281
|
"Resource": "arn:aws:s3:::mysite.com/*"
|
148
282
|
}
|
149
283
|
]
|
150
284
|
}
|
151
285
|
```
|
152
286
|
|
153
|
-
|
287
|
+
If you're using additional features, you may need these permissions as well:
|
288
|
+
|
289
|
+
```json
|
290
|
+
{
|
291
|
+
"Version": "2012-10-17",
|
292
|
+
"Statement": [
|
293
|
+
{
|
294
|
+
"Effect": "Allow",
|
295
|
+
"Action": [
|
296
|
+
"s3:PutBucketWebsite",
|
297
|
+
"s3:PutBucketVersioning"
|
298
|
+
],
|
299
|
+
"Resource": "arn:aws:s3:::mysite.com",
|
300
|
+
"Condition": {
|
301
|
+
"Bool": {
|
302
|
+
"aws:SecureTransport": "true"
|
303
|
+
}
|
304
|
+
}
|
305
|
+
}
|
306
|
+
]
|
307
|
+
}
|
308
|
+
```
|
309
|
+
|
310
|
+
This policy grants only the specific permissions needed:
|
311
|
+
|
312
|
+
- **For the bucket itself**:
|
313
|
+
- `s3:ListBucket`: To list objects in the bucket
|
314
|
+
- `s3:GetBucketLocation`: To determine the bucket's region
|
315
|
+
- `s3:GetBucketVersioning`: To check versioning status
|
316
|
+
- `s3:PutBucketVersioning`: If using the `version_bucket` option
|
317
|
+
- `s3:PutBucketWebsite`: If using website configuration (index/error documents)
|
318
|
+
|
319
|
+
- **For objects in the bucket**:
|
320
|
+
- `s3:PutObject`: To create/update objects
|
321
|
+
- `s3:PutObjectAcl`: To set ACLs on objects
|
322
|
+
- `s3:GetObject`: To retrieve objects for comparison
|
323
|
+
- `s3:GetObjectAcl`: To read existing ACLs
|
324
|
+
- `s3:DeleteObject`: To delete stray objects (when `delete: true`)
|
325
|
+
- `s3:HeadObject`: To retrieve object metadata via HEAD requests
|
326
|
+
|
327
|
+
The source code shows that middleman-s3_sync uses HEAD requests (`object.head`) to compare resources, checks and sets bucket versioning when configured, and can set website configuration for index and error documents.
|
328
|
+
|
329
|
+
Note: You can further restrict these permissions by adding conditions or limiting them to specific prefixes if you're only publishing to a subdirectory of the bucket.
|
154
330
|
|
155
331
|
## Command Line Usage
|
156
332
|
|
@@ -182,17 +358,17 @@ You can specify which environment to run Middleman under using the
|
|
182
358
|
|
183
359
|
$ middleman s3_sync --environment=production
|
184
360
|
|
185
|
-
You can set up separate sync environments in config.rb like this:
|
361
|
+
You can set up separate sync environments in config.rb like this:
|
186
362
|
|
187
363
|
```ruby
|
188
364
|
configure :staging do
|
189
365
|
activate :s3_sync do |s3_sync|
|
190
366
|
s3_sync.bucket = '<bucket'
|
191
|
-
...
|
367
|
+
...
|
192
368
|
end
|
193
369
|
end
|
194
370
|
```
|
195
|
-
|
371
|
+
|
196
372
|
See the Usage section above for all the s3_sync. options to include. Currently, the .s3_sync file does not allow separate environments.
|
197
373
|
|
198
374
|
#### Dry Run
|
@@ -291,7 +467,7 @@ The following keys can be set:
|
|
291
467
|
You can pass the `expires` key to the `caching_policy` and
|
292
468
|
`default_caching_policy` methods if you insist on setting the expires
|
293
469
|
header on a results. You will need to pass it a Time object indicating
|
294
|
-
when the
|
470
|
+
when the resource is set to expire.
|
295
471
|
|
296
472
|
> Note that the `Cache-Control` header will take precedence over the
|
297
473
|
> `Expires` header if both are present.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Middleman
|
2
|
+
module Sitemap
|
3
|
+
class Resource
|
4
|
+
def redirect?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
module Extensions
|
10
|
+
class RedirectResource < Resource
|
11
|
+
def target_url
|
12
|
+
@target_url ||= ::Middleman::Util.url_for(@store.app, @request_path, relative: false, find_resource: true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def redirect?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
require 'aws-sdk-cloudfront'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module Middleman
|
5
|
+
module S3Sync
|
6
|
+
module CloudFront
|
7
|
+
class << self
|
8
|
+
include Status
|
9
|
+
|
10
|
+
def invalidate(invalidation_paths, options)
|
11
|
+
return unless should_invalidate?(options)
|
12
|
+
return if invalidation_paths.empty? && !options.cloudfront_invalidate_all
|
13
|
+
|
14
|
+
paths = prepare_invalidation_paths(invalidation_paths, options)
|
15
|
+
return if paths.empty?
|
16
|
+
|
17
|
+
say_status "Invalidating CloudFront distribution #{options.cloudfront_distribution_id}"
|
18
|
+
|
19
|
+
if options.dry_run
|
20
|
+
say_status "#{ANSI.yellow{'DRY RUN:'}} Would invalidate #{paths.length} paths in CloudFront"
|
21
|
+
paths.each { |path| say_status " #{path}" } if options.verbose
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
# Split paths into batches to respect CloudFront limits
|
26
|
+
batch_size = [options.cloudfront_invalidation_batch_size, 3000].min
|
27
|
+
path_batches = paths.each_slice(batch_size).to_a
|
28
|
+
|
29
|
+
invalidation_ids = []
|
30
|
+
|
31
|
+
path_batches.each_with_index do |batch, index|
|
32
|
+
say_status "Creating invalidation batch #{index + 1}/#{path_batches.length} (#{batch.length} paths)"
|
33
|
+
|
34
|
+
invalidation_id = create_invalidation(batch, options)
|
35
|
+
invalidation_ids << invalidation_id if invalidation_id
|
36
|
+
|
37
|
+
# Add a small delay between batches to avoid rate limiting
|
38
|
+
sleep(1) if path_batches.length > 1 && index < path_batches.length - 1
|
39
|
+
end
|
40
|
+
|
41
|
+
if invalidation_ids.any?
|
42
|
+
say_status "CloudFront invalidation(s) created: #{invalidation_ids.join(', ')}"
|
43
|
+
|
44
|
+
if options.cloudfront_wait
|
45
|
+
say_status "Waiting for CloudFront invalidation(s) to complete..."
|
46
|
+
wait_for_invalidations(invalidation_ids, options)
|
47
|
+
say_status "CloudFront invalidation(s) completed successfully"
|
48
|
+
else
|
49
|
+
say_status "Invalidations may take 10-15 minutes to complete"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
invalidation_ids
|
54
|
+
rescue Aws::CloudFront::Errors::ServiceError => e
|
55
|
+
say_status "#{ANSI.red{'CloudFront invalidation failed:'}} #{e.message}"
|
56
|
+
raise e unless options.verbose # Re-raise unless we're being verbose
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def should_invalidate?(options)
|
62
|
+
return false unless options.cloudfront_invalidate
|
63
|
+
|
64
|
+
unless options.cloudfront_distribution_id
|
65
|
+
say_status "#{ANSI.yellow{'CloudFront invalidation skipped:'}} no distribution ID provided"
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
def prepare_invalidation_paths(invalidation_paths, options)
|
73
|
+
if options.cloudfront_invalidate_all
|
74
|
+
return ['/*']
|
75
|
+
end
|
76
|
+
|
77
|
+
# Normalize paths for CloudFront
|
78
|
+
paths = invalidation_paths.map do |path|
|
79
|
+
# Ensure path starts with /
|
80
|
+
normalized_path = path.start_with?('/') ? path : "/#{path}"
|
81
|
+
|
82
|
+
# Remove any double slashes
|
83
|
+
normalized_path.gsub(/\/+/, '/')
|
84
|
+
end.uniq.sort
|
85
|
+
|
86
|
+
# Remove any paths that would be covered by a wildcard
|
87
|
+
if paths.include?('/*')
|
88
|
+
paths = ['/*']
|
89
|
+
else
|
90
|
+
# Remove redundant paths (e.g., if we have /path/* and /path/file.html)
|
91
|
+
paths = remove_redundant_paths(paths)
|
92
|
+
end
|
93
|
+
|
94
|
+
say_status "Prepared #{paths.length} paths for CloudFront invalidation" if options.verbose
|
95
|
+
|
96
|
+
paths
|
97
|
+
end
|
98
|
+
|
99
|
+
def remove_redundant_paths(paths)
|
100
|
+
# Sort paths to ensure wildcards come before specific files
|
101
|
+
sorted_paths = paths.sort
|
102
|
+
result = []
|
103
|
+
|
104
|
+
sorted_paths.each do |path|
|
105
|
+
# Check if this path is already covered by a wildcard we've added
|
106
|
+
is_redundant = result.any? do |existing_path|
|
107
|
+
if existing_path.end_with?('/*')
|
108
|
+
# Check if current path is under this wildcard
|
109
|
+
wildcard_prefix = existing_path[0..-3] # Remove /*
|
110
|
+
path.start_with?(wildcard_prefix + '/')
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
result << path unless is_redundant
|
117
|
+
end
|
118
|
+
|
119
|
+
result
|
120
|
+
end
|
121
|
+
|
122
|
+
def create_invalidation(paths, options)
|
123
|
+
caller_reference = "middleman-s3_sync-#{Time.now.to_i}-#{SecureRandom.hex(4)}"
|
124
|
+
|
125
|
+
response = cloudfront_client(options).create_invalidation({
|
126
|
+
distribution_id: options.cloudfront_distribution_id,
|
127
|
+
invalidation_batch: {
|
128
|
+
paths: {
|
129
|
+
quantity: paths.length,
|
130
|
+
items: paths
|
131
|
+
},
|
132
|
+
caller_reference: caller_reference
|
133
|
+
}
|
134
|
+
})
|
135
|
+
|
136
|
+
response.invalidation.id
|
137
|
+
rescue Aws::CloudFront::Errors::ServiceError => e
|
138
|
+
say_status "#{ANSI.red{'Failed to create CloudFront invalidation:'}} #{e.message}"
|
139
|
+
say_status "Paths: #{paths.join(', ')}" if options.verbose
|
140
|
+
raise e unless options.verbose
|
141
|
+
nil
|
142
|
+
end
|
143
|
+
|
144
|
+
def cloudfront_client(options)
|
145
|
+
client_options = {
|
146
|
+
region: 'us-east-1' # CloudFront is always in us-east-1
|
147
|
+
}
|
148
|
+
|
149
|
+
# Use the same credentials as S3 if available
|
150
|
+
if options.aws_access_key_id && options.aws_secret_access_key
|
151
|
+
client_options.merge!({
|
152
|
+
access_key_id: options.aws_access_key_id,
|
153
|
+
secret_access_key: options.aws_secret_access_key
|
154
|
+
})
|
155
|
+
|
156
|
+
# If using an assumed role
|
157
|
+
client_options.merge!({
|
158
|
+
session_token: options.aws_session_token
|
159
|
+
}) if options.aws_session_token
|
160
|
+
end
|
161
|
+
|
162
|
+
Aws::CloudFront::Client.new(client_options)
|
163
|
+
end
|
164
|
+
|
165
|
+
def wait_for_invalidations(invalidation_ids, options)
|
166
|
+
invalidation_ids.each do |invalidation_id|
|
167
|
+
say_status "Waiting for invalidation #{invalidation_id}..."
|
168
|
+
|
169
|
+
client = cloudfront_client(options)
|
170
|
+
client.wait_until(:invalidation_completed,
|
171
|
+
distribution_id: options.cloudfront_distribution_id,
|
172
|
+
id: invalidation_id
|
173
|
+
) do |waiter|
|
174
|
+
waiter.max_attempts = 30 # Wait up to 30 minutes (30 * 60s checks)
|
175
|
+
waiter.delay = 60 # Check every 60 seconds
|
176
|
+
|
177
|
+
waiter.before_attempt do |attempt|
|
178
|
+
say_status "Checking invalidation status (attempt #{attempt}/30)..." if options.verbose
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
rescue Aws::Waiters::Errors::WaiterFailed => e
|
183
|
+
say_status "#{ANSI.yellow{'Warning:'}} CloudFront invalidation wait timed out: #{e.message}"
|
184
|
+
say_status "Invalidation is still in progress but sync will continue"
|
185
|
+
rescue Aws::CloudFront::Errors::ServiceError => e
|
186
|
+
say_status "#{ANSI.yellow{'Warning:'}} CloudFront invalidation wait failed: #{e.message}"
|
187
|
+
say_status "Invalidation may still be in progress"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -6,6 +6,7 @@ module Middleman
|
|
6
6
|
:http_prefix,
|
7
7
|
:acl,
|
8
8
|
:bucket,
|
9
|
+
:endpoint,
|
9
10
|
:region,
|
10
11
|
:aws_access_key_id,
|
11
12
|
:aws_secret_access_key,
|
@@ -22,6 +23,7 @@ module Middleman
|
|
22
23
|
:dry_run,
|
23
24
|
:verbose,
|
24
25
|
:content_types,
|
26
|
+
:ignore_paths,
|
25
27
|
:index_document,
|
26
28
|
:error_document
|
27
29
|
]
|
@@ -67,6 +69,10 @@ module Middleman
|
|
67
69
|
(@path_style.nil? ? true : @path_style)
|
68
70
|
end
|
69
71
|
|
72
|
+
def ignore_paths
|
73
|
+
@ignore_paths.nil? ? [] : @ignore_paths
|
74
|
+
end
|
75
|
+
|
70
76
|
def prefix=(prefix)
|
71
77
|
http_prefix = @http_prefix ? @http_prefix.sub(%r{^/}, "") : ""
|
72
78
|
if http_prefix.split("/").first == prefix
|