class CfnModel::Transforms::Serverless

Handle transformation of model elements performed by the Serverless trasnform, see docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-aws-serverless.html

Public Class Methods

instance() click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 20
def self.instance
  @instance ||= Serverless.new
  @instance
end

Public Instance Methods

perform_transform(cfn_hash) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 9
def perform_transform(cfn_hash)
  with_line_numbers = false
  resources = cfn_hash['Resources'].clone
  resources.each do |resource_name, resource|
    next unless matching_resource_type?(resource['Type'], 'AWS::Serverless::Function')

    with_line_numbers = true if resource['Type'].is_a? Hash
    replace_serverless_function cfn_hash, resource_name, with_line_numbers
  end
end

Private Instance Methods

add_serverlessrestapi_event(paths_hash, event, function_name) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 284
def add_serverlessrestapi_event(paths_hash, event, function_name)
  paths_hash[event['Properties']['Path']] = {
    event['Properties']['Method'] => {
      'responses' => {},
      'x-amazon-apigateway-integration' => {
        'httpMethod' => 'POST',
        'type' => 'aws_proxy',
        'uri' => { 'Fn::Sub' => "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${#{function_name}.Arn}/invocations" }
      }
    }
  }
end
bucket_from_uri(uri) click to toggle source

Bucket is 3rd element of an S3 URI split on '/'

# File lib/cfn-model/transforms/serverless.rb, line 28
def bucket_from_uri(uri)
  uri.split('/')[2]
end
format_function_role(serverless_function, function_name) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 71
def format_function_role(serverless_function, function_name)
  getatt_hash = { 'Fn::GetAtt' => %W[#{function_name}Role Arn] }
  serverless_function['Properties']['Role'] || getatt_hash
end
format_resource_type(type, line_no, numbers) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 58
def format_resource_type(type, line_no, numbers)
  numbers ? { 'value' => type, 'line' => line_no } : type
end
function_role(serverless_function, function_name, with_line_numbers) click to toggle source

Return the hash structure of the '<function_name>Role' AWS::IAM::Role resource as created by Serverless transform

# File lib/cfn-model/transforms/serverless.rb, line 159
def function_role(serverless_function, function_name, with_line_numbers)
  fn_role = {
    'Type' => format_resource_type('AWS::IAM::Role', -1, with_line_numbers),
    'Properties' => {
      'ManagedPolicyArns' => function_role_managed_policies(serverless_function['Properties']),
      'AssumeRolePolicyDocument' => lambda_service_can_assume_role
    }
  }
  function_role_policies(fn_role, serverless_function['Properties'], function_name)
  fn_role
end
function_role_managed_policies(function_properties) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 171
def function_role_managed_policies(function_properties)
  # Always set AWSLambdaBasicExecutionRole policy
  base_policies = %w[arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole]

  # Return base_policies if no policies assigned to the function
  return base_policies unless function_properties['Policies']

  # If the SAM function Policies property is a string, append and return
  return base_policies | %W[arn:aws:iam::aws:policy/#{function_properties['Policies']}] if \
    function_properties['Policies'].is_a? String

  # Iterate on Policies property and add if String
  policy_names = function_properties['Policies'].select { |policy| policy.is_a? String }
  base_policies | policy_names.map { |name| "arn:aws:iam::aws:policy/#{name}" }
end
function_role_policies(role, function_properties, fn_name) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 187
def function_role_policies(role, function_properties, fn_name)
  # Return if no policies assigned to the function
  return unless function_properties['Policies']

  # Process inline policies from SAM function
  return if function_properties['Policies'].is_a? String

  # Iterate on Policies property and add if Hash
  policy_hashes = function_properties['Policies'].select do |policy|
    policy.is_a?(Hash) && policy.keys.first !~ /Policy/
  end
  return if policy_hashes.empty?

  # Create policy documents
  policy_documents = policy_hashes.map.with_index do |policy, index|
    {
      'PolicyDocument' => policy,
      'PolicyName' => "#{fn_name}RolePolicy#{index}"
    }
  end

  role['Properties']['Policies'] = policy_documents
end
lambda_function(handler:, code_bucket: nil, code_key: nil, role:, runtime:, reserved_concurrent_executions:, with_line_numbers: false) click to toggle source

Return the hash structure of a AWS::Lambda::Function as created by Serverless transform

# File lib/cfn-model/transforms/serverless.rb, line 223
def lambda_function(handler:,
                    code_bucket: nil,
                    code_key: nil,
                    role:,
                    runtime:,
                    reserved_concurrent_executions:,
                    with_line_numbers: false)
  fn_resource = {
    'Type' => format_resource_type('AWS::Lambda::Function', -1, with_line_numbers),
    'Properties' => {
      'Handler' => handler,
      'Role' => role,
      'Runtime' => runtime
    }
  }
  fn_resource['Properties']['ReservedConcurrentExecutions'] = reserved_concurrent_executions unless reserved_concurrent_executions.nil?

  lambda_function_code(fn_resource, code_bucket, code_key)
end
lambda_function_code(fn_resource, code_bucket, code_key) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 211
def lambda_function_code(fn_resource, code_bucket, code_key)
  if code_bucket && code_key
    fn_resource['Properties']['Code'] = {
      'S3Bucket' => code_bucket,
      'S3Key' => code_key
    }
  end
  fn_resource
end
lambda_service_can_assume_role() click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 144
def lambda_service_can_assume_role
  {
    'Version' => '2012-10-17',
    'Statement' => [
      {
        'Action' => ['sts:AssumeRole'],
        'Effect' => 'Allow',
        'Principal' => { 'Service' => ['lambda.amazonaws.com'] }
      }
    ]
  }
end
matching_line_number_enriched_resource_type?(resource_type, type_to_match) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 54
def matching_line_number_enriched_resource_type?(resource_type, type_to_match)
  resource_type.is_a?(Hash) && resource_type['value'].eql?(type_to_match)
end
matching_resource_type?(resource_type, type_to_match) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 45
def matching_resource_type?(resource_type, type_to_match)
  matching_string_resource_type?(resource_type, type_to_match) ||
    matching_line_number_enriched_resource_type?(resource_type, type_to_match)
end
matching_string_resource_type?(resource_type, type_to_match) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 50
def matching_string_resource_type?(resource_type, type_to_match)
  resource_type.is_a?(String) && resource_type.eql?(type_to_match)
end
object_key_from_uri(uri) click to toggle source

Object key is 4th element to end of an S3 URI split on '/'

# File lib/cfn-model/transforms/serverless.rb, line 33
def object_key_from_uri(uri)
  uri.split('/')[3..-1].join('/')
end
replace_serverless_function(cfn_hash, resource_name, with_line_numbers) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 108
def replace_serverless_function(cfn_hash, resource_name, with_line_numbers)
  serverless_function = cfn_hash['Resources'][resource_name]

  lambda_fn_params = serverless_function_properties(cfn_hash,
                                                    serverless_function,
                                                    resource_name,
                                                    with_line_numbers)

  cfn_hash['Resources'][resource_name] = lambda_function(
    handler: lambda_fn_params[:handler],
    code_bucket: lambda_fn_params[:code_bucket],
    code_key: lambda_fn_params[:code_key],
    role: lambda_fn_params[:role],
    runtime: lambda_fn_params[:runtime],
    reserved_concurrent_executions: lambda_fn_params[:reserved_concurrent_executions],
    with_line_numbers: lambda_fn_params[:with_line_numbers]
  )
  unless serverless_function['Properties']['Role']
    cfn_hash['Resources'][resource_name + 'Role'] = function_role(serverless_function,
                                                                  resource_name,
                                                                  with_line_numbers)
  end

  transform_function_events(cfn_hash, serverless_function, resource_name, with_line_numbers) if \
    serverless_function['Properties']['Events']

  # Handle passing along cfn-nag specific metadata. SAM itself does not support metadata during transformation.
  # https://github.com/aws/serverless-application-model/issues/264
  if serverless_function.key?('Metadata') && serverless_function['Metadata'].key?('cfn_nag')
    cfn_hash['Resources'][resource_name]['Metadata'] = serverless_function['Metadata']
    unless serverless_function['Properties']['Role']
      cfn_hash['Resources'][resource_name + 'Role']['Metadata'] = serverless_function['Metadata']
    end
  end
end
resolve_globals_function_property(cfn_hash, property_name) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 62
def resolve_globals_function_property(cfn_hash, property_name)
  cfn_hash['Globals'] && cfn_hash['Globals']['Function'] && cfn_hash['Globals']['Function'][property_name]
end
s3_uri?(uri) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 37
def s3_uri?(uri)
  if uri.is_a? String
    uri[0..4].eql? 's3://'
  else
    false
  end
end
serverless_function_properties(cfn_hash, serverless_function, fn_name, with_line_numbers) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 89
def serverless_function_properties(cfn_hash, serverless_function, fn_name, with_line_numbers)
  code_uri = serverless_function_property(serverless_function, cfn_hash, 'CodeUri')

  lambda_fn_params = {
    handler: serverless_function_property(serverless_function, cfn_hash, 'Handler'),
    role: format_function_role(serverless_function, fn_name),
    runtime: serverless_function_property(serverless_function, cfn_hash, 'Runtime'),
    reserved_concurrent_executions: serverless_function_property(serverless_function, cfn_hash, 'ReservedConcurrentExecutions'),
    with_line_numbers: with_line_numbers
  }

  lambda_fn_params = transform_code_uri(
    lambda_fn_params,
    code_uri
  )

  lambda_fn_params
end
serverless_function_property(serverless_function, cfn_hash, property_name) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 66
def serverless_function_property(serverless_function, cfn_hash, property_name)
  serverless_function['Properties'][property_name] || \
    resolve_globals_function_property(cfn_hash, property_name)
end
serverlessrestapi_base(with_line_nos) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 268
def serverlessrestapi_base(with_line_nos)
  {
    'Type' => format_resource_type('AWS::ApiGateway::RestApi', -1, with_line_nos),
    'Properties' => {
      'Body' => {
        'info' => {
          'title' => { 'Ref' => 'AWS::StackName' },
          'version' => '1.0'
        },
        'paths' => {},
        'swagger' => '2.0'
      }
    }
  }
end
serverlessrestapi_deployment(with_line_nos) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 297
def serverlessrestapi_deployment(with_line_nos)
  {
    'Type' => format_resource_type('AWS::ApiGateway::Deployment', -1, with_line_nos),
    'Properties' => {
      'Description' => 'Generated by cfn-model',
      'RestApiId' => { 'Ref' => 'ServerlessRestApi' },
      'StageDescription' => {
        'AccessLogSetting' => {
          'DestinationArn' => 'arn:aws:logs:region:account:group/ApiLogs',
          'Format' => '$context.requestId'
        }
      },
      'StageName' => 'Stage'
    }
  }
end
serverlessrestapi_resources(cfn_hash, event, func_name, with_line_numbers) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 252
def serverlessrestapi_resources(cfn_hash, event, func_name, with_line_numbers)
  # ServerlessRestApi
  cfn_hash['Resources']['ServerlessRestApi'] ||= serverlessrestapi_base with_line_numbers
  add_serverlessrestapi_event(
    cfn_hash['Resources']['ServerlessRestApi']['Properties']['Body']['paths'],
    event,
    func_name
  )

  # ServerlessRestApiDeployment
  cfn_hash['Resources']['ServerlessRestApiDeployment'] = serverlessrestapi_deployment with_line_numbers

  # ServerlessRestApiProdStage
  cfn_hash['Resources']['ServerlessRestApiProdStage'] = serverlessrestapi_stage with_line_numbers
end
serverlessrestapi_stage(with_line_nos) click to toggle source
# File lib/cfn-model/transforms/serverless.rb, line 314
def serverlessrestapi_stage(with_line_nos)
  {
    'Type' => format_resource_type('AWS::ApiGateway::Stage', -1, with_line_nos),
    'Properties' => {
      'DeploymentId' => { 'Ref' => 'ServerlessRestApiDeployment' },
      'RestApiId' => { 'Ref' => 'ServerlessRestApi' },
      'StageName' => 'Prod'
    }
  }
end
transform_code_uri(lambda_fn_params, code_uri) click to toggle source

i question whether we need to carry out the transform this far given cfn_nag likely won't ever opine on bucket names or object keys

# File lib/cfn-model/transforms/serverless.rb, line 78
def transform_code_uri(lambda_fn_params, code_uri)
  if s3_uri? code_uri
    lambda_fn_params[:code_bucket] = bucket_from_uri code_uri
    lambda_fn_params[:code_key] = object_key_from_uri code_uri
  elsif code_uri.is_a? Hash
    lambda_fn_params[:code_bucket] = code_uri['Bucket']
    lambda_fn_params[:code_key] = code_uri['Key']
  end
  lambda_fn_params
end
transform_function_events(cfn_hash, serverless_function, function_name, with_line_numbers) click to toggle source

Return the Event structure of a AWS::Lambda::Function as created by Serverless transform

# File lib/cfn-model/transforms/serverless.rb, line 245
def transform_function_events(cfn_hash, serverless_function, function_name, with_line_numbers)
  serverless_function['Properties']['Events'].each do |_, event|
    serverlessrestapi_resources(cfn_hash, event, function_name, with_line_numbers) if \
      matching_resource_type?(event['Type'], 'Api')
  end
end