bosh-workspace 0.8.5 → 0.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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