bosh-workspace 0.9.2 → 0.9.3

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