bosh-workspace 0.8.5 → 0.9.0.rc1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -2
  4. data/.ruby-gemset +1 -1
  5. data/.travis.yml +2 -0
  6. data/Guardfile +2 -2
  7. data/README.md +4 -4
  8. data/bosh-workspace.gemspec +2 -1
  9. data/lib/bosh/cli/commands/deployment_patch.rb +96 -0
  10. data/lib/bosh/cli/commands/prepare.rb +6 -4
  11. data/lib/bosh/workspace/credentials.rb +30 -0
  12. data/lib/bosh/workspace/deployment_patch.rb +90 -0
  13. data/lib/bosh/workspace/helpers/git_credentials_helper.rb +95 -0
  14. data/lib/bosh/workspace/helpers/spiff_helper.rb +1 -0
  15. data/lib/bosh/workspace/project_deployment.rb +2 -44
  16. data/lib/bosh/workspace/release.rb +20 -23
  17. data/lib/bosh/workspace/schemas/credentials.rb +27 -0
  18. data/lib/bosh/workspace/schemas/deployment_patch.rb +15 -0
  19. data/lib/bosh/workspace/schemas/project_deployment.rb +21 -0
  20. data/lib/bosh/workspace/schemas/releases.rb +16 -0
  21. data/lib/bosh/workspace/schemas/stemcells.rb +25 -0
  22. data/lib/bosh/workspace/shell.rb +67 -0
  23. data/lib/bosh/workspace/tasks/bosh_command_runner.rb +29 -0
  24. data/lib/bosh/workspace/tasks/deployment.rb +63 -0
  25. data/lib/bosh/workspace/tasks/workspace.rake +69 -0
  26. data/lib/bosh/workspace/tasks.rb +15 -0
  27. data/lib/bosh/workspace/version.rb +1 -1
  28. data/lib/bosh/workspace.rb +14 -0
  29. data/spec/assets/foo-boshrelease-repo-new-structure.zip +0 -0
  30. data/spec/assets/foo-boshrelease-repo-updated.zip +0 -0
  31. data/spec/assets/foo-boshrelease-repo.zip +0 -0
  32. data/spec/assets/foo-boshworkspace.zip +0 -0
  33. data/spec/commands/deployment_patch_spec.rb +152 -0
  34. data/spec/commands/prepare_spec.rb +5 -3
  35. data/spec/credentials_spec.rb +46 -0
  36. data/spec/deployment_patch_spec.rb +171 -0
  37. data/spec/helpers/git_credentials_helper_spec.rb +160 -0
  38. data/spec/helpers/spiff_helper_spec.rb +16 -3
  39. data/spec/project_deployment_spec.rb +52 -163
  40. data/spec/release_spec.rb +208 -80
  41. data/spec/schemas/credentials_spec.rb +28 -0
  42. data/spec/schemas/deployment_patch_spec.rb +30 -0
  43. data/spec/schemas/project_deployment_spec.rb +45 -0
  44. data/spec/schemas/releases_spec.rb +31 -0
  45. data/spec/schemas/stemcells_spec.rb +37 -0
  46. data/spec/shell_spec.rb +70 -0
  47. data/spec/spec_helper.rb +11 -5
  48. data/spec/support/shared_contexts/rake.rb +37 -0
  49. data/spec/tasks/bosh_command_runner_spec.rb +39 -0
  50. data/spec/tasks/deployment_spec.rb +80 -0
  51. data/spec/tasks/workspace_task_spec.rb +99 -0
  52. metadata +69 -7
@@ -0,0 +1,25 @@
1
+ module Bosh::Workspace
2
+ module Schemas
3
+ class Stemcells < Membrane::Schemas::Base
4
+ def validate(object)
5
+ Membrane::SchemaParser.parse do
6
+ [{
7
+ "name" => String,
8
+ "version" => StemcellVersion.new
9
+ }]
10
+ end.validate object
11
+ end
12
+ end
13
+
14
+ class StemcellVersion < Membrane::Schemas::Base
15
+ def validate(object)
16
+ return if object.is_a? Integer
17
+ return if object.is_a? Float
18
+ return if object == "latest"
19
+ return if object.to_s =~ /^\d+\.\d+$/
20
+ raise Membrane::SchemaValidationError.new(
21
+ "Should match: latest, version.patch or version. Given: #{object}")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ module Bosh
2
+ module Workspace
3
+ class Shell
4
+ def initialize(stdout = $stdout)
5
+ @stdout = stdout
6
+ end
7
+
8
+ def run(command, options = {})
9
+ output_lines = run_command(command, options)
10
+ output_lines = tail(output_lines, options)
11
+
12
+ command_output = output_lines.join("\n")
13
+ report(command, command_output, options)
14
+ command_output
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :stdout
20
+
21
+ def run_command(command, options)
22
+ stdout.puts command if options[:output_command]
23
+ lines = []
24
+
25
+ if options[:env]
26
+ # Wrap in a shell because existing api to Shell#run takes a string
27
+ # which makes it really hard to pass it to popen with custom environment.
28
+ popen_args = [options[:env], ENV['SHELL'] || 'bash', '-c', command]
29
+ else
30
+ popen_args = command
31
+ end
32
+
33
+ IO.popen(popen_args) do |io|
34
+ io.each do |line|
35
+ stdout.puts line.chomp
36
+ stdout.flush
37
+ lines << line.chomp
38
+ end
39
+ end
40
+
41
+ lines
42
+ end
43
+
44
+ def tail(lines, options)
45
+ line_number = options[:last_number]
46
+ line_number ? lines.last(line_number) : lines
47
+ end
48
+
49
+ def report(cmd, command_output, options)
50
+ return if command_exited_successfully?
51
+
52
+ err_msg = "Failed: '#{cmd}' from #{pwd}, with exit status #{$?.to_i}\n\n #{command_output}"
53
+ options[:ignore_failures] ? stdout.puts("#{err_msg}, continuing anyway") : raise(err_msg)
54
+ end
55
+
56
+ def command_exited_successfully?
57
+ $?.success?
58
+ end
59
+
60
+ def pwd
61
+ Dir.pwd
62
+ rescue Errno::ENOENT
63
+ 'a deleted directory'
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,29 @@
1
+ module Bosh::Workspace::Tasks
2
+ class BoshCommandRunner
3
+ attr_reader :target, :username, :password
4
+ attr_accessor :deployment_file
5
+
6
+ def initialize(target, username, password)
7
+ @target = target
8
+ @username = username
9
+ @password = password
10
+ @shell = Bosh::Workspace::Shell.new
11
+ end
12
+
13
+ def run(command, options = {})
14
+ options.merge! default_options
15
+ args = ['-n', '-t', target]
16
+ args.concat ['-d', deployment_file] if deployment_file
17
+ @shell.run "bundle exec bosh #{args.join(' ')} #{command}", options
18
+ end
19
+
20
+ private
21
+
22
+ def default_options
23
+ {
24
+ output_command: true,
25
+ env: { "BOSH_USER" => username, "BOSH_PASSWORD" => password }
26
+ }
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,63 @@
1
+ module Bosh::Workspace::Tasks
2
+ class Deployment
3
+ def initialize(deployment)
4
+ schema.validate deployment
5
+ @raw = OpenStruct.new(deployment)
6
+ end
7
+
8
+ def name
9
+ file = File.join 'deployments', file_name
10
+ YAML.load_file(file)["name"]
11
+ end
12
+
13
+ def target
14
+ return @raw.target unless @raw.target =~ /@/
15
+ @raw.target.split('@')[1]
16
+ end
17
+
18
+ def username
19
+ return "admin" unless @raw.target =~ /@/
20
+ @raw.target.match(/^([^@:]+)/)[1] || "admin"
21
+ end
22
+
23
+ def password
24
+ return "admin" unless @raw.target =~ /@/
25
+ match = @raw.target.match(/^[^:@]+:([^@]+)/)
26
+ match && match[1] || "admin"
27
+ end
28
+
29
+ def merged_file
30
+ File.join ".deployments", file_name
31
+ end
32
+
33
+ def file_name
34
+ @raw.name + ".yml"
35
+ end
36
+
37
+ def errands
38
+ @raw.errands
39
+ end
40
+
41
+ def apply_patch
42
+ @raw.apply_patch
43
+ end
44
+
45
+ def create_patch
46
+ @raw.create_patch
47
+ end
48
+
49
+ private
50
+
51
+ def schema
52
+ Membrane::SchemaParser.parse do
53
+ {
54
+ "name" => /^((?!\.yml).)*$/, # Should not contain .yml
55
+ "target" => String,
56
+ optional("apply_patch") => String,
57
+ optional("create_patch") => String,
58
+ optional("errands") => [String]
59
+ }
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,69 @@
1
+ namespace :workspace do
2
+ include Bosh::Workspace::Tasks
3
+
4
+ desc "Apply or create patches as defined in deployments.yml"
5
+ task :patch do
6
+ with_deployments do |deployment|
7
+ if apply_patch_path = deployment.apply_patch
8
+ bosh "apply deployment patch #{apply_patch_path}"
9
+ end
10
+
11
+ if create_patch_path = deployment.create_patch
12
+ bosh "create deployment patch #{create_patch_path}"
13
+ end
14
+ end
15
+ end
16
+
17
+ desc "Deploy deployments as defined in deployments.yml"
18
+ task :deploy do
19
+ with_deployments do
20
+ bosh "prepare deployment"
21
+ bosh_deploy
22
+ end
23
+ end
24
+
25
+ desc "Verifies deployments by running errands specified in deployments.yml"
26
+ task :run_errands do
27
+ with_deployments do |deployment|
28
+ deployment.errands.each do |errand|
29
+ bosh "run errand #{errand}"
30
+ end if deployment.errands
31
+ end
32
+ end
33
+
34
+ desc "Cleans up by deleting all deployments specified in deployments.yml"
35
+ task :clean do
36
+ unless ENV["DESTROY_DEPLOYMENTS"]
37
+ raise "Set DESTROY_DEPLOYMENTS to confirm deployment destruction"
38
+ end
39
+
40
+ with_deployments(set_deployment: false) do |deployment|
41
+ bosh "delete deployment #{deployment.name} --force", ignore_failures: true
42
+ end
43
+ end
44
+
45
+ def with_deployments(options = {})
46
+ deployments.each do |d|
47
+ @cli = BoshCommandRunner.new(d.target, d.username, d.password)
48
+ unless options[:set_deployment] == false
49
+ @cli.deployment_file = d.merged_file
50
+ end
51
+ yield d
52
+ end
53
+ end
54
+
55
+ def deployments
56
+ @deployments ||= begin
57
+ YAML.load_file("deployments.yml").map { |d| Deployment.new(d) }
58
+ end
59
+ end
60
+
61
+ def bosh_deploy
62
+ out = bosh("deploy", last_number: 1)
63
+ exit 1 if out =~ /error/
64
+ end
65
+
66
+ def bosh(command, options = {})
67
+ @cli.run command, options
68
+ end
69
+ end
@@ -0,0 +1,15 @@
1
+ require "yaml"
2
+ require "membrane"
3
+
4
+ module Bosh
5
+ module Workspace
6
+ module Tasks; end
7
+ end
8
+ end
9
+
10
+ require "bosh/workspace/shell"
11
+ require "bosh/workspace/tasks/bosh_command_runner.rb"
12
+ require "bosh/workspace/tasks/deployment.rb"
13
+
14
+ rake_paths = File.expand_path('tasks/**/*.rake', File.dirname(__FILE__))
15
+ Dir.glob(rake_paths).each { |r| import r } if defined? import
@@ -1,5 +1,5 @@
1
1
  module Bosh
2
2
  module Manifests
3
- VERSION = "0.8.5"
3
+ VERSION = "0.9.0.rc1"
4
4
  end
5
5
  end
@@ -1,17 +1,31 @@
1
1
  module Bosh; module Workspace; end; end
2
2
 
3
+ require "membrane"
4
+ require "shellwords"
5
+ require "rugged"
6
+ require "hashdiff"
3
7
  require "cli/core_ext"
4
8
  require "cli/validation"
5
9
 
6
10
  require "bosh/workspace/helpers/spiff_helper"
7
11
  require "bosh/workspace/helpers/project_deployment_helper"
12
+ require "bosh/workspace/helpers/git_credentials_helper"
8
13
  require "bosh/workspace/helpers/release_helper"
9
14
  require "bosh/workspace/helpers/stemcell_helper"
10
15
  require "bosh/workspace/helpers/dns_helper"
11
16
 
17
+ require "bosh/workspace/schemas/project_deployment"
18
+ require "bosh/workspace/schemas/deployment_patch"
19
+ require "bosh/workspace/schemas/releases"
20
+ require "bosh/workspace/schemas/stemcells"
21
+ require "bosh/workspace/schemas/credentials"
22
+
23
+ require "bosh/workspace/shell"
12
24
  require "bosh/workspace/manifest_builder"
13
25
  require "bosh/workspace/release"
14
26
  require "bosh/workspace/stemcell"
15
27
  require "bosh/workspace/project_deployment"
16
28
  require "bosh/workspace/stub_file"
29
+ require "bosh/workspace/deployment_patch"
30
+ require "bosh/workspace/credentials"
17
31
  require "bosh/workspace/version"
Binary file
Binary file
@@ -0,0 +1,152 @@
1
+ require "bosh/cli/commands/deployment_patch"
2
+
3
+ module Bosh::Cli::Command
4
+ include Bosh::Workspace
5
+
6
+ describe DeploymentPatch do
7
+ let(:command) { DeploymentPatch.new }
8
+ let(:patch) do
9
+ instance_double 'Bosh::Workspace::DeploymentPatch', templates_ref: ref
10
+ end
11
+ let(:current_patch) { instance_double 'Bosh::Workspace::DeploymentPatch' }
12
+ let(:deployment_file) { 'deployments/foo.yml' }
13
+ let(:patch_file) { 'patch.yml' }
14
+ let(:project_dir) { File.realpath Dir.mktmpdir }
15
+ let(:changes?) { nil }
16
+ let(:valid?) { true }
17
+ let(:ref) { "baz" }
18
+ let(:changes) do
19
+ { stemcells: "foo", releases: "bar", templates_ref: ref }
20
+ end
21
+
22
+ before do
23
+ Dir.chdir project_dir
24
+ allow(Bosh::Workspace::DeploymentPatch).to receive(:create)
25
+ .with(deployment_file, /templates/).and_return(current_patch)
26
+ allow(Bosh::Workspace::DeploymentPatch).to receive(:from_file)
27
+ .with(patch_file).and_return(patch)
28
+ allow(current_patch).to receive(:changes?).with(patch)
29
+ .and_return(changes?)
30
+ expect(command).to receive(:require_project_deployment)
31
+ allow(command).to receive_message_chain("project_deployment.file")
32
+ .and_return(deployment_file)
33
+ end
34
+
35
+ describe '.create' do
36
+ it 'writes to file' do
37
+ expect(current_patch).to receive(:to_file).with(patch_file)
38
+ expect(command).to receive(:say).with /wrote patch/i
39
+ command.create(patch_file)
40
+ end
41
+ end
42
+
43
+ describe '.apply' do
44
+ let(:patch_valid?) { true }
45
+
46
+ before do
47
+ expect(patch).to receive(:valid?).and_return(patch_valid?)
48
+ end
49
+
50
+ context 'with non valid patch' do
51
+ let(:patch_valid?) { false }
52
+
53
+ it "raises an error" do
54
+ expect(patch).to receive(:errors).and_return(['foo', 'bar'])
55
+ expect(command).to receive(:say).with(/validation errors/i)
56
+ expect(command).to receive(:say).with(/foo/)
57
+ expect(command).to receive(:say).with(/bar/)
58
+ expect { command.apply(patch_file) }.to raise_error(/is not valid/)
59
+ end
60
+ end
61
+
62
+ context 'with changes' do
63
+ let(:changes?) { true }
64
+ let(:index) do
65
+ instance_double('Rugged::Index',
66
+ read_tree: true, write_tree: true, add_all: true)
67
+ end
68
+ let(:repo) { instance_double 'Rugged::Repository', index: index }
69
+
70
+ def expect_patch_changes_table
71
+ expect(command).to receive(:say) do |s|
72
+ subject = s.to_s.delete ' '
73
+ expect(subject).to include "stemcells|foo"
74
+ expect(subject).to include "releases|bar"
75
+ end
76
+ end
77
+
78
+ before do
79
+ allow(repo).to receive_message_chain('head.target.tree')
80
+ expect(current_patch).to receive(:changes).with(patch)
81
+ .and_return(changes)
82
+ end
83
+
84
+ context 'no dry-run' do
85
+ before do
86
+ allow(command).to receive(:fetch_repo).with(/templates/)
87
+ expect(patch).to receive(:apply).with(deployment_file, /templates/)
88
+ expect(command).to receive(:say).with /successfully applied/i
89
+ expect_patch_changes_table
90
+ end
91
+
92
+ context 'without no-commit' do
93
+ before do
94
+ expect(Rugged::Repository).to receive(:new)
95
+ .with(project_dir).and_return(repo)
96
+ end
97
+
98
+ def expect_commit(message)
99
+ expect(Rugged::Commit).to receive(:create) do |repo, options|
100
+ expect(options[:message]).to match message
101
+ end
102
+ end
103
+
104
+ it 'applies changes, shows changes and commits' do
105
+ expect_commit "Applied stemcells foo," \
106
+ " releases bar, templates_ref baz"
107
+ command.apply(patch_file)
108
+ end
109
+
110
+ context 'without templates_ref' do
111
+ let(:ref) { nil }
112
+ let(:changes) do
113
+ { stemcells: "foo", releases: "bar" }
114
+ end
115
+
116
+ it 'applies changes, shows changes and commits' do
117
+ expect_commit("Applied stemcells foo, releases bar")
118
+ expect(command).to_not receive(:fetch_repo)
119
+ command.apply(patch_file)
120
+ end
121
+ end
122
+ end
123
+
124
+ context 'no-commit' do
125
+ it 'applies changes and shows changes' do
126
+ command.add_option(:no_commit, true)
127
+ command.apply(patch_file)
128
+ end
129
+ end
130
+ end
131
+
132
+ context 'dry-run' do
133
+ it 'only shows changes' do
134
+ expect(command).to receive(:say).with /deployment patch/i
135
+ expect_patch_changes_table
136
+ command.add_option(:dry_run, true)
137
+ command.apply(patch_file)
138
+ end
139
+ end
140
+ end
141
+
142
+ context 'without changes' do
143
+ let(:changes?) { false }
144
+
145
+ it 'says no changes' do
146
+ expect(command).to receive(:say).with /no changes/i
147
+ command.apply(patch_file)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -5,7 +5,7 @@ describe Bosh::Cli::Command::Prepare do
5
5
  let(:command) { Bosh::Cli::Command::Prepare.new }
6
6
  let(:release) do
7
7
  instance_double("Bosh::Workspace::Release",
8
- name: "foo", version: "1", repo_dir: ".releases/foo",
8
+ name: "foo", version: "1", repo_dir: ".releases/foo", git_url: "/.git",
9
9
  name_version: "foo/1", manifest_file: "releases/foo-1.yml")
10
10
  end
11
11
  let(:stemcell) do
@@ -24,7 +24,7 @@ describe Bosh::Cli::Command::Prepare do
24
24
  end
25
25
 
26
26
  describe "prepare_release(s/_repos)" do
27
- let(:releases) { [ release ] }
27
+ let(:releases) { [release] }
28
28
  let(:stemcells) { [] }
29
29
  let(:ref) { nil }
30
30
 
@@ -33,6 +33,8 @@ describe Bosh::Cli::Command::Prepare do
33
33
  expect(release).to receive(:ref).and_return(ref)
34
34
  expect(command).to receive(:release_uploaded?)
35
35
  .with(release.name, release.version).and_return(release_uploaded)
36
+ expect(command).to receive(:fetch_or_clone_repo)
37
+ .with(release.repo_dir, release.git_url)
36
38
  end
37
39
 
38
40
  context "release uploaded" do
@@ -80,7 +82,7 @@ describe Bosh::Cli::Command::Prepare do
80
82
  context "stemcell not uploaded" do
81
83
  let(:stemcell_uploaded) { false }
82
84
 
83
- before do
85
+ before do
84
86
  allow(stemcell).to receive(:downloaded?)
85
87
  .and_return(stemcell_downloaded)
86
88
  end
@@ -0,0 +1,46 @@
1
+ module Bosh::Workspace
2
+ describe Credentials do
3
+ let(:credentials) do
4
+ [{ "url" => "foo", "private_key" => "foobarkey" }]
5
+ end
6
+
7
+ before do
8
+ expect(YAML).to receive(:load_file).with(:file).and_return(credentials)
9
+ end
10
+
11
+ subject do
12
+ Credentials.new(:file)
13
+ end
14
+
15
+ describe '#find_by_url' do
16
+ it "returns credentials when found multiple times" do
17
+ expect(subject.find_by_url("foo")).to eq({ private_key: "foobarkey" })
18
+ expect(subject.find_by_url("foo")).to eq({ private_key: "foobarkey" })
19
+ end
20
+
21
+ it "returns nil when not found" do
22
+ expect(subject.find_by_url("bar")).to be nil
23
+ end
24
+ end
25
+
26
+ describe '#perform_validation' do
27
+ context "valid" do
28
+ it "validates" do
29
+ allow_any_instance_of(Schemas::Credentials)
30
+ .to receive(:validate).with(credentials)
31
+ expect(subject).to be_valid
32
+ end
33
+ end
34
+
35
+ context "invalid" do
36
+ it "has errors" do
37
+ allow_any_instance_of(Schemas::Credentials)
38
+ .to receive(:validate).with(credentials)
39
+ .and_raise(Membrane::SchemaValidationError.new("foo"))
40
+ expect(subject).to_not be_valid
41
+ expect(subject.errors).to include "foo"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end