cfndsl-pipeline 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,41 @@
1
+ ---
2
+
3
+ TagStandard:
4
+ ServiceName:
5
+ Default: axt
6
+ Type: String
7
+ LogicalName: pServiceName
8
+ Label: Service pServiceName
9
+ EnvironmentID:
10
+ Default: dev1
11
+ AllowedPattern: (^dev|^sit|^prod)([-])?(\d{1,2})
12
+ Type: String
13
+ LogicalName: pEnvironmentID
14
+ Label: Environment ID
15
+ CostCentre:
16
+ Default: CC_DEVOPS
17
+ Type: String
18
+ LogicalName: pCostCentre
19
+ AllowedPattern: '[a-zA-Z0-9+-=._:/@ ]*'
20
+ Label: Cost Centre
21
+ Owner:
22
+ Default: 'Jane Doe'
23
+ Type: String
24
+ LogicalName: pOwner
25
+ Label: Owner
26
+ ApplicationID:
27
+ Default: CMDB-NS2010
28
+ Type: String
29
+ LogicalName: pApplicationID
30
+ Label:
31
+ AppCategory:
32
+ Default: B
33
+ Type: String
34
+ AllowedPattern: '[ABCD]'
35
+ LogicalName: pAppCategory
36
+ Label: Application Category
37
+ SupportGroup:
38
+ Default: 'DevOps Support'
39
+ Type: String
40
+ LogicalName: pSupportGroup
41
+ Label: Support Group
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
2
  #
3
- #
4
3
  # The MIT License
5
4
  #
6
5
  # Copyright (c) 2019 Cam Maxwell (cameron.maxwell@gmail.com)
@@ -22,44 +21,45 @@
22
21
  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
22
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
23
  # THE SOFTWARE.
25
- #
24
+ #
26
25
 
27
26
  require 'cfndsl'
27
+ require 'cfn-nag'
28
28
  require 'fileutils'
29
29
 
30
+ require_relative 'monkey-patches/cfndsl_patch'
31
+ require_relative 'monkey-patches/stdout_capture'
30
32
  require_relative 'options'
31
33
  require_relative 'params'
32
- require_relative 'monkey_patches'
33
- require_relative 'stdout_capture'
34
- require_relative 'run-cfndsl'
35
- require_relative 'run-cfn_nag'
36
- require_relative 'run-syntax'
34
+ require_relative 'exec_cfndsl'
35
+ require_relative 'exec_cfn_nag'
36
+ require_relative 'exec_syntax'
37
37
 
38
38
  module CfnDslPipeline
39
+ # Main pipeline
39
40
  class Pipeline
40
-
41
41
  attr_accessor :input_filename, :output_dir, :options, :base_name, :template, :output_filename, :output_file, :syntax_report
42
42
 
43
- def initialize (output_dir, options)
43
+ def initialize(output_dir, options)
44
44
  self.input_filename = ''
45
45
  self.output_file = nil
46
46
  self.template = nil
47
47
  self.options = options || nil
48
48
  self.syntax_report = []
49
49
  FileUtils.mkdir_p output_dir
50
- abort "Could not create output directory #{output_dir}" if Dir[output_dir] == nil
50
+ abort "Could not create output directory #{output_dir}" unless Dir[output_dir]
51
51
  self.output_dir = output_dir
52
52
  end
53
53
 
54
54
  def build(input_filename, cfndsl_extras)
55
- abort "Input file #{input_filename} doesn't exist!" if !File.file?(input_filename)
55
+ abort "Input file #{input_filename} doesn't exist!" unless File.file?(input_filename)
56
56
  self.input_filename = "#{input_filename}"
57
57
  self.base_name = File.basename(input_filename, '.*')
58
- self.output_filename = File.expand_path("#{self.output_dir}/#{self.base_name}.yaml")
58
+ self.output_filename = File.expand_path("#{output_dir}/#{base_name}.yaml")
59
59
  exec_cfndsl cfndsl_extras
60
- exec_syntax_validation if self.options.validate_syntax
61
- exec_dump_params if self.options.dump_deploy_params
62
- exec_cfn_nag if self.options.validate_cfn_nag
60
+ exec_syntax_validation if options.validate_syntax
61
+ exec_dump_params if options.dump_deploy_params
62
+ exec_cfn_nag if options.validate_cfn_nag
63
63
  end
64
64
  end
65
65
  end
@@ -0,0 +1,124 @@
1
+
2
+ require 'optparse'
3
+
4
+ module CfnDslPipeline
5
+ # Command Line Options processing
6
+ class CliOptions
7
+ attr_accessor :output, :template, :pipeline, :cfndsl_extras, :op
8
+
9
+ USAGE = "Usage: #{File.basename(__FILE__)} input file [ -o output_dir ] [ -b bucket ] OPTIONS [ include1 include2 etc.. ]"
10
+
11
+ def initialize
12
+ @output = './'
13
+ @cfndsl_extras = []
14
+ @pipeline = CfnDslPipeline::Options.new
15
+ parse && validate
16
+ end
17
+
18
+ private
19
+
20
+ # rubocop:disable Metrics/AbcSize
21
+ # rubocop:disable Metrics/MethodLength
22
+ def parse
23
+ @op = OptionParser.new do |opts|
24
+ opts.banner = USAGE
25
+
26
+ opts.on('-o', '--output dir', 'Optional output directory. Default is current directory') do |dir|
27
+ @output = dir
28
+ end
29
+
30
+ opts.on('-b', '--bucket', 'Optional Existing S3 bucket for cost estimation and large template syntax validation') do |bucket|
31
+ pipeline.validation_bucket = bucket
32
+ end
33
+
34
+ opts.on('-p', '--params', 'Create cloudformation deploy compatible params file') do
35
+ pipeline.dump_deploy_params = true
36
+ end
37
+
38
+ opts.on('-s', '--syntax', 'Enable syntax check') do
39
+ pipeline.validate_syntax = true
40
+ end
41
+
42
+ opts.on('--syntax-report', 'Save template syntax report') do
43
+ pipeline.save_syntax_report = true
44
+ end
45
+
46
+ opts.on('-a', '--audit', 'Enable cfn_nag audit') do
47
+ pipeline.validate_cfn_nag = false
48
+ end
49
+
50
+ opts.on('--audit-rule-dir', 'cfn_nag audit custom rules directory') do
51
+ pipeline.cfn_nag[:rule_directory] = true
52
+ end
53
+
54
+ opts.on('--audit-report', 'Save cfn_nag audit report') do
55
+ pipeline.save_audit_report = true
56
+ end
57
+
58
+ opts.on('--audit-debug', 'Enable cfn_nag rule debug output') do
59
+ pipeline.debug_audit = true
60
+ end
61
+
62
+ opts.on('-e', '--estimate-costs', 'Generate URL for AWS simple cost calculator') do
63
+ pipeline.estimate_cost = true
64
+ end
65
+
66
+ opts.on('-r', '--aws-region', 'AWS region to use. Default: ap-southeast-2') do |region|
67
+ pipeline.aws_region = region
68
+ end
69
+
70
+ opts.on_tail('-h', '--help', 'show this message') do
71
+ puts opts
72
+ exit
73
+ end
74
+
75
+ opts.on_tail('-d', '--debug', 'show pipeline debug messages') do
76
+ pipeline.debug = true
77
+ exit
78
+ end
79
+
80
+ opts.on_tail('-v', '--version', 'Show version') do
81
+ puts CfnDsl::Pipeline::VERSION
82
+ exit
83
+ end
84
+ end
85
+ @op.parse!
86
+
87
+ # first non-dash parameter is the mandatory input file
88
+ @template = ARGV.pop
89
+
90
+ ARGV.each do |arg|
91
+ @cfndsl_extras << [:yaml, arg]
92
+ end if ARGV.length > 0
93
+
94
+ pipeline
95
+ end
96
+ # rubocop:enable Metrics/AbcSize
97
+ # rubocop:enable Metrics/MethodLength
98
+
99
+ def fatal(msg)
100
+ puts msg
101
+ puts @op
102
+ exit 1
103
+ end
104
+
105
+ # rubocop:disable Metrics/CyclomaticComplexity
106
+ # rubocop:disable Metrics/PerceivedComplexity
107
+ def validate
108
+ # Exit on invalid option combinations
109
+ fatal 'Error: Input template file does not exist.' unless @template && File.file?(@template)
110
+
111
+ if @pipeline.save_syntax_report
112
+ fatal 'Error: save syntax report is set, but syntax validation was not enabled.' unless @pipeline.validate_syntax && !@pipeline.save_syntax_report
113
+ end
114
+ # rubocop:disable Style/GuardClause
115
+ if @pipeline.cfn_nag.rule_directory || @pipeline.debug_audit || @pipeline.save_audit_report
116
+ fatal 'Error: Audit options set, but audit was not enabled' unless @pipeline.validate_cfn_nag
117
+ fatal 'Error: cfn_nag rule directory does not exist' unless File.directory?(@pipeline.cfn_nag.rule_directory)
118
+ end
119
+ # rubocop:enable Style/GuardClause
120
+ end
121
+ # rubocop:enable Metrics/CyclomaticComplexity
122
+ # rubocop:enable Metrics/PerceivedComplexity
123
+ end
124
+ end
@@ -0,0 +1,58 @@
1
+ require 'cfn-nag'
2
+ require 'logging'
3
+ require 'colorize'
4
+
5
+ module CfnDslPipeline
6
+ # Add cfn_nag functions to pipeline
7
+ class Pipeline
8
+ def exec_cfn_nag
9
+ puts 'Auditing template with cfn-nag...'
10
+ configure_cfn_nag_logging
11
+ cfn_nag = CfnNag.new(config: options.cfn_nag)
12
+ result = cfn_nag.audit(cloudformation_string: template)
13
+ save_report result
14
+ display_report result
15
+ show_summary result
16
+ end
17
+
18
+ private
19
+
20
+ def configure_cfn_nag_logging
21
+ CfnNagLogging.configure_logging([:debug]) if options.debug_audit
22
+ end
23
+
24
+ def display_report(result)
25
+ ColoredStdoutResults.new.render([
26
+ {
27
+ filename: "#{@base_name}",
28
+ file_results: result
29
+ }
30
+ ])
31
+ end
32
+
33
+ def save_report(result)
34
+ return unless options.save_audit_report
35
+ report_data = Capture.capture do
36
+ SimpleStdoutResults.new.render([
37
+ {
38
+ filename: "#{@base_name}",
39
+ file_results: result
40
+ }
41
+ ])
42
+ end
43
+ filename = "#{output_dir}/#{base_name}.audit"
44
+ File.open(File.expand_path(filename), 'w').puts report_data['stdout']
45
+ puts "Saved audit report to #{filename}"
46
+ end
47
+
48
+ def show_summary(result)
49
+ if result[:failure_count] > 0
50
+ puts "Audit failed. #{result[:failure_count]} error(s) found ( ಠ ʖ̯ ಠ) ".red
51
+ elsif result[:violations].count > 0
52
+ puts "Audit passed with #{result[:warning_count]} warnings. (._.) ".yellow
53
+ else
54
+ puts 'Audit passed! ヽ( ゚ヮ゚)/ ヽ(´ー`)ノ'.green
55
+ end
56
+ end
57
+ end
58
+ end
@@ -3,16 +3,18 @@ require 'cfndsl/globals'
3
3
  require 'cfndsl/version'
4
4
 
5
5
  module CfnDslPipeline
6
+ #
6
7
  class Pipeline
7
- def exec_cfndsl(cfndsl_extras)
8
- print "Generating CloudFormation template...\n"
9
- model = CfnDsl.eval_file_with_extras("#{@input_filename}", cfndsl_extras)
8
+ def exec_cfndsl(cfndsl_extras)
9
+ puts 'Generating CloudFormation template...'
10
+
11
+ model = CfnDsl.eval_file_with_extras("#{@input_filename}", cfndsl_extras, (options.debug_cfndsl ? STDOUT : nil))
10
12
  @template = JSON.parse(model.to_json).to_yaml
11
13
  File.open(@output_filename, 'w') do |file|
12
14
  file.puts @template
13
15
  end
14
16
  @output_file = File.open(@output_filename)
15
- puts " #{@output_file.size} bytes written to #{@output_filename}"
17
+ puts "#{@output_file.size} bytes written to #{@output_filename}"
16
18
  end
17
19
  end
18
- end
20
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ require 'aws-sdk-cloudformation'
3
+ require 'aws-sdk-s3'
4
+ require 'uuid'
5
+
6
+ module CfnDslPipeline
7
+ #
8
+ class Pipeline
9
+ attr_accessor :s3_client
10
+
11
+ def initialize
12
+ self.s3_client = Aws::S3::Client.new(region: aws_region)
13
+ end
14
+ # rubocop:disable Metrics/AbcSize
15
+ def exec_syntax_validation
16
+ puts 'Validating template syntax...'
17
+ if options.estimate_cost || (output_file.size > 51_200)
18
+ puts 'Filesize is greater than 51200, or cost estimation required. Validating via S3 bucket '
19
+ uuid = UUID.new
20
+ bucket = determine_bucket
21
+ object_name = uuid.generate.to_s
22
+ upload_template(bucket, object_name)
23
+ self.syntax_report = s3_validate_syntax(bucket, object_name)
24
+ estimate_cost(bucket_name, object_name)
25
+ unless options.validation_bucket
26
+ puts 'Deleting temporary S3 bucket...'
27
+ bucket.delete!
28
+ end
29
+ else
30
+ self.syntax_report = local_validate_syntax
31
+ end
32
+ save_syntax_report
33
+ end
34
+ # rubocop:enable Metrics/AbcSize
35
+
36
+ private
37
+
38
+ def determine_bucket
39
+ if options.validation_bucket
40
+ bucket_name = options.validation_bucket
41
+ puts "Using existing S3 bucket #{bucket_name}..."
42
+ bucket = s3_client.bucket(options.validation_bucket)
43
+ else
44
+ bucket_name = "arch-code-#{uuid.generate}"
45
+ puts "Creating temporary S3 bucket #{bucket_name}..."
46
+ bucket = s3_client.bucket(bucket_name)
47
+ bucket.create
48
+ end
49
+ bucket
50
+ end
51
+
52
+ def save_syntax_report
53
+ return unless options.save_syntax_report
54
+ report_filename = "#{output_dir}/#{base_name}.report"
55
+ puts "Syntax validation report written to #{report_filename}"
56
+ File.open(File.expand_path(report_filename), 'w').puts syntax_report.to_hash.to_yaml
57
+ end
58
+
59
+ def upload_template(bucket, object_name)
60
+ puts 'Uploading template to temporary S3 bucket...'
61
+ object = bucket.object(object_name)
62
+ object.upload_file(output_file)
63
+ puts "https://s3.amazonaws.com/#{bucket_name}/#{object_name}"
64
+ end
65
+
66
+ def estimate_cost(bucket, object_name)
67
+ return unless options.estimate_cost
68
+ puts 'Estimate cost of template...'
69
+ client = Aws::CloudFormation::Client.new(region: options.aws_region)
70
+ costing = client.estimate_template_cost(template_url: "https://#{bucket.url}/#{object_name}")
71
+ puts "Cost Calculator URL is: #{costing.url}"
72
+ end
73
+
74
+ def s3_validate_syntax(bucket, object_name)
75
+ return unless options.validate_syntax
76
+ puts 'Validating template syntax in S3 Bucket...'
77
+ client = Aws::CloudFormation::Client.new(region: options.aws_region)
78
+ client.validate_template(template_url: "https://s3.amazonaws.com/#{bucket.url}/#{object_name}")
79
+ end
80
+
81
+ def local_validate_syntax
82
+ puts 'Validating template syntax locally...'
83
+ client = Aws::CloudFormation::Client.new(region: options.aws_region)
84
+ client.validate_template(template_body: template)
85
+ end
86
+ end
87
+ end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require 'cfndsl/globals'
4
3
  require 'cfndsl/version'
5
- PARAM_PROPS = %w[Description Default AllowedPattern AllowedValues].freeze
6
- HAS_PROPAGATABLE_TAGS = %w[CfnDsl::AWS::Types::AWS_AutoScaling_AutoScalingGroup].freeze
7
- HAS_MAPPED_TAGS = %w[CfnDsl::AWS::Types::AWS_Serverless_Function CfnDsl::AWS::Types::AWS_Serverless_SimpleTable CfnDsl::AWS::Types::AWS_Serverless_Application].freeze
4
+ require 'cfndsl/external_parameters'
5
+ require 'cfndsl/aws/cloud_formation_template'
6
+
7
+ PARAM_PROPS = %w([Description Default AllowedPattern AllowedValues]).freeze
8
+ HAS_PROPAGATABLE_TAGS = %w([CfnDsl::AWS::Types::AWS_AutoScaling_AutoScalingGroup]).freeze
9
+ # rubocop:disable Metrics/LineLength
10
+ HAS_MAPPED_TAGS = %w([CfnDsl::AWS::Types::AWS_Serverless_Function CfnDsl::AWS::Types::AWS_Serverless_SimpleTable CfnDsl::AWS::Types::AWS_Serverless_Application]).freeze
11
+ # rubocop:enable Metrics/LineLength
8
12
 
9
13
  # Automatically add Parameters for Tag values
10
14
  CfnDsl::CloudFormationTemplate.class_eval do
@@ -21,11 +25,20 @@ CfnDsl::CloudFormationTemplate.class_eval do
21
25
  send(key, props[key]) if props[key]
22
26
  end
23
27
  end
24
- end if external_parameters[:TagStandard].kind_of?(Hash)
28
+ end if external_parameters[:TagStandard].is_a?(Hash)
25
29
  end
26
30
  end
27
31
 
28
32
  module CfnDsl
33
+ # Add ability to reset params when being used in loops in rakefiles etc
34
+ class ExternalParameters
35
+ class << self
36
+ def reset
37
+ @parameters = self.class.defaults.clone
38
+ end
39
+ end
40
+ end
41
+
29
42
  # extends CfnDsl esource Properties to automatically substitute
30
43
  # FnSub recuraively
31
44
  class PropertyDefinition < JSONable
@@ -36,9 +49,8 @@ module CfnDsl
36
49
  def fix_substitutions(val)
37
50
  return val unless defined? val.class.to_s.downcase
38
51
  meth = "fix_#{val.class.to_s.downcase}"
39
- if respond_to?(meth.to_sym)
40
- return send(meth, val)
41
- end
52
+
53
+ return send(meth, val) if respond_to?(meth.to_sym)
42
54
  val
43
55
  end
44
56
 
@@ -62,27 +74,31 @@ module CfnDsl
62
74
  apply_tag_standard
63
75
  end
64
76
 
77
+ private
78
+
65
79
  def apply_tag_standard
66
80
  return unless defined? external_parameters[:TagStandard]
67
- return unless external_parameters[:TagStandard].kind_of?(Hash)
68
-
69
- resource_type = self.class.to_s
81
+ return unless external_parameters[:TagStandard].is_a?(Hash)
82
+ apply_tags(external_parameters[:TagStandard]) if defined? self.Tag
83
+ apply_tags_map(external_parameters[:TagStandard]) if HAS_MAPPED_TAGS.include? self.class.to_s
84
+ end
70
85
 
71
- if defined? self.Tag
72
- external_parameters[:TagStandard].each do |tag_name, props|
73
- send(:Tag) do
74
- Key tag_name.to_s
75
- Value Ref(props['LogicalName'] || tag_name)
76
- PropagateAtLaunch true if HAS_PROPAGATABLE_TAGS.include? resource_type
77
- end
78
- end
79
- elsif HAS_MAPPED_TAGS.include? resource_type
80
- tag_map = {}
81
- external_parameters[:TagStandard].each do |tag_name, props|
82
- tag_map[tag_name.to_s] = Ref(props['LogicalName'] || tag_name)
86
+ def apply_tags(tags)
87
+ tags.each do |tag_name, props|
88
+ send(:Tag) do
89
+ Key tag_name.to_s
90
+ Value Ref(props['LogicalName'] || tag_name)
91
+ PropagateAtLaunch true if HAS_PROPAGATABLE_TAGS.include? self.class.to_s
83
92
  end
84
- Tags tag_map
85
93
  end
86
94
  end
95
+
96
+ def apply_tags_map(tags)
97
+ tag_map = {}
98
+ tags.each do |tag_name, props|
99
+ tag_map[tag_name.to_s] = Ref(props['LogicalName'] || tag_name)
100
+ end
101
+ Tags tag_map
102
+ end
87
103
  end
88
104
  end