license-acceptance 0.0.1 → 0.2.1

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