hippo-cli 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/cli/apply_config.rb +4 -5
- data/cli/apply_services.rb +3 -3
- data/cli/console.rb +6 -6
- data/cli/deploy.rb +11 -22
- data/cli/install.rb +22 -23
- data/cli/kubectl.rb +3 -3
- data/cli/secrets_edit.rb +34 -0
- data/cli/secrets_key.rb +20 -0
- data/lib/hippo.rb +19 -0
- data/lib/hippo/cli.rb +210 -0
- data/lib/hippo/deployment_monitor.rb +82 -0
- data/lib/hippo/error.rb +0 -12
- data/lib/hippo/image.rb +74 -0
- data/lib/hippo/manifest.rb +81 -0
- data/lib/hippo/object_definition.rb +63 -0
- data/lib/hippo/secret.rb +61 -114
- data/lib/hippo/secret_manager.rb +43 -32
- data/lib/hippo/stage.rb +157 -7
- data/lib/hippo/util.rb +56 -33
- data/lib/hippo/version.rb +1 -1
- data/template/Hippofile +12 -13
- data/template/config/{production/env-vars.yaml → env-vars.yaml} +0 -0
- data/template/jobs/{upgrade → deploy}/db-migration.yaml +0 -0
- data/template/stages/production.yaml +1 -0
- metadata +31 -16
- metadata.gz.sig +0 -0
- data/cli/build.rb +0 -16
- data/cli/edit-secret.rb +0 -45
- data/cli/objects.rb +0 -34
- data/cli/publish.rb +0 -17
- data/cli/secrets.rb +0 -23
- data/cli/status.rb +0 -15
- data/lib/hippo/build_spec.rb +0 -32
- data/lib/hippo/cli_steps.rb +0 -315
- data/lib/hippo/kubernetes.rb +0 -200
- data/lib/hippo/recipe.rb +0 -126
- data/lib/hippo/repository.rb +0 -122
- data/lib/hippo/yaml_part.rb +0 -47
data/lib/hippo/kubernetes.rb
DELETED
@@ -1,200 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'liquid'
|
4
|
-
require 'hippo/util'
|
5
|
-
|
6
|
-
module Hippo
|
7
|
-
class Kubernetes
|
8
|
-
OBJECT_DIRECTORY_NAMES = %w[config deployments jobs/install jobs/deploy services].freeze
|
9
|
-
|
10
|
-
include Hippo::Util
|
11
|
-
|
12
|
-
def initialize(recipe, options)
|
13
|
-
@recipe = recipe
|
14
|
-
@options = options
|
15
|
-
end
|
16
|
-
|
17
|
-
# Load and return a set of objects from a given path.
|
18
|
-
# Parse them through the templating and return them in the appropriate
|
19
|
-
# context.
|
20
|
-
#
|
21
|
-
# @param stage [Hippo::Stage]
|
22
|
-
# @param commit [String] the commit ref
|
23
|
-
# @param path [String]
|
24
|
-
# @return
|
25
|
-
def objects(path, stage, commit, deploy_id: nil)
|
26
|
-
time = Time.now
|
27
|
-
|
28
|
-
yamls = load_yaml_from_directory(path)
|
29
|
-
yamls |= load_yaml_from_directory(File.join(path, stage.name))
|
30
|
-
|
31
|
-
yamls.map do |yaml_part|
|
32
|
-
object = yaml_part.parse(@recipe, stage, commit)
|
33
|
-
|
34
|
-
# Unless a namespace has been specified in the metadata we will
|
35
|
-
# want to add the namespace for the current stage.
|
36
|
-
if object['metadata'].nil? || object['metadata']['namespace'].nil?
|
37
|
-
object['metadata'] ||= {}
|
38
|
-
object['metadata']['namespace'] = stage.namespace
|
39
|
-
end
|
40
|
-
|
41
|
-
object['metadata']['annotations'] ||= {}
|
42
|
-
object['metadata']['labels'] ||= {}
|
43
|
-
|
44
|
-
add_default_labels(object, stage)
|
45
|
-
|
46
|
-
# Add some information to Deployments to reflect the latest
|
47
|
-
# information about this deployment.
|
48
|
-
if object['kind'] == 'Deployment'
|
49
|
-
if deploy_id
|
50
|
-
object['metadata']['labels']['hippo.adam.ac/deployID'] ||= deploy_id
|
51
|
-
end
|
52
|
-
|
53
|
-
if commit
|
54
|
-
object['metadata']['annotations']['hippo.adam.ac/commitRef'] ||= commit.objectish
|
55
|
-
object['metadata']['annotations']['hippo.adam.ac/commitMessage'] ||= commit.message
|
56
|
-
end
|
57
|
-
|
58
|
-
if pod_metadata = object.dig('spec', 'template', 'metadata')
|
59
|
-
pod_metadata['labels'] ||= {}
|
60
|
-
pod_metadata['annotations'] ||= {}
|
61
|
-
# add_default_labels(pod_metadata, stage)
|
62
|
-
if deploy_id
|
63
|
-
pod_metadata['labels']['hippo.adam.ac/deployID'] = deploy_id
|
64
|
-
end
|
65
|
-
|
66
|
-
if commit
|
67
|
-
pod_metadata['annotations']['hippo.adam.ac/commitRef'] = commit.objectish
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
object
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def apply_namespace(stage)
|
77
|
-
namespace = {
|
78
|
-
'kind' => 'Namespace',
|
79
|
-
'apiVersion' => 'v1',
|
80
|
-
'metadata' => {
|
81
|
-
'name' => stage.namespace
|
82
|
-
}
|
83
|
-
}
|
84
|
-
add_default_labels(namespace, stage)
|
85
|
-
apply_with_kubectl(stage, namespace.to_yaml)
|
86
|
-
end
|
87
|
-
|
88
|
-
# Apply the given configuration with kubectl
|
89
|
-
#
|
90
|
-
# @param config [Array<Hippo::YAMLPart>, String]
|
91
|
-
# @return [void]
|
92
|
-
def apply_with_kubectl(stage, yaml_parts)
|
93
|
-
unless yaml_parts.is_a?(String)
|
94
|
-
yaml_parts = [yaml_parts] unless yaml_parts.is_a?(Array)
|
95
|
-
yaml_parts = yaml_parts.map { |yp| yp.hash.to_yaml }.join("\n---\n")
|
96
|
-
end
|
97
|
-
|
98
|
-
command = ['kubectl']
|
99
|
-
command += ['--context', stage.context] if stage.context
|
100
|
-
command += ['apply', '-f', '-']
|
101
|
-
|
102
|
-
Open3.popen3(command.join(' ')) do |stdin, stdout, stderr, wt|
|
103
|
-
stdin.puts yaml_parts
|
104
|
-
stdin.close
|
105
|
-
|
106
|
-
stdout = stdout.read.strip
|
107
|
-
stderr = stderr.read.strip
|
108
|
-
|
109
|
-
if wt.value.success?
|
110
|
-
puts stdout
|
111
|
-
stdout.split("\n").each_with_object({}) do |line, hash|
|
112
|
-
if line =~ %r{\A([\w\/\-\.]+) (\w+)\z}
|
113
|
-
hash[Regexp.last_match(1)] = Regexp.last_match(2)
|
114
|
-
end
|
115
|
-
end
|
116
|
-
else
|
117
|
-
raise Error, "[kubectl] #{stderr}"
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# Get details of objects using kubectl.
|
123
|
-
#
|
124
|
-
# @param stage [Hippo::Stage]
|
125
|
-
# @param names [Array<String>]
|
126
|
-
# @raises [Hippo::Error]
|
127
|
-
# @return [Array<Hash>]
|
128
|
-
def get_with_kubectl(stage, *names)
|
129
|
-
command = stage.kubectl_base_command
|
130
|
-
command += ['get', names, '-o', 'yaml']
|
131
|
-
command = command.flatten.reject(&:nil?)
|
132
|
-
|
133
|
-
Open3.popen3(*command) do |_, stdout, stderr, wt|
|
134
|
-
if wt.value.success?
|
135
|
-
yaml = YAML.safe_load(stdout.read, permitted_classes: [Time])
|
136
|
-
yaml['items'] || [yaml]
|
137
|
-
else
|
138
|
-
raise Error, "[kutectl] #{stderr.read}"
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
# Delete a named job from the cluster
|
144
|
-
#
|
145
|
-
# @param stage [Hippo::Stage]
|
146
|
-
# @param name [String]
|
147
|
-
# @raises [Hippo::Error]
|
148
|
-
# @return [void]
|
149
|
-
def delete_job(stage, name)
|
150
|
-
command = stage.kubectl_base_command
|
151
|
-
command += ['delete', 'job', name]
|
152
|
-
|
153
|
-
Open3.popen3(*command) do |_, stdout, stderr, wt|
|
154
|
-
if wt.value.success?
|
155
|
-
puts stdout.read
|
156
|
-
true
|
157
|
-
else
|
158
|
-
stderr = stderr.read
|
159
|
-
if stderr =~ /\"#{name}\" not found/
|
160
|
-
false
|
161
|
-
else
|
162
|
-
raise Error, "[kutectl] #{stderr.read}"
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
# Poll the named jobs and return them when all are complete
|
169
|
-
# or the number of checks is exceeded.
|
170
|
-
#
|
171
|
-
# @param stage [Hippo::Stage]
|
172
|
-
# @param names [Array<String>]
|
173
|
-
# @param times [Integer]
|
174
|
-
# @return [Array<Boolean, Array<Hash>]
|
175
|
-
def wait_for_jobs(stage, names, times = 120)
|
176
|
-
jobs = nil
|
177
|
-
times.times do
|
178
|
-
jobs = get_with_kubectl(stage, *names)
|
179
|
-
|
180
|
-
# Are all the jobs completed?
|
181
|
-
if jobs.all? { |j| j['status']['active'].nil? }
|
182
|
-
return [false, jobs]
|
183
|
-
else
|
184
|
-
sleep 2
|
185
|
-
end
|
186
|
-
end
|
187
|
-
[true, jobs]
|
188
|
-
end
|
189
|
-
|
190
|
-
private
|
191
|
-
|
192
|
-
def add_default_labels(object, stage)
|
193
|
-
object['metadata']['labels'] ||= {}
|
194
|
-
object['metadata']['labels']['app.kubernetes.io/name'] = @recipe.name
|
195
|
-
object['metadata']['labels']['app.kubernetes.io/instance'] = stage.name
|
196
|
-
object['metadata']['labels']['app.kubernetes.io/managed-by'] = 'hippo'
|
197
|
-
object
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
data/lib/hippo/recipe.rb
DELETED
@@ -1,126 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'yaml'
|
4
|
-
require 'hippo/build_spec'
|
5
|
-
require 'hippo/error'
|
6
|
-
require 'hippo/kubernetes'
|
7
|
-
require 'hippo/repository'
|
8
|
-
require 'hippo/stage'
|
9
|
-
require 'hippo/util'
|
10
|
-
|
11
|
-
module Hippo
|
12
|
-
class Recipe
|
13
|
-
include Util
|
14
|
-
|
15
|
-
class RecipeNotFound < Error
|
16
|
-
end
|
17
|
-
|
18
|
-
class << self
|
19
|
-
# Load a new Recipe class from a given file.
|
20
|
-
#
|
21
|
-
# @param path [String] path to recipe file.
|
22
|
-
# @return [Hippo::Recipe]
|
23
|
-
def load_from_file(path)
|
24
|
-
unless File.file?(path)
|
25
|
-
raise RecipeNotFound, "No recipe file found at #{path}"
|
26
|
-
end
|
27
|
-
|
28
|
-
hash = YAML.load_file(path)
|
29
|
-
new(hash, path)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
# @param hash [Hash] the raw hash from the underlying yaml file
|
34
|
-
def initialize(hash, hippofile_path = nil)
|
35
|
-
@hash = hash
|
36
|
-
@hippofile_path = hippofile_path
|
37
|
-
end
|
38
|
-
|
39
|
-
attr_reader :path
|
40
|
-
|
41
|
-
# Return the root directory where the Hippofile is located
|
42
|
-
#
|
43
|
-
def root
|
44
|
-
File.dirname(@hippofile_path)
|
45
|
-
end
|
46
|
-
|
47
|
-
# Return the repository that this manifest should be working with
|
48
|
-
#
|
49
|
-
# @return [Hippo::Repository]
|
50
|
-
def repository
|
51
|
-
return unless @hash['repository']
|
52
|
-
|
53
|
-
@repository ||= Repository.new(@hash['repository'])
|
54
|
-
end
|
55
|
-
|
56
|
-
# Return the app name
|
57
|
-
#
|
58
|
-
# @return [String, nil]
|
59
|
-
def name
|
60
|
-
@hash['name']
|
61
|
-
end
|
62
|
-
|
63
|
-
# Return kubernetes configuration
|
64
|
-
#
|
65
|
-
# @return [Hippo::Kubernetes]
|
66
|
-
def kubernetes
|
67
|
-
@kubernetes ||= Kubernetes.new(self, @hash['kubernetes'] || {})
|
68
|
-
end
|
69
|
-
|
70
|
-
# Return the stages for this recipe
|
71
|
-
#
|
72
|
-
# @return [Hash<Hippo::Stage>]
|
73
|
-
def stages
|
74
|
-
@stages ||= begin
|
75
|
-
yamls = load_yaml_from_directory(File.join(root, 'stages'))
|
76
|
-
yamls.each_with_object({}) do |yaml, hash|
|
77
|
-
stage = Stage.new(yaml)
|
78
|
-
hash[stage.name] = stage
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
# Return the builds for this recipe
|
84
|
-
#
|
85
|
-
# @return [Hash<Hippo::BuildSpec>]
|
86
|
-
def build_specs
|
87
|
-
@build_specs ||= @hash['builds'].each_with_object({}) do |(key, options), hash|
|
88
|
-
hash[key] = BuildSpec.new(self, key, options)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Return configuration
|
93
|
-
#
|
94
|
-
# @return []
|
95
|
-
def console
|
96
|
-
@hash['console']
|
97
|
-
end
|
98
|
-
|
99
|
-
# Return the template variables that should be exposed
|
100
|
-
#
|
101
|
-
# @return [Hash]
|
102
|
-
def template_vars
|
103
|
-
{
|
104
|
-
'repository' => repository ? repository.template_vars : nil,
|
105
|
-
'builds' => build_specs.each_with_object({}) { |(_, bs), h| h[bs.name] = bs.template_vars }
|
106
|
-
}
|
107
|
-
end
|
108
|
-
|
109
|
-
# Parse a string through the template parser
|
110
|
-
#
|
111
|
-
# @param string [String]
|
112
|
-
# @return [String]
|
113
|
-
def parse(stage, commit, string)
|
114
|
-
template = Liquid::Template.parse(string)
|
115
|
-
template_variables = template_vars
|
116
|
-
template_variables['stage'] = stage.template_vars
|
117
|
-
if commit
|
118
|
-
template_variables['commit'] = {
|
119
|
-
'ref' => commit.objectish,
|
120
|
-
'message' => commit.message
|
121
|
-
}
|
122
|
-
end
|
123
|
-
template.render(template_variables)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
data/lib/hippo/repository.rb
DELETED
@@ -1,122 +0,0 @@
|
|
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(ENV['HOME'], '.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/yaml_part.rb
DELETED
@@ -1,47 +0,0 @@
|
|
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
|