From a25e14ddfa34825a8c36e63d7c45ce51336132ba Mon Sep 17 00:00:00 2001 From: Anders Larsson Date: Wed, 20 Mar 2024 15:05:37 +0100 Subject: [PATCH] Add parameter api_config for API configuration --- REFERENCE.md | 62 ++++++++++++++++----- lib/puppet/functions/vas/api_fetch.rb | 78 +++++++++++++++------------ manifests/init.pp | 27 ++++++++-- spec/classes/data_types_spec.rb | 21 ++++---- spec/classes/init_spec.rb | 32 ++++++----- spec/classes/parameter_spec.rb | 16 +++--- spec/functions/api_fetch_spec.rb | 54 ++++++++----------- types/api/config.pp | 8 +++ 8 files changed, 181 insertions(+), 117 deletions(-) create mode 100644 types/api/config.pp diff --git a/REFERENCE.md b/REFERENCE.md index a7ec0f1..6e310d7 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -12,6 +12,10 @@ * [`vas::api_fetch`](#vas--api_fetch): Query a remote HTTP-based service for entries to be added to users_allow. +### Data types + +* [`Vas::API::Config`](#Vas--API--Config): API configuration + ## Classes ### `vas` @@ -167,6 +171,7 @@ The following parameters are available in the `vas` class: * [`api_users_allow_url`](#-vas--api_users_allow_url) * [`api_token`](#-vas--api_token) * [`api_ssl_verify`](#-vas--api_ssl_verify) +* [`api_config`](#-vas--api_config) ##### `manage_nis` @@ -1186,6 +1191,7 @@ Default value: `false` Data type: `Optional[Stdlib::HTTPSUrl]` The URL towards the API. +Deprecated parameter, replaced by $api_config. Will be removed next major releaase. Default value: `undef` @@ -1194,6 +1200,7 @@ Default value: `undef` Data type: `Optional[String[1]]` Security token for authenticated access to the API. +Deprecated parameter, replaced by $api_config. Will be removed next major releaase. Default value: `undef` @@ -1202,9 +1209,18 @@ Default value: `undef` Data type: `Boolean` Whether TLS connections should be verified or not. +Deprecated parameter, replaced by $api_config. Will be removed next major releaase Default value: `false` +##### `api_config` + +Data type: `Optional[Vas::API::Config]` + +API configuration + +Default value: `undef` + ## Functions ### `vas::api_fetch` @@ -1218,10 +1234,19 @@ Query a remote HTTP-based service for entries to be added to users_allow. ##### Calling the function ```puppet -vas::api_fetch("https://host.domain.tld/api/${facts['trusted.certname']}") +vas::api_fetch([{'url' => "https://host.domain.tld/api/${facts['trusted.certname']}"}]) +``` + +##### Multiple servers with different tokens, ssl_verify enabled + +```puppet +vas::api_fetch([ + {'url' => "https://host1.domain.tld/api/${facts['trusted.certname']}", 'token' => 'token123', 'ssl_verify' => true}, + {'url' => "https://host2.domain.tld/api/${facts['trusted.certname']}", 'token' => 'token321', 'ssl_verify' => true}, +]) ``` -#### `vas::api_fetch(Stdlib::HTTPUrl $url, String[1] $token, Optional[Boolean] $ssl_verify)` +#### `vas::api_fetch(Vas::API::Config $config)` Query a remote HTTP-based service for entries to be added to users_allow. @@ -1232,24 +1257,37 @@ Returns: `Hash` Key 'content' with [Array] if API responds. Key 'errors' with [A ###### Calling the function ```puppet -vas::api_fetch("https://host.domain.tld/api/${facts['trusted.certname']}") +vas::api_fetch([{'url' => "https://host.domain.tld/api/${facts['trusted.certname']}"}]) ``` -##### `url` +###### Multiple servers with different tokens, ssl_verify enabled -Data type: `Stdlib::HTTPUrl` +```puppet +vas::api_fetch([ + {'url' => "https://host1.domain.tld/api/${facts['trusted.certname']}", 'token' => 'token123', 'ssl_verify' => true}, + {'url' => "https://host2.domain.tld/api/${facts['trusted.certname']}", 'token' => 'token321', 'ssl_verify' => true}, +]) +``` -URL to connect to +##### `config` -##### `token` +Data type: `Vas::API::Config` -Data type: `String[1]` +Hash with API configuration -Token used for authentication +## Data types -##### `ssl_verify` +### `Vas::API::Config` -Data type: `Optional[Boolean]` +API configuration + +Alias of -Whether TLS connections should be verified or not +```puppet +Array[Struct[ + url => Stdlib::HttpsUrl, + token => Optional[String[1]], + ssl_verify => Optional[Boolean], + ]] +``` diff --git a/lib/puppet/functions/vas/api_fetch.rb b/lib/puppet/functions/vas/api_fetch.rb index d73eb6a..fd9fe6a 100644 --- a/lib/puppet/functions/vas/api_fetch.rb +++ b/lib/puppet/functions/vas/api_fetch.rb @@ -3,52 +3,62 @@ require 'net/http' require 'net/https' require 'openssl' - # @param url URL to connect to - # @param token Token used for authentication - # @param ssl_verify Whether TLS connections should be verified or not + # @param config Hash with API configuration # @return [Hash] Key 'content' with [Array] if API responds. Key 'errors' with [Array[String]] if errors happens. # @example Calling the function - # vas::api_fetch("https://host.domain.tld/api/${facts['trusted.certname']}") + # vas::api_fetch([{'url' => "https://host.domain.tld/api/${facts['trusted.certname']}"}]) + # @example Multiple servers with different tokens, ssl_verify enabled + # vas::api_fetch([ + # {'url' => "https://host1.domain.tld/api/${facts['trusted.certname']}", 'token' => 'token123', 'ssl_verify' => true}, + # {'url' => "https://host2.domain.tld/api/${facts['trusted.certname']}", 'token' => 'token321', 'ssl_verify' => true}, + # ]) + # dispatch :api_fetch do - param 'Stdlib::HTTPUrl', :url - param 'String[1]', :token - optional_param 'Boolean', :ssl_verify + param 'Vas::API::Config', :config return_type 'Hash' end - def api_fetch(url, token, ssl_verify = false) - uri = URI.parse(url) + def api_fetch(config) + data = {} - req = Net::HTTP::Get.new(uri.to_s) - req['Authorization'] = "Bearer #{token}" - req['Accept'] = 'text/plain' + config.shuffle.each do |entry| + url = entry['url'] + uri = URI.parse(url) - https = Net::HTTP.new(uri.host, uri.port) - https.use_ssl = true - unless ssl_verify - https.verify_mode = OpenSSL::SSL::VERIFY_NONE - end - https.open_timeout = 2 - https.read_timeout = 2 + req = Net::HTTP::Get.new(uri.to_s) + req['Authorization'] = "Bearer #{entry['token']}" if entry.key?('token') + req['Accept'] = 'text/plain' - data = {} - begin - response = https.start do |cx| - cx.request(req) + https = Net::HTTP.new(uri.host, uri.port) + https.use_ssl = true + # Set SSL::VERIFY_NONE if key ssl_verify is not present or if set to false + # Should be true by default in next major release + if !entry.key?('ssl_verify') || !entry['ssl_verify'] + https.verify_mode = OpenSSL::SSL::VERIFY_NONE end + https.open_timeout = 2 + https.read_timeout = 2 + + begin + response = https.start do |cx| + cx.request(req) + end - case response - when Net::HTTPSuccess - data['content'] = if response.body.empty? - [] - else - response.body.split("\n") - end - else - (data['errors'] ||= []) << "#{url} returns HTTP code: #{response.code}" + case response + when Net::HTTPSuccess + data['content'] = if response.body.empty? + [] + else + response.body.split("\n") + end + # Successful response received, break loop + break + else + (data['errors'] ||= []) << "#{url} returns HTTP code: #{response.code}" + end + rescue => error + (data['errors'] ||= []) << "#{url} connection failed: #{error.message}" end - rescue => error - (data['errors'] ||= []) << "#{url} connection failed: #{error.message}" end data diff --git a/manifests/init.pp b/manifests/init.pp index 13d63b1..beb472f 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -465,12 +465,18 @@ # # @param api_users_allow_url # The URL towards the API. +# Deprecated parameter, replaced by $api_config. Will be removed next major releaase. # # @param api_token # Security token for authenticated access to the API. +# Deprecated parameter, replaced by $api_config. Will be removed next major releaase. # # @param api_ssl_verify # Whether TLS connections should be verified or not. +# Deprecated parameter, replaced by $api_config. Will be removed next major releaase +# +# @param api_config +# API configuration class vas ( Boolean $manage_nis = true, String[1] $package_version = 'installed', @@ -587,6 +593,7 @@ Array[String[1]] $kpasswd_servers = [], Stdlib::Port $kpasswd_server_port = 464, Boolean $api_enable = false, + Optional[Vas::API::Config] $api_config = undef, Optional[Stdlib::HTTPSUrl] $api_users_allow_url = undef, Optional[String[1]] $api_token = undef, Boolean $api_ssl_verify = false, @@ -673,10 +680,22 @@ } # functionality - if $api_enable == true and ($api_users_allow_url == undef or $api_token == undef) { - fail('vas::api_enable is set to true but required parameters vas::api_users_allow_url and/or vas::api_token missing') - } elsif $api_enable == true { - $api_users_allow_data = vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) + if $api_enable == true { + if $api_config { + $api_config_real = $api_config + } elsif $api_users_allow_url and $api_token { + warning('$api_users_allow_url and $api_token deprecated and will be removed next major release. Use $api_config') + + $api_config_real = [{ + 'url' => $api_users_allow_url, + 'token' => $api_token, + 'ssl_verify' => $api_ssl_verify, + }] + } else { + fail('vas::api_enable is set to true but required parameter $api_config missing') + } + + $api_users_allow_data = vas::api_fetch($api_config_real) if $api_users_allow_data['content'] { $manage_users_allow = true diff --git a/spec/classes/data_types_spec.rb b/spec/classes/data_types_spec.rb index ee4fac6..0e4d647 100644 --- a/spec/classes/data_types_spec.rb +++ b/spec/classes/data_types_spec.rb @@ -13,13 +13,6 @@ ], } - headers = { - 'Accept' => 'text/plain', - 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', - 'Authorization' => 'Bearer somesecret', - 'User-Agent' => 'Ruby' - } - on_supported_os(test_on).each do |_os, os_facts| describe 'variable data type and content validations' do let(:node) { 'data-types.example.com' } @@ -49,7 +42,7 @@ }, 'Boolean/API' => { name: ['api_enable'], - params: { api_users_allow_url: 'https://api.example.local', api_token: 'somesecret', }, + params: { api_config: [{ 'url': 'https://api.example.local', 'token': 'somesecret' }] }, valid: [true, false], invalid: ['true', 'false', ['array'], { 'ha' => 'sh' }, 3, 2.42, nil], message: 'expects a Boolean', @@ -172,7 +165,15 @@ invalid: ['true', 'false', 'string', ['array'], { 'ha' => 'sh' }, 3, 2.42], message: 'expects a value of type Boolean or Enum', }, - + 'Vas::API::Config' => { + name: ['api_config'], + params: { api_enable: true }, + valid: [[{ 'url': 'https://test.ing' }], + [{ 'url': 'https://test.ing', 'token': 'mysecret', 'ssl_verify': true }], + [{ 'url': 'https://test.ing', 'token': 'mysecret', 'ssl_verify': true }, { 'url': 'https://test.ing' }]], + invalid: ['http://str.ing', 'string', ['array'], { 'ha' => 'sh' }, 3, 2.42, false], + message: "(parameter 'api_config' expects a Vas::API::Config|parameter 'api_config' index 0 expects a Struct value)", + }, } validations.sort.each do |type, var| mandatory_params = {} if mandatory_params.nil? @@ -184,10 +185,8 @@ it do stub_request(:get, 'https://test.ing/') - .with(headers: headers) stub_request(:get, 'https://api.example.local') - .with(headers: headers) is_expected.to compile end diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb index 9b52802..f631c42 100644 --- a/spec/classes/init_spec.rb +++ b/spec/classes/init_spec.rb @@ -279,45 +279,47 @@ end it { - is_expected.to compile.and_raise_error(%r{vas::api_enable is set to true but required parameters vas::api_users_allow_url and/or vas::api_token missing}) + is_expected.to compile.and_raise_error(%r{vas::api_enable is set to true but required parameter \$api_config missing}) } context 'param "api_users_allow_url" set' do let(:params) do - api_enabled.merge( + super().merge( 'api_users_allow_url': 'https://host.domain.tld', ) end it { - is_expected.to compile.and_raise_error(%r{vas::api_enable is set to true but required parameters vas::api_users_allow_url and/or vas::api_token missing}) + is_expected.to compile.and_raise_error(%r{vas::api_enable is set to true but required parameter \$api_config missing}) } end context 'param "api_token" set' do let(:params) do - api_enabled.merge( + super().merge( 'api_token': 'mytoken', ) end it { - is_expected.to compile.and_raise_error(%r{vas::api_enable is set to true but required parameters vas::api_users_allow_url and/or vas::api_token missing}) + is_expected.to compile.and_raise_error(%r{vas::api_enable is set to true but required parameter \$api_config missing}) } end context 'with required parameters' do let(:params) do - api_enabled.merge( - 'api_users_allow_url': 'https://host.domain.tld', - 'api_token': 'mytoken', + super().merge( + 'api_config': [{ + 'url': 'https://host.domain.tld', + 'token': 'mytoken', + }], ) end context 'and queries successfully' do context 'with no return entries' do let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_config) { return { content => [] } }' end @@ -333,9 +335,7 @@ context 'and users_allow parameter specified' do let(:params) do - api_enabled.merge( - 'api_users_allow_url': 'https://host.domain.tld', - 'api_token': 'mytoken', + super().merge( 'users_allow_entries': ['user1@example.com', 'user2@example.com'], ) end @@ -355,7 +355,7 @@ context 'with it returning "apiuser@example.com"' do let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_config) { return { content => ["apiuser@example.com"]} }' end @@ -372,9 +372,7 @@ context 'and users_allow parameter specified' do let(:params) do - api_enabled.merge( - 'api_users_allow_url': 'https://host.domain.tld', - 'api_token': 'mytoken', + super().merge( 'users_allow_entries': ['user1@example.com', 'user2@example.com'], ) end @@ -396,7 +394,7 @@ context 'and queries fails' do let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_data) { return { error => ["https://host.domain.tld returns HTTP code: 502"] } }' end diff --git a/spec/classes/parameter_spec.rb b/spec/classes/parameter_spec.rb index 4a0f5b2..fdff0ce 100644 --- a/spec/classes/parameter_spec.rb +++ b/spec/classes/parameter_spec.rb @@ -905,7 +905,7 @@ end it 'fails' do - expect { is_expected.to contain_class('vas') }.to raise_error(Puppet::Error, %r{api_token missing}) + expect { is_expected.to contain_class('vas') }.to raise_error(Puppet::Error, %r{api_config missing}) end end @@ -918,7 +918,7 @@ } end let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_data) { return { content => [] } }' end @@ -937,7 +937,7 @@ } end let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_data) { return { content => ["apiuser@test.ing"] } }' end @@ -957,7 +957,7 @@ } end let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_data) { return { content => [] } }' end @@ -977,7 +977,7 @@ } end let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_data) { return { content => ["apiuser@test.ing"] } }' end @@ -1002,7 +1002,7 @@ end it 'fails' do - expect { is_expected.to contain_class('vas') }.to raise_error(Puppet::Error, %r{api_users_allow_url .* missing}) + expect { is_expected.to contain_class('vas') }.to raise_error(Puppet::Error, %r{api_config missing}) end end @@ -1015,7 +1015,7 @@ } end let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_data) { return { content => [] } }' end @@ -1034,7 +1034,7 @@ } end let(:pre_condition) do - 'function vas::api_fetch($api_users_allow_url, $api_token, $api_ssl_verify) { + 'function vas::api_fetch($api_data) { return { content => "apiuser@test.ing" } }' end diff --git a/spec/functions/api_fetch_spec.rb b/spec/functions/api_fetch_spec.rb index ba2ca44..7ac8b9b 100644 --- a/spec/functions/api_fetch_spec.rb +++ b/spec/functions/api_fetch_spec.rb @@ -12,41 +12,33 @@ } url = 'https://api.example.local/' - - describe 'raises an error when arguments are missing' do + params = [ + { + 'url' => url, + 'token' => 'somesecret', + }, + ] + + describe 'raises an error when parameters are missing' do describe 'no arguments' do it do is_expected.to run .with_params - .and_raise_error(ArgumentError, '\'vas::api_fetch\' expects between 2 and 3 arguments, got none') + .and_raise_error(ArgumentError, '\'vas::api_fetch\' expects 1 argument, got none') end end - describe 'token argument is missing' do + describe 'when required key url is missing in $config' do + params_missing = [{}] + it do is_expected.to run - .with_params(url) - .and_raise_error(ArgumentError, '\'vas::api_fetch\' expects between 2 and 3 arguments, got 1') + .with_params(params_missing) + .and_raise_error(ArgumentError, '\'vas::api_fetch\' parameter \'config\' index 0 expects size to be between 1 and 3, got 0') end end end - describe 'raises an error when url argument is not a string' do - it do - is_expected.to run - .with_params(1, 'somesecret') - .and_raise_error(ArgumentError, %r{'vas::api_fetch' parameter 'url' expects a match for Stdlib::HTTPUrl.* got Integer}) - end - end - - describe 'raises an error when token argument is not a string' do - it do - is_expected.to run - .with_params(url, 1) - .and_raise_error(ArgumentError, '\'vas::api_fetch\' parameter \'token\' expects a String value, got Integer') - end - end - describe 'api call' do it 'when request times out' do stub_request(:get, url).with( @@ -54,11 +46,11 @@ ).to_timeout is_expected.to run - .with_params(url, 'somesecret') + .with_params(params) .and_return({ 'errors' => ['https://api.example.local/ connection failed: execution expired'] }) end - it 'returns an array containing http response code and body' do + it 'returns a hash containing key \'content\' with an array of contents' do response_body = "line1\nline2" stub_request(:get, url).with( @@ -66,11 +58,11 @@ ).to_return(body: response_body, status: 200) is_expected.to run - .with_params(url, 'somesecret') + .with_params(params) .and_return({ 'content' => ['line1', 'line2'] }) end - it 'returns an array containing http response code and an empty array when response body is empty' do + it 'returns a hash containing key \'content\' with an empty array' do response_body = '' stub_request(:get, url).with( @@ -78,27 +70,27 @@ ).to_return(body: response_body, status: 200) is_expected.to run - .with_params(url, 'somesecret') + .with_params(params) .and_return({ 'content' => [] }) end - it 'returns nil when http response code is not success' do + it 'returns a hash containing key \'errors\' when non-sucess http response code is received' do stub_request(:get, url).with( headers: headers, ).to_return(body: nil, status: 404) is_expected.to run - .with_params(url, 'somesecret') + .with_params(params) .and_return({ 'errors' => ['https://api.example.local/ returns HTTP code: 404'] }) end - it 'returns an array containing 0 and error when error occurs' do + it 'returns a hash containing key \'errors\' any other error occurs' do stub_request(:get, url).with( headers: headers, ).and_raise(StandardError.new('error')) is_expected.to run - .with_params(url, 'somesecret') + .with_params(params) .and_return({ 'errors' => ['https://api.example.local/ connection failed: error'] }) end end diff --git a/types/api/config.pp b/types/api/config.pp new file mode 100644 index 0000000..62b954a --- /dev/null +++ b/types/api/config.pp @@ -0,0 +1,8 @@ +# @summary API configuration +type Vas::API::Config = Array[ + Struct[ + url => Stdlib::HttpsUrl, + token => Optional[String[1]], + ssl_verify => Optional[Boolean], + ] +]