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