//--- Methods ---
        public void Initialize(ModuleBuilder builder)
        {
            _builder = builder;

            // add module variables
            var moduleItem = _builder.AddVariable(
                parent: null,
                name: "Module",
                description: "Module Variables",
                type: "String",
                scope: null,
                value: "",
                allow: null,
                encryptionContext: null
                );

            _builder.AddVariable(
                parent: moduleItem,
                name: "Id",
                description: "Module ID",
                type: "String",
                scope: null,
                value: FnRef("AWS::StackName"),
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "Namespace",
                description: "Module Namespace",
                type: "String",
                scope: null,
                value: _builder.Namespace,
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "Name",
                description: "Module Name",
                type: "String",
                scope: null,
                value: _builder.Name,
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "FullName",
                description: "Module FullName",
                type: "String",
                scope: null,
                value: _builder.FullName,
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "Version",
                description: "Module Version",
                type: "String",
                scope: null,
                value: _builder.Version.ToString(),
                allow: null,
                encryptionContext: null
                );
            _builder.AddCondition(
                parent: moduleItem,
                name: "IsNested",
                description: "Module is nested",
                value: FnNot(FnEquals(FnRef("DeploymentRoot"), ""))
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "RootId",
                description: "Root Module ID",
                type: "String",
                scope: null,
                value: FnIf("Module::IsNested", FnRef("DeploymentRoot"), FnRef("Module::Id")),
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "Info",
                description: "Module Fullname, Version, and Origin",
                type: "String",
                scope: null,
                value: _builder.ModuleInfo.ToString(),
                allow: null,
                encryptionContext: null
                );

            // add module variables
            var deploymentItem = _builder.AddVariable(
                parent: null,
                name: "Deployment",
                description: "Deployment Variables",
                type: "String",
                scope: null,
                value: "",
                allow: null,
                encryptionContext: null
                );

            // add deployment variables
            _builder.AddVariable(
                parent: deploymentItem,
                name: "Tier",
                description: "Deployment tier name",
                type: "String",
                scope: null,
                value: FnSelect("0", FnSplit("-", FnRef("DeploymentPrefix"))),
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: deploymentItem,
                name: "TierPrefix",
                description: "Deployment tier prefix",
                type: "String",
                scope: null,
                value: FnRef("DeploymentPrefix"),
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: deploymentItem,
                name: "TierLowercase",
                description: "Deployment tier name in lowercase characters",
                type: "String",
                scope: null,
                value: FnSelect("0", FnSplit("-", FnRef("DeploymentPrefixLowercase"))),
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: deploymentItem,
                name: "TierPrefixLowercase",
                description: "Deployment tier prefix in lowercase characters",
                type: "String",
                scope: null,
                value: FnRef("DeploymentPrefixLowercase"),
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: deploymentItem,
                name: "BucketName",
                description: "Deployment S3 Bucket Name",
                type: "String",
                scope: null,
                value: FnRef("DeploymentBucketName"),
                allow: null,
                encryptionContext: null
                );

            // create module IAM role used by all functions
            _builder.TryGetOverride("Module::Role.PermissionsBoundary", out var rolePermissionsBoundary);
            var moduleRoleItem = _builder.AddResource(
                parent: moduleItem,
                name: "Role",
                description: null,
                scope: null,
                resource: new Humidifier.IAM.Role {
                AssumeRolePolicyDocument = new Humidifier.PolicyDocument {
                    Version   = "2012-10-17",
                    Statement = new[] {
                        new Humidifier.Statement {
                            Sid       = "ModuleLambdaPrincipal",
                            Effect    = "Allow",
                            Principal = new Humidifier.Principal {
                                Service = "lambda.amazonaws.com"
                            },
                            Action = "sts:AssumeRole"
                        }
                    }.ToList()
                },
                PermissionsBoundary = rolePermissionsBoundary,
                Policies            = new[] {
                    new Humidifier.IAM.Policy {
                        PolicyName     = FnSub("${AWS::StackName}ModulePolicy"),
                        PolicyDocument = new Humidifier.PolicyDocument {
                            Version   = "2012-10-17",
                            Statement = new List <Humidifier.Statement>()
                        }
                    }
                }.ToList()
            },
                resourceExportAttribute: null,
                dependsOn: null,
                condition: null,
                pragmas: null,
                deletionPolicy: null
                );

            moduleRoleItem.DiscardIfNotReachable = true;

            // add overridable logging retention variable
            if (!_builder.TryGetOverride("Module::LogRetentionInDays", out var logRetentionInDays))
            {
                logRetentionInDays = 30;
            }
            _builder.AddVariable(
                parent: moduleItem,
                name: "LogRetentionInDays",
                description: "Number days CloudWatch Log streams are retained for",
                type: "Number",
                scope: null,
                value: logRetentionInDays,
                allow: null,
                encryptionContext: null
                );

            // add LambdaSharp Module Options
            var section = "LambdaSharp Module Options";

            _builder.AddParameter(
                name: "Secrets",
                section: section,
                label: "Comma-separated list of additional KMS secret keys",
                description: "Secret Keys (ARNs)",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: "",
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null,
                deletionPolicy: null
                );
            _builder.AddParameter(
                name: "XRayTracing",
                section: section,
                label: "Enable AWS X-Ray tracing mode for module resources",
                description: "AWS X-Ray Tracing",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: XRayTracingLevel.Disabled.ToString(),
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: new[] {
                XRayTracingLevel.Disabled.ToString(),
                XRayTracingLevel.RootModule.ToString(),
                XRayTracingLevel.AllModules.ToString()
            },
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null,
                deletionPolicy: null
                ).DiscardIfNotReachable = true;
            _builder.AddCondition(
                parent: null,
                name: "XRayIsEnabled",
                description: null,
                value: FnNot(FnEquals(FnRef("XRayTracing"), XRayTracingLevel.Disabled.ToString()))
                );
            _builder.AddCondition(
                parent: null,
                name: "XRayNestedIsEnabled",
                description: null,
                value: FnEquals(FnRef("XRayTracing"), XRayTracingLevel.AllModules.ToString())
                );

            // check if module might depend on core services
            if (_builder.HasLambdaSharpDependencies || _builder.HasModuleRegistration)
            {
                _builder.AddParameter(
                    name: "LambdaSharpCoreServices",
                    section: section,
                    label: "Integrate with LambdaSharp.Core services",
                    description: "Use LambdaSharp.Core Services",
                    type: "String",
                    scope: null,
                    noEcho: null,
                    defaultValue: "Disabled",
                    constraintDescription: null,
                    allowedPattern: null,
                    allowedValues: new[] {
                    "Disabled",
                    "Enabled"
                },
                    maxLength: null,
                    maxValue: null,
                    minLength: null,
                    minValue: null,
                    allow: null,
                    properties: null,
                    arnAttribute: null,
                    encryptionContext: null,
                    pragmas: null,
                    deletionPolicy: null
                    ).DiscardIfNotReachable = true;
                _builder.AddCondition(
                    parent: null,
                    name: "UseCoreServices",
                    description: null,
                    value: FnEquals(FnRef("LambdaSharpCoreServices"), "Enabled")
                    );
            }

            // import lambdasharp dependencies (unless requested otherwise)
            if (_builder.HasLambdaSharpDependencies)
            {
                // add LambdaSharp Module Internal resource imports
                var lambdasharp = _builder.AddVariable(
                    parent: null,
                    name: "LambdaSharp",
                    description: "LambdaSharp Core Imports",
                    type: "String",
                    scope: null,
                    value: "",
                    allow: null,
                    encryptionContext: null
                    );
                _builder.AddImport(
                    parent: lambdasharp,
                    name: "DeadLetterQueue",
                    description: null,

                    // TODO (2018-12-01, bjorg): consider using 'AWS::SQS::Queue'
                    type: "String",
                    scope: null,
                    allow: null,
                    module: "LambdaSharp.Core",
                    encryptionContext: null,
                    out var _
                    );
                _builder.AddImport(
                    parent: lambdasharp,
                    name: "LoggingStream",
                    description: null,

                    // NOTE (2018-12-11, bjorg): we use type 'String' to be more flexible with the type of values we're willing to take
                    type: "String",
                    scope: null,
                    allow: null,
                    module: "LambdaSharp.Core",
                    encryptionContext: null,
                    out var _
                    );
                _builder.AddImport(
                    parent: lambdasharp,
                    name: "LoggingStreamRole",
                    description: null,

                    // NOTE (2018-12-11, bjorg): we use type 'String' to be more flexible with the type of values we're willing to take
                    type: "String",
                    scope: null,
                    allow: null,
                    module: "LambdaSharp.Core",
                    encryptionContext: null,
                    out var _
                    );
            }

            // add module variables
            if (TryGetModuleVariable("DeadLetterQueue", out var deadLetterQueueVariable, out var deadLetterQueueCondition))
            {
                _builder.AddVariable(
                    parent: moduleItem,
                    name: "DeadLetterQueue",
                    description: "Module Dead Letter Queue (ARN)",
                    type: "String",
                    scope: null,
                    value: deadLetterQueueVariable,
                    allow: null,
                    encryptionContext: null
                    );
                _builder.AddGrant(
                    name: "DeadLetterQueue",
                    awsType: null,
                    reference: FnRef("Module::DeadLetterQueue"),
                    allow: new[] {
                    "sqs:SendMessage"
                },
                    condition: deadLetterQueueCondition
                    );
            }
            if (TryGetModuleVariable("LoggingStream", out var loggingStreamVariable, out var _))
            {
                _builder.AddVariable(
                    parent: moduleItem,
                    name: "LoggingStream",
                    description: "Module Logging Stream (ARN)",
                    type: "String",
                    scope: null,
                    value: loggingStreamVariable,
                    allow: null,
                    encryptionContext: null
                    );
            }
            if (TryGetModuleVariable("LoggingStreamRole", out var loggingStreamRoleVariable, out var _))
            {
                _builder.AddVariable(
                    parent: moduleItem,
                    name: "LoggingStreamRole",
                    description: "Module Logging Stream Role (ARN)",
                    type: "String",
                    scope: null,
                    value: loggingStreamRoleVariable,
                    allow: null,
                    encryptionContext: null
                    );
            }

            // add KMS permissions for secrets in module
            if (_builder.Secrets.Any())
            {
                _builder.AddGrant(
                    name: "EmbeddedSecrets",
                    awsType: null,
                    reference: _builder.Secrets.ToList(),
                    allow: new[] {
                    "kms:Decrypt",
                    "kms:Encrypt"
                },
                    condition: null
                    );
            }

            // add decryption function for secret parameters and values
            var decryptSecretFunctionEnvironment = new Dictionary <string, object> {
                ["MODULE_ROLE_SECRETSPOLICY"] = FnIf(
                    "Module::Role::SecretsPolicy::Condition",
                    FnRef("Module::Role::SecretsPolicy"),
                    FnRef("AWS::NoValue")
                    )
            };

            _builder.AddInlineFunction(
                parent: moduleItem,
                name: "DecryptSecretFunction",
                description: "Module secret decryption function",
                environment: decryptSecretFunctionEnvironment,
                sources: null,
                condition: null,
                pragmas: new[] {
                "no-registration",
                "no-dead-letter-queue",
                "no-wildcard-scoped-variables"
            },
                timeout: "30",
                memory: "128",
                code: DecryptSecretFunctionCode
                ).DiscardIfNotReachable = true;

            // add LambdaSharp Deployment Settings
            section = "LambdaSharp Deployment Settings (DO NOT MODIFY)";
            _builder.AddParameter(
                name: "DeploymentBucketName",
                section: section,
                label: "Deployment S3 bucket name",
                description: "Deployment S3 Bucket Name",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: null,
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null,
                deletionPolicy: null
                );
            _builder.AddParameter(
                name: "DeploymentPrefix",
                section: section,
                label: "Deployment tier prefix",
                description: "Deployment Tier Prefix",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: null,
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null,
                deletionPolicy: null
                );
            _builder.AddParameter(
                name: "DeploymentPrefixLowercase",
                section: section,
                label: "Deployment tier prefix (lowercase)",
                description: "Deployment Tier Prefix (lowercase)",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: null,
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null,
                deletionPolicy: null
                );
            _builder.AddParameter(
                name: "DeploymentRoot",
                section: section,
                label: "Root stack name for nested deployments, blank otherwise",
                description: "Root Stack Name",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: "",
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null,
                deletionPolicy: null
                );
            _builder.AddParameter(
                name: "DeploymentChecksum",
                section: section,
                label: "CloudFormation template MD5 checksum",
                description: "Deployment Checksum",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: "",
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null,
                deletionPolicy: null
                );

            // add conditional KMS permissions for secrets parameter
            _builder.AddGrant(
                name: "Secrets",
                awsType: null,
                reference: FnSplit(",", FnRef("Secrets")),
                allow: new List <string> {
                "kms:Decrypt",
                "kms:Encrypt"
            },
                condition: FnNot(FnEquals(FnRef("Secrets"), ""))
                );

            // permissions needed for writing to log streams (but not for creating log groups!)
            _builder.AddGrant(
                name: "LogStream",
                awsType: null,
                reference: "arn:aws:logs:*:*:*",
                allow: new[] {
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            },
                condition: null
                );

            // permissions needed for reading state of CloudFormation stack (used by Finalizer to confirm a delete operation is happening)
            _builder.AddGrant(
                name: "CloudFormation",
                awsType: null,
                reference: FnRef("AWS::StackId"),
                allow: new[] {
                "cloudformation:DescribeStacks"
            },
                condition: null
                );

            // permissions needed for X-Ray lambda daemon to upload tracing information
            _builder.AddGrant(
                name: "AWSXRay",
                awsType: null,
                reference: "*",
                allow: new[] {
                "xray:PutTraceSegments",
                "xray:PutTelemetryRecords",
                "xray:GetSamplingRules",
                "xray:GetSamplingTargets",
                "xray:GetSamplingStatisticSummaries"
            },
                condition: null
                );

            // permission needed for posting events to the default event bus
            _builder.AddGrant(
                name: "EventBus",
                awsType: null,
                reference: FnSub("arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default"),
                allow: new[] {
                "events:PutEvents"
            },
                condition: null
                );

            // check if lambdasharp specific resources need to be initialized
            var functions = _builder.Items.OfType <FunctionItem>().ToList();

            if (_builder.TryGetItem("Module::DeadLetterQueue", out _))
            {
                foreach (var function in functions.Where(f => f.HasDeadLetterQueue))
                {
                    // initialize dead-letter queue
                    function.Function.DeadLetterConfig = new Humidifier.Lambda.FunctionTypes.DeadLetterConfig {
                        TargetArn = FnRef("Module::DeadLetterQueue")
                    };
                }
            }

            // TODO (2020-06-30, bjorg): should we also check for function.Function.Properties["VpcConfig"]?

            // permissions needed for lambda functions to exist in a VPC
            if (functions.Any(function => function.Function.VpcConfig != null))
            {
                _builder.AddGrant(
                    name: "VpcNetworkInterfaces",
                    awsType: null,
                    reference: "*",
                    allow: new[] {
                    "ec2:DescribeNetworkInterfaces",
                    "ec2:CreateNetworkInterface",
                    "ec2:DeleteNetworkInterface"
                },
                    condition: null
                    );
            }

            // add module registration
            if (_builder.HasModuleRegistration)
            {
                // create module registration
                _builder.AddResource(
                    parent: moduleItem,
                    name: "Registration",
                    description: null,
                    type: "LambdaSharp::Registration::Module",
                    scope: null,
                    allow: null,
                    properties: new Dictionary <string, object> {
                    ["ModuleInfo"] = _builder.ModuleInfo.ToString(),
                    ["ModuleId"]   = FnRef("AWS::StackName")
                },
                    dependsOn: null,
                    arnAttribute: null,
                    condition: "UseCoreServices",
                    pragmas: null,
                    deletionPolicy: null
                    );

                // handle function registrations
                var registeredFunctions = _builder.Items
                                          .OfType <FunctionItem>()
                                          .Where(function => function.HasFunctionRegistration)
                                          .ToList();
                if (registeredFunctions.Any())
                {
                    // create registration-related resources for functions
                    foreach (var function in registeredFunctions)
                    {
                        // create function registration
                        _builder.AddResource(
                            parent: function,
                            name: "Registration",
                            description: null,
                            type: "LambdaSharp::Registration::Function",
                            scope: null,
                            allow: null,
                            properties: new Dictionary <string, object> {
                            ["ModuleId"]             = FnRef("Module::Id"),
                            ["FunctionId"]           = FnRef(function.FullName),
                            ["FunctionName"]         = function.Name,
                            ["FunctionLogGroupName"] = FnSub($"/aws/lambda/${{{function.FullName}}}"),
                            ["FunctionPlatform"]     = "AWS Lambda",
                            ["FunctionFramework"]    = function.Function.Runtime,
                            ["FunctionLanguage"]     = function.Language,
                            ["FunctionMaxMemory"]    = function.Function.MemorySize,
                            ["FunctionMaxDuration"]  = function.Function.Timeout
                        },
                            dependsOn: new[] { "Module::Registration" },
                            arnAttribute: null,
                            condition: (function.Condition != null)
                                ? FnAnd(FnCondition("UseCoreServices"), FnCondition(function.Condition))
                                : "UseCoreServices",
                            pragmas: null,
                            deletionPolicy: null
                            );

                        // create function log-group subscription
                        if (
                            _builder.TryGetItem("Module::LoggingStream", out _) &&
                            _builder.TryGetItem("Module::LoggingStreamRole", out _)
                            )
                        {
                            _builder.AddResource(
                                parent: function,
                                name: "LogGroupSubscription",
                                description: null,
                                scope: null,
                                resource: new Humidifier.Logs.SubscriptionFilter {
                                DestinationArn = FnRef("Module::LoggingStream"),
                                FilterPattern  = "-\"*** \"",
                                LogGroupName   = FnRef($"{function.FullName}::LogGroup"),
                                RoleArn        = FnRef("Module::LoggingStreamRole")
                            },
                                resourceExportAttribute: null,
                                dependsOn: null,
                                condition: (function.Condition != null)
                                    ? FnAnd(FnCondition("UseCoreServices"), FnCondition(function.Condition))
                                    : "UseCoreServices",
                                pragmas: null,
                                deletionPolicy: null
                                );
                        }
                    }
                }

                // add app registration
                var registeredApps = _builder.Items
                                     .OfType <AppItem>()
                                     .Where(app => app.HasAppRegistration)
                                     .ToList();
                if (registeredApps.Any())
                {
                    // create registration-related resources for functions
                    foreach (var app in registeredApps)
                    {
                        // create app log-group subscription
                        if (
                            _builder.TryGetItem("Module::LoggingStream", out _) &&
                            _builder.TryGetItem("Module::LoggingStreamRole", out _)
                            )
                        {
                            _builder.AddResource(
                                parent: app,
                                name: "LogGroupSubscription",
                                description: null,
                                scope: null,
                                resource: new Humidifier.Logs.SubscriptionFilter {
                                DestinationArn = FnRef("Module::LoggingStream"),
                                FilterPattern  = "-\"*** \"",
                                LogGroupName   = FnRef($"{app.FullName}::LogGroup"),
                                RoleArn        = FnRef("Module::LoggingStreamRole")
                            },
                                resourceExportAttribute: null,
                                dependsOn: null,
                                condition: "UseCoreServices",
                                pragmas: null,
                                deletionPolicy: null
                                );

                            // create app registration
                            _builder.AddResource(
                                parent: app,
                                name: "Registration",
                                description: null,
                                type: "LambdaSharp::Registration::App",
                                scope: null,
                                allow: null,
                                properties: new Dictionary <string, object> {
                                ["ModuleId"]     = FnRef("Module::Id"),
                                ["AppLogGroup"]  = FnRef($"{app.FullName}::LogGroup"),
                                ["AppId"]        = FnRef(app.FullName),
                                ["AppName"]      = app.Name,
                                ["AppPlatform"]  = FnRef($"{app.FullName}::AppPlatform"),
                                ["AppFramework"] = FnRef($"{app.FullName}::AppFramework"),
                                ["AppLanguage"]  = FnRef($"{app.FullName}::AppLanguage")
                            },
                                dependsOn: new[] { "Module::Registration" },
                                arnAttribute: null,
                                condition: "UseCoreServices",
                                pragmas: null,
                                deletionPolicy: null
                                );
                        }
                    }
                }
            }
        }
Exemplo n.º 2
0
        private void ConvertItem(AModuleItem parent, int index, ModuleItemNode node, IEnumerable <string> expectedTypes)
        {
            var type = DeterminNodeType("item", index, node, ModuleItemNode.FieldCombinations, expectedTypes);

            switch (type)
            {
            case "Parameter":
                AtLocation(node.Parameter, () => {
                    // validation
                    if (node.Properties != null)
                    {
                        Validate(node.Type != null, "'Type' attribute is required");
                    }
                    Validate((node.Allow == null) || (node.Type == "AWS") || ResourceMapping.IsCloudFormationType(node.Type), "'Allow' attribute can only be used with AWS resource types");
                    Validate(parent == null, "'Parameter' cannot be nested");

                    // create input parameter item
                    _builder.AddParameter(
                        name: node.Parameter,
                        section: node.Section,
                        label: node.Label,
                        description: node.Description,
                        type: node.Type ?? "String",
                        scope: ConvertScope(node.Scope),
                        noEcho: node.NoEcho,
                        defaultValue: node.Default,
                        constraintDescription: node.ConstraintDescription,
                        allowedPattern: node.AllowedPattern,
                        allowedValues: node.AllowedValues,
                        maxLength: node.MaxLength,
                        maxValue: node.MaxValue,
                        minLength: node.MinLength,
                        minValue: node.MinValue,
                        allow: node.Allow,
                        properties: ParseToDictionary("Properties", node.Properties),
                        arnAttribute: node.DefaultAttribute,
                        encryptionContext: node.EncryptionContext,
                        pragmas: node.Pragmas
                        );
                });
                break;

            case "Import":
                AtLocation(node.Import, () => {
                    // validation
                    Validate((node.Allow == null) || (node.Type == "AWS") || ResourceMapping.IsCloudFormationType(node.Type), "'Allow' attribute can only be used with AWS resource types");
                    Validate(node.Module != null, "missing 'Module' attribute");

                    // create input parameter item
                    _builder.AddImport(
                        parent: parent,
                        name: node.Import,
                        description: node.Description,
                        type: node.Type ?? "String",
                        scope: ConvertScope(node.Scope),
                        allow: node.Allow,
                        module: node.Module ?? "Bad.Module",
                        encryptionContext: node.EncryptionContext
                        );
                });
                break;

            case "Variable":
                AtLocation(node.Variable, () => {
                    // validation
                    Validate(node.Value != null, "missing 'Value' attribute");
                    Validate((node.EncryptionContext == null) || (node.Type == "Secret"), "item must have Type 'Secret' to use 'EncryptionContext' section");
                    Validate((node.Type != "Secret") || !(node.Value is IList <object>), "item with type 'Secret' cannot have a list of values");

                    // create variable item
                    _builder.AddVariable(
                        parent: parent,
                        name: node.Variable,
                        description: node.Description,
                        type: node.Type ?? "String",
                        scope: ConvertScope(node.Scope),
                        value: node.Value ?? "",
                        allow: null,
                        encryptionContext: node.EncryptionContext
                        );
                });
                break;

            case "Group":
                AtLocation(node.Group, () => {
                    // create namespace item
                    var result = _builder.AddVariable(
                        parent: parent,
                        name: node.Group,
                        description: node.Description,
                        type: "String",
                        scope: null,
                        value: "",
                        allow: null,
                        encryptionContext: null
                        );

                    // recurse
                    ConvertItems(result, expectedTypes);
                });
                break;

            case "Resource":
                AtLocation(node.Resource, () => {
                    if (node.Value != null)
                    {
                        // validation
                        Validate((node.Allow == null) || (node.Type == null) || ResourceMapping.IsCloudFormationType(node.Type), "'Allow' attribute can only be used with AWS resource types");
                        Validate(node.If == null, "'If' attribute cannot be used with a referenced resource");
                        Validate(node.Properties == null, "'Properties' section cannot be used with a referenced resource");
                        if (node.Value is IList <object> values)
                        {
                            foreach (var arn in values)
                            {
                                ValidateARN(arn);
                            }
                        }
                        else
                        {
                            ValidateARN(node.Value);
                        }

                        // create variable item
                        _builder.AddVariable(
                            parent: parent,
                            name: node.Resource,
                            description: node.Description,
                            type: node.Type ?? "String",
                            scope: ConvertScope(node.Scope),
                            value: node.Value,
                            allow: node.Allow,
                            encryptionContext: node.EncryptionContext
                            );
                    }
                    else
                    {
                        // validation
                        Validate(node.Type != null, "missing 'Type' attribute");
                        Validate((node.Allow == null) || ResourceMapping.IsCloudFormationType(node.Type ?? ""), "'Allow' attribute can only be used with AWS resource types");

                        // create resource item
                        _builder.AddResource(
                            parent: parent,
                            name: node.Resource,
                            description: node.Description,
                            type: node.Type ?? "AWS",
                            scope: ConvertScope(node.Scope),
                            allow: node.Allow,
                            properties: ParseToDictionary("Properties", node.Properties),
                            dependsOn: ConvertToStringList(node.DependsOn),
                            arnAttribute: node.DefaultAttribute,
                            condition: node.If,
                            pragmas: node.Pragmas
                            );
                    }
                });
                break;

            case "Nested":
                AtLocation(node.Nested, () => {
                    // validation
                    if (node.Module == null)
                    {
                        LogError("missing 'Module' attribute");
                    }
                    else if (!ModuleInfo.TryParse(node.Module, out var moduleInfo))
                    {
                        LogError("invalid value for 'Module' attribute");
                    }
                    else
                    {
                        // create nested module item
                        _builder.AddNestedModule(
                            parent: parent,
                            name: node.Nested,
                            description: node.Description,
                            moduleInfo: moduleInfo,
                            scope: ConvertScope(node.Scope),
                            dependsOn: node.DependsOn,
                            parameters: node.Parameters
                            );
                    }
                });
                break;

            case "Package":

                // package resource
                AtLocation(node.Package, () => {
                    // discover files to package
                    var files = new List <KeyValuePair <string, string> >();
                    if (node.Files != null)
                    {
                        string folder;
                        string filePattern;
                        SearchOption searchOption;
                        var packageFiles = Path.Combine(Settings.WorkingDirectory, node.Files);
                        if ((packageFiles.EndsWith("/", StringComparison.Ordinal) || Directory.Exists(packageFiles)))
                        {
                            folder       = Path.GetFullPath(packageFiles);
                            filePattern  = "*";
                            searchOption = SearchOption.AllDirectories;
                        }
                        else
                        {
                            folder       = Path.GetDirectoryName(packageFiles);
                            filePattern  = Path.GetFileName(packageFiles);
                            searchOption = SearchOption.TopDirectoryOnly;
                        }
                        if (Directory.Exists(folder))
                        {
                            foreach (var filePath in Directory.GetFiles(folder, filePattern, searchOption))
                            {
                                var relativeFilePathName = Path.GetRelativePath(folder, filePath);
                                files.Add(new KeyValuePair <string, string>(relativeFilePathName, filePath));
                            }
                            files = files.OrderBy(file => file.Key).ToList();
                        }
                        else
                        {
                            LogError($"cannot find folder '{Path.GetRelativePath(Settings.WorkingDirectory, folder)}'");
                        }
                    }
                    else
                    {
                        LogError("missing 'Files' attribute");
                    }

                    // create package resource item
                    _builder.AddPackage(
                        parent: parent,
                        name: node.Package,
                        description: node.Description,
                        scope: ConvertScope(node.Scope),
                        files: files
                        );
                });
                break;

            case "Function":
                AtLocation(node.Function, () => {
                    // validation
                    Validate(node.Memory != null, "missing 'Memory' attribute");
                    Validate(int.TryParse(node.Memory, out _), "invalid 'Memory' value");
                    Validate(node.Timeout != null, "missing 'Timeout' attribute");
                    Validate(int.TryParse(node.Timeout, out _), "invalid 'Timeout' value");
                    ValidateFunctionSource(node.Sources ?? new FunctionSourceNode[0]);

                    // determine function type
                    var project  = node.Project;
                    var language = node.Language;
                    var runtime  = node.Runtime;
                    var handler  = node.Handler;
                    DetermineFunctionType(node.Function, ref project, ref language, ref runtime, ref handler);

                    // create function item
                    var sources = AtLocation("Sources", () => node.Sources
                                             ?.Select((source, eventIndex) => ConvertFunctionSource(node, eventIndex, source))
                                             .Where(evt => evt != null)
                                             .ToList()
                                             );
                    _builder.AddFunction(
                        parent: parent,
                        name: node.Function,
                        description: node.Description,
                        scope: ConvertScope(node.Scope),
                        project: project,
                        language: language,
                        environment: node.Environment,
                        sources: sources,
                        condition: node.If,
                        pragmas: node.Pragmas,
                        timeout: node.Timeout,
                        runtime: runtime,
                        memory: node.Memory,
                        handler: handler,
                        properties: ParseToDictionary("Properties", node.Properties)
                        );
                });
                break;

            case "Condition":
                AtLocation(node.Condition, () => {
                    AtLocation("Value", () => {
                        Validate(node.Value != null, "missing 'Value' attribute");
                    });
                    _builder.AddCondition(
                        parent: parent,
                        name: node.Condition,
                        description: node.Description,
                        value: node.Value
                        );
                });
                break;

            case "Mapping":
                AtLocation(node.Mapping, () => {
                    IDictionary <string, IDictionary <string, string> > topLevelResults = new Dictionary <string, IDictionary <string, string> >();
                    if (node.Value is IDictionary topLevelDictionary)
                    {
                        AtLocation("Value", () => {
                            Validate(topLevelDictionary.Count > 0, "missing top-level mappings");

                            // iterate over top-level dictionary
                            foreach (DictionaryEntry topLevel in topLevelDictionary)
                            {
                                AtLocation((string)topLevel.Key, () => {
                                    var secondLevelResults = new Dictionary <string, string>();
                                    topLevelResults[(string)topLevel.Key] = secondLevelResults;

                                    // convert top-level entry
                                    if (topLevel.Value is IDictionary secondLevelDictionary)
                                    {
                                        Validate(secondLevelDictionary.Count > 0, "missing second-level mappings");

                                        // iterate over second-level dictionary
                                        foreach (DictionaryEntry secondLevel in secondLevelDictionary)
                                        {
                                            AtLocation((string)secondLevel.Key, () => {
                                                // convert second-level entry
                                                if (secondLevel.Value is string secondLevelValue)
                                                {
                                                    secondLevelResults[(string)secondLevel.Key] = secondLevelValue;
                                                }
                                                else
                                                {
                                                    LogError("invalid value");
                                                }
                                            });
                                        }
                                    }
Exemplo n.º 3
0
        //--- Methods ---
        public void Initialize(ModuleBuilder builder)
        {
            _builder = builder;

            // add module variables
            var moduleItem = _builder.AddVariable(
                parent: null,
                name: "Module",
                description: "Module Variables",
                type: "String",
                scope: null,
                value: "",
                allow: null,
                encryptionContext: null
                );

            _builder.AddVariable(
                parent: moduleItem,
                name: "Id",
                description: "Module ID",
                type: "String",
                scope: null,
                value: FnRef("AWS::StackName"),
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "Owner",
                description: "Module Owner",
                type: "String",
                scope: null,
                value: _builder.Owner,
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "Name",
                description: "Module Name",
                type: "String",
                scope: null,
                value: _builder.Name,
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "FullName",
                description: "Module FullName",
                type: "String",
                scope: null,
                value: _builder.FullName,
                allow: null,
                encryptionContext: null
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "Version",
                description: "Module Version",
                type: "String",
                scope: null,
                value: _builder.Version.ToString(),
                allow: null,
                encryptionContext: null
                );
            _builder.AddCondition(
                parent: moduleItem,
                name: "IsNested",
                description: "Module is nested",
                value: FnNot(FnEquals(FnRef("DeploymentRoot"), ""))
                );
            _builder.AddVariable(
                parent: moduleItem,
                name: "RootId",
                description: "Root Module ID",
                type: "String",
                scope: null,
                value: FnIf("Module::IsNested", FnRef("DeploymentRoot"), FnRef("Module::Id")),
                allow: null,
                encryptionContext: null
                );

            // add overridable logging retention variable
            if (!_builder.TryGetOverride("Module::LogRetentionInDays", out var logRetentionInDays))
            {
                logRetentionInDays = 30;
            }
            _builder.AddVariable(
                parent: moduleItem,
                name: "LogRetentionInDays",
                description: "Number days log entries are retained for",
                type: "Number",
                scope: null,
                value: logRetentionInDays,
                allow: null,
                encryptionContext: null
                );

            // add LambdaSharp Module Options
            var section = "LambdaSharp Module Options";

            _builder.AddParameter(
                name: "Secrets",
                section: section,
                label: "Comma-separated list of additional KMS secret keys",
                description: "Secret Keys (ARNs)",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: "",
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null
                );
            var secretsIsEmpty = _builder.AddCondition(
                parent: null,
                name: "SecretsIsEmpty",
                description: null,
                value: FnEquals(FnRef("Secrets"), "")
                );

            _builder.AddParameter(
                name: "XRayTracing",
                section: section,
                label: "AWS X-Ray tracing mode for module functions",
                description: "AWS X-Ray Tracing Mode",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: "PassThrough",
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: new[] { "Active", "PassThrough" },
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null
                );

            // import lambdasharp dependencies (unless requested otherwise)
            if (_builder.HasLambdaSharpDependencies)
            {
                // add LambdaSharp Module Internal resource imports
                var lambdasharp = _builder.AddVariable(
                    parent: null,
                    name: "LambdaSharp",
                    description: "LambdaSharp Core Imports",
                    type: "String",
                    scope: null,
                    value: "",
                    allow: null,
                    encryptionContext: null
                    );
                _builder.AddImport(
                    parent: lambdasharp,
                    name: "DeadLetterQueue",
                    description: null,

                    // TODO (2018-12-01, bjorg): consider using 'AWS::SQS::Queue'
                    type: "String",
                    scope: null,
                    allow: null /* new[] {
                                 * "sqs:SendMessage"
                                 * }*/,
                    module: "LambdaSharp.Core",
                    encryptionContext: null
                    );
                _builder.AddImport(
                    parent: lambdasharp,
                    name: "LoggingStream",
                    description: null,

                    // NOTE (2018-12-11, bjorg): we use type 'String' to be more flexible with the type of values we're willing to take
                    type: "String",
                    scope: null,
                    allow: null,
                    module: "LambdaSharp.Core",
                    encryptionContext: null
                    );
                _builder.AddImport(
                    parent: lambdasharp,
                    name: "LoggingStreamRole",
                    description: null,

                    // NOTE (2018-12-11, bjorg): we use type 'String' to be more flexible with the type of values we're willing to take
                    type: "String",
                    scope: null,
                    allow: null,
                    module: "LambdaSharp.Core",
                    encryptionContext: null
                    );
                _builder.AddImport(
                    parent: lambdasharp,
                    name: "DefaultSecretKey",
                    description: null,

                    // TODO (2018-12-01, bjorg): consider using 'AWS::KMS::Key'
                    type: "String",
                    scope: null,

                    // NOTE (2018-12-11, bjorg): we grant decryption access later as part of a bulk permissioning operation
                    allow: null,
                    module: "LambdaSharp.Core",
                    encryptionContext: null
                    );
            }

            // add module variables
            if (TryGetModuleVariable("DeadLetterQueue", out var deadLetterQueueVariable))
            {
                _builder.AddVariable(
                    parent: moduleItem,
                    name: "DeadLetterQueue",
                    description: "Module Dead Letter Queue (ARN)",
                    type: "String",
                    scope: null,
                    value: deadLetterQueueVariable,
                    allow: null,
                    encryptionContext: null
                    );
                _builder.AddGrant(
                    sid: "ModuleDeadLetterQueueLogging",
                    awsType: null,
                    reference: FnRef("Module::DeadLetterQueue"),
                    allow: new[] {
                    "sqs:SendMessage"
                }
                    );
            }
            if (TryGetModuleVariable("LoggingStream", out var loggingStreamVariable))
            {
                _builder.AddVariable(
                    parent: moduleItem,
                    name: "LoggingStream",
                    description: "Module Logging Stream (ARN)",
                    type: "String",
                    scope: null,
                    value: loggingStreamVariable,
                    allow: null,
                    encryptionContext: null
                    );
            }
            if (TryGetModuleVariable("LoggingStreamRole", out var loggingStreamRoleVariable))
            {
                _builder.AddVariable(
                    parent: moduleItem,
                    name: "LoggingStreamRole",
                    description: "Module Logging Stream Role (ARN)",
                    type: "String",
                    scope: null,
                    value: loggingStreamRoleVariable,
                    allow: null,
                    encryptionContext: null
                    );
            }
            if (TryGetModuleVariable("DefaultSecretKey", out var defaultSecretKeyVariable))
            {
                _builder.AddVariable(
                    parent: moduleItem,
                    name: "DefaultSecretKey",
                    description: "Module Default Secret Key (ARN)",
                    type: "String",
                    scope: null,
                    value: defaultSecretKeyVariable,
                    allow: null,
                    encryptionContext: null
                    );
                _builder.AddSecret(FnRef("Module::DefaultSecretKey"));
            }

            // add decryption permission for secrets
            var secretsReference = _builder.Secrets.Any()
                ? FnSplit(
                ",",
                FnIf(
                    secretsIsEmpty.FullName,
                    FnJoin(",", _builder.Secrets),
                    FnJoin(
                        ",",
                        _builder.Secrets.Append(FnRef("Secrets")).ToList()
                        )
                    )
                )
                : FnIf(
                secretsIsEmpty.FullName,

                // TODO (2018-11-26, bjorg): this hack does not work to bypass the error of an empty list :(
                "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:key/12345678-1234-1234-1234-123456789012",
                FnSplit(",", FnRef("Secrets"))
                );

            _builder.AddGrant(
                sid: "SecretsDecryption",
                awsType: null,
                reference: secretsReference,
                allow: new[] {
                "kms:Decrypt",
                "kms:Encrypt",
                "kms:GenerateDataKey",
                "kms:GenerateDataKeyWithoutPlaintext"
            }
                );

            // add decryption function for secret parameters and values
            _builder.AddInlineFunction(
                parent: moduleItem,
                name: "DecryptSecretFunction",
                description: "Module secret decryption function",
                environment: null,
                sources: null,
                condition: null,
                pragmas: new[] {
                "no-function-registration",
                "no-dead-letter-queue",
                "no-wildcard-scoped-variables"
            },
                timeout: "30",
                memory: "128",
                code: DecryptSecretFunctionCode
                ).DiscardIfNotReachable = true;

            // add LambdaSharp Deployment Settings
            section = "LambdaSharp Deployment Settings (DO NOT MODIFY)";
            _builder.AddParameter(
                name: "DeploymentBucketName",
                section: section,
                label: "Deployment S3 bucket name",
                description: "Deployment S3 Bucket Name",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: null,
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null
                );
            _builder.AddParameter(
                name: "DeploymentPrefix",
                section: section,
                label: "Deployment tier prefix",
                description: "Deployment Tier Prefix",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: null,
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null
                );
            _builder.AddParameter(
                name: "DeploymentPrefixLowercase",
                section: section,
                label: "Deployment tier prefix (lowercase)",
                description: "Deployment Tier Prefix (lowercase)",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: null,
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null
                );
            _builder.AddParameter(
                name: "DeploymentRoot",
                section: section,
                label: "Root stack name for nested deployments, blank otherwise",
                description: "Root Stack Name",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: "",
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null
                );
            _builder.AddParameter(
                name: "DeploymentChecksum",
                section: section,
                label: "CloudFormation template MD5 checksum",
                description: "Deployment Checksum",
                type: "String",
                scope: null,
                noEcho: null,
                defaultValue: "",
                constraintDescription: null,
                allowedPattern: null,
                allowedValues: null,
                maxLength: null,
                maxValue: null,
                minLength: null,
                minValue: null,
                allow: null,
                properties: null,
                arnAttribute: null,
                encryptionContext: null,
                pragmas: null
                );

            // create module IAM role used by all functions
            _builder.AddResource(
                parent: moduleItem,
                name: "Role",
                description: null,
                scope: null,
                resource: new Humidifier.IAM.Role {
                AssumeRolePolicyDocument = new Humidifier.PolicyDocument {
                    Version   = "2012-10-17",
                    Statement = new[] {
                        new Humidifier.Statement {
                            Sid       = "ModuleLambdaPrincipal",
                            Effect    = "Allow",
                            Principal = new Humidifier.Principal {
                                Service = "lambda.amazonaws.com"
                            },
                            Action = "sts:AssumeRole"
                        }
                    }.ToList()
                },
                Policies = new[] {
                    new Humidifier.IAM.Policy {
                        PolicyName     = FnSub("${AWS::StackName}ModulePolicy"),
                        PolicyDocument = new Humidifier.PolicyDocument {
                            Version   = "2012-10-17",
                            Statement = new List <Humidifier.Statement>()
                        }
                    }
                }.ToList()
            },
                resourceExportAttribute: null,
                dependsOn: null,
                condition: null,
                pragmas: null
                ).DiscardIfNotReachable = true;

            // permissions needed for writing to log streams (but not for creating log groups!)
            _builder.AddGrant(
                sid: "ModuleLogStreamAccess",
                awsType: null,
                reference: "arn:aws:logs:*:*:*",
                allow: new[] {
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            }
                );

            // permissions needed for X-Ray daemon to upload tracing information
            _builder.AddGrant(
                sid: "AWSXRayWriteAccess",
                awsType: null,
                reference: "*",
                allow: new[] {
                "xray:PutTraceSegments",
                "xray:PutTelemetryRecords",
                "xray:GetSamplingRules",
                "xray:GetSamplingTargets",
                "xray:GetSamplingStatisticSummaries"
            }
                );

            // check if lambdasharp specific resources need to be initialized
            var functions = _builder.Items.OfType <FunctionItem>().ToList();

            if (_builder.TryGetItem("Module::DeadLetterQueue", out _))
            {
                foreach (var function in functions.Where(f => f.HasDeadLetterQueue))
                {
                    // initialize dead-letter queue
                    function.Function.DeadLetterConfig = new Humidifier.Lambda.FunctionTypes.DeadLetterConfig {
                        TargetArn = FnRef("Module::DeadLetterQueue")
                    };
                }
            }

            // permissions needed for lambda functions to exist in a VPC
            if (functions.Any(function => function.Function.VpcConfig != null))
            {
                _builder.AddGrant(
                    sid: "ModuleVpcNetworkInterfaces",
                    awsType: null,
                    reference: "*",
                    allow: new[] {
                    "ec2:DescribeNetworkInterfaces",
                    "ec2:CreateNetworkInterface",
                    "ec2:DeleteNetworkInterface"
                }
                    );
            }

            // add module registration
            if (_builder.HasModuleRegistration)
            {
                _builder.AddDependency("LambdaSharp.Core", Settings.ToolVersion.GetCompatibleBaseVersion(), maxVersion: null, bucketName: null);

                // create module registration
                _builder.AddResource(
                    parent: moduleItem,
                    name: "Registration",
                    description: null,
                    type: "LambdaSharp::Registration::Module",
                    scope: null,
                    allow: null,
                    properties: new Dictionary <string, object> {
                    ["Module"]   = _builder.Info,
                    ["ModuleId"] = FnRef("AWS::StackName")
                },
                    dependsOn: null,
                    arnAttribute: null,
                    condition: null,
                    pragmas: null
                    );

                // handle function registrations
                var registeredFunctions = _builder.Items
                                          .OfType <FunctionItem>()
                                          .Where(function => function.HasFunctionRegistration)
                                          .ToList();
                if (registeredFunctions.Any())
                {
                    // create registration-related resources for functions
                    foreach (var function in registeredFunctions)
                    {
                        // create function registration
                        _builder.AddResource(
                            parent: function,
                            name: "Registration",
                            description: null,
                            type: "LambdaSharp::Registration::Function",
                            scope: null,
                            allow: null,
                            properties: new Dictionary <string, object> {
                            ["ModuleId"]             = FnRef("AWS::StackName"),
                            ["FunctionId"]           = FnRef(function.FullName),
                            ["FunctionName"]         = function.Name,
                            ["FunctionLogGroupName"] = FnSub($"/aws/lambda/${{{function.FullName}}}"),
                            ["FunctionPlatform"]     = "AWS Lambda",
                            ["FunctionFramework"]    = function.Function.Runtime,
                            ["FunctionLanguage"]     = function.Language,
                            ["FunctionMaxMemory"]    = function.Function.MemorySize,
                            ["FunctionMaxDuration"]  = function.Function.Timeout
                        },
                            dependsOn: new[] { "Module::Registration" },
                            arnAttribute: null,
                            condition: function.Condition,
                            pragmas: null
                            );

                        // create function log-group subscription
                        if (
                            _builder.TryGetItem("Module::LoggingStream", out _) &&
                            _builder.TryGetItem("Module::LoggingStreamRole", out _)
                            )
                        {
                            _builder.AddResource(
                                parent: function,
                                name: "LogGroupSubscription",
                                description: null,
                                scope: null,
                                resource: new Humidifier.Logs.SubscriptionFilter {
                                DestinationArn = FnRef("Module::LoggingStream"),
                                FilterPattern  = "-\"*** \"",
                                LogGroupName   = FnRef($"{function.FullName}::LogGroup"),
                                RoleArn        = FnRef("Module::LoggingStreamRole")
                            },
                                resourceExportAttribute: null,
                                dependsOn: null,
                                condition: function.Condition,
                                pragmas: null
                                );
                        }
                    }
                }
            }
        }