bosh-workspace 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/bosh-workspace.gemspec +5 -4
  3. data/lib/bosh/cli/commands/deployment_patch.rb +0 -1
  4. data/lib/bosh/cli/commands/prepare.rb +12 -11
  5. data/lib/bosh/workspace.rb +4 -3
  6. data/lib/bosh/workspace/credentials.rb +12 -5
  7. data/lib/bosh/workspace/git_credentials_provider.rb +80 -0
  8. data/lib/bosh/workspace/{git_remote_url.rb → helpers/git_protocol_helper.rb} +4 -8
  9. data/lib/bosh/workspace/helpers/project_deployment_helper.rb +10 -2
  10. data/lib/bosh/workspace/helpers/release_helper.rb +33 -7
  11. data/lib/bosh/workspace/manifest_builder.rb +6 -8
  12. data/lib/bosh/workspace/merge_tool.rb +73 -0
  13. data/lib/bosh/workspace/project_deployment.rb +20 -5
  14. data/lib/bosh/workspace/release.rb +103 -67
  15. data/lib/bosh/workspace/schemas/credentials.rb +22 -0
  16. data/lib/bosh/workspace/schemas/project_deployment.rb +14 -1
  17. data/lib/bosh/workspace/version.rb +1 -1
  18. data/spec/assets/bin/spruce +0 -0
  19. data/spec/assets/manifests-repo/deployments/foo.yml +1 -0
  20. data/spec/assets/manifests-repo/stubs/foo.yml +4 -0
  21. data/spec/commands/prepare_spec.rb +39 -12
  22. data/spec/credentials_spec.rb +8 -0
  23. data/spec/git_credentials_provider_spec.rb +82 -0
  24. data/spec/{git_remote_url_spec.rb → helpers/git_protocol_helper_spec.rb} +10 -11
  25. data/spec/helpers/project_deployment_helper_spec.rb +12 -1
  26. data/spec/helpers/release_helper_spec.rb +157 -73
  27. data/spec/manifest_builder_spec.rb +6 -5
  28. data/spec/merge_tool_spec.rb +98 -0
  29. data/spec/project_deployment_spec.rb +43 -1
  30. data/spec/release_spec.rb +369 -354
  31. data/spec/rugged_spec.rb +64 -0
  32. data/spec/schemas/credentials_spec.rb +22 -5
  33. data/spec/spec_helper.rb +1 -0
  34. metadata +35 -15
  35. data/lib/bosh/workspace/helpers/git_credentials_helper.rb +0 -111
  36. data/lib/bosh/workspace/helpers/spiff_helper.rb +0 -34
  37. data/spec/helpers/git_credentials_helper_spec.rb +0 -190
  38. data/spec/helpers/spiff_helper_spec.rb +0 -68
@@ -6,18 +6,17 @@ module Bosh::Workspace
6
6
 
7
7
  def initialize(file)
8
8
  @file = file
9
- err("Deployment file does not exist: #{file}") unless File.exist? @file
10
- @manifest = Psych.load(ERB.new(File.read(@file)).result)
9
+ err("Deployment file does not exist: #{file}") unless File.exist?(@file)
11
10
  end
12
11
 
13
12
  def perform_validation(options = {})
14
- Schemas::ProjectDeployment.new.validate @manifest
13
+ Schemas::ProjectDeployment.new.validate manifest
15
14
  rescue Membrane::SchemaValidationError => e
16
15
  errors << e.message
17
16
  end
18
17
 
19
18
  def director_uuid
20
- @director_uuid || @manifest["director_uuid"]
19
+ @director_uuid || manifest["director_uuid"]
21
20
  end
22
21
 
23
22
  def merged_file
@@ -28,9 +27,25 @@ module Bosh::Workspace
28
27
  end
29
28
  end
30
29
 
30
+ def merge_tool
31
+ Bosh::Workspace::MergeTool.new(manifest['merge_tool'])
32
+ end
33
+
34
+ def manifest
35
+ return @manifest unless @manifest.nil?
36
+ renderer = Bosh::Template::Renderer.new(context: stub.to_json)
37
+ @manifest = Psych.load(renderer.render(file))
38
+ end
39
+
40
+ def stub
41
+ return @stub unless @stub.nil?
42
+ stub_file = File.expand_path(File.join(file_dirname, "../stubs", file_basename))
43
+ @stub = File.exist?(stub_file) ? Psych.load(File.read(stub_file)) : {}
44
+ end
45
+
31
46
  %w[name templates releases stemcells meta domain_name].each do |var|
32
47
  define_method var do
33
- @manifest[var]
48
+ manifest[var]
34
49
  end
35
50
  end
36
51
 
@@ -1,35 +1,25 @@
1
1
  module Bosh::Workspace
2
2
  class Release
3
+ REFSPEC = ['HEAD:refs/remotes/origin/HEAD']
3
4
  attr_reader :name, :git_url, :repo_dir
4
5
 
5
- def initialize(release, releases_dir)
6
- @name = release["name"]
7
- @ref = release["ref"]
8
- @path = release["path"]
9
- @spec_version = release["version"].to_s
10
- @git_url = release["git"]
11
- @repo_dir = File.join(releases_dir, @name)
12
- @url = release["url"]
6
+ def initialize(release, releases_dir, credentials_callback, options = {})
7
+ @name = release["name"]
8
+ @ref = release["ref"]
9
+ @path = release["path"]
10
+ @spec_version = release["version"].to_s
11
+ @git_url = release["git"]
12
+ @repo_dir = File.join(releases_dir, @name)
13
+ @url = release["url"]
14
+ @credentials_callback = credentials_callback
15
+ @offline = options[:offline]
13
16
  end
14
17
 
15
- def update_repo
18
+ def update_repo(options = {})
19
+ fetch_repo
16
20
  hash = ref || release[:commit]
17
21
  update_repo_with_ref(repo, hash)
18
- end
19
-
20
- def update_submodule(submodule)
21
- update_repo_with_ref(submodule.repository, submodule.head_oid)
22
- end
23
-
24
- def required_submodules
25
- required = []
26
- symlink_templates.each do |template|
27
- submodule = submodule_for(template)
28
- if submodule
29
- required.push(submodule)
30
- end
31
- end
32
- required
22
+ update_submodules
33
23
  end
34
24
 
35
25
  def manifest_file
@@ -62,8 +52,55 @@ module Bosh::Workspace
62
52
 
63
53
  private
64
54
 
55
+ def update_submodules
56
+ required_submodules.each do |submodule|
57
+ fetch_repo(submodule_repo(submodule.path, submodule.url))
58
+ update_repo_with_ref(submodule.repository, submodule.head_oid)
59
+ end
60
+ end
61
+
62
+ def required_submodules
63
+ symlink_templates.map { |t| submodule_for(t) }.compact
64
+ end
65
+
66
+ def repo_path(path)
67
+ File.join(@repo_dir, path)
68
+ end
69
+
70
+ def submodule_repo(path, url)
71
+ dir = repo_path(path)
72
+ repo_exists?(dir) ? open_repo(dir) : init_repo(dir, url)
73
+ end
74
+
65
75
  def repo
66
- @repo ||= Rugged::Repository.new(repo_dir)
76
+ repo_exists? ? open_repo : init_repo
77
+ end
78
+
79
+ def fetch_repo(repo = repo)
80
+ return if offline?
81
+ repo.fetch('origin', REFSPEC, credentials: @credentials_callback)
82
+ commit = repo.references['refs/remotes/origin/HEAD'].resolve.target_id
83
+ update_repo_with_ref(repo, commit)
84
+ end
85
+
86
+ def repo_exists?(dir = @repo_dir)
87
+ File.exist?(File.join(dir, '.git'))
88
+ end
89
+
90
+ def open_repo(dir = @repo_dir)
91
+ Rugged::Repository.new(dir)
92
+ end
93
+
94
+ def init_repo(dir = @repo_dir, url = @git_url)
95
+ offline_err(url) if offline?
96
+ FileUtils.mkdir_p File.dirname(dir)
97
+ Rugged::Repository.init_at(dir).tap do |repo|
98
+ repo.remotes.create('origin', url)
99
+ end
100
+ end
101
+
102
+ def offline_err(url)
103
+ err "Cloning repo: '#{url}' not allowed in offline mode"
67
104
  end
68
105
 
69
106
  def update_repo_with_ref(repository, ref)
@@ -93,68 +130,67 @@ module Bosh::Workspace
93
130
  final_releases = []
94
131
  releases_tree.walk_blobs(:preorder) do |_, entry|
95
132
  next if entry[:filemode] == 40960 # Skip symlinks
96
- path = File.join(releases_dir, entry[:name])
97
- blame = Rugged::Blame.new(repo, path).reduce { |memo, hunk|
98
- if memo.nil? || hunk[:final_signature][:time] > memo[:final_signature][:time]
99
- hunk
100
- else
101
- memo
102
- end
133
+ next unless version = entry[:name][/#{@name}-(.+)\.yml/, 1]
134
+ blame = repo_blame(File.join(releases_dir, entry[:name]))
135
+ final_releases << {
136
+ version: version,
137
+ manifest: blame[:orig_path],
138
+ commit: blame[:final_commit_id]
103
139
  }
104
- commit_id = blame[:final_commit_id]
105
- manifest = blame[:orig_path]
106
- version = entry[:name][/#{@name}-(.+)\.yml/, 1]
107
- if ! version.nil?
108
- final_releases.push({
109
- version: version, manifest: manifest, commit: commit_id
110
- })
111
- end
112
140
  end
113
-
114
141
  final_releases.sort! { |a, b| a[:version].to_i <=> b[:version].to_i }
115
142
  end
116
143
  end
117
144
 
145
+ def repo_blame(path)
146
+ Rugged::Blame.new(repo, path).reduce do |m, h|
147
+ return h unless m
148
+ return h if h[:final_signature][:time] > m[:final_signature][:time]
149
+ return m
150
+ end
151
+ end
152
+
118
153
  def release
119
- return final_releases.last if @spec_version == "latest"
120
- release = final_releases.find { |v| v[:version] == @spec_version }
121
- unless release
122
- err("Could not find version: #{@spec_version} for release: #{@name}")
154
+ latest_offline_warning if latest? && offline?
155
+ return final_releases.last if latest?
156
+ final_releases.find { |v| v[:version] == @spec_version }.tap do |release|
157
+ missing_release_err(@spec_version, @name) unless release
123
158
  end
124
- release
159
+ end
160
+
161
+ def missing_release_err(version, name)
162
+ err "Could not find version: #{version} for release: #{name}"
163
+ end
164
+
165
+ def latest_offline_warning
166
+ warning "Using 'latest' local version since in offline mode"
125
167
  end
126
168
 
127
169
  def templates_dir
128
- File.join(repo.workdir, "templates")
170
+ repo_path 'templates'
129
171
  end
130
172
 
131
173
  def symlink_target(file)
132
- if File.readlink(file).start_with?("/")
133
- return File.readlink(file)
134
- else
135
- return File.expand_path(File.join(File.dirname(file), File.readlink(file)))
136
- end
174
+ return File.readlink(file) if File.readlink(file).start_with?("/")
175
+ File.expand_path(File.join(File.dirname(file), File.readlink(file)))
137
176
  end
138
177
 
139
178
  def submodule_for(file)
140
- repo.submodules.each do |submodule|
141
- if file.start_with?(File.join(repo.workdir, submodule.path))
142
- return submodule
143
- end
144
- end
145
- false
179
+ repo.submodules.find { |s| file.start_with? repo_path(s.path) }
146
180
  end
147
181
 
148
182
  def symlink_templates
149
- templates = []
150
- if FileTest.exists?(templates_dir)
151
- Find.find(templates_dir) do |file|
152
- if FileTest.symlink?(file)
153
- templates.push(symlink_target(file))
154
- end
155
- end
156
- end
157
- templates
183
+ return [] unless File.exist?(templates_dir)
184
+ Find.find(templates_dir)
185
+ .select { |f| File.symlink?(f) }.map { |f| symlink_target(f) }
186
+ end
187
+
188
+ def offline?
189
+ @offline
190
+ end
191
+
192
+ def latest?
193
+ @spec_version == "latest"
158
194
  end
159
195
  end
160
196
  end
@@ -1,10 +1,32 @@
1
1
  module Bosh::Workspace
2
2
  module Schemas
3
3
  class Credentials < Membrane::Schemas::Base
4
+ include Bosh::Workspace::GitProtocolHelper
5
+
4
6
  def validate(object)
5
7
  Membrane::SchemaParser.parse do
6
8
  [enum(UsernamePassword.new, SshKey.new)]
7
9
  end.validate object
10
+ validate_protocol_credentials_combination(object)
11
+ end
12
+
13
+ def validate_protocol_credentials_combination(object)
14
+ object.each do |creds|
15
+ case git_protocol_from_url(creds['url'])
16
+ when :https, :http
17
+ next if creds.keys.include? 'username'
18
+ validation_err "Provide username/password for: #{creds['url']}"
19
+ when :ssh
20
+ next if creds.keys.include? 'private_key'
21
+ validation_err "Provide private_key for: #{creds['url']}"
22
+ else
23
+ validation_err "Credentials not supported for: #{creds['url']}"
24
+ end
25
+ end
26
+ end
27
+
28
+ def validation_err(message)
29
+ raise Membrane::SchemaValidationError.new(message)
8
30
  end
9
31
 
10
32
  class UsernamePassword < Membrane::Schemas::Base
@@ -12,10 +12,23 @@ module Bosh::Workspace
12
12
  "releases" => Releases.new,
13
13
  "stemcells" => Stemcells.new,
14
14
  "templates" => [String],
15
- "meta" => Hash
15
+ "meta" => Hash,
16
+ optional("merge_tool") => MergeTool.new
16
17
  }
17
18
  end.validate object
18
19
  end
20
+
21
+ class MergeTool < Membrane::Schemas::Base
22
+ def validate(object)
23
+ return if object.is_a? String
24
+ return if object.is_a? Hash &&
25
+ (%w(name version) & object.keys).size == 2 &&
26
+ object['version'] =~ /^\d+(\.\d+){1,2}|current$/
27
+ raise Membrane::SchemaValidationError.new(
28
+ "Should match: String, object.name and object.version. Given: #{object}")
29
+ end
30
+ end
31
+
19
32
  end
20
33
  end
21
34
  end
@@ -1,5 +1,5 @@
1
1
  module Bosh
2
2
  module Manifests
3
- VERSION = "0.9.2"
3
+ VERSION = "0.9.3"
4
4
  end
5
5
  end
File without changes
@@ -3,3 +3,4 @@ name: foo
3
3
  templates:
4
4
  - foo/bar.yml
5
5
  meta: {}
6
+ stub_value: <%= p('stub.value') %>
@@ -0,0 +1,4 @@
1
+ ---
2
+ properties:
3
+ stub:
4
+ value: value
@@ -3,10 +3,11 @@ require "bosh/cli/commands/prepare"
3
3
  describe Bosh::Cli::Command::Prepare do
4
4
  describe "#prepare" do
5
5
  let(:command) { Bosh::Cli::Command::Prepare.new }
6
+ let(:url) { nil }
6
7
  let(:release) do
7
8
  instance_double("Bosh::Workspace::Release",
8
9
  name: "foo", version: "1", repo_dir: ".releases/foo", git_url: "/.git",
9
- release_dir: '.releases/foo/sub', name_version: "foo/1", url: nil,
10
+ release_dir: '.releases/foo/sub', name_version: "foo/1", url: url,
10
11
  manifest_file: "releases/foo-1.yml")
11
12
  end
12
13
  let(:stemcell) do
@@ -30,18 +31,24 @@ describe Bosh::Cli::Command::Prepare do
30
31
  let(:stemcells) { [] }
31
32
  let(:ref) { nil }
32
33
 
34
+ context "when only performing local operations" do
35
+ before { command.add_option(:local, true) }
36
+ let(:releases) { [] }
37
+
38
+ it "enables offline mode" do
39
+ expect(command).to receive(:offline!)
40
+ command.prepare
41
+ end
42
+ end
43
+
33
44
  context "release with git " do
34
45
  before do
35
- allow(release).to receive(:required_submodules).and_return(subrepos)
36
-
37
46
  expect(release).to receive(:update_repo)
38
- expect(release).to_not receive(:update_submodule)
39
47
  expect(release).to receive(:ref).and_return(ref)
40
48
  expect(command).to receive(:release_uploaded?)
41
- .with(release.name, release.version).and_return(release_uploaded)
42
- expect(command).to receive(:fetch_or_clone_repo)
43
- .with(release.repo_dir, release.git_url)
49
+ .with(release.name, release.version).and_return(release_uploaded)
44
50
  end
51
+
45
52
  context "release uploaded" do
46
53
  let(:release_uploaded) { true }
47
54
 
@@ -64,14 +71,25 @@ describe Bosh::Cli::Command::Prepare do
64
71
  end
65
72
  end
66
73
  end
74
+
75
+ context "release not uploaded with url" do
76
+ let(:release_uploaded) { false }
77
+ let(:url) { "bosh.io/foo/bar.tgz" }
78
+
79
+ it "does uploads a remote release" do
80
+ expect(command).to receive(:release_upload_from_url)
81
+ .with(release.url)
82
+ command.prepare
83
+ end
84
+ end
67
85
  end
68
86
 
69
87
  context "if the release git_url is not given" do
70
88
  let(:release_uploaded) { false }
71
89
  let(:release) do
72
90
  instance_double("Bosh::Workspace::Release",
73
- name: "foo", version: "1", repo_dir: ".releases/foo", git_url: nil,
74
- name_version: "foo/1", manifest_file: "releases/foo-1.yml")
91
+ name: "foo", version: "1", repo_dir: ".releases/foo", git_url: nil,
92
+ name_version: "foo/1", manifest_file: "releases/foo-1.yml")
75
93
  end
76
94
 
77
95
  it "notifies the user that the git property must be specified" do
@@ -110,21 +128,30 @@ describe Bosh::Cli::Command::Prepare do
110
128
  context "stemcell downloaded" do
111
129
  let(:stemcell_downloaded) { true }
112
130
 
113
- it "does not upload the stemcell" do
131
+ it "uploads the already downloaded stemcell" do
114
132
  expect(command).to_not receive(:stemcell_download)
115
133
  expect(command).to receive(:stemcell_upload).with(stemcell.file)
116
134
  command.prepare
117
135
  end
118
136
  end
119
137
 
120
- context "stemcell downloaded" do
138
+ context "when stemcell not downloaded" do
121
139
  let(:stemcell_downloaded) { false }
122
140
 
123
- it "does not upload the stemcell" do
141
+ it "downloads and uploads the stemcell" do
124
142
  expect(command).to receive(:stemcell_download).with(stemcell.file_name)
125
143
  expect(command).to receive(:stemcell_upload).with(stemcell.file)
126
144
  command.prepare
127
145
  end
146
+
147
+ context "while being offline" do
148
+ before { command.offline! }
149
+
150
+ it "raises an error" do
151
+ expect(command).to_not receive(:stemcell_download)
152
+ expect{command.prepare}.to raise_error /not available offline/
153
+ end
154
+ end
128
155
  end
129
156
  end
130
157
  end