minimal_pipeline 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c77dd539e2d2d4c1a1639ed735bb8ef0d1e0802e59a1240e16af9e2baa03954d
4
+ data.tar.gz: 22fc7cc213e129f5ceada0fde056f61cfaa3722078b65b1dbc99b77d8fa48bcf
5
+ SHA512:
6
+ metadata.gz: fd61094908f6d9407dbbf1b3e7b8218013125a71d0fe9582887547b297138acfc3952cab760661ab1856a4b80b1d99c12f5771128d6b5b31f27272ad93cd29c8
7
+ data.tar.gz: cf8b54cdedd4b8bccbbe5539cf6ff15742fc3968bc2ff74aa37f7db0bb9e77d6fd25905af81c635e5250b0d7120df3c8ef0f8cbc1c8399c91cef7c5de7891879
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk'
4
+ require 'crossing'
5
+ require 'keystore'
6
+ require 'open3'
7
+ require 'docker'
8
+ require 'json'
9
+
10
+ # Top level namespace for automation pipeline classes
11
+ class MinimalPipeline
12
+ # autoload libraries
13
+ autoload(:Cloudformation, 'minimal_pipeline/cloudformation')
14
+ autoload(:Crossing, 'minimal_pipeline/crossing')
15
+ autoload(:Docker, 'minimal_pipeline/docker')
16
+ autoload(:Keystore, 'minimal_pipeline/keystore')
17
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk'
4
+
5
+ class MinimalPipeline
6
+ # Here is an example of how to use this class to deploy CloudFormation Stacks.
7
+ #
8
+ # ```
9
+ # cloudformation = MinimalPipeline::Cloudformation.new
10
+ #
11
+ # cloudformation_parameters = {
12
+ # 'Vpc' => 'vpc-123456',
13
+ # 'AsgSubnets' => %w[sg-one sg-two sg-three],
14
+ # 'ElbSecurityGroup' => 'sg-123456',
15
+ # 'CertName' => 'example'
16
+ # }
17
+ #
18
+ # stack_parameters = {
19
+ # stack_name: stack_name,
20
+ # template_body: File.read('provisioning/elb.json'),
21
+ # capabilities: ['CAPABILITY_IAM'],
22
+ # parameters: cloudformation.params(cloudformation_parameters)
23
+ # }
24
+ #
25
+ # cloudformation.deploy_stack('EXAMPLE_ELB', stack_parameters)
26
+ # name = cloudformation.stack_output(stack_name, 'LoadBalancerName')
27
+ # ```
28
+ #
29
+ # You will need the following environment variables to be present:
30
+ # * `AWS_REGION` or `region`
31
+ class Cloudformation
32
+ # Instance of `Aws::CloudFormation::Client`
33
+ attr_reader(:client)
34
+
35
+ # Sets up `Aws::CloudFormation::Client`
36
+ # Requires environment variable `AWS_REGION` or `region` to be set.
37
+ def initialize
38
+ raise 'You must set env variable AWS_REGION.' if ENV['AWS_REGION'].nil?
39
+
40
+ region = ENV['AWS_REGION']
41
+ @client = Aws::CloudFormation::Client.new(region: region)
42
+ end
43
+
44
+ # Converts a parameter Hash into a CloudFormation friendly structure
45
+ #
46
+ # @param parameters [Hash] Key value pair of parameters for a CFN stack.
47
+ # @return [Hash] CloudFormation friendly data structure of parameter
48
+ def params(parameters)
49
+ parameter_list = []
50
+ parameters.each do |k, v|
51
+ parameter_list.push(parameter_key: k, parameter_value: v)
52
+ end
53
+ parameter_list
54
+ end
55
+
56
+ # Retrieves the CloudFormation stack output of a single value
57
+ #
58
+ # @param stack [String] The name of the CloudFormation stack
59
+ # @param output [String] The name of the output to fetch the value of
60
+ # @return [String] The value of the output for the CloudFormation stack
61
+ def stack_output(stack, output)
62
+ resp = @client.describe_stacks(stack_name: stack)
63
+
64
+ raise "#{stack.upcase} stack does not exist!" if resp.stacks.empty?
65
+
66
+ resp.stacks.first.outputs.each do |stack_output|
67
+ zero_output = stack_output.output_key.casecmp(output).zero?
68
+ return stack_output.output_value if zero_output
69
+ end
70
+
71
+ raise "#{stack.upcase} stack does not have a(n) '#{output}' output!"
72
+ end
73
+
74
+ # Creates or Updates a CloudFormation stack. Checks to see if the stack
75
+ # already exists and takes the appropriate action. Pauses until a final
76
+ # stack state is reached.
77
+
78
+ # @param stack_name [String] The name of the CloudFormation stack
79
+ # @param stack_parameters [Hash] Parameters to be passed into the stack
80
+ def deploy_stack(stack_name, stack_parameters)
81
+ unless @client.describe_stacks(stack_name: stack_name).stacks.empty?
82
+ puts 'Updating the existing stack'
83
+ @client.update_stack(stack_parameters)
84
+ @client.wait_until(:stack_update_complete, stack_name: stack_name)
85
+ end
86
+ rescue Aws::CloudFormation::Errors::ValidationError => error
87
+ if error.to_s.include? 'No updates are to be performed.'
88
+ puts "Nothing to do."
89
+ elsif error.to_s.include? 'Template error'
90
+ raise error
91
+ else
92
+ puts 'Creating a new stack'
93
+ @client.create_stack(stack_parameters)
94
+ @client.wait_until(:stack_create_complete, stack_name: stack_name)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk'
4
+ require 'crossing'
5
+
6
+ class MinimalPipeline
7
+ # Here is an example of how to use this class to interact with Crossing.
8
+ #
9
+ # ```
10
+ # crossing = MinimalPipeline::Crossing.new
11
+ #
12
+ # # Upload
13
+ # crossing.upload_content('my-config-bucket', 'example.txt', 'foo')
14
+ #
15
+ # # Download
16
+ # content = crossing.download_file('my-config-bucket', 'example.txt')
17
+ # puts content # Outputs 'foo'
18
+ # ```
19
+ #
20
+ # You will need the following environment variables to be present:
21
+ # * `AWS_REGION` or `region`
22
+ # * `keystore_kms_id`
23
+ #
24
+ # For more information on Crossing see https://github.com/stelligent/crossing
25
+ class Crossing
26
+ def initialize
27
+ raise 'You must set env variable AWS_REGION.' if ENV['AWS_REGION'].nil?
28
+ raise '`keystore_kms_id` in environment!' \
29
+ if ENV['keystore_kms_id'].nil?
30
+
31
+ region = ENV['AWS_REGION']
32
+ kms = Aws::KMS::Client.new(region: region)
33
+ s3 = Aws::S3::Encryption::Client.new(kms_key_id: ENV['keystore_kms_id'],
34
+ kms_client: kms,
35
+ region: region)
36
+ @crossing = ::Crossing.new(s3)
37
+ end
38
+
39
+ # Securely uploads a file to an S3 bucket
40
+ #
41
+ # @param config_bucket [String] The name of the S3 bucket
42
+ # @param filename [String] The name of the file to save content to in the
43
+ # bucket
44
+ # @param content [String] The content to store in the file
45
+ def upload_content(config_bucket, filename, content)
46
+ @crossing.put_content(config_bucket, filename, content)
47
+ end
48
+
49
+ # Securely downloads a file from an S3 bucket
50
+ #
51
+ # @param config_bucket [String] The name of the S3 bucket
52
+ # @param filename [String] The name of the file that contains the desired
53
+ # content
54
+ # @return [String] The content that was stored in the file
55
+ def download_file(config_bucket, filename)
56
+ @crossing.get_content(config_bucket, filename)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'docker'
5
+ require 'json'
6
+ require 'securerandom'
7
+
8
+ class MinimalPipeline
9
+ # Here is an example of how to use this class to manage Docker containers.
10
+ #
11
+ # ```
12
+ # docker = MinimalPipeline::Docker.new
13
+ # keystore = MinimalPipeline::Keystore.new
14
+ #
15
+ # deploy_env = ENV['DEPLOY_ENV']
16
+ # docker_repo = keystore.retrieve("#{deploy_env}_EXAMPLE_ECR_REPO")
17
+ # docker_image = "#{docker_repo}/example:latest"
18
+ # docker.build_docker_image(docker_image, 'containers/example')
19
+ # docker.push_docker_image(docker_image)
20
+ # ```
21
+ class Docker
22
+ include Rake::DSL
23
+
24
+ def initialize
25
+ end
26
+
27
+ # Finds the absolute path to a given executable
28
+ #
29
+ # @param cmd [String] The name of the executable to locate
30
+ # @return [String] The absolute path to the executable
31
+ def which(cmd)
32
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
33
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
34
+ exts.each do |ext|
35
+ exe = File.join(path, "#{cmd}#{ext}")
36
+ return exe if File.executable?(exe) && !File.directory?(exe)
37
+ end
38
+ end
39
+ nil
40
+ end
41
+
42
+ # Outputs JSON build output lines as human readible text
43
+ #
44
+ # @param build_output [String] Raw JSON build output line
45
+ def build_output(build_output)
46
+ # Each line of the response is its own JSON structure
47
+ build_output.each_line do |l|
48
+ if (log = JSON.parse(l)) && log.key?('stream')
49
+ $stdout.puts log['stream']
50
+ end
51
+ end
52
+ rescue JSON::ParserError
53
+ $stdout.puts "Bad JSON parse\n"
54
+ $stdout.puts build_output
55
+ end
56
+
57
+ # Logs in to AWS ECR
58
+ def ecr_login
59
+ region = ENV['AWS_REGION'] || ENV['region']
60
+ `$(aws ecr get-login --region #{region})`
61
+ end
62
+
63
+ # Cleans up docker images
64
+ #
65
+ # @param image_id [String] The Docker container ID to delete
66
+ def clean_up_image(image_id)
67
+ image = ::Docker::Image.get(image_id)
68
+ image.remove(force: true)
69
+ end
70
+
71
+ # Builds a docker image from a Dockerfile
72
+ #
73
+ # @param image_id [String] The name of the docker image
74
+ # @param dir [String] The path to the Dockerfile
75
+ # @param build_args [Hash] Additional build args to pass to Docker
76
+ # @param timeout [Integer] The Docker build timeout
77
+ def build_docker_image(image_id, dir = '.', build_args: {}, timeout: 600)
78
+ %w[HTTP_PROXY HTTPS_PROXY NO_PROXY http_proxy https_proxy
79
+ no_proxy].each do |arg|
80
+ build_args[arg] = ENV[arg] if ENV[arg]
81
+ end
82
+
83
+ args = {
84
+ 'nocache' => 'true',
85
+ 'pull' => 'true',
86
+ 't' => image_id,
87
+ 'buildargs' => JSON.dump(build_args)
88
+ }
89
+ puts "Build args: #{args.inspect}"
90
+ ::Docker.options[:read_timeout] = timeout
91
+ ::Docker::Image.build_from_dir(dir, args) { |v| build_output(v) }
92
+ end
93
+
94
+ # Pushes a docker image from local to AWS ECR.
95
+ # This handles login, the upload, and local cleanup of the container
96
+ #
97
+ # @param image_id [String] The name of the docker image
98
+ def push_docker_image(image_id)
99
+ ecr_login
100
+ docker_bin = which('docker')
101
+ raise "docker_push: no docker binary: #{image_id}" unless docker_bin
102
+ stdout, stderr, status = Open3.capture3(docker_bin, 'push', image_id)
103
+ raise "stdout: #{stdout}\nstderr: #{stderr}\nstatus: #{status}" \
104
+ unless status.exitstatus.zero?
105
+ clean_up_image(image_id)
106
+ end
107
+
108
+ # List all containers in the `containers` directory
109
+ # @return [Array] List of container names
110
+ def list_containers
111
+ containers = Dir.glob('containers/*')
112
+ # Grab container name from containers/name/Dockerfile
113
+ containers.map{ |container| container.split('/')[1] }
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'keystore'
4
+ require 'aws-sdk'
5
+
6
+ class MinimalPipeline
7
+ # Here is an example of how to use this class to interact with the Keystore.
8
+ #
9
+ # ```
10
+ # keystore = MinimalPipeline::Keystore.new
11
+ #
12
+ # # Store
13
+ # keystore.store('EXAMPLE_KEY', 'foo')
14
+ #
15
+ # # Retrieve
16
+ # content = keystore.retrieve('EXAMPLE_KEY')
17
+ # puts content # Outputs 'foo'
18
+ # ```
19
+ #
20
+ # You will need the following environment variables to be present:
21
+ # * `AWS_REGION` or `region`
22
+ # * `keystore_table`
23
+ # * `keystore_kms_id`
24
+ #
25
+ # For more information on the Keystore please see
26
+ # https://github.com/stelligent/keystore
27
+ class Keystore
28
+ # Initializes a `Keystore` client
29
+ # Requires environment variables `AWS_REGION` or `region` to be set.
30
+ # Also requires `keystore_kms_id` and `keystore_table`
31
+ def initialize
32
+ raise 'You must set env variable AWS_REGION.' if ENV['AWS_REGION'].nil?
33
+ raise 'Missing `keystore_table` or `keystore_kms_id` in environment!' \
34
+ if ENV['keystore_table'].nil? || ENV['keystore_kms_id'].nil?
35
+
36
+ region = ENV['AWS_REGION']
37
+ kms = Aws::KMS::Client.new(region: region)
38
+ dynamo = Aws::DynamoDB::Client.new(region: region)
39
+ @keystore = ::Keystore.new(dynamo: dynamo,
40
+ table_name: ENV['keystore_table'],
41
+ kms: kms,
42
+ key_id: ENV['keystore_kms_id'])
43
+ end
44
+
45
+ # Retrieves a value from the Keystore
46
+ #
47
+ # @param keyname [String] The name of the Keystore key
48
+ # @return [String] The value stored in the Keystore for the given key
49
+ def retrieve(keyname)
50
+ @keystore.retrieve(key: keyname)
51
+ end
52
+
53
+ # Stores a value in the Keystore
54
+ #
55
+ # @param keyname [String] The name of the Keystore key
56
+ # @param value [String] The value to store for the given key
57
+ def store(keyname, value)
58
+ @keystore.store(key: keyname, value: value)
59
+ end
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minimal_pipeline
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Mayowa Aladeojebi
8
+ - Jesse Adams
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2018-08-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: aws-sdk
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: crossing
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '='
33
+ - !ruby/object:Gem::Version
34
+ version: 0.1.8
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '='
40
+ - !ruby/object:Gem::Version
41
+ version: 0.1.8
42
+ - !ruby/object:Gem::Dependency
43
+ name: docker-api
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '='
47
+ - !ruby/object:Gem::Version
48
+ version: 1.34.2
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '='
54
+ - !ruby/object:Gem::Version
55
+ version: 1.34.2
56
+ - !ruby/object:Gem::Dependency
57
+ name: keystore
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '='
61
+ - !ruby/object:Gem::Version
62
+ version: 0.1.7
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - '='
68
+ - !ruby/object:Gem::Version
69
+ version: 0.1.7
70
+ - !ruby/object:Gem::Dependency
71
+ name: rake
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rubocop
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '='
89
+ - !ruby/object:Gem::Version
90
+ version: 0.55.0
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '='
96
+ - !ruby/object:Gem::Version
97
+ version: 0.55.0
98
+ description: Helper gem to orchestrate pipeline tasks
99
+ email:
100
+ - mayowa.aladeojebi@stelligent.com
101
+ - jesse.adams@stelligent.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - lib/minimal_pipeline.rb
107
+ - lib/minimal_pipeline/cloudformation.rb
108
+ - lib/minimal_pipeline/crossing.rb
109
+ - lib/minimal_pipeline/docker.rb
110
+ - lib/minimal_pipeline/keystore.rb
111
+ homepage: https://github.com/stelligent/minimal-pipeline-gem
112
+ licenses:
113
+ - 0BSD
114
+ metadata: {}
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ required_rubygems_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ requirements: []
130
+ rubyforge_project:
131
+ rubygems_version: 2.7.6
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: Helper gem to manage pipeline tasks
135
+ test_files: []