Thanks to visit codestin.com
Credit goes to github.com

Skip to content

CloudFormation v2 Engine: Base Support for Fn::Base64 #12700

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ def __init__(self, scope: Scope, value: Any):
FnSelect: Final[str] = "Fn::Select"
FnSplit: Final[str] = "Fn::Split"
FnGetAZs: Final[str] = "Fn::GetAZs"
FnBase64: Final[str] = "Fn::Base64"
INTRINSIC_FUNCTIONS: Final[set[str]] = {
RefKey,
FnIfKey,
Expand All @@ -439,6 +440,7 @@ def __init__(self, scope: Scope, value: Any):
FnSelect,
FnSplit,
FnGetAZs,
FnBase64,
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import base64
import re
from typing import Any, Final, Generic, Optional, TypeVar

Expand Down Expand Up @@ -45,6 +46,8 @@
from localstack.services.cloudformation.stores import get_cloudformation_store
from localstack.services.cloudformation.v2.entities import ChangeSet
from localstack.utils.aws.arns import get_partition
from localstack.utils.run import to_str
from localstack.utils.strings import to_bytes
from localstack.utils.urls import localstack_host

_AWS_URL_SUFFIX = localstack_host().host # The value in AWS is "amazonaws.com"
Expand Down Expand Up @@ -748,6 +751,31 @@ def _compute_fn_get_a_zs(region) -> Any:

return PreprocEntityDelta(before=before, after=after)

def visit_node_intrinsic_function_fn_base64(
self, node_intrinsic_function: NodeIntrinsicFunction
) -> PreprocEntityDelta:
# TODO: add further support for schema validation
arguments_delta = self.visit(node_intrinsic_function.arguments)
arguments_before = arguments_delta.before
arguments_after = arguments_delta.after

def _compute_fn_base_64(string) -> Any:
if not isinstance(string, str):
raise RuntimeError(f"Invalid valueToEncode for Fn::Base64: '{string}'")
# Ported from v1:
base64_string = to_str(base64.b64encode(to_bytes(string)))
return base64_string

before = Nothing
if not is_nothing(arguments_before):
before = _compute_fn_base_64(arguments_before)

after = Nothing
if not is_nothing(arguments_after):
after = _compute_fn_base_64(arguments_after)

return PreprocEntityDelta(before=before, after=after)

def visit_node_intrinsic_function_fn_find_in_map(
self, node_intrinsic_function: NodeIntrinsicFunction
) -> PreprocEntityDelta:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ def visit_node_intrinsic_function_fn_get_a_zs(
):
self.visit_children(node_intrinsic_function)

def visit_node_intrinsic_function_fn_base64(
self, node_intrinsic_function: NodeIntrinsicFunction
):
self.visit_children(node_intrinsic_function)

def visit_node_intrinsic_function_fn_sub(self, node_intrinsic_function: NodeIntrinsicFunction):
self.visit_children(node_intrinsic_function)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ def test_and_or_functions(
bucket_names = [b["Name"] for b in buckets["Buckets"]]
assert (bucket_name in bucket_names) == expected_bucket_created

@pytest.mark.skip(reason="CFNV2:Fn::Base64")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@markers.aws.validated
def test_base64_sub_and_getatt_functions(self, deploy_cfn_template):
template_path = os.path.join(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import pytest
from localstack_snapshot.snapshots.transformer import RegexTransformer

from localstack.services.cloudformation.v2.utils import is_v2_engine
from localstack.testing.aws.util import is_aws_cloud
from localstack.testing.pytest import markers
from localstack.utils.strings import long_uid


@pytest.mark.skipif(
condition=not is_v2_engine() and not is_aws_cloud(), reason="Requires the V2 engine"
)
@markers.snapshot.skip_snapshot_verify(
paths=[
"per-resource-events..*",
"delete-describe..*",
#
# Before/After Context
"$..Capabilities",
"$..NotificationARNs",
"$..IncludeNestedStacks",
"$..Scope",
"$..Details",
"$..Parameters",
"$..Replacement",
]
)
class TestChangeSetFnBase64:
@markers.aws.validated
def test_fn_base64_add_to_static_property(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
template_1 = {
"Resources": {
"Topic1": {"Type": "AWS::SNS::Topic", "Properties": {"DisplayName": name1}}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Base64": "HelloWorld"}},
}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_base64_remove_from_static_property(
self,
snapshot,
capture_update_process,
):
name1 = f"topic-name-1-{long_uid()}"
snapshot.add_transformer(RegexTransformer(name1, "topic-name-1"))
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Base64": "HelloWorld"}},
}
}
}
template_2 = {
"Resources": {
"Topic1": {"Type": "AWS::SNS::Topic", "Properties": {"DisplayName": name1}}
}
}
capture_update_process(snapshot, template_1, template_2)

@markers.aws.validated
def test_fn_base64_change_input_string(
self,
snapshot,
capture_update_process,
):
template_1 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Base64": "OldValue"}},
}
}
}
template_2 = {
"Resources": {
"Topic1": {
"Type": "AWS::SNS::Topic",
"Properties": {"DisplayName": {"Fn::Base64": "NewValue"}},
}
}
}
capture_update_process(snapshot, template_1, template_2)
Loading
Loading