hippo-cli 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/bin/hippo +36 -0
- data/cli/apply_config.rb +17 -0
- data/cli/apply_services.rb +15 -0
- data/cli/build.rb +16 -0
- data/cli/console.rb +31 -0
- data/cli/deploy.rb +45 -0
- data/cli/edit-secret.rb +45 -0
- data/cli/help.rb +21 -0
- data/cli/init.rb +25 -0
- data/cli/install.rb +44 -0
- data/cli/kubectl.rb +16 -0
- data/cli/objects.rb +34 -0
- data/cli/publish.rb +17 -0
- data/cli/secrets.rb +23 -0
- data/cli/status.rb +15 -0
- data/lib/hippo/build_spec.rb +32 -0
- data/lib/hippo/cli_steps.rb +274 -0
- data/lib/hippo/error.rb +18 -0
- data/lib/hippo/kubernetes.rb +200 -0
- data/lib/hippo/recipe.rb +126 -0
- data/lib/hippo/repository.rb +122 -0
- data/lib/hippo/secret.rb +165 -0
- data/lib/hippo/secret_manager.rb +99 -0
- data/lib/hippo/stage.rb +34 -0
- data/lib/hippo/util.rb +50 -0
- data/lib/hippo/version.rb +3 -0
- data/lib/hippo/yaml_part.rb +47 -0
- data/lib/hippo.rb +7 -0
- data/template/Hippofile +19 -0
- data/template/config/production/env-vars.yaml +7 -0
- data/template/deployments/web.yaml +29 -0
- data/template/deployments/worker.yaml +26 -0
- data/template/jobs/install/load-schema.yaml +22 -0
- data/template/jobs/upgrade/db-migration.yaml +22 -0
- data/template/services/main.ingress.yaml +13 -0
- data/template/services/web.svc.yaml +11 -0
- data/template/stages/production.yaml +6 -0
- data.tar.gz.sig +0 -0
- metadata +163 -0
- metadata.gz.sig +1 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'git'
|
5
|
+
require 'hippo/error'
|
6
|
+
|
7
|
+
module Hippo
|
8
|
+
class Repository
|
9
|
+
def initialize(options)
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def url
|
14
|
+
@options['url']
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return the path where this repository is stored on the local
|
18
|
+
# computer.
|
19
|
+
#
|
20
|
+
# @return [String]
|
21
|
+
def path
|
22
|
+
return @options['path'] if @options['path']
|
23
|
+
|
24
|
+
@path ||= begin
|
25
|
+
digest = Digest::SHA256.hexdigest(url)
|
26
|
+
File.join('', 'tmp', 'hippo-repos', digest)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Clone this repository into the working directory for this
|
31
|
+
# application.
|
32
|
+
#
|
33
|
+
# @return [Boolean]
|
34
|
+
def clone
|
35
|
+
if File.directory?(path)
|
36
|
+
raise RepositoryAlreadyClonedError, "Repository has already been cloned to #{path}. Maybe you just want to pull?"
|
37
|
+
end
|
38
|
+
|
39
|
+
@git = Git.clone(url, path)
|
40
|
+
true
|
41
|
+
rescue Git::GitExecuteError => e
|
42
|
+
raise RepositoryCloneError, e.message
|
43
|
+
end
|
44
|
+
|
45
|
+
# Has this been cloned?
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
def cloned?
|
49
|
+
File.directory?(path)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Fetch the latest copy of this repository
|
53
|
+
#
|
54
|
+
# @return [Boolean]
|
55
|
+
def fetch
|
56
|
+
git.fetch
|
57
|
+
true
|
58
|
+
rescue Git::GitExecuteError => e
|
59
|
+
raise RepositoryFetchError, e.message
|
60
|
+
end
|
61
|
+
|
62
|
+
# Checkout the version of the application for the given commit or
|
63
|
+
# branch name in the local copy.
|
64
|
+
#
|
65
|
+
# @param ref [String]
|
66
|
+
# @return [Boolean]
|
67
|
+
def checkout(ref)
|
68
|
+
git.checkout("origin/#{ref}")
|
69
|
+
true
|
70
|
+
rescue Git::GitExecuteError => e
|
71
|
+
if e.message =~ /did not match any file\(s\) known to git/
|
72
|
+
raise RepositoryCheckoutError, "No branch named '#{ref}' found in repository"
|
73
|
+
else
|
74
|
+
raise RepositoryCheckoutError, e.message
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return the commit reference for the currently checked out branch
|
79
|
+
#
|
80
|
+
# @return [String]
|
81
|
+
def commit
|
82
|
+
git.log(1).first
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get the commit reference for the given branch on the remote
|
86
|
+
# repository by asking it directly.
|
87
|
+
#
|
88
|
+
# @param name [String]
|
89
|
+
# @return [Git::Commit]
|
90
|
+
def commit_for_branch(branch)
|
91
|
+
git.object("origin/#{branch}").log(1).first
|
92
|
+
rescue Git::GitExecuteError => e
|
93
|
+
if e.message =~ /Not a valid object name/
|
94
|
+
raise Error, "'#{branch}' is not a valid branch name in repository"
|
95
|
+
else
|
96
|
+
raise
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Return the template variables for a repository
|
101
|
+
#
|
102
|
+
# @return [Hash]
|
103
|
+
def template_vars
|
104
|
+
{
|
105
|
+
'url' => url,
|
106
|
+
'path' => path
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def git
|
113
|
+
@git ||= begin
|
114
|
+
if cloned?
|
115
|
+
Git.open(path)
|
116
|
+
else
|
117
|
+
raise Error, 'Could not create a git instance because there is no repository cloned for it.'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/hippo/secret.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require 'openssl'
|
5
|
+
require 'encryptor'
|
6
|
+
require 'hippo/util'
|
7
|
+
|
8
|
+
module Hippo
|
9
|
+
class Secret
|
10
|
+
include Hippo::Util
|
11
|
+
|
12
|
+
HEADER = [
|
13
|
+
'# This file is encrypted and managed by Hippo.',
|
14
|
+
'# Use `hippo secret [stage] [name]` to make changes to it.',
|
15
|
+
'#',
|
16
|
+
'# Note: this cannot be applied directly to your Kubernetes server because',
|
17
|
+
'# HippoEncryptedSecret is not a valid object. It will be automatically ',
|
18
|
+
'# converted to a Secret when it is applied by Hippo.'
|
19
|
+
].join("\n")
|
20
|
+
|
21
|
+
def initialize(manager, name)
|
22
|
+
@manager = manager
|
23
|
+
@name = name
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the path to the stored encrypted secret file
|
27
|
+
#
|
28
|
+
# @return [String]
|
29
|
+
def path
|
30
|
+
File.join(@manager.recipe.root, 'secrets', @manager.stage.name, "#{@name}.yaml")
|
31
|
+
end
|
32
|
+
|
33
|
+
# Does the secret file currently exist on the file system?
|
34
|
+
#
|
35
|
+
# @return [Boolean]
|
36
|
+
def exists?
|
37
|
+
File.file?(path)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the secret file as it should be applied to Kubernetes.
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
def to_secret_yaml
|
44
|
+
decrypted_parts.map do |part, _array|
|
45
|
+
part['kind'] = 'Secret'
|
46
|
+
part['metadata'] ||= {}
|
47
|
+
part['metadata']['namespace'] = @manager.stage.namespace
|
48
|
+
|
49
|
+
part['data'].each do |key, value|
|
50
|
+
part['data'][key] = Base64.encode64(value).gsub("\n", '').strip
|
51
|
+
end
|
52
|
+
part
|
53
|
+
end.map { |p| p.hash.to_yaml } .join("---\n")
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the secret file as it should be displayed for editting
|
57
|
+
#
|
58
|
+
# @return [String]
|
59
|
+
def to_editable_yaml
|
60
|
+
decrypted_parts.map { |p| p.hash.to_yaml } .join("---\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Edit a secret file by opening an editor and allow changes to be made.
|
64
|
+
# When the editor completes, finish by writing the file back to the disk.
|
65
|
+
#
|
66
|
+
# @return [void]
|
67
|
+
def edit
|
68
|
+
return unless exists?
|
69
|
+
|
70
|
+
# Obtain a list of parts and map them to the name of the secret
|
71
|
+
# in the file.
|
72
|
+
original_decrypted_part_data = parse_parts(load_yaml_from_path(path), :decrypt)
|
73
|
+
original_encrypted_part_data = load_yaml_from_path(path).each_with_object({}) do |part, hash|
|
74
|
+
next if part.nil? || part.empty?
|
75
|
+
|
76
|
+
hash[part.dig('metadata', 'name')] = part['data']
|
77
|
+
end
|
78
|
+
original_part_data = original_decrypted_part_data.each_with_object({}) do |part, hash|
|
79
|
+
name = part.dig('metadata', 'name')
|
80
|
+
enc = original_encrypted_part_data[name]
|
81
|
+
next if enc.nil?
|
82
|
+
|
83
|
+
hash[part.dig('metadata', 'name')] = part['data'].each_with_object({}) do |(key, value), hash2|
|
84
|
+
hash2[key] = [value, enc[key]]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Open the editor and gather what the user provides.
|
89
|
+
saved_contents = open_in_editor("secret-#{@name}", to_editable_yaml)
|
90
|
+
|
91
|
+
# This saved contents should now be validated to ensure it is valid
|
92
|
+
# YAML and, if so, it should be encrypted and then saved into the
|
93
|
+
# secret file as needed.
|
94
|
+
begin
|
95
|
+
yaml_parts = load_yaml_from_data(saved_contents)
|
96
|
+
parts = parse_parts(yaml_parts, :encrypt, original_part_data)
|
97
|
+
write(parts)
|
98
|
+
rescue StandardError => e
|
99
|
+
raise
|
100
|
+
puts "An error occurred parsing your file: #{e.message}"
|
101
|
+
saved_contents = open_in_editor("secret-#{@name}", saved_contents)
|
102
|
+
retry
|
103
|
+
end
|
104
|
+
|
105
|
+
puts "#{@name} secret has been editted"
|
106
|
+
end
|
107
|
+
|
108
|
+
# Create a new templated encrypted secret with the given name
|
109
|
+
#
|
110
|
+
# @return [void]
|
111
|
+
def create
|
112
|
+
template = {
|
113
|
+
'apiVersion' => 'v1',
|
114
|
+
'kind' => 'HippoEncryptedSecret',
|
115
|
+
'metadata' => {
|
116
|
+
'name' => @name
|
117
|
+
},
|
118
|
+
'data' => {
|
119
|
+
'example' => @manager.encrypt('This is an example secret!')
|
120
|
+
}
|
121
|
+
}
|
122
|
+
write([template])
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def decrypted_parts
|
128
|
+
return unless exists?
|
129
|
+
|
130
|
+
yaml_parts = load_yaml_from_path(path)
|
131
|
+
parse_parts(yaml_parts, :decrypt)
|
132
|
+
end
|
133
|
+
|
134
|
+
def parse_parts(yaml_parts, method, skips = {})
|
135
|
+
yaml_parts.each_with_object([]) do |part, array|
|
136
|
+
next if part.hash.nil? || part.hash.empty?
|
137
|
+
|
138
|
+
part['data'].each do |key, value|
|
139
|
+
skip = skips[part.dig('metadata', 'name')]
|
140
|
+
part['data'][key] = if skip && skip[key] && skip[key][0] == value
|
141
|
+
skip[key][1]
|
142
|
+
else
|
143
|
+
@manager.public_send(method, value.to_s)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
array << part
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Write the array of given parts into a file along with a suitable
|
152
|
+
# explanatory header.
|
153
|
+
#
|
154
|
+
# @return [void]
|
155
|
+
def write(parts)
|
156
|
+
parts = parts.map(&:to_yaml).join("---\n")
|
157
|
+
data_to_write = HEADER + "\n" + parts
|
158
|
+
|
159
|
+
FileUtils.mkdir_p(File.dirname(path))
|
160
|
+
File.open(path, 'w') do |f|
|
161
|
+
f.write(data_to_write)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'hippo/secret'
|
5
|
+
|
6
|
+
module Hippo
|
7
|
+
class SecretManager
|
8
|
+
attr_reader :recipe
|
9
|
+
attr_reader :stage
|
10
|
+
attr_reader :key
|
11
|
+
|
12
|
+
CIPHER = OpenSSL::Cipher.new('aes-256-gcm')
|
13
|
+
|
14
|
+
def initialize(recipe, stage)
|
15
|
+
@recipe = recipe
|
16
|
+
@stage = stage
|
17
|
+
end
|
18
|
+
|
19
|
+
# Generate and publish a new secret key to the Kubernetes API.
|
20
|
+
#
|
21
|
+
# @return [void]
|
22
|
+
def create_key
|
23
|
+
if key_available?
|
24
|
+
raise Hippo::Error, 'A key already exists on Kubernetes. Remove this first.'
|
25
|
+
end
|
26
|
+
|
27
|
+
CIPHER.encrypt
|
28
|
+
secret_key = CIPHER.random_key
|
29
|
+
secret_key64 = Base64.encode64(secret_key).gsub("\n", '').strip
|
30
|
+
object = {
|
31
|
+
'apiVersion' => 'v1',
|
32
|
+
'kind' => 'Secret',
|
33
|
+
'type' => 'hippo.adam.ac/secret-encryption-key',
|
34
|
+
'metadata' => { 'name' => 'hippo-secret-key', 'namespace' => @stage.namespace },
|
35
|
+
'data' => { 'key' => Base64.encode64(secret_key64).gsub("\n", '').strip }
|
36
|
+
}
|
37
|
+
@recipe.kubernetes.apply_with_kubectl(object.to_yaml)
|
38
|
+
@key = secret_key
|
39
|
+
end
|
40
|
+
|
41
|
+
# Download the current key from the Kubernetes API and set it as the
|
42
|
+
# key for this instance
|
43
|
+
#
|
44
|
+
# @return [void]
|
45
|
+
def download_key
|
46
|
+
return if @key
|
47
|
+
|
48
|
+
value = @recipe.kubernetes.get_with_kubectl(@stage, 'secret', 'hippo-secret-key').first
|
49
|
+
return if value.nil?
|
50
|
+
return if value.dig('data', 'key').nil?
|
51
|
+
|
52
|
+
@key = Base64.decode64(Base64.decode64(value['data']['key']))
|
53
|
+
rescue Hippo::Error => e
|
54
|
+
raise unless e.message =~ /not found/
|
55
|
+
end
|
56
|
+
|
57
|
+
def key_available?
|
58
|
+
download_key
|
59
|
+
!@key.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
def secret(name)
|
63
|
+
Secret.new(self, name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def secrets
|
67
|
+
Dir[File.join('secrets', @stage.name, '**', '*.{yml,yaml}')].map do |path|
|
68
|
+
secret(path.split('/').last.sub(/\.ya?ml\z/, ''))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def encrypt(value)
|
73
|
+
CIPHER.encrypt
|
74
|
+
iv = CIPHER.random_iv
|
75
|
+
salt = SecureRandom.random_bytes(16)
|
76
|
+
encrypted_value = Encryptor.encrypt(value: value.to_s, key: @key, iv: iv, salt: salt)
|
77
|
+
'encrypted:' + Base64.encode64([
|
78
|
+
Base64.encode64(encrypted_value),
|
79
|
+
Base64.encode64(salt),
|
80
|
+
Base64.encode64(iv)
|
81
|
+
].join('---')).gsub("\n", '')
|
82
|
+
end
|
83
|
+
|
84
|
+
# Decrypt the given value value and return it
|
85
|
+
#
|
86
|
+
# @param value [String]
|
87
|
+
# @return [String]
|
88
|
+
def decrypt(value)
|
89
|
+
value = value.to_s
|
90
|
+
if value =~ /\Aencrypted:(.*)/
|
91
|
+
value = Base64.decode64(Regexp.last_match(1))
|
92
|
+
encrypted_value, salt, iv = value.split('---', 3).map { |s| Base64.decode64(s) }
|
93
|
+
Encryptor.decrypt(value: encrypted_value, key: @key, iv: iv, salt: salt).to_s
|
94
|
+
else
|
95
|
+
value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/hippo/stage.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hippo
|
4
|
+
class Stage
|
5
|
+
def initialize(options)
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def name
|
10
|
+
@options['name']
|
11
|
+
end
|
12
|
+
|
13
|
+
def branch
|
14
|
+
@options['branch']
|
15
|
+
end
|
16
|
+
|
17
|
+
def namespace
|
18
|
+
@options['namespace']
|
19
|
+
end
|
20
|
+
|
21
|
+
def template_vars
|
22
|
+
{
|
23
|
+
'name' => name,
|
24
|
+
'branch' => branch,
|
25
|
+
'namespace' => namespace,
|
26
|
+
'vars' => @options['vars'] || {}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def kubectl(*command)
|
31
|
+
"kubectl -n #{namespace} #{command.join(' ')}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/hippo/util.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'hippo/error'
|
4
|
+
require 'hippo/yaml_part'
|
5
|
+
|
6
|
+
module Hippo
|
7
|
+
module Util
|
8
|
+
def load_yaml_from_path(path)
|
9
|
+
return nil if path.nil?
|
10
|
+
return nil unless File.file?(path)
|
11
|
+
|
12
|
+
data = File.read(path)
|
13
|
+
load_yaml_from_data(data, path)
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_yaml_from_data(data, path = nil)
|
17
|
+
parts = data.split(/^\-\-\-$/)
|
18
|
+
parts.each_with_index.map do |part, index|
|
19
|
+
YAMLPart.new(part, path, index)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_yaml_from_directory(path)
|
24
|
+
return [] if path.nil?
|
25
|
+
return [] unless File.directory?(path)
|
26
|
+
|
27
|
+
Dir[File.join(path, '*.{yaml,yml}')].sort.each_with_object([]) do |path, array|
|
28
|
+
yaml = load_yaml_from_path(path)
|
29
|
+
next if yaml.nil?
|
30
|
+
|
31
|
+
array << yaml
|
32
|
+
end.flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
def open_in_editor(name, contents)
|
36
|
+
tmp_root = File.join(ENV['HOME'], '.hippo')
|
37
|
+
FileUtils.mkdir_p(tmp_root)
|
38
|
+
begin
|
39
|
+
tmpfile = Tempfile.new([name, '.yaml'], tmp_root)
|
40
|
+
tmpfile.write(contents)
|
41
|
+
tmpfile.close
|
42
|
+
system("#{ENV['EDITOR']} #{tmpfile.path}")
|
43
|
+
tmpfile.open
|
44
|
+
tmpfile.read
|
45
|
+
ensure
|
46
|
+
tmpfile.unlink
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hippo
|
4
|
+
class YAMLPart
|
5
|
+
attr_reader :yaml
|
6
|
+
|
7
|
+
def initialize(yaml, path, index)
|
8
|
+
@yaml = yaml.strip
|
9
|
+
@path = path
|
10
|
+
@index = index
|
11
|
+
end
|
12
|
+
|
13
|
+
def hash
|
14
|
+
@hash ||= YAML.safe_load(@yaml)
|
15
|
+
rescue Psych::SyntaxError => e
|
16
|
+
raise Error, "YAML parsing error in #{@path} (index #{@index}) (#{e.message})"
|
17
|
+
end
|
18
|
+
|
19
|
+
def empty?
|
20
|
+
@yaml.nil? ||
|
21
|
+
@yaml.empty? ||
|
22
|
+
hash.nil? ||
|
23
|
+
hash.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_yaml
|
27
|
+
hash.to_yaml
|
28
|
+
end
|
29
|
+
|
30
|
+
def dig(*args)
|
31
|
+
hash.dig(*args)
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](name)
|
35
|
+
hash[name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def []=(name, value)
|
39
|
+
hash[name] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse(recipe, stage, commit)
|
43
|
+
parsed_part = recipe.parse(stage, commit, @yaml)
|
44
|
+
self.class.new(parsed_part, @path, @index)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/hippo.rb
ADDED
data/template/Hippofile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Welcome to your Hippofile 🦛
|
2
|
+
#
|
3
|
+
# In here you configure how you wish to build, publish and
|
4
|
+
# deploy your application using Docker & Kubernetes.
|
5
|
+
|
6
|
+
# Where is the application that you wish to deploy hosted?
|
7
|
+
repository:
|
8
|
+
url: git@github.com:username/repo
|
9
|
+
|
10
|
+
# You can request multiple builds from the same repository to be built if
|
11
|
+
# required. In most cases you'll probably only need one. You can reference
|
12
|
+
# these in your Kubernetes objects to specify image details in PodSpecs.
|
13
|
+
builds:
|
14
|
+
app:
|
15
|
+
dockerfile: Dockerfile
|
16
|
+
# This is the name of the image where the built-image should be uploaded
|
17
|
+
# to when build. You should not include any tag after the name. The
|
18
|
+
# commit ref that you're building will be automatically added.
|
19
|
+
image-name: myorg/myapp
|
@@ -0,0 +1,29 @@
|
|
1
|
+
kind: Deployment
|
2
|
+
apiVersion: apps/v1
|
3
|
+
metadata:
|
4
|
+
name: web
|
5
|
+
spec:
|
6
|
+
selector:
|
7
|
+
matchLabels:
|
8
|
+
process_type: web
|
9
|
+
replicas: 1
|
10
|
+
template:
|
11
|
+
metadata:
|
12
|
+
labels:
|
13
|
+
process_type: web
|
14
|
+
spec:
|
15
|
+
containers:
|
16
|
+
- name: myapp
|
17
|
+
image: "{{ builds.app.image-name }}:{{ commit.ref }}"
|
18
|
+
imagePullPolicy: Always
|
19
|
+
command:
|
20
|
+
- bundle
|
21
|
+
- exec
|
22
|
+
- puma
|
23
|
+
- -C
|
24
|
+
- config/puma.rb
|
25
|
+
envFrom:
|
26
|
+
- configMapRef:
|
27
|
+
name: env-vars
|
28
|
+
ports:
|
29
|
+
- containerPort: 3000
|
@@ -0,0 +1,26 @@
|
|
1
|
+
kind: Deployment
|
2
|
+
apiVersion: apps/v1
|
3
|
+
metadata:
|
4
|
+
name: worker
|
5
|
+
spec:
|
6
|
+
selector:
|
7
|
+
matchLabels:
|
8
|
+
app: worker
|
9
|
+
replicas: 1
|
10
|
+
template:
|
11
|
+
metadata:
|
12
|
+
labels:
|
13
|
+
app: worker
|
14
|
+
spec:
|
15
|
+
containers:
|
16
|
+
- name: myapp
|
17
|
+
image: "{{ builds.app.image-name }}:{{ commit.ref }}"
|
18
|
+
imagePullPolicy: Always
|
19
|
+
command:
|
20
|
+
- bundle
|
21
|
+
- exec
|
22
|
+
- rake
|
23
|
+
- jobs:work
|
24
|
+
envFrom:
|
25
|
+
- configMapRef:
|
26
|
+
name: env-vars
|
@@ -0,0 +1,22 @@
|
|
1
|
+
kind: Job
|
2
|
+
apiVersion: batch/v1
|
3
|
+
metadata:
|
4
|
+
name: load-schema
|
5
|
+
spec:
|
6
|
+
backoffLimit: 0
|
7
|
+
ttlSecondsAfterFinished: 300
|
8
|
+
template:
|
9
|
+
spec:
|
10
|
+
restartPolicy: Never
|
11
|
+
containers:
|
12
|
+
- name: myapp
|
13
|
+
image: "{{ builds.app.image-name }}:{{ commit.ref }}"
|
14
|
+
imagePullPolicy: Always
|
15
|
+
command:
|
16
|
+
- bundle
|
17
|
+
- exec
|
18
|
+
- rake
|
19
|
+
- db:schema:load
|
20
|
+
envFrom:
|
21
|
+
- configMapRef:
|
22
|
+
name: env-vars
|
@@ -0,0 +1,22 @@
|
|
1
|
+
kind: Job
|
2
|
+
apiVersion: batch/v1
|
3
|
+
metadata:
|
4
|
+
name: migration
|
5
|
+
spec:
|
6
|
+
backoffLimit: 0
|
7
|
+
ttlSecondsAfterFinished: 60
|
8
|
+
template:
|
9
|
+
spec:
|
10
|
+
restartPolicy: Never
|
11
|
+
containers:
|
12
|
+
- name: myapp
|
13
|
+
image: "{{ builds.app.image-name }}:{{ commit.ref }}"
|
14
|
+
imagePullPolicy: Always
|
15
|
+
command:
|
16
|
+
- bundle
|
17
|
+
- exec
|
18
|
+
- rake
|
19
|
+
- db:migrate
|
20
|
+
envFrom:
|
21
|
+
- configMapRef:
|
22
|
+
name: env-vars
|
data.tar.gz.sig
ADDED
Binary file
|