puppet-resource_api 0.1.0

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.
data/NOTICE ADDED
@@ -0,0 +1,17 @@
1
+ Puppet Resource API
2
+
3
+ Copyright (C) 2017 Puppet, Inc.
4
+
5
+ Puppet Labs can be contacted at: info@puppetlabs.com
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Puppet::ResourceApi [![Build Status](https://travis-ci.org/puppetlabs/puppet-resource_api.svg?branch=master)](https://travis-ci.org/puppetlabs/puppet-resource_api) [![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/qvor6rkh0d1e4suc?svg=true)](https://ci.appveyor.com/project/puppetlabs/puppet-resource-api) [![Coverage Status](https://coveralls.io/repos/github/puppetlabs/puppet-resource_api/badge.svg?branch=master)](https://coveralls.io/github/puppetlabs/puppet-resource_api?branch=master) [![codecov](https://codecov.io/gh/puppetlabs/puppet-resource_api/branch/master/graph/badge.svg)](https://codecov.io/gh/puppetlabs/puppet-resource_api)
2
+
3
+
4
+ This is an implementation of the [Resource API](https://github.com/DavidS/puppet-specifications/blob/resourceapi/language/resource-api/README.md) proposal. Find a working example of a new-style provider in the [experimental puppetlabs-apt branch](https://github.com/DavidS/puppetlabs-apt/blob/resource-api-experiments/lib/puppet/provider/apt_key2/apt_key2.rb). There is also the corresponding [type](https://github.com/DavidS/puppetlabs-apt/blob/resource-api-experiments/lib/puppet/type/apt_key2.rb), and [new unit tests](https://github.com/DavidS/puppetlabs-apt/blob/resource-api-experiments/spec/unit/puppet/provider/apt_key2/apt_key2_spec.rb) for 100% coverage.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'puppet-resource_api'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install puppet-resource_api
21
+
22
+ ## Usage
23
+
24
+ The [Resource API](https://github.com/DavidS/puppet-specifications/blob/resourceapi/language/resource-api/README.md) explains the usage and capabilities of this gem.
25
+
26
+ Already working:
27
+ * basic type and provider definition, using `name`, `desc`, and `attributes`
28
+ * the `canonicalize` and `remote_resource` features
29
+ * all the logging facilities
30
+ * executing the new provider under any of the following commands:
31
+ * `puppet apply`
32
+ * `puppet resource`
33
+ * `puppet agent`
34
+ * `puppet device` (if applicable)
35
+
36
+
37
+ There are still a few notable gaps between the implementation, and the specification:
38
+ * Only a single runtime environment (the puppet commands) is currently implemented.
39
+ * `auto*` definitions
40
+ * the Commands API is mostly implemented, but deployment is blocked on upstream work (PDK-580). You can use regular Ruby `system()` calls as a workaround, with all their attendant encoding, and safety issues.
41
+
42
+ ## Development
43
+
44
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
45
+
46
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
47
+
48
+ ## Contributing
49
+
50
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/puppet-resource_api.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require 'rubocop/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ RuboCop::RakeTask.new(:rubocop) do |t|
8
+ t.options = ['--display-cop-names']
9
+ end
10
+
11
+ namespace :spec do
12
+ desc 'Run RSpec code examples with coverage collection'
13
+ task :coverage do
14
+ ENV['COVERAGE'] = 'yes'
15
+ Rake::Task['spec'].execute
16
+ end
17
+ end
18
+
19
+ desc 'Check for unapproved licenses in dependencies'
20
+ task(:license_finder) do
21
+ system('license_finder --decisions-file=.dependency_decisions.yml') || raise(StandardError, 'Unapproved license(s) found on dependencies')
22
+ end
23
+
24
+ task :default => :spec
data/appveyor.yml ADDED
@@ -0,0 +1,39 @@
1
+ # version: 1.0.{build}-{branch}
2
+
3
+ environment:
4
+ matrix:
5
+ - RUBY_VERSION: 24-x64
6
+ - RUBY_VERSION: 21-x64
7
+
8
+ install:
9
+ - set PATH=C:\Ruby%RUBY_VERSION%\bin;C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin;%PATH%
10
+ - SET LOG_SPEC_ORDER=true
11
+ - SET COVERAGE=yes
12
+ # Due to a bug in the version of OpenSSL shipped with Ruby 2.4.1 on Windows
13
+ # (https://bugs.ruby-lang.org/issues/11033). Errors are ignored because the
14
+ # mingw gem calls out to pacman to install OpenSSL which is already
15
+ # installed, causing gem to raise a warning that powershell determines to be
16
+ # a fatal error.
17
+ - ps: |
18
+ $ErrorActionPreference = "SilentlyContinue"
19
+ if($env:RUBY_VERSION -eq "24-x64") {
20
+ gem install openssl "~> 2.0.4" --no-rdoc --no-ri -- --with-openssl-dir=C:\msys64\mingw64
21
+ }
22
+ $host.SetShouldExit(0)
23
+ - bundle install --jobs 4 --retry 2 --without development
24
+
25
+ build: off
26
+
27
+ before_test:
28
+ - ruby -v
29
+ - gem -v
30
+ - bundle -v
31
+ - type Gemfile.lock
32
+
33
+ test_script:
34
+ - bundle exec rake
35
+
36
+ # Uncomment this block to enable RDP access to the AppVeyor test instance for
37
+ # debugging purposes.
38
+ #on_finish:
39
+ # - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "puppet/resource_api"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/ext/mkrf_conf.rb ADDED
@@ -0,0 +1,20 @@
1
+ # Based on the example from https://en.wikibooks.org/wiki/Ruby_Programming/RubyGems#How_to_install_different_versions_of_gems_depending_on_which_version_of_ruby_the_installee_is_using
2
+ require 'rubygems'
3
+ require 'rubygems/command.rb'
4
+ require 'rubygems/dependency_installer.rb'
5
+ begin
6
+ Gem::Command.build_args = ARGV
7
+ rescue NoMethodError # rubocop:disable Lint/HandleExceptions
8
+ end
9
+ inst = Gem::DependencyInstaller.new
10
+ begin
11
+ if RbConfig::CONFIG['host_os'] =~ %r{mswin|msys|mingw32}i
12
+ inst.install 'childprocess', '~> 0.7'
13
+ end
14
+ rescue # rubocop:disable Lint/RescueWithoutErrorClass
15
+ exit(1)
16
+ end
17
+
18
+ f = File.open(File.join(File.dirname(__FILE__), 'Rakefile'), 'w') # create dummy rakefile to indicate success
19
+ f.write("task :default\n")
20
+ f.close
@@ -0,0 +1,236 @@
1
+ require 'pathname'
2
+ # require 'puppet/resource_api/command'
3
+ require 'puppet/resource_api/errors'
4
+ require 'puppet/resource_api/glue'
5
+ require 'puppet/resource_api/puppet_context'
6
+ require 'puppet/resource_api/version'
7
+ require 'puppet/type'
8
+
9
+ module Puppet::ResourceApi
10
+ def register_type(definition)
11
+ raise Puppet::DevError, 'requires a Hash as definition, not %{other_type}' % { other_type: definition.class } unless definition.is_a? Hash
12
+ raise Puppet::DevError, 'requires a name' unless definition.key? :name
13
+ raise Puppet::DevError, 'requires attributes' unless definition.key? :attributes
14
+
15
+ # prepare the ruby module for the provider
16
+ # this has to happen before Puppet::Type.newtype starts autoloading providers
17
+ # it also needs to be guarded against the namespace already being defined by something
18
+ # else to avoid ruby warnings
19
+ unless Puppet::Provider.const_defined?(class_name_from_type_name(definition[:name]))
20
+ Puppet::Provider.const_set(class_name_from_type_name(definition[:name]), Module.new)
21
+ end
22
+
23
+ Puppet::Type.newtype(definition[:name].to_sym) do
24
+ @docs = definition[:docs]
25
+ has_namevar = false
26
+ namevar_name = nil
27
+
28
+ # Keeps a copy of the provider around. Weird naming to avoid clashes with puppet's own `provider` member
29
+ define_singleton_method(:my_provider) do
30
+ @my_provider ||= Puppet::ResourceApi.load_provider(definition[:name]).new
31
+ end
32
+
33
+ # make the provider available in the instance's namespace
34
+ def my_provider
35
+ self.class.my_provider
36
+ end
37
+
38
+ if definition[:features] && definition[:features].include?('remote_resource')
39
+ apply_to_device
40
+ end
41
+
42
+ define_method(:initialize) do |attributes|
43
+ # $stderr.puts "A: #{attributes.inspect}"
44
+ attributes = attributes.to_hash if attributes.is_a? Puppet::Resource
45
+ # $stderr.puts "B: #{attributes.inspect}"
46
+ if definition.key?(:features) && definition[:features].include?('canonicalize')
47
+ attributes = my_provider.canonicalize(context, [attributes])[0]
48
+ end
49
+ # $stderr.puts "C: #{attributes.inspect}"
50
+ super(attributes)
51
+ end
52
+
53
+ definition[:attributes].each do |name, options|
54
+ # puts "#{name}: #{options.inspect}"
55
+
56
+ # TODO: using newparam everywhere would suppress change reporting
57
+ # that would allow more fine-grained reporting through context,
58
+ # but require more invest in hooking up the infrastructure to emulate existing data
59
+ param_or_property = if [:read_only, :parameter, :namevar].include? options[:behaviour]
60
+ :newparam
61
+ else
62
+ :newproperty
63
+ end
64
+ send(param_or_property, name.to_sym) do
65
+ unless options[:type]
66
+ raise Puppet::DevError, "#{definition[:name]}.#{name} has no type"
67
+ end
68
+
69
+ if options[:desc]
70
+ desc "#{options[:desc]} (a #{options[:type]})"
71
+ else
72
+ warn("#{definition[:name]}.#{name} has no docs")
73
+ end
74
+
75
+ if options[:behaviour] == :namevar
76
+ # puts 'setting namevar'
77
+ # raise Puppet::DevError, "namevar must be called 'name', not '#{name}'" if name.to_s != 'name'
78
+ isnamevar
79
+ has_namevar = true
80
+ namevar_name = name
81
+ end
82
+
83
+ # read-only values do not need type checking, but can have default values
84
+ if options[:behaviour] != :read_only
85
+ # TODO: this should use Pops infrastructure to avoid hardcoding stuff, and enhance type fidelity
86
+ # validate do |v|
87
+ # type = Puppet::Pops::Types::TypeParser.singleton.parse(options[:type]).normalize
88
+ # if type.instance?(v)
89
+ # return true
90
+ # else
91
+ # inferred_type = Puppet::Pops::Types::TypeCalculator.infer_set(value)
92
+ # error_msg = Puppet::Pops::Types::TypeMismatchDescriber.new.describe_mismatch("#{DEFINITION[:name]}.#{name}", type, inferred_type)
93
+ # raise Puppet::ResourceError, error_msg
94
+ # end
95
+ # end
96
+
97
+ if options.key? :default
98
+ defaultto options[:default]
99
+ end
100
+
101
+ case options[:type]
102
+ when 'String'
103
+ # require any string value
104
+ newvalues %r{} do
105
+ end
106
+ # rubocop:disable Lint/BooleanSymbol
107
+ when 'Boolean'
108
+ newvalues 'true', 'false', :true, :false, true, false
109
+
110
+ munge do |v|
111
+ case v
112
+ when 'true', :true
113
+ true
114
+ when 'false', :false
115
+ false
116
+ else
117
+ v
118
+ end
119
+ end
120
+ # rubocop:enable Lint/BooleanSymbol
121
+ when 'Integer'
122
+ newvalue %r{^-?\d+$} do
123
+ end
124
+ munge do |v|
125
+ Puppet::Pops::Utils.to_n(v)
126
+ end
127
+ when 'Float', 'Numeric'
128
+ newvalue Puppet::Pops::Patterns::NUMERIC do
129
+ end
130
+ munge do |v|
131
+ Puppet::Pops::Utils.to_n(v)
132
+ end
133
+ when 'Enum[present, absent]'
134
+ newvalue :absent do
135
+ end
136
+ newvalue :present do
137
+ end
138
+ when 'Variant[Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{16}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{40}\Z/]]'
139
+ # the namevar needs to be a Parameter, which only has newvalue*s*
140
+ newvalues(%r{\A(0x)?[0-9a-fA-F]{8}\Z}, %r{\A(0x)?[0-9a-fA-F]{16}\Z}, %r{\A(0x)?[0-9a-fA-F]{40}\Z})
141
+ when 'Optional[String]'
142
+ newvalues(%r{}, :undef) do
143
+ end
144
+ when 'Variant[Stdlib::Absolutepath, Pattern[/\A(https?|ftp):\/\//]]'
145
+ # TODO: this is wrong, but matches original implementation
146
+ [/^\//, /\A(https?|ftp):\/\//].each do |v| # rubocop:disable Style/RegexpLiteral
147
+ newvalues v do
148
+ end
149
+ end
150
+ when 'Pattern[/\A((hkp|http|https):\/\/)?([a-z\d])([a-z\d-]{0,61}\.)+[a-z\d]+(:\d{2,5})?$/]'
151
+ newvalues(/\A((hkp|http|https):\/\/)?([a-z\d])([a-z\d-]{0,61}\.)+[a-z\d]+(:\d{2,5})?$/) # rubocop:disable Style/RegexpLiteral
152
+ else
153
+ raise Puppet::DevError, "Datatype #{options[:type]} is not yet supported in this prototype"
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ define_singleton_method(:instances) do
160
+ # puts 'instances'
161
+ # force autoloading of the provider
162
+ provider(name)
163
+ my_provider.get(context).map do |resource_hash|
164
+ Puppet::ResourceApi::TypeShim.new(resource_hash[namevar_name], resource_hash)
165
+ end
166
+ end
167
+
168
+ define_method(:retrieve) do
169
+ # puts "retrieve(#{title.inspect})"
170
+ result = Puppet::Resource.new(self.class, title)
171
+ current_state = my_provider.get(context).find { |h| h[namevar_name] == title }
172
+
173
+ # require 'pry'; binding.pry
174
+
175
+ if current_state
176
+ current_state.each do |k, v|
177
+ result[k] = v
178
+ end
179
+ else
180
+ result[:name] = title
181
+ result[:ensure] = :absent
182
+ end
183
+
184
+ # puts "retrieved #{current_state.inspect}"
185
+
186
+ @rapi_current_state = current_state
187
+ result
188
+ end
189
+
190
+ define_method(:flush) do
191
+ # puts 'flush'
192
+ # require'pry';binding.pry
193
+ target_state = Hash[@parameters.map { |k, v| [k, v.value] }]
194
+ # remove puppet's injected metaparams
195
+ target_state.delete(:loglevel)
196
+ target_state = my_provider.canonicalize(context, [target_state]).first if definition.key?(:features) && definition[:features].include?('canonicalize')
197
+
198
+ retrieve unless @rapi_current_state
199
+
200
+ # require 'pry'; binding.pry
201
+ return if @rapi_current_state == target_state
202
+
203
+ # puts "@rapi_current_state: #{@rapi_current_state.inspect}"
204
+ # puts "target_state: #{target_state.inspect}"
205
+
206
+ my_provider.set(context, title => { is: @rapi_current_state, should: target_state })
207
+ raise 'Execution encountered an error' if context.failed?
208
+ end
209
+
210
+ define_singleton_method(:context) do
211
+ @context ||= PuppetContext.new(definition[:name])
212
+ end
213
+
214
+ def context
215
+ self.class.context
216
+ end
217
+ end
218
+ end
219
+ module_function :register_type
220
+
221
+ def load_provider(type_name)
222
+ class_name = class_name_from_type_name(type_name)
223
+ type_name_sym = type_name.to_sym
224
+
225
+ # loads the "puppet/provider/#{type_name}/#{type_name}" file through puppet
226
+ Puppet::Type.type(type_name_sym).provider(type_name_sym)
227
+ Puppet::Provider.const_get(class_name).const_get(class_name)
228
+ rescue NameError
229
+ raise Puppet::DevError, "class #{class_name} not found in puppet/provider/#{type_name}/#{type_name}"
230
+ end
231
+ module_function :load_provider
232
+
233
+ def self.class_name_from_type_name(type_name)
234
+ type_name.to_s.split('_').map(&:capitalize).join
235
+ end
236
+ end
@@ -0,0 +1,138 @@
1
+ require 'puppet/util'
2
+ require 'puppet/util/network_device'
3
+
4
+ class Puppet::ResourceApi::BaseContext
5
+ def initialize(typename)
6
+ @typename = typename
7
+ end
8
+
9
+ def device
10
+ # TODO: evaluate facter_url setting for loading config if there is no `current` NetworkDevice
11
+ raise 'no device configured' unless Puppet::Util::NetworkDevice.current
12
+ Puppet::Util::NetworkDevice.current
13
+ end
14
+
15
+ def failed?
16
+ @failed
17
+ end
18
+
19
+ [:debug, :info, :notice, :warning, :err].each do |level|
20
+ define_method(level) do |*args|
21
+ if args.length == 1
22
+ message = "#{@context || @typename}: #{args.last}"
23
+ elsif args.length == 2
24
+ resources = format_titles(args.first)
25
+ message = "#{resources}: #{args.last}"
26
+ else
27
+ message = args.map(&:to_s).join(', ')
28
+ end
29
+ send_log(level, message)
30
+ end
31
+ end
32
+
33
+ [:creating, :updating, :deleting].each do |method|
34
+ define_method(method) do |titles, message: method.to_s.capitalize, &block|
35
+ start_time = Time.now
36
+ setup_context(titles, message)
37
+ begin
38
+ debug('Start')
39
+ block.call
40
+ notice("Finished in #{format_seconds(Time.now - start_time)} seconds")
41
+ rescue StandardError => e
42
+ err("Failed after #{format_seconds(Time.now - start_time)} seconds: #{e}")
43
+ @failed = true
44
+ ensure
45
+ @context = nil
46
+ end
47
+ end
48
+ end
49
+
50
+ def failing(titles, message: 'Failing')
51
+ start_time = Time.now
52
+ setup_context(titles, message)
53
+ begin
54
+ debug('Start')
55
+ yield
56
+ warning("Finished failing in #{format_seconds(Time.now - start_time)} seconds")
57
+ rescue StandardError => e
58
+ err("Failed after #{format_seconds(Time.now - start_time)} seconds: #{e}")
59
+ @failed = true
60
+ ensure
61
+ @context = nil
62
+ end
63
+ end
64
+
65
+ def processing(title, is, should, message: 'Processing')
66
+ raise "#{__method__} only accepts a single resource title" if title.respond_to?(:each)
67
+ start_time = Time.now
68
+ setup_context(title, message)
69
+ begin
70
+ debug("Starting processing of #{title} from #{is} to #{should}")
71
+ yield
72
+ notice("Finished processing #{title} in #{format_seconds(Time.now - start_time)} seconds: #{should}")
73
+ rescue StandardError => e
74
+ err("Failed processing #{title} after #{format_seconds(Time.now - start_time)} seconds: #{e}")
75
+ @failed = true
76
+ ensure
77
+ @context = nil
78
+ end
79
+ end
80
+
81
+ [:created, :updated, :deleted].each do |method|
82
+ define_method(method) do |titles, message: method.to_s.capitalize|
83
+ notice("#{message}: #{titles}")
84
+ end
85
+ end
86
+
87
+ def processed(title, is, should)
88
+ raise "#{__method__} only accepts a single resource title" if title.respond_to?(:each)
89
+ notice("Processed #{title} from #{is} to #{should}")
90
+ end
91
+
92
+ def attribute_changed(title, attribute, is, should, message: nil)
93
+ raise "#{__method__} only accepts a single resource title" if title.respond_to?(:each)
94
+ printable_is = 'nil'
95
+ printable_should = 'nil'
96
+ if is
97
+ printable_is = is.is_a?(Numeric) ? is : "'#{is}'"
98
+ end
99
+ if should
100
+ printable_should = should.is_a?(Numeric) ? should : "'#{should}'"
101
+ end
102
+ notice("#{title}: attribute '#{attribute}' changed from #{printable_is} to #{printable_should}#{message ? ": #{message}" : ''}")
103
+ end
104
+
105
+ def failed(titles, message: 'Updating has failed')
106
+ setup_context(titles)
107
+ begin
108
+ err(message)
109
+ # raise message
110
+ ensure
111
+ @context = nil
112
+ end
113
+ end
114
+
115
+ def send_log(_level, _message)
116
+ raise 'Received send_log() on an unprepared BaseContext. Use IOContext, or PuppetContext instead.'
117
+ end
118
+
119
+ private
120
+
121
+ def format_titles(titles)
122
+ if titles.length.zero? && !titles.is_a?(String)
123
+ @typename
124
+ else
125
+ "#{@typename}[#{[titles].flatten.compact.join(', ')}]"
126
+ end
127
+ end
128
+
129
+ def setup_context(titles, message = nil)
130
+ @context = format_titles(titles)
131
+ @context += ": #{message}" if message
132
+ end
133
+
134
+ def format_seconds(seconds)
135
+ return '%.6f' % seconds if seconds < 1
136
+ '%.2f' % seconds
137
+ end
138
+ end