moonshot 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cdc444c7d1abfc032353018913e6a8eccb90b035
4
+ data.tar.gz: 82a6a2f524bc812d9c0c7155b1437a10c378594d
5
+ SHA512:
6
+ metadata.gz: 870266482ea59c020549ec66f4cd114b4cdb2dff89957a18387ff3eb6409b5f7f1b831c06c0503397ed79cc597bfdc659b8d9100bfeb9d2df3f177aad5953538
7
+ data.tar.gz: efbef2587e5b0305c608bcdd21695388145a98f981097eb5be875dcb27faa1cbeac1e354631891b3fd8ced6b3d898d7c3d59050199ea21c006decb65e8072c84
@@ -0,0 +1,60 @@
1
+ # The S3Bucket stores builds in an S3 Bucket.
2
+ #
3
+ # For example:
4
+ #
5
+ # def MyApplication < Moonshot::CLI
6
+ # self.artifact_repository = S3Bucket.new('my-application-builds')
7
+ # end
8
+ class Moonshot::ArtifactRepository::S3Bucket
9
+ include Moonshot::ResourcesHelper
10
+ include Moonshot::CredsHelper
11
+ include Moonshot::DoctorHelper
12
+
13
+ attr_reader :bucket_name
14
+
15
+ def initialize(bucket_name)
16
+ @bucket_name = bucket_name
17
+ end
18
+
19
+ def store_hook(build_mechanism, version_name)
20
+ unless build_mechanism.respond_to?(:output_file)
21
+ raise "S3Bucket does not know how to store artifacts from #{build_mechanism.class}, no method '#output_file'." # rubocop:disable LineLength
22
+ end
23
+
24
+ file = build_mechanism.output_file
25
+ bucket_name = @bucket_name
26
+ key = filename_for_version(version_name)
27
+
28
+ ilog.start_threaded "Uploading #{file} to s3://#{bucket_name}/#{key}" do |s|
29
+ s3_client.put_object(key: key, body: File.open(file), bucket: bucket_name)
30
+ s.success "Uploaded s3://#{bucket_name}/#{key} successfully."
31
+ end
32
+ end
33
+
34
+ def filename_for_version(version_name)
35
+ "#{version_name}.tar.gz"
36
+ end
37
+
38
+ private
39
+
40
+ def doctor_check_bucket_exists
41
+ s3_client.get_bucket_location(bucket: @bucket_name)
42
+ success "Bucket '#{@bucket_name}' exists."
43
+ rescue => e
44
+ # This is warning because the role you use for deployment may not actually
45
+ # be able to read builds, however the instance role assigned to the nodes
46
+ # might.
47
+ str = "Could not get information about bucket '#{@bucket_name}'."
48
+ warning(str, e.message)
49
+ end
50
+
51
+ def doctor_check_bucket_writable
52
+ s3_client.put_object(key: 'test-object', body: '', bucket: @bucket_name)
53
+ s3_client.delete_object(key: 'test-object', bucket: @bucket_name)
54
+ success 'Bucket is writable, new builds can be uploaded.'
55
+ rescue => e
56
+ # This is a warning because you may deploy to an environment where you have
57
+ # read access to builds, but could not publish a new build.
58
+ warning('Could not write to bucket, you may still be able to deploy existing builds.', e.message) # rubocop:disable LineLength
59
+ end
60
+ end
@@ -0,0 +1,89 @@
1
+ require 'moonshot/artifact_repository/s3_bucket'
2
+ require 'moonshot/shell'
3
+ require 'securerandom'
4
+ require 'semantic'
5
+ require 'tmpdir'
6
+
7
+ module Moonshot::ArtifactRepository
8
+ # S3 Bucket repository backed by GitHub releases.
9
+ # If a SemVer package isn't found in S3, it is copied from GitHub releases.
10
+ class S3BucketViaGithubReleases < S3Bucket
11
+ include Moonshot::BuildMechanism
12
+ include Moonshot::Shell
13
+
14
+ # @override
15
+ # If release version, transfer from GitHub to S3.
16
+ def store_hook(build_mechanism, version)
17
+ if release?(version)
18
+ if (@output_file = build_mechanism.output_file)
19
+ attach_release_asset(version, @output_file)
20
+ # Upload to s3.
21
+ super
22
+ else
23
+ # If there is no output file, assume it's on GitHub already.
24
+ transfer_release_asset_to_s3(version)
25
+ end
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ # @override
32
+ # If release version, transfer from GitHub to S3.
33
+ # @todo This is a super hacky place to handle the transfer, give
34
+ # artifact repositories a hook before deploy.
35
+ def filename_for_version(version)
36
+ s3_name = super
37
+ if !@output_file && release?(version) && !in_s3?(s3_name)
38
+ github_to_s3(version, s3_name)
39
+ end
40
+ s3_name
41
+ end
42
+
43
+ private
44
+
45
+ def release?(version)
46
+ ::Semantic::Version.new(version)
47
+ rescue ArgumentError
48
+ false
49
+ end
50
+
51
+ def in_s3?(key)
52
+ s3_client.head_object(key: key, bucket: bucket_name)
53
+ rescue ::Aws::S3::Errors::NotFound
54
+ false
55
+ end
56
+
57
+ def attach_release_asset(version, file)
58
+ # -m '' leaves message unchanged.
59
+ cmd = "hub release edit #{version} -m '' --attach=#{file}"
60
+ sh_step(cmd)
61
+ end
62
+
63
+ def transfer_release_asset_to_s3(version)
64
+ ilog.start_threaded "Transferring #{version} to S3" do |s|
65
+ key = filename_for_version(version)
66
+ s.success "Uploaded s3://#{bucket_name}/#{key} successfully."
67
+ end
68
+ end
69
+
70
+ def github_to_s3(version, s3_name)
71
+ Dir.mktmpdir('github_to_s3', Dir.getwd) do |tmpdir|
72
+ Dir.chdir(tmpdir) do
73
+ sh_out("hub release download #{version}")
74
+ file = File.open(Dir.glob("*#{version}*.tar.gz").fetch(0))
75
+ s3_client.put_object(key: s3_name, body: file, bucket: bucket_name)
76
+ end
77
+ end
78
+ end
79
+
80
+ def doctor_check_hub_release_download
81
+ sh_out('hub release download --help')
82
+ rescue
83
+ critical '`hub release download` command missing, upgrade hub.' \
84
+ ' See https://github.com/github/hub/pull/1103'
85
+ else
86
+ success '`hub release download` command available.'
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,148 @@
1
+ require 'forwardable'
2
+ require 'moonshot/shell'
3
+ require 'open3'
4
+ require 'semantic'
5
+ require 'shellwords'
6
+ require 'tempfile'
7
+ require 'vandamme'
8
+
9
+ module Moonshot::BuildMechanism
10
+ # A build mechanism that creates a tag and GitHub release.
11
+ class GithubRelease # rubocop:disable Metrics/ClassLength
12
+ extend Forwardable
13
+ include Moonshot::ResourcesHelper
14
+ include Moonshot::DoctorHelper
15
+ include Moonshot::Shell
16
+
17
+ def_delegator :@build_mechanism, :output_file
18
+
19
+ # @param build_mechanism Delegates building after GitHub release is created.
20
+ def initialize(build_mechanism)
21
+ @build_mechanism = build_mechanism
22
+ end
23
+
24
+ def doctor_hook
25
+ super
26
+ @build_mechanism.doctor_hook
27
+ end
28
+
29
+ def resources=(r)
30
+ super
31
+ @build_mechanism.resources = r
32
+ end
33
+
34
+ def pre_build_hook(version)
35
+ @semver = ::Semantic::Version.new(version)
36
+ @target_version = [@semver.major, @semver.minor, @semver.patch].join('.')
37
+ sh_step('git fetch --tags upstream')
38
+ @sha = `git rev-parse HEAD`.chomp
39
+ validate_commit
40
+ @changes = validate_changelog(@target_version)
41
+ confirm_or_fail(@semver)
42
+ @build_mechanism.pre_build_hook(version)
43
+ end
44
+
45
+ def build_hook(version)
46
+ assert_state(version)
47
+ git_tag(version, @sha, @changes)
48
+ git_push_tag('upstream', version)
49
+ hub_create_release(@semver, @sha, @changes)
50
+ ilog.msg("#{releases_url}/tag/#{version}")
51
+ @build_mechanism.build_hook(version)
52
+ end
53
+
54
+ def post_build_hook(version)
55
+ assert_state(version)
56
+ @build_mechanism.post_build_hook(version)
57
+ end
58
+
59
+ private
60
+
61
+ # We carry state between hooks, make sure that's still valid.
62
+ def assert_state(version)
63
+ raise "#{version} != #{@semver}" unless version == @semver.to_s
64
+ end
65
+
66
+ def confirm_or_fail(version)
67
+ say("\nCommit Summary", :yellow)
68
+ say("#{@commit_detail}\n")
69
+ say('Commit CI Status', :yellow)
70
+ say("#{@ci_statuses}\n")
71
+ say("Changelog for #{version}", :yellow)
72
+ say("#{@changes}\n\n")
73
+
74
+ q = "Do you wan't to tag and release this commit as #{version}? [y/n]"
75
+ raise Thor::Error, 'Release declined.' unless yes?(q)
76
+ end
77
+
78
+ def git_tag(tag, sha, annotation)
79
+ cmd = "git tag -a #{tag} #{sha} --file=-"
80
+ sh_step(cmd, stdin: annotation)
81
+ end
82
+
83
+ def git_push_tag(remote, tag)
84
+ cmd = "git push #{remote} refs/tags/#{tag}:refs/tags/#{tag}"
85
+ sh_step(cmd) do
86
+ sleep 2 # GitHub needs a moment to register the tag.
87
+ end
88
+ end
89
+
90
+ def hub_create_release(semver, commitish, changelog_entry)
91
+ message = "#{semver}\n\n#{changelog_entry}"
92
+ cmd = "hub release create #{semver} --commitish=#{commitish}"
93
+ cmd << ' --prerelease' if semver.pre || semver.build
94
+ cmd << " --message=#{Shellwords.escape(message)}"
95
+ sh_step(cmd)
96
+ end
97
+
98
+ def validate_commit
99
+ cmd = "git show --stat #{@sha}"
100
+ sh_step(cmd, msg: "Validate commit #{@sha}.") do |_, out|
101
+ @commit_detail = out
102
+ end
103
+ cmd = "hub ci-status --verbose #{@sha}"
104
+ sh_step(cmd, msg: "Check CI status for #{@sha}.") do |_, out|
105
+ @ci_statuses = out
106
+ end
107
+ end
108
+
109
+ def validate_changelog(version)
110
+ changes = nil
111
+ ilog.start_threaded('Validate `CHANGELOG.md`.') do |step|
112
+ changes = fetch_changes(version)
113
+ step.success
114
+ end
115
+ changes
116
+ end
117
+
118
+ def fetch_changes(version)
119
+ parser = Vandamme::Parser.new(
120
+ changelog: File.read('CHANGELOG.md'),
121
+ format: 'markdown'
122
+ )
123
+ parser.parse.fetch(version) do
124
+ raise "#{version} not found in CHANGELOG.md"
125
+ end
126
+ end
127
+
128
+ def releases_url
129
+ `hub browse -u -- releases`.chomp
130
+ end
131
+
132
+ def doctor_check_upstream
133
+ sh_out('git remote | grep ^upstream$')
134
+ rescue => e
135
+ critical "git remote `upstream` not found.\n#{e.message}"
136
+ else
137
+ success 'git remote `upstream` exists.'
138
+ end
139
+
140
+ def doctor_check_hub_auth
141
+ sh_out('hub ci-status 0.0.0')
142
+ rescue => e
143
+ critical "`hub` failed, install hub and authorize it.\n#{e.message}"
144
+ else
145
+ success '`hub` installed and authorized.'
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,84 @@
1
+ require 'open3'
2
+ include Open3
3
+
4
+ # Compile a release artifact using a shell script.
5
+ #
6
+ # The output file will be deleted before the script is run, and is expected to
7
+ # exist after the script exits. Any non-zero exit status will be consider a
8
+ # build failure, and any output will be displayed to the user.
9
+ #
10
+ # Creating a new Script BuildMechanism looks like this:
11
+ #
12
+ # class MyReleaseTool < Moonshot::CLI
13
+ # include Moonshot::BuildMechanism
14
+ # self.build_mechanism = Script.new('script/build.sh')
15
+ # end
16
+ #
17
+ class Moonshot::BuildMechanism::Script
18
+ include Moonshot::ResourcesHelper
19
+ include Moonshot::DoctorHelper
20
+
21
+ attr_reader :output_file
22
+
23
+ def initialize(script, output_file: 'output.tar.gz')
24
+ @script = script
25
+ @output_file = output_file
26
+ end
27
+
28
+ def pre_build_hook(_version)
29
+ File.delete(@output_file) if File.exist?(@output_file)
30
+ end
31
+
32
+ def build_hook(version)
33
+ env = {
34
+ 'VERSION' => version,
35
+ 'OUTPUT_FILE' => @output_file
36
+ }
37
+ ilog.start_threaded "Running Script: #{@script}" do |s|
38
+ run_script(s, env: env)
39
+ end
40
+ end
41
+
42
+ def post_build_hook(_version)
43
+ unless File.exist?(@output_file) # rubocop:disable GuardClause
44
+ raise Thor::Error, 'Build command did not produce output file!'
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def run_script(step, env: {}) # rubocop:disable AbcSize
51
+ popen2e(env, @script) do |_, out, wait|
52
+ output = []
53
+
54
+ loop do
55
+ str = out.gets
56
+ unless str.nil?
57
+ output << str.chomp
58
+ ilog.debug(str.chomp)
59
+ end
60
+ break if out.eof?
61
+ end
62
+
63
+ result = wait.value
64
+ if result.exitstatus == 0
65
+ step.success "Build script #{@script} exited successfully!"
66
+ end
67
+ unless result.exitstatus == 0
68
+ ilog.error "Build script failed with exit status #{result.exitstatus}!"
69
+ ilog.error 'Last 10 lines of output follows:'
70
+ output.pop(10).each { |l| ilog.error l }
71
+
72
+ step.failure "Build script #{@script} failed with exit status #{result.exitstatus}!"
73
+ end
74
+ end
75
+ end
76
+
77
+ def doctor_check_script_exists
78
+ if File.exist?(@script)
79
+ success "Script '#{@script}' exists."
80
+ else
81
+ critical "Could not find build script '#{@script}'!"
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,70 @@
1
+ require 'moonshot/shell'
2
+
3
+ module Moonshot::BuildMechanism
4
+ # This simply waits for Travis-CI to finish building a job matching the
5
+ # version and 'BUILD=1'.
6
+ class TravisDeploy
7
+ include Moonshot::ResourcesHelper
8
+ include Moonshot::DoctorHelper
9
+ include Moonshot::Shell
10
+
11
+ attr_reader :output_file
12
+
13
+ def initialize(slug, pro: false)
14
+ @slug = slug
15
+ @endpoint = pro ? '--pro' : '--org'
16
+ @cli_args = "-r #{@slug} #{@endpoint}"
17
+ end
18
+
19
+ def pre_build_hook(_)
20
+ end
21
+
22
+ def build_hook(version)
23
+ job_number = find_build_and_job(version)
24
+ wait_for_job(job_number)
25
+ check_build(version)
26
+ end
27
+
28
+ def post_build_hook(_)
29
+ end
30
+
31
+ private
32
+
33
+ def find_build_and_job(version)
34
+ job_number = nil
35
+ ilog.start_threaded('Find Travis CI build') do |step|
36
+ sleep 2
37
+ build_out = sh_out("bundle exec travis show #{@cli_args} #{version}")
38
+ unless (job_number = build_out.match(/^#(\d+\.\d+) .+BUILD=1.+/)[1])
39
+ raise "Build for #{version} not found.\n#{build_out}"
40
+ end
41
+ step.success("Travis CI ##{job_number.gsub(/\..*/, '')} running.")
42
+ end
43
+ job_number
44
+ end
45
+
46
+ def wait_for_job(job_number)
47
+ cmd = "bundle exec travis logs #{@cli_args} #{job_number}"
48
+ # This log tailing fails at the end of the file. travis bug.
49
+ sh_step(cmd, fail: false)
50
+ end
51
+
52
+ def check_build(version)
53
+ cmd = "bundle exec travis show #{@cli_args} #{version}"
54
+ sh_step(cmd) do |step, out|
55
+ raise "Build didn't pass.\n#{build_out}" \
56
+ if out =~ /^#(\d+\.\d+) (?!passed).+BUILD=1.+/
57
+
58
+ step.success("Travis CI build for #{version} passed.")
59
+ end
60
+ end
61
+
62
+ def doctor_check_travis_auth
63
+ sh_out("bundle exec travis raw #{@endpoint} repos/#{@slug}")
64
+ rescue => e
65
+ critical "`travis` not available or not authorized.\n#{e.message}"
66
+ else
67
+ success '`travis` installed and authorized.'
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,55 @@
1
+ require 'forwardable'
2
+ require 'semantic'
3
+
4
+ # This proxies build request do different mechanisms. One for semver compliant
5
+ # releases and another for everything else.
6
+ class Moonshot::BuildMechanism::VersionProxy
7
+ extend Forwardable
8
+ include Moonshot::ResourcesHelper
9
+
10
+ def_delegator :@active, :output_file
11
+
12
+ def initialize(release:, dev:)
13
+ @release = release
14
+ @dev = dev
15
+ end
16
+
17
+ def doctor_hook
18
+ @release.doctor_hook
19
+ @dev.doctor_hook
20
+ end
21
+
22
+ def resources=(r)
23
+ super
24
+ @release.resources = r
25
+ @dev.resources = r
26
+ end
27
+
28
+ def pre_build_hook(version)
29
+ active(version).pre_build_hook(version)
30
+ end
31
+
32
+ def build_hook(version)
33
+ active(version).build_hook(version)
34
+ end
35
+
36
+ def post_build_hook(version)
37
+ active(version).post_build_hook(version)
38
+ end
39
+
40
+ private
41
+
42
+ def active(version)
43
+ @active = if release?(version)
44
+ @release
45
+ else
46
+ @dev
47
+ end
48
+ end
49
+
50
+ def release?(version)
51
+ ::Semantic::Version.new(version)
52
+ rescue ArgumentError
53
+ false
54
+ end
55
+ end
@@ -0,0 +1,146 @@
1
+ require 'interactive-logger'
2
+
3
+ # Base class for Moonshot-powered project tooling.
4
+ module Moonshot
5
+ # The main entry point for Moonshot, this class should be extended by
6
+ # project tooling.
7
+ class CLI < Thor # rubocop:disable ClassLength
8
+ class_option(:name, aliases: 'n', default: nil, type: :string)
9
+ class_option(:interactive_logger, type: :boolean, default: true)
10
+ class_option(:verbose, aliases: 'v', type: :boolean)
11
+
12
+ class << self
13
+ attr_accessor :application_name
14
+ attr_accessor :artifact_repository
15
+ attr_accessor :auto_prefix_stack
16
+ attr_accessor :build_mechanism
17
+ attr_accessor :deployment_mechanism
18
+ attr_accessor :default_parent_stack
19
+ attr_reader :plugins
20
+
21
+ def plugin(plugin)
22
+ @plugins ||= []
23
+ @plugins << plugin
24
+ end
25
+
26
+ def parent(value)
27
+ @default_parent_stack = value
28
+ end
29
+
30
+ def check_class_configuration
31
+ raise Thor::Error, 'No application_name is set!' unless application_name
32
+ end
33
+
34
+ def exit_on_failure?
35
+ true
36
+ end
37
+
38
+ def inherited(base)
39
+ base.include(Moonshot::ArtifactRepository)
40
+ base.include(Moonshot::BuildMechanism)
41
+ base.include(Moonshot::DeploymentMechanism)
42
+ end
43
+ end
44
+
45
+ def initialize(*args)
46
+ super
47
+ @log = Logger.new(STDOUT)
48
+ @log.formatter = proc do |s, d, _, msg|
49
+ "[#{self.class.name} #{s} #{d.strftime('%T')}] #{msg}\n"
50
+ end
51
+ @log.level = options[:verbose] ? Logger::DEBUG : Logger::INFO
52
+
53
+ EnvironmentParser.parse(@log)
54
+ self.class.check_class_configuration
55
+ end
56
+
57
+ no_tasks do
58
+ # Build a Moonshot::Controller from the CLI options.
59
+ def controller # rubocop:disable AbcSize, CyclomaticComplexity, PerceivedComplexity
60
+ Moonshot::Controller.new do |config|
61
+ config.app_name = self.class.application_name
62
+ config.artifact_repository = self.class.artifact_repository
63
+ config.auto_prefix_stack = self.class.auto_prefix_stack
64
+ config.build_mechanism = self.class.build_mechanism
65
+ config.deployment_mechanism = self.class.deployment_mechanism
66
+ config.environment_name = options[:name]
67
+ config.logger = @log
68
+
69
+ # Degrade to a more compatible logger if the terminal seems outdated,
70
+ # or at the users request.
71
+ if !$stdout.isatty || !options[:interactive_logger]
72
+ config.interactive_logger = InteractiveLoggerProxy.new(@log)
73
+ end
74
+
75
+ config.show_all_stack_events = true if options[:show_all_events]
76
+ config.plugins = self.class.plugins if self.class.plugins
77
+
78
+ if options[:parent]
79
+ config.parent_stacks << options[:parent]
80
+ elsif self.class.default_parent_stack
81
+ config.parent_stacks << self.class.default_parent_stack
82
+ end
83
+ end
84
+ rescue => e
85
+ raise Thor::Error, e.message
86
+ end
87
+ end
88
+
89
+ desc :list, 'List stacks for this application.'
90
+ def list
91
+ controller.list
92
+ end
93
+
94
+ desc :create, 'Create a new environment.'
95
+ option(
96
+ :parent,
97
+ type: :string,
98
+ aliases: '-p',
99
+ desc: "Parent stack to import parameters from. (Default: #{default_parent_stack || 'None'})")
100
+ option :deploy, default: true, type: :boolean, aliases: '-d',
101
+ desc: 'Choose if code should be deployed after stack is created'
102
+ option :show_all_events, desc: 'Show all stack events during update. (Default: errors only)'
103
+ def create
104
+ controller.create
105
+ controller.deploy_code if options[:deploy]
106
+ end
107
+
108
+ desc :update, 'Update the CloudFormation stack within an environment.'
109
+ option :show_all_events, desc: 'Show all stack events during update. (Default: errors only)'
110
+ def update
111
+ controller.update
112
+ end
113
+
114
+ desc :status, 'Get the status of an existing environment.'
115
+ def status
116
+ controller.status
117
+ end
118
+
119
+ desc 'deploy-code', 'Create a build from the working directory, and deploy it.' # rubocop:disable LineLength
120
+ def deploy_code
121
+ controller.deploy_code
122
+ end
123
+
124
+ desc 'build-version VERSION', 'Build a tarball of the software, ready for deployment.' # rubocop:disable LineLength
125
+ def build_version(version_name)
126
+ controller.build_version(version_name)
127
+ end
128
+
129
+ desc 'deploy-version VERSION_NAME', 'Deploy a versioned release to both EB environments in an environment.' # rubocop:disable LineLength
130
+ def deploy_version(version_name)
131
+ controller.deploy_version(version_name)
132
+ end
133
+
134
+ desc :delete, 'Delete an existing environment.'
135
+ option :show_all_events, desc: 'Show all stack events during update. (Default: errors only)'
136
+ def delete
137
+ controller.delete
138
+ end
139
+
140
+ desc :doctor, 'Run configuration checks against current environment.'
141
+ def doctor
142
+ success = controller.doctor
143
+ raise Thor::Error, 'One or more checks failed.' unless success
144
+ end
145
+ end
146
+ end