//--- 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 ); } } } } }
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"); } }); } }
//--- 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 ); } } } } }