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.
@@ -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
@@ -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
@@ -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
@@ -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