license-acceptance 0.0.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +97 -0
  4. data/config/product_info.toml +1 -0
  5. data/lib/license_acceptance/acceptor.rb +117 -0
  6. data/lib/license_acceptance/arg_acceptance.rb +33 -0
  7. data/lib/license_acceptance/cli_flags/mixlib_cli.rb +22 -0
  8. data/lib/license_acceptance/cli_flags/thor.rb +21 -0
  9. data/lib/license_acceptance/config.rb +65 -0
  10. data/lib/license_acceptance/env_acceptance.rb +19 -0
  11. data/lib/license_acceptance/file_acceptance.rb +97 -0
  12. data/lib/license_acceptance/logger.rb +19 -0
  13. data/lib/license_acceptance/product.rb +23 -0
  14. data/lib/license_acceptance/product_reader.rb +108 -0
  15. data/lib/license_acceptance/product_relationship.rb +13 -0
  16. data/lib/license_acceptance/prompt_acceptance.rb +104 -0
  17. data/lib/license_acceptance/version.rb +3 -0
  18. data/spec/license_acceptance/acceptor_spec.rb +222 -0
  19. data/spec/license_acceptance/arg_acceptance_spec.rb +37 -0
  20. data/spec/license_acceptance/cli_flags/mixlib_cli_spec.rb +14 -0
  21. data/spec/license_acceptance/cli_flags/thor_spec.rb +14 -0
  22. data/spec/license_acceptance/config_spec.rb +113 -0
  23. data/spec/license_acceptance/env_acceptance_spec.rb +43 -0
  24. data/spec/license_acceptance/file_acceptance_spec.rb +121 -0
  25. data/spec/license_acceptance/product_reader_spec.rb +139 -0
  26. data/spec/license_acceptance/product_spec.rb +13 -0
  27. data/spec/license_acceptance/prompt_acceptance_spec.rb +100 -0
  28. data/spec/spec_helper.rb +25 -0
  29. metadata +184 -22
  30. data/.gitignore +0 -11
  31. data/.rspec +0 -3
  32. data/.travis.yml +0 -7
  33. data/LICENSE +0 -1
  34. data/README.md +0 -35
  35. data/lib/license/acceptance/version.rb +0 -5
  36. data/lib/license/acceptance.rb +0 -8
  37. data/license-acceptance.gemspec +0 -42
@@ -0,0 +1,19 @@
1
+ require "license_acceptance/config"
2
+
3
+ module LicenseAcceptance
4
+ module Logger
5
+
6
+ def self.initialize(logger)
7
+ @logger ||= logger
8
+ end
9
+
10
+ def self.logger
11
+ @logger
12
+ end
13
+
14
+ def logger
15
+ Logger.logger
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ module LicenseAcceptance
2
+ class Product
3
+
4
+ attr_reader :name, :pretty_name, :filename
5
+
6
+ def initialize(name, pretty_name, filename)
7
+ @name = name
8
+ @pretty_name = pretty_name
9
+ @filename = filename
10
+ end
11
+
12
+ def ==(other)
13
+ return false if other.class != Product
14
+ if other.name == name &&
15
+ other.pretty_name == pretty_name &&
16
+ other.filename == filename
17
+ return true
18
+ end
19
+ return false
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,108 @@
1
+ require "tomlrb"
2
+ require "license_acceptance/logger"
3
+ require "license_acceptance/product"
4
+ require "license_acceptance/product_relationship"
5
+
6
+ module LicenseAcceptance
7
+ class ProductReader
8
+ include Logger
9
+
10
+ attr_accessor :products, :relationships
11
+
12
+ def read
13
+ logger.debug("Reading products and relationships...")
14
+ location = get_location
15
+ self.products = {}
16
+ self.relationships = {}
17
+
18
+ toml = Tomlrb.load_file(location, symbolize_keys: false)
19
+ raise InvalidProductInfo.new(location) if toml.empty? || toml["products"].nil? || toml["relationships"].nil?
20
+
21
+ for product in toml["products"]
22
+ products[product["name"]] = Product.new(product["name"], product["pretty_name"], product["filename"])
23
+ end
24
+
25
+ for parent_name, children in toml["relationships"]
26
+ parent = products[parent_name]
27
+ raise UnknownParent.new(parent_name) if parent.nil?
28
+ # Its fine to not have a relationship entry, but not fine to have
29
+ # a relationship where the children are nil or empty.
30
+ if children.nil? || children.empty? || !children.is_a?(Array)
31
+ raise NoChildRelationships.new(parent)
32
+ end
33
+ children.map! do |child_name|
34
+ child = products[child_name]
35
+ raise UnknownChild.new(child_name) if child.nil?
36
+ child
37
+ end
38
+ relationships[parent] = children
39
+ end
40
+
41
+ logger.debug("Successfully read products and relationships")
42
+ end
43
+
44
+ def get_location
45
+ # For local development point this to the product_info.toml at the root of the repo.
46
+ # When bundled as a gem we will use the the relative path to find the file in the
47
+ # gem package.
48
+ if ENV["CHEF_LICENSE_PRODUCT_INFO"]
49
+ return ENV["CHEF_LICENSE_PRODUCT_INFO"]
50
+ end
51
+ File.absolute_path(File.join(__FILE__, "../../../config/product_info.toml"))
52
+ end
53
+
54
+ def lookup(parent_name, parent_version)
55
+ parent_product = products.fetch(parent_name) do
56
+ raise UnknownProduct.new(parent_name)
57
+ end
58
+ children = relationships.fetch(parent_product, [])
59
+ if !parent_version.is_a? String
60
+ raise ProductVersionTypeError.new(parent_version)
61
+ end
62
+ ProductRelationship.new(parent_product, children, parent_version)
63
+ end
64
+
65
+ end
66
+
67
+ class UnknownProduct < RuntimeError
68
+ def initialize(product)
69
+ msg = "Unknown product '#{product}' - this represents a developer error"
70
+ super(msg)
71
+ end
72
+ end
73
+
74
+ class InvalidProductInfo < RuntimeError
75
+ def initialize(path)
76
+ msg = "Product info at path #{path} is invalid. Must list Products and relationships."
77
+ super(msg)
78
+ end
79
+ end
80
+
81
+ class UnknownParent < RuntimeError
82
+ def initialize(product)
83
+ msg = "Could not find product #{product} from relationship parents"
84
+ super(msg)
85
+ end
86
+ end
87
+
88
+ class UnknownChild < RuntimeError
89
+ def initialize(product)
90
+ msg = "Could not find product #{product} from relationship children"
91
+ super(msg)
92
+ end
93
+ end
94
+
95
+ class NoChildRelationships < RuntimeError
96
+ def initialize(product)
97
+ msg = "No child relationships for #{product.name}, should be removed from product info or fixed"
98
+ super(msg)
99
+ end
100
+ end
101
+
102
+ class ProductVersionTypeError < RuntimeError
103
+ def initialize(product_version)
104
+ msg = "Product versions must be specified as a string, provided type is '#{product_version.class}'"
105
+ super(msg)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,13 @@
1
+ module LicenseAcceptance
2
+ class ProductRelationship
3
+
4
+ attr_reader :parent, :children, :parent_version
5
+
6
+ def initialize(parent, children, parent_version)
7
+ @parent = parent
8
+ @children = children
9
+ @parent_version = parent_version
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,104 @@
1
+ require 'tty-prompt'
2
+ require 'pastel'
3
+ require "license_acceptance/logger"
4
+ require "timeout"
5
+
6
+ module LicenseAcceptance
7
+ class PromptAcceptance
8
+ include Logger
9
+
10
+ attr_reader :output
11
+
12
+ def initialize(config)
13
+ @output = config.output
14
+ end
15
+
16
+ WIDTH = 50.freeze
17
+ PASTEL = Pastel.new
18
+ BORDER = "+---------------------------------------------+".freeze
19
+ YES = PASTEL.green.bold("yes")
20
+ CHECK = PASTEL.green("✔")
21
+
22
+ def request(missing_licenses, &persist_callback)
23
+ logger.debug("Requesting a license for #{missing_licenses.map(&:name)}")
24
+ c = missing_licenses.size
25
+ s = c > 1 ? "s": ""
26
+
27
+ acceptance_question = "Do you accept the #{c} product license#{s} (#{YES}/no)?"
28
+ output.puts <<~EOM
29
+ #{BORDER}
30
+ Chef License Acceptance
31
+
32
+ Before you can continue, #{c} product license#{s}
33
+ must be accepted. View the license at
34
+ https://www.chef.io/end-user-license-agreement/
35
+
36
+ License#{s} that need accepting:
37
+ * #{missing_licenses.map(&:pretty_name).join("\n * ")}
38
+
39
+ #{acceptance_question}
40
+
41
+ EOM
42
+
43
+ if ask(output, c, s, persist_callback)
44
+ output.puts BORDER
45
+ return true
46
+ end
47
+
48
+ output.puts <<~EOM
49
+
50
+ If you do not accept this license you will
51
+ not be able to use Chef products.
52
+
53
+ #{acceptance_question}
54
+
55
+ EOM
56
+
57
+ answer = ask(output, c, s, persist_callback)
58
+ if answer != "yes"
59
+ output.puts BORDER
60
+ end
61
+ return answer
62
+ end
63
+
64
+ private
65
+
66
+ def ask(output, c, s, persist_callback)
67
+ logger.debug("Attempting to request interactive prompt on TTY")
68
+ prompt = TTY::Prompt.new(track_history: false, active_color: :bold, interrupt: :exit, output: output)
69
+
70
+ answer = "no"
71
+ Timeout::timeout(60, PromptTimeout) do
72
+ answer = prompt.ask(">") do |q|
73
+ q.modify :down, :trim
74
+ q.required true
75
+ q.messages[:required?] = "You must enter 'yes' or 'no'"
76
+ q.validate /^\s*(yes|no)\s*$/i
77
+ q.messages[:valid?] = "You must enter 'yes' or 'no'"
78
+ end
79
+ rescue PromptTimeout
80
+ prompt.unsubscribe(prompt.reader)
81
+ output.puts "Prompt timed out. Use non-interactive flags or enter an answer within 60 seconds."
82
+ end
83
+
84
+ if answer == "yes"
85
+ output.puts
86
+ output.puts "Persisting #{c} product license#{s}..."
87
+ errs = persist_callback.call
88
+ if errs.empty?
89
+ output.puts "#{CHECK} #{c} product license#{s} persisted.\n\n"
90
+ else
91
+ output.puts <<~EOM
92
+ #{CHECK} #{c} product license#{s} accepted.
93
+ Could not persist acceptance:\n\t* #{errs.map(&:message).join("\n\t* ")}
94
+ EOM
95
+ end
96
+ return true
97
+ end
98
+ return false
99
+ end
100
+
101
+ end
102
+
103
+ class PromptTimeout < StandardError; end
104
+ end
@@ -0,0 +1,3 @@
1
+ module LicenseAcceptance
2
+ VERSION = "0.2.1"
3
+ end
@@ -0,0 +1,222 @@
1
+ require "spec_helper"
2
+ require "license_acceptance/acceptor"
3
+
4
+ RSpec.describe LicenseAcceptance::Acceptor do
5
+ it "has a version number" do
6
+ expect(LicenseAcceptance::VERSION).not_to be nil
7
+ end
8
+
9
+ let(:output) do
10
+ d = StringIO.new
11
+ allow(d).to receive(:isatty).and_return(true)
12
+ d
13
+ end
14
+ let(:opts) { { output: output } }
15
+ let(:acc) { LicenseAcceptance::Acceptor.new(opts) }
16
+ let(:product) { "chef_client" }
17
+ let(:version) { "version" }
18
+ let(:relationship) { instance_double(LicenseAcceptance::ProductRelationship) }
19
+ let(:p1) { instance_double(LicenseAcceptance::Product) }
20
+ let(:missing) { [p1] }
21
+
22
+ describe "#check_and_persist!" do
23
+ let(:err) { LicenseAcceptance::LicenseNotAcceptedError.new([product]) }
24
+ it "outputs an error message to stdout and exits when license acceptance is declined" do
25
+ expect(acc).to receive(:check_and_persist).and_raise(err)
26
+ expect { acc.check_and_persist!(product, version) }.to raise_error(SystemExit)
27
+ expect(output.string).to match(/#{product}/)
28
+ end
29
+ end
30
+
31
+ describe "#check_and_persist" do
32
+ let(:reader) { instance_double(LicenseAcceptance::ProductReader) }
33
+ let(:file_acc) { instance_double(LicenseAcceptance::FileAcceptance) }
34
+ let(:arg_acc) { instance_double(LicenseAcceptance::ArgAcceptance) }
35
+ let(:prompt_acc) { instance_double(LicenseAcceptance::PromptAcceptance) }
36
+ let(:env_acc) { instance_double(LicenseAcceptance::EnvAcceptance) }
37
+
38
+ before do
39
+ expect(LicenseAcceptance::ProductReader).to receive(:new).and_return(reader)
40
+ expect(LicenseAcceptance::FileAcceptance).to receive(:new).and_return(file_acc)
41
+ expect(LicenseAcceptance::ArgAcceptance).to receive(:new).and_return(arg_acc)
42
+ expect(LicenseAcceptance::PromptAcceptance).to receive(:new).and_return(prompt_acc)
43
+ expect(LicenseAcceptance::EnvAcceptance).to receive(:new).and_return(env_acc)
44
+ end
45
+
46
+ describe "when check-no-persist environment variable is set" do
47
+ it "returns true" do
48
+ expect(env_acc).to receive(:check_no_persist).and_return(true)
49
+ expect(acc.check_and_persist(product, version)).to eq(true)
50
+ end
51
+ end
52
+
53
+ describe "when check-no-persist command line argument is set" do
54
+ it "returns true" do
55
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
56
+ expect(arg_acc).to receive(:check_no_persist).and_return(true)
57
+ expect(acc.check_and_persist(product, version)).to eq(true)
58
+ end
59
+ end
60
+
61
+ describe "when there are no missing licenses" do
62
+ it "returns true" do
63
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
64
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
65
+ expect(reader).to receive(:read)
66
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
67
+ expect(file_acc).to receive(:check).with(relationship).and_return([])
68
+ expect(acc.check_and_persist(product, version)).to eq(true)
69
+ end
70
+ end
71
+
72
+ describe "when the user accepts as an environment variable" do
73
+ it "returns true" do
74
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
75
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
76
+ expect(reader).to receive(:read)
77
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
78
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
79
+ expect(env_acc).to receive(:check).with(ENV).and_return(true)
80
+ expect(file_acc).to receive(:persist).with(relationship, missing).and_return([])
81
+ expect(acc.check_and_persist(product, version)).to eq(true)
82
+ expect(output.string).to match(/1 product license accepted./)
83
+ end
84
+
85
+ describe "when persist is set to false" do
86
+ let(:opts) { { output: output, persist: false } }
87
+
88
+ it "returns true" do
89
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
90
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
91
+ expect(reader).to receive(:read)
92
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
93
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
94
+ expect(env_acc).to receive(:check).with(ENV).and_return(true)
95
+ expect(acc.check_and_persist(product, version)).to eq(true)
96
+ expect(output.string).to_not match(/accepted./)
97
+ end
98
+ end
99
+
100
+ describe "when file persistance fails" do
101
+ it "returns true" do
102
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
103
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
104
+ expect(reader).to receive(:read)
105
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
106
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
107
+ expect(env_acc).to receive(:check).with(ENV).and_return(true)
108
+ expect(file_acc).to receive(:persist).with(relationship, missing).and_return([StandardError.new("foo")])
109
+ expect(acc.check_and_persist(product, version)).to eq(true)
110
+ expect(output.string).to match(/Could not persist acceptance:/)
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "when the user accepts as an arg" do
116
+ it "returns true" do
117
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
118
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
119
+ expect(reader).to receive(:read)
120
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
121
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
122
+ expect(env_acc).to receive(:check).and_return(false)
123
+ expect(arg_acc).to receive(:check).with(ARGV).and_return(true)
124
+ expect(file_acc).to receive(:persist).with(relationship, missing).and_return([])
125
+ expect(acc.check_and_persist(product, version)).to eq(true)
126
+ expect(output.string).to match(/1 product license accepted./)
127
+ end
128
+
129
+ describe "when persist is set to false" do
130
+ let(:opts) { { output: output, persist: false } }
131
+
132
+ it "returns true" do
133
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
134
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
135
+ expect(reader).to receive(:read)
136
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
137
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
138
+ expect(env_acc).to receive(:check).and_return(false)
139
+ expect(arg_acc).to receive(:check).with(ARGV).and_return(true)
140
+ expect(acc.check_and_persist(product, version)).to eq(true)
141
+ expect(output.string).to_not match(/accepted./)
142
+ end
143
+ end
144
+
145
+ describe "when file persistance fails" do
146
+ it "returns true" do
147
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
148
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
149
+ expect(reader).to receive(:read)
150
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
151
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
152
+ expect(env_acc).to receive(:check).and_return(false)
153
+ expect(arg_acc).to receive(:check).with(ARGV).and_return(true)
154
+ expect(file_acc).to receive(:persist).with(relationship, missing).and_return([StandardError.new("bar")])
155
+ expect(acc.check_and_persist(product, version)).to eq(true)
156
+ expect(output.string).to match(/Could not persist acceptance:/)
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "when the prompt is not a tty" do
162
+ let(:opts) { { output: File.open(File::NULL, "w") } }
163
+ it "raises a LicenseNotAcceptedError error" do
164
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
165
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
166
+ expect(reader).to receive(:read)
167
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
168
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
169
+ expect(env_acc).to receive(:check).and_return(false)
170
+ expect(arg_acc).to receive(:check).and_return(false)
171
+ expect(prompt_acc).to_not receive(:request)
172
+ expect { acc.check_and_persist(product, version) }.to raise_error(LicenseAcceptance::LicenseNotAcceptedError)
173
+ end
174
+ end
175
+
176
+ describe "when the user accepts with the prompt" do
177
+ it "returns true" do
178
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
179
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
180
+ expect(reader).to receive(:read)
181
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
182
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
183
+ expect(env_acc).to receive(:check).and_return(false)
184
+ expect(arg_acc).to receive(:check).and_return(false)
185
+ expect(prompt_acc).to receive(:request).with(missing).and_yield.and_return(true)
186
+ expect(file_acc).to receive(:persist).with(relationship, missing)
187
+ expect(acc.check_and_persist(product, version)).to eq(true)
188
+ end
189
+
190
+ describe "when persist is set to false" do
191
+ let(:opts) { { output: output, persist: false } }
192
+
193
+ it "returns true" do
194
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
195
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
196
+ expect(reader).to receive(:read)
197
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
198
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
199
+ expect(env_acc).to receive(:check).and_return(false)
200
+ expect(arg_acc).to receive(:check).and_return(false)
201
+ expect(prompt_acc).to receive(:request).with(missing).and_yield.and_return(true)
202
+ expect(acc.check_and_persist(product, version)).to eq(true)
203
+ end
204
+ end
205
+ end
206
+
207
+ describe "when the user declines with the prompt" do
208
+ it "raises a LicenseNotAcceptedError error" do
209
+ expect(env_acc).to receive(:check_no_persist).and_return(false)
210
+ expect(arg_acc).to receive(:check_no_persist).and_return(false)
211
+ expect(reader).to receive(:read)
212
+ expect(reader).to receive(:lookup).with(product, version).and_return(relationship)
213
+ expect(file_acc).to receive(:check).with(relationship).and_return(missing)
214
+ expect(env_acc).to receive(:check).and_return(false)
215
+ expect(arg_acc).to receive(:check).and_return(false)
216
+ expect(prompt_acc).to receive(:request).with(missing).and_return(false)
217
+ expect { acc.check_and_persist(product, version) }.to raise_error(LicenseAcceptance::LicenseNotAcceptedError)
218
+ end
219
+ end
220
+
221
+ end
222
+ end