This commit is contained in:
+73
-17
@@ -174,21 +174,28 @@ def configure_cors(bucket, origins, endpoint, aws_env):
|
||||
"""Reconcile read-only browser access without exposing publication credentials."""
|
||||
if origins is None:
|
||||
return
|
||||
if not origins:
|
||||
config = None
|
||||
if origins:
|
||||
config = {
|
||||
"CORSRules": [{
|
||||
"AllowedOrigins": origins,
|
||||
"AllowedMethods": ["GET", "HEAD"],
|
||||
"AllowedHeaders": ["*"],
|
||||
"ExposeHeaders": ["ETag"],
|
||||
"MaxAgeSeconds": 3600,
|
||||
}],
|
||||
}
|
||||
set_cors_configuration(bucket, config, endpoint, aws_env)
|
||||
|
||||
|
||||
def set_cors_configuration(bucket, config, endpoint, aws_env):
|
||||
"""Apply an exact bucket CORS configuration, or remove it when absent."""
|
||||
if config is None:
|
||||
run([
|
||||
"aws", "--endpoint-url", endpoint, "s3api", "delete-bucket-cors",
|
||||
"--bucket", bucket,
|
||||
], env=aws_env)
|
||||
return
|
||||
config = {
|
||||
"CORSRules": [{
|
||||
"AllowedOrigins": origins,
|
||||
"AllowedMethods": ["GET", "HEAD"],
|
||||
"AllowedHeaders": ["*"],
|
||||
"ExposeHeaders": ["ETag"],
|
||||
"MaxAgeSeconds": 3600,
|
||||
}],
|
||||
}
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", encoding="utf-8") as handle:
|
||||
json.dump(config, handle)
|
||||
handle.flush()
|
||||
@@ -198,11 +205,61 @@ def configure_cors(bucket, origins, endpoint, aws_env):
|
||||
], env=aws_env)
|
||||
|
||||
|
||||
def configure_artifact_cors(artifact, credential_env_names=None):
|
||||
configure_cors(
|
||||
artifact["bucket"], artifact["cors_origins"], artifact["s3_endpoint"],
|
||||
publication_aws_env(artifact, credential_env_names),
|
||||
)
|
||||
def get_cors_configuration(bucket, endpoint, aws_env):
|
||||
"""Read the exact bucket CORS configuration for rollback."""
|
||||
result = _aws_capture([
|
||||
"aws", "--endpoint-url", endpoint, "s3api", "get-bucket-cors",
|
||||
"--bucket", bucket, "--output", "json",
|
||||
], aws_env)
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
config = json.loads(result.stdout)
|
||||
except json.JSONDecodeError as error:
|
||||
raise RuntimeError(f"get-bucket-cors returned invalid JSON for {bucket}") from error
|
||||
if not isinstance(config, dict) or not isinstance(config.get("CORSRules"), list):
|
||||
raise RuntimeError(f"get-bucket-cors returned an invalid policy for {bucket}")
|
||||
return config
|
||||
error = f"{result.stdout}\n{result.stderr}"
|
||||
if "NoSuchCORSConfiguration" in error:
|
||||
return None
|
||||
raise RuntimeError(f"get-bucket-cors failed for {bucket}: {error.strip()}")
|
||||
|
||||
|
||||
def reconcile_artifact_cors(artifacts, credential_env_names=None):
|
||||
"""Reconcile all policies, restoring the prior set if any write fails."""
|
||||
snapshots = []
|
||||
for artifact in artifacts:
|
||||
if artifact["cors_origins"] is None:
|
||||
continue
|
||||
aws_env = publication_aws_env(artifact, credential_env_names)
|
||||
snapshots.append((
|
||||
artifact,
|
||||
aws_env,
|
||||
get_cors_configuration(artifact["bucket"], artifact["s3_endpoint"], aws_env),
|
||||
))
|
||||
|
||||
attempted = []
|
||||
try:
|
||||
for artifact, aws_env, previous in snapshots:
|
||||
attempted.append((artifact, aws_env, previous))
|
||||
configure_cors(
|
||||
artifact["bucket"], artifact["cors_origins"], artifact["s3_endpoint"], aws_env,
|
||||
)
|
||||
except Exception as error:
|
||||
rollback_errors = []
|
||||
for artifact, aws_env, previous in reversed(attempted):
|
||||
try:
|
||||
set_cors_configuration(
|
||||
artifact["bucket"], previous, artifact["s3_endpoint"], aws_env,
|
||||
)
|
||||
except Exception as rollback_error:
|
||||
rollback_errors.append(f"{artifact['bucket']}: {rollback_error}")
|
||||
if rollback_errors:
|
||||
raise RuntimeError(
|
||||
f"CORS reconciliation failed ({error}); rollback also failed for "
|
||||
f"{'; '.join(rollback_errors)}"
|
||||
) from error
|
||||
raise
|
||||
|
||||
|
||||
def publish_route_immutables(artifact, route, site_dir, credential_env_names=None):
|
||||
@@ -499,8 +556,7 @@ def deploy_static(site_name, site_dir, action_dir, token, cfg):
|
||||
# Reconcile every browser-read policy before publishing mutable content.
|
||||
# A CORS failure therefore cannot leave a new channel pointing at a release
|
||||
# whose cross-origin assets browsers cannot consume.
|
||||
for artifact in cfg["artifacts"]:
|
||||
configure_artifact_cors(artifact, credential_env_names)
|
||||
reconcile_artifact_cors(cfg["artifacts"], credential_env_names)
|
||||
for route in cfg["routes"]:
|
||||
s3_sync(
|
||||
artifact_by_name[route["artifact"]], route, site_dir, credential_env_names,
|
||||
|
||||
Reference in New Issue
Block a user