conan_deploy_stackato3 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,91 @@
1
+ require 'conan/has_options'
2
+ class Deployment < HasOptions
3
+
4
+ attr_accessor :environment, :org, :ship, :shipcloud, :app_ids, :facility_id, :additional_mappings, :custom_smoke_test_path
5
+
6
+ def initialize(environment, org, ship)
7
+ # inherit options from the environment
8
+ #super(environment.options) # TODO: copy?!
9
+ super({})
10
+ @environment = environment
11
+ @org = org
12
+ @ship = ship
13
+ @shipcloud = "#{org}-#{ship}"
14
+ @app_ids = []
15
+ @additional_mappings = []
16
+ @facility_id = nil
17
+ @enabled = true
18
+ @randomid = SecureRandom.hex(3)
19
+ @custom_smoke_test_path = nil
20
+ end
21
+
22
+ def apps(*app_ids)
23
+ @app_ids = app_ids
24
+ end
25
+
26
+ def facility(facility_id)
27
+ @facility_id = facility_id
28
+ end
29
+
30
+ def hostnames(*hostnames)
31
+ hostnames.each do |hostname|
32
+ @additional_mappings.push(hostname) unless hostname.empty?
33
+ end
34
+ end
35
+
36
+ def enabled(b)
37
+ @enabled = b
38
+ end
39
+
40
+ def enabled?
41
+ @enabled
42
+ end
43
+
44
+ def name(app)
45
+ "#{app.id}-#{@environment.id}-#{@ship}"
46
+ end
47
+
48
+ def unique_name(app)
49
+ "#{name(app)}-#{@randomid}"
50
+ end
51
+
52
+ def dns_name(app)
53
+ "#{@environment.id}.#{app.url_segment}.#{@ship}.#{@org}.mtnsatcloud.com"
54
+ end
55
+
56
+ def agnostic_dns_name(app)
57
+ "#{app.url_segment}.mtnsat.io"
58
+ end
59
+
60
+ def active_urls(app)
61
+ [ dns_name(app), agnostic_dns_name(app) ] + @additional_mappings
62
+ end
63
+
64
+ def inactive_urls(app)
65
+ active_urls(app).map{ |url| "x.#{url}" }
66
+ end
67
+
68
+ def manifest_url(app)
69
+ "http://#{dns_name(app)}/status/manifest"
70
+ end
71
+
72
+ def active_smoke_test_url(app)
73
+ "http://#{dns_name(app)}/#{@custom_smoke_test_path || 'status/healthcheck'}"
74
+ end
75
+
76
+ def inactive_smoke_test_url(app)
77
+ "http://x.#{dns_name(app)}/#{@custom_smoke_test_path || 'status/healthcheck'}"
78
+ end
79
+
80
+ def paas_target
81
+ "https://api.paasv2.#{@ship}.#{@org}.mtnsatcloud.com"
82
+ end
83
+
84
+ def to_s
85
+ "#{org}-#{ship}"
86
+ end
87
+
88
+ def smoke_test_path(path=nil)
89
+ @custom_smoke_test_path = path
90
+ end
91
+ end
@@ -0,0 +1,24 @@
1
+ class Environment
2
+ attr_accessor :id, :upstream_env, :deployments
3
+
4
+ def initialize(env_id)
5
+ @id = env_id
6
+ @deployments = []
7
+ @org_holder = nil
8
+ end
9
+
10
+ def promotes_from(env_id)
11
+ @upstream_env = env_id
12
+ end
13
+
14
+ def org(org_name, &block)
15
+ @org_holder = org_name
16
+ self.instance_eval(&block)
17
+ end
18
+
19
+ def deploy(ship, &block)
20
+ d = Deployment.new(self, @org_holder, ship)
21
+ d.instance_eval(&block)
22
+ @deployments << d
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ class HasOptions
2
+ attr_accessor :options
3
+
4
+ def initialize(options)
5
+ @options = options
6
+ end
7
+
8
+ def option(key, value)
9
+ if @options[key].nil?
10
+ @options[key] = [value]
11
+ else
12
+ @options[key] << value
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require 'conan/application'
2
+ class JvmApp < Application
3
+ attr_accessor :sha1, :artifact_repo
4
+
5
+ def initialize(id, project_name, artifact_repo, options, url_segment=nil)
6
+ super(id, project_name, :jvm, options, url_segment)
7
+ @sha1 = nil
8
+ @artifact_repo = artifact_repo
9
+ end
10
+
11
+ def download(location)
12
+ super
13
+ puts "- Provision NewRelic Agent jar"
14
+ @artifact_repo.downloadNewRelicAgent(File.join(location, @artifact_id), '3.2.3', 'c07cd82e033af9628cd7f90df874daee7000e471')
15
+ end
16
+ end
@@ -0,0 +1,100 @@
1
+ class Manifest
2
+
3
+ def initialize(options, artifact_repo, output)
4
+ @pipelines = {}
5
+ @environments = {}
6
+ @options = options
7
+ @artifact_repo = artifact_repo
8
+ @output = output
9
+ end
10
+
11
+ # select a pipeline and environment to do operations on
12
+ def select(pipeline_id, env_id)
13
+ env = @environments[env_id]
14
+ raise ArgumentError.new "Invalid environment id: '#{env_id}'. Valid options are #{@environments.keys.join(", ")}" if env.nil?
15
+ @current_environment = env
16
+ pl = @pipelines[pipeline_id]
17
+ raise ArgumentError.new "Invalid pipeline id: '#{pipeline_id}'. Valid options are #{@pipelines.keys.join(", ")}" if pl.nil?
18
+ @current_pipeline = pl
19
+ pl.load!(env, @options[:'deploy-shipcloud'])
20
+ end
21
+
22
+ def pipeline(pipeline_id, &block)
23
+ pl = Pipeline.new(pipeline_id, @artifact_repo, @output, @options)
24
+ pl.instance_eval(&block)
25
+ @pipelines[pipeline_id] = pl
26
+ pl
27
+ end
28
+
29
+ def environment(env_id, &block)
30
+ e = Environment.new(env_id)
31
+ e.instance_eval(&block)
32
+ @environments[env_id] = e
33
+ e
34
+ end
35
+
36
+ def appVersion(pipeline_id, app_id)
37
+ @pipelines[pipeline_id].appVersion(app_id)
38
+ end
39
+
40
+ def provision
41
+ validateThatEnvironmentIsSelected
42
+ # TODO banners
43
+ puts "Update #{@current_environment.id} manifest files for #{@current_pipeline.id} pipeline"
44
+ @current_pipeline.update(@current_environment, @environments[@current_environment.upstream_env])
45
+ end
46
+
47
+ def configure
48
+ validateThatEnvironmentIsSelected
49
+ puts "Configure #{@current_environment.id} manifest files for #{@current_pipeline.id} pipeline"
50
+ @current_pipeline.configure(@current_environment)
51
+ end
52
+
53
+ def bg_configure
54
+ validateThatEnvironmentIsSelected
55
+ puts "Blue/green configure #{@current_environment.id} manifest files for #{@current_pipeline.id} pipeline"
56
+ @current_pipeline.configure(@current_environment, true)
57
+ end
58
+
59
+ def paas
60
+ paas_user = @options[:'paas-user'] || ENV['PAAS_USER']
61
+ paas_pwd = @options[:'paas-password'] || ENV['PAAS_PASSWORD']
62
+ paas_space = @options[:'paas-space'] || ENV['PAAS_SPACE']
63
+ dry_run = @options[:'dry-run'] || false
64
+
65
+ Stackato.new(paas_user, paas_pwd, paas_space, dry_run)
66
+ end
67
+
68
+ def deploy
69
+ validateThatEnvironmentIsSelected
70
+ puts "Deploy #{@current_environment.id} for #{@current_pipeline.id} pipeline"
71
+
72
+ nr_api_key = @options[:'new-relic-api-key'] || ENV['NEWRELIC_API_KEY']
73
+ force = @options[:'force-deploy'] || false
74
+
75
+ @current_pipeline.deploy(@current_environment, paas, force) { |app_name|
76
+ if (nr_api_key)
77
+ puts 'Reporting Deployment to New Relic'
78
+ job = @options[:'job-url'] || 'nexus-app-manifest'
79
+ NewRelic.alertDeployment(nr_api_key, app_name, "Deployed by #{job}")
80
+ else
81
+ puts "WARNING: Skipping New Relic alert. No API defined"
82
+ end
83
+ }
84
+ end
85
+
86
+ def bg_deploy
87
+ validateThatEnvironmentIsSelected
88
+ puts "Blue/green deploy #{@current_environment.id} for #{@current_pipeline.id} pipeline"
89
+
90
+ force = @options[:'force-deploy'] || false
91
+
92
+ @current_pipeline.bg_deploy(@current_environment, paas, force)
93
+ end
94
+
95
+ def validateThatEnvironmentIsSelected
96
+ unless @current_environment && @current_pipeline
97
+ raise ScriptError, 'Environment and Pipeline must be selected'
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,14 @@
1
+ module ManifestBuilder
2
+ def self.build(options, artifact_repo=nil, outthingy=nil, &block)
3
+ manifest_dir = options[:directory]
4
+ output_type = options[:format]
5
+
6
+ artifact_repo ||= DefaultArtifactRepository.new
7
+ outthingy ||= StackatoOutThingy.new(manifest_dir)
8
+
9
+ manifest = Manifest.new(options, artifact_repo, outthingy)
10
+ manifest.instance_eval(&block)
11
+ manifest.select(options[:pipeline], options[:environment])
12
+ manifest
13
+ end
14
+ end
@@ -0,0 +1,16 @@
1
+ require 'rest_client'
2
+
3
+ module NewRelic
4
+ def self.alertDeployment(api_key, app_name, description)
5
+ response = RestClient.post(
6
+ 'https://api.newrelic.com/deployments.xml',
7
+ :params => {
8
+ :'deployment[app_name]' => app_name,
9
+ :'deployment[description]' => description
10
+ },
11
+ :headers => { 'x-api-key' => api_key }
12
+ )
13
+ puts response
14
+ end
15
+ end
16
+
@@ -0,0 +1,31 @@
1
+ require 'conan/version'
2
+
3
+ module Conan
4
+ class Options
5
+ def initialize(options = {})
6
+ @options = defaults.merge(options)
7
+ end
8
+
9
+ def []=(k, v)
10
+ @options[k] = v
11
+ end
12
+
13
+ def [](k)
14
+ @options[k]
15
+ end
16
+
17
+ def to_s
18
+ @options.to_s
19
+ end
20
+
21
+ private
22
+ def defaults
23
+ {
24
+ :version => Conan::VERSION,
25
+ :directory => Dir.pwd,
26
+ :format => :stackato,
27
+ :action => :provision
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,180 @@
1
+ require 'daphne_util'
2
+ require 'conan/templates'
3
+
4
+ class OutThingy
5
+ attr_accessor :output_dir, :time
6
+
7
+ def initialize(output_dir)
8
+ @output_dir = output_dir
9
+ @time = Time.new
10
+ end
11
+
12
+ def outputToFile(file, &block)
13
+ o = File.join(@output_dir, file)
14
+ puts "Write: #{o}"
15
+ FileUtils.mkdir_p File.dirname(o)
16
+ File.open(o, 'w') { |f| yield f }
17
+ end
18
+
19
+ def loadAppMetadataFromDeploymentIfExists(app, deploy)
20
+ loadAppMetadataFromDeployment(app, deploy, true)
21
+ end
22
+ end
23
+
24
+ class StackatoOutThingy < OutThingy
25
+
26
+ attr_accessor :environments_dir, :apps_dir
27
+
28
+ @@stackato_file_name = "stackato.yml"
29
+ @@metadata_file_name = "metadata.xml"
30
+ @environments_dir = nil
31
+ @apps_dir = nil
32
+
33
+ def initialize(output_dir)
34
+ super(output_dir)
35
+ workdir = File.join(output_dir,'tmp')
36
+ @environments_dir = File.join(output_dir,'environments')
37
+ @apps_dir = File.join(workdir,'apps')
38
+ FileUtils.mkdir_p environments_dir
39
+ FileUtils.mkdir_p apps_dir
40
+ end
41
+
42
+ def clean(app, environment)
43
+ # TODO: cleanup
44
+ end
45
+
46
+ def provision(app)
47
+ app.download(@apps_dir)
48
+ end
49
+
50
+ def filePath(app, deploy)
51
+ "#{deploy.environment.id}/#{deploy.shipcloud}/#{app.id}"
52
+ end
53
+
54
+ def workingDir(app)
55
+ File.join(apps_dir, app.artifact_id)
56
+ end
57
+
58
+ # TODO this needs a refactor, it's knoted up with App
59
+ def loadAppMetadataFromDeployment(app, deploy, fail_silent=false)
60
+ p = filePath(app, deploy)
61
+ path = File.join(@environments_dir, p)
62
+ metadata_file = File.join(path, @@metadata_file_name)
63
+
64
+ if (File.exists? path)
65
+ File.open(metadata_file, 'r') { |f| app.readArtifactMetadata(f.read) }
66
+ else
67
+ raise "Build meta-data was not found: #{path}" unless fail_silent
68
+ end
69
+ end
70
+
71
+ def writeArtifactMetadata(app, deploy)
72
+ if (deploy.enabled?)
73
+ path = filePath(app, deploy)
74
+ outputToFile("environments/#{path}/#{@@metadata_file_name}") { |f|
75
+ f.write(app.artifact_meta_data)
76
+ }
77
+ end
78
+ end
79
+
80
+ def writeConfiguration(app, deploy, bg=false)
81
+ if (deploy.enabled?)
82
+ target_dir = workingDir(app)
83
+ templates_dir = File.join(target_dir, "deploy-templates")
84
+
85
+ daphne_tokens = [ "#{app.id}", "#{deploy.environment.id}" ]
86
+ daphne_tokens = daphne_tokens << "facility-#{deploy.facility_id}" unless deploy.facility_id.nil?
87
+
88
+ n_changes = Templates.evaluate templates_dir, target_dir, {
89
+ :app => app,
90
+ :deploy => deploy,
91
+
92
+ #daphne, generate oauth app tokens
93
+ #TODO: seems like this should be refactored
94
+ :oauth_id => DaphneUtil.generate_id(*daphne_tokens),
95
+ :oauth_secret => DaphneUtil.generate_secret(*daphne_tokens),
96
+ :api_key => DaphneUtil.generate_api_key(*daphne_tokens),
97
+
98
+ # short-cuts
99
+ :app_id => app.id,
100
+ :deploy_base_name => deploy.name(app),
101
+ :deploy_name => bg ? deploy.unique_name(app) : deploy.name(app),
102
+ :deploy_urls => bg ? deploy.inactive_urls(app) : deploy.active_urls(app),
103
+ :env => deploy.environment.id,
104
+ :org => deploy.org,
105
+ :ship => deploy.ship,
106
+ :infra => deploy.shipcloud, # TODO: remove infra key, this is just to avoid breaking existing templates
107
+ :shipcloud => deploy.shipcloud,
108
+ :facility => deploy.facility_id,
109
+ :options => deploy.options
110
+ }
111
+ end
112
+ #TODO: can this be used to provide idempotency for config-only deploymnent?
113
+ puts "#{n_changes} files changed"
114
+ end
115
+ end
116
+
117
+ class PropertiesFileOutThingy < OutThingy
118
+
119
+ def clean(app, environment)
120
+ # cleanup current
121
+ FileUtils.rm Dir.glob("#{@output_dir}/#{app.id}-deploy-#{environment.id}-*.properties")
122
+ end
123
+
124
+ def fileName(app, deploy)
125
+ "#{app.id}-deploy-#{deploy.environment.id}-#{deploy.shipcloud}.properties"
126
+ end
127
+
128
+ def loadAppMetadataFromDeployment(app, deploy, fail_silent=false)
129
+ path = File.join(@output_dir, fileName(app, deploy))
130
+ if (File.exists? path)
131
+ props = Utils::Properties.load_from_file(path, true)
132
+ app.version = props.get(:DEPLOY_VERSION) # TODO: preserve SHA1
133
+ else
134
+ raise "File not found: #{path}" unless fail_silent
135
+ end
136
+ end
137
+
138
+ def writeArtifactMetadata(app, deploy)
139
+ file_name = fileName(app, deploy)
140
+ out_file = File.join(@output_dir, file_name)
141
+ if (!deploy.enabled?)
142
+ if (File.exists? out_file)
143
+ File.delete(out_file)
144
+ end
145
+ out_file = File.join(@output_dir, "DISABLED-#{file_name}")
146
+ end
147
+ puts "Write: #{out_file}"
148
+ File.open(out_file, 'w') { |f|
149
+ # TODO: would like @time in the manifest as audit field, but not sure it's such a good idea yet
150
+ # f << "\# #{file_name}\n\# #{@time}\n"
151
+ f << "\# #{file_name}\n"
152
+ f << "DEPLOY_ENV=#{deploy.environment.id}\n"
153
+ f << "DEPLOY_INFRA=#{deploy.shipcloud}\n"
154
+ if (deploy.facility_id)
155
+ f << "FACILITY=facility-#{deploy.facility_id}\n"
156
+ end
157
+ unless (deploy.options.empty?)
158
+ options = []
159
+ deploy.options.each do |k, v|
160
+ options << k + '|' + v.join(',')
161
+ end
162
+ p options.join(';')
163
+ f << "OPTIONS=#{options.join(';')}\n"
164
+ end
165
+ f << "APP=#{app.id}\n"
166
+ f << "DEPLOY_SHA1=#{app.sha1}\n"
167
+ f << "DEPLOY_VERSION=#{app.version}\n"
168
+ f << "TYPE=#{app.platform_type}\n"
169
+ f << "\n"
170
+ }
171
+ end
172
+
173
+ def provision(app)
174
+ #noop
175
+ end
176
+
177
+ def writeConfiguration(app, deploy)
178
+ # noop
179
+ end
180
+ end
@@ -0,0 +1,102 @@
1
+ class Pipeline
2
+ include ApiHelper
3
+
4
+ attr_accessor :id, :apps
5
+
6
+ def initialize(id, artifact_repo, output, options)
7
+ @id = id
8
+ @artifact_repo = artifact_repo
9
+ @output = output
10
+ @apps = {}
11
+ @options = options
12
+ end
13
+
14
+ def app(app_id, project_name, platform_type, url_segment=nil)
15
+ if (@options[:'deploy-app-name'].nil?) || (@options[:'deploy-app-name'] == app_id.to_s)
16
+ puts "No app specified, or we matched the specified app: #{@options[:'deploy-app-name']}"
17
+ @apps[app_id] = case platform_type
18
+ when :jvm
19
+ JvmApp.new(app_id, project_name, @artifact_repo, @options, url_segment)
20
+ when :rails_zip
21
+ RailsZipApp.new(app_id, project_name, @artifact_repo, @options, url_segment)
22
+ else
23
+ raise "unknown platform type: #{platform_type}"
24
+ end
25
+ else
26
+ puts "App #{app_id} doesn't match specified app #{@options[:'deploy-app-name']}, skipping"
27
+ end
28
+ end
29
+
30
+ def appVersion(app_id)
31
+ @apps[app_id].version
32
+ end
33
+
34
+ def load!(environment, deploy_shipcloud)
35
+ eachAppDeployment(environment) { |app, deploy|
36
+ # load the persisted meta-data
37
+ @output.loadAppMetadataFromDeploymentIfExists(app, deploy)
38
+ }
39
+ @deploy_shipcloud = deploy_shipcloud
40
+ end
41
+
42
+ def update(environment, from_upstream_env)
43
+ @apps.values.each { |app|
44
+ puts ''
45
+ if !(@options[:'deploy-app-version'].nil?)
46
+ puts "Using version from options: #{@options[:'deploy-app-version']}"
47
+ app.findRequestedVersion(@options[:'deploy-app-version'])
48
+ elsif (from_upstream_env)
49
+ # promote versions from an upstream deploy
50
+ # TODO: this needs improving
51
+ puts "Promote #{app.id} from #{from_upstream_env.id} to #{environment.id}"
52
+ upstream_deploy = from_upstream_env.deployments.select { |d| d.app_ids.select {|x| x == app.id }.size > 0 }.first
53
+ @output.loadAppMetadataFromDeployment(app, upstream_deploy)
54
+ else
55
+ puts "Find latest #{app.artifact_id}"
56
+ # lookup most recent from repo
57
+ app.findRequestedVersion('LATEST')
58
+ end
59
+ @output.clean(app, environment)
60
+ # download artifacts
61
+ @output.provision(app)
62
+ }
63
+
64
+ # write out deployment artifact meta-data
65
+ eachAppDeployment(environment) { |app, deploy| @output.writeArtifactMetadata(app, deploy) }
66
+ end
67
+
68
+ def configure(environment, bg=false)
69
+ eachAppDeployment(environment) { |app, deploy| @output.writeConfiguration(app, deploy, bg) }
70
+ end
71
+
72
+ def deploy(environment, paas, force=false)
73
+ eachAppDeployment(environment) { |app, deploy|
74
+ work_dir = @output.workingDir(app)
75
+ if (force)
76
+ paas.deploy(work_dir, app, deploy, true)
77
+ else
78
+ paas.deploy(work_dir, app, deploy) unless app.isDeployedAt(deploy.manifest_url(app))
79
+ end
80
+ }
81
+ end
82
+
83
+ def bg_deploy(environment, paas, force=false)
84
+ apps = []
85
+ eachAppDeployment(environment) { |app, deploy|
86
+ if (force or !app.isDeployedAt(deploy.manifest_url(app)))
87
+ paas.bg_deploy(@output.workingDir(app), app, deploy)
88
+ end
89
+ }
90
+ end
91
+
92
+ def eachAppDeployment(environment)
93
+ selected_deployments = environment.deployments.select{ |d| @deploy_shipcloud.nil? || d.shipcloud == @deploy_shipcloud }
94
+ puts "WARNING no deploys selected: #{@deploy_shipcloud}" if selected_deployments.empty?
95
+ selected_deployments.each do |d|
96
+ d.app_ids.each do |a|
97
+ app = @apps[a]
98
+ yield app, d unless app.nil?
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,10 @@
1
+ require 'conan/application'
2
+ class RailsZipApp < Application
3
+ attr_accessor :sha1, :artifact_repo
4
+
5
+ def initialize(id, project_name, artifact_repo, options, url_segment=nil)
6
+ super(id, project_name, :rails_zip, options, url_segment)
7
+ @sha1 = nil
8
+ @artifact_repo = artifact_repo
9
+ end
10
+ end