hippo-cli 1.0.1 → 1.1.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 +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
|