diff --git a/lib/puppet/provider/minio_bucket/minio_bucket.rb b/lib/puppet/provider/minio_bucket/minio_bucket.rb new file mode 100644 index 0000000..fc47124 --- /dev/null +++ b/lib/puppet/provider/minio_bucket/minio_bucket.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'puppet/resource_api/simple_provider' +require 'puppet_x/minio/client' + +# Implementation for the minio_bucket type using the Resource API. +class Puppet::Provider::MinioBucket::MinioBucket < Puppet::ResourceApi::SimpleProvider + def get(context) + context.debug('Returning list of minio buckets') + return [] unless PuppetX::Minio::Client.installed? || PuppetX::Minio::Client.alias_set? + + @instances = [] + PuppetX::Minio::Client.execute("ls #{PuppetX::Minio::Client.alias}").each do |json_bucket| + name = json_bucket['key'].chomp('/') + # `mcli stat` returns an array + data = PuppetX::Minio::Client.execute("stat #{PuppetX::Minio::Client.alias}/#{name}").first + + @instances << to_puppet_bucket(data) + end + @instances + end + + def create(context, name, should) + context.notice("Creating '#{name}' with #{should.inspect}") + + flags = [] + flags << "--region=#{should[:region]}" if should[:region] + flags << '--with-lock' if should[:enable_object_lock] + + PuppetX::Minio::Client.execute("mb #{flags.join(' ')} #{PuppetX::Minio::Client.alias}/#{name}") + end + + def update(context, name, should) + context.notice("Updating '#{name}' with #{should.inspect}") + + operations = [] + operations << "retention clear #{PuppetX::Minio::Client.alias}/#{name}" unless should[:enable_object_lock] + + operations.each do |op| + PuppetX::Minio::Client.execute(op) + end + end + + def delete(context, name) + context.notice("Deleting '#{name}'") + PuppetX::Minio::Client.execute("rb --force #{PuppetX::Minio::Client.alias}/#{name}") + end + + def to_puppet_bucket(json) + name = json['url'].delete_prefix("#{PuppetX::Minio::Client.alias}/") + region = json['metadata']['location'] + enable_object_lock = ! json['metadata']['ObjectLock']['enabled'].empty? + + { + ensure: 'present', + name: name, + region: region, + enable_object_lock: enable_object_lock, + } + end + + def insync?(context, _name, property_name, is_hash, should_hash) + context.debug("Checking whether #{property_name} is out of sync") + case property_name + when :region + # It's not possible to move a bucket to a different region + # after creation. + true + end + end +end diff --git a/lib/puppet/provider/minio_client_alias/minio_client_alias.rb b/lib/puppet/provider/minio_client_alias/minio_client_alias.rb index fef206b..e119e21 100644 --- a/lib/puppet/provider/minio_client_alias/minio_client_alias.rb +++ b/lib/puppet/provider/minio_client_alias/minio_client_alias.rb @@ -3,6 +3,7 @@ require 'json' require 'puppet/resource_api/simple_provider' require 'puppet_x/minio/client' +require 'puppet_x/minio/util' LEGACY_PATH_SUPPORT_MAP ||= { '': 'auto', @@ -12,6 +13,8 @@ # Implementation for the minio_client_alias type using the Resource API. class Puppet::Provider::MinioClientAlias::MinioClientAlias < Puppet::ResourceApi::SimpleProvider + include PuppetX::Minio::Util + def get(context) context.debug('Returning list of minio client aliases') return [] unless PuppetX::Minio::Client.installed? @@ -80,12 +83,4 @@ def to_puppet_alias(json) path_lookup_support: path_lookup_support, } end - - def unwrap_maybe_sensitive(param) - if param.is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive) - return param.unwrap - end - - param - end end diff --git a/lib/puppet/provider/minio_group/minio_group.rb b/lib/puppet/provider/minio_group/minio_group.rb new file mode 100644 index 0000000..eb7f47e --- /dev/null +++ b/lib/puppet/provider/minio_group/minio_group.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'puppet/resource_api/simple_provider' +require 'puppet_x/minio/client' + +GROUP_STATUS_MAP ||= { + 'enabled': true, + 'disabled': false, +}.freeze + +# Implementation for the minio_group type using the Resource API. +class Puppet::Provider::MinioGroup::MinioGroup < Puppet::ResourceApi::SimpleProvider + def get(context) + context.debug('Returning list of minio groups') + return [] unless PuppetX::Minio::Client.installed? || PuppetX::Minio::Client.alias_set? + + # `mcli admin group list` returns an array + json_groups = PuppetX::Minio::Client.execute("admin group list #{PuppetX::Minio::Client.alias}").first + return [] unless json_groups.key?('groups') + + @instances = [] + json_groups['groups'].each do |group| + # `mcli admin group info` returns an array + json_group_info = PuppetX::Minio::Client.execute("admin group info #{PuppetX::Minio::Client.alias} #{group}").first + @instances << to_puppet_group(json_group_info) + end + @instances + end + + def create(context, name, should) + context.notice("Creating '#{name}' with #{should.inspect}") + + operations = [] + operations << "admin group add #{PuppetX::Minio::Client.alias} #{name} #{should[:members].join(' ')}" + operations << "admin group disable #{PuppetX::Minio::Client.alias} #{name}" unless should[:enabled] + + operations.each do |op| + PuppetX::Minio::Client.execute(op) + end + end + + def update(context, name, should) + context.notice("Updating '#{name}' with #{should.inspect}") + + # TODO: Do a proper update instead of recreating the group + delete(context, name) + create(context, name, should) + end + + def delete(context, name) + context.notice("Deleting '#{name}'") + + members = PuppetX::Minio::Client.execute("admin group info #{PuppetX::Minio::Client.alias} #{name}").first['members'] + operations = [] + + operations << "admin group remove #{PuppetX::Minio::Client.alias} #{name} #{members.join(' ')}" + operations << "admin group remove #{PuppetX::Minio::Client.alias} #{name}" + + operations.each do |op| + PuppetX::Minio::Client.execute(op) + end + end + + def to_puppet_group(json) + policies = if json['groupPolicy'].nil? then nil else json['groupPolicy'].split(',') end + + { + ensure: 'present', + name: json['groupName'], + members: json['members'] || [], + enabled: GROUP_STATUS_MAP[json['groupStatus'].to_sym], + } + end +end diff --git a/lib/puppet/provider/minio_policy/minio_policy.rb b/lib/puppet/provider/minio_policy/minio_policy.rb new file mode 100644 index 0000000..6677380 --- /dev/null +++ b/lib/puppet/provider/minio_policy/minio_policy.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'json' +require 'tempfile' + +require 'puppet/resource_api/simple_provider' +require 'puppet_x/minio/client' + +# Implementation for the minio_policy type using the Resource API. +class Puppet::Provider::MinioPolicy::MinioPolicy < Puppet::ResourceApi::SimpleProvider + def get(context) + context.debug('Returning list of minio policies') + return [] unless PuppetX::Minio::Client.installed? || PuppetX::Minio::Client.alias_set? + + json_policies = PuppetX::Minio::Client.execute("admin policy list #{PuppetX::Minio::Client.alias}") + return [] if json_policies.empty? + + @instances = [] + json_policies.each do |policy| + # `mcli admin policy info` returns an array + json_policy_info = PuppetX::Minio::Client.execute("admin policy info #{PuppetX::Minio::Client.alias} #{policy['policy']}").first + @instances << to_puppet_policy(json_policy_info) + end + @instances + end + + def create(context, name, should) + context.notice("Creating '#{name}' with #{should.inspect}") + + f = Tempfile.new(["#{name}-policy", '.json']) + begin + json_policy = {:Version => should[:version], :Statement => should[:statement]}.to_json + + f.write(json_policy) + f.rewind + + PuppetX::Minio::Client.execute("admin policy add #{PuppetX::Minio::Client.alias} #{name} #{f.path}") + ensure + f.close + f.unlink + end + end + + def update(context, name, should) + context.notice("Updating '#{name}' with #{should.inspect}") + + # There's currently no way to update an existing policy via the client, + # so delete and recreate the policy + delete(context, name) + create(context, name, should) + end + + def delete(context, name) + context.notice("Deleting '#{name}'") + PuppetX::Minio::Client.execute("admin policy remove #{PuppetX::Minio::Client.alias} #{name}") + end + + def to_puppet_policy(json) + statements = [] + json['policyJSON']['Statement'].each do |s| + statements << sanitize_statement(s) + end + + { + ensure: 'present', + name: json['policy'], + version: json['policyJSON']['Version'], + statement: statements, + } + end + + def sanitize_statement(statement) + statement.transform_keys!(&:capitalize) + + ['Action', 'Resource', :Action, :Resource].each do |k| + statement[k].sort! unless statement[k].nil? + end + + statement + end + + def canonicalize(_context, resources) + resources.each do |r| + unless r[:statement].nil? + r[:statement].each do |s| + s = sanitize_statement(s) + end + end + end + end +end diff --git a/lib/puppet/provider/minio_policy_assignment/minio_policy_assignment.rb b/lib/puppet/provider/minio_policy_assignment/minio_policy_assignment.rb new file mode 100644 index 0000000..44d4308 --- /dev/null +++ b/lib/puppet/provider/minio_policy_assignment/minio_policy_assignment.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'puppet/resource_api/simple_provider' +require 'puppet_x/minio/client' + +# Implementation for the minio_policy_assignment type using the Resource API. +class Puppet::Provider::MinioPolicyAssignment::MinioPolicyAssignment < Puppet::ResourceApi::SimpleProvider + def get(context) + context.debug('Returning list of minio policy assignments') + return [] unless PuppetX::Minio::Client.installed? || PuppetX::Minio::Client.alias_set? + + @instances = [] + PuppetX::Minio::Client.execute("admin user list #{PuppetX::Minio::Client.alias}").each do |json_user| + # `mcli admin user info` returns an array + json_user_info = PuppetX::Minio::Client.execute("admin user info #{PuppetX::Minio::Client.alias} #{json_user['accessKey']}").first + @instances << to_puppet_policy_assignment(json_user_info, 'user') + end + + # `mcli admin group list` returns an array + json_groups = PuppetX::Minio::Client.execute("admin group list #{PuppetX::Minio::Client.alias}").first + json_groups.fetch('groups', []).each do |group| + # `mcli admin group info` returns an array + json_group_info = PuppetX::Minio::Client.execute("admin group info #{PuppetX::Minio::Client.alias} #{group}").first + @instances << to_puppet_policy_assignment(json_group_info, 'group') + end + + @instances + end + + def update(context, name, should) + context.notice("Updating '#{name}' with #{should.inspect}") + PuppetX::Minio::Client.execute("admin policy set #{PuppetX::Minio::Client.alias} #{should[:policies].join(',')} #{should[:subject_type]}=#{should[:subject]}") + end + + def create(context, name, should) + context.warning('`create` method not implemented for `minio_policy_assignment` provider.') + end + + def delete(context, name) + context.warning('`delete` method not implemented for `minio_policy_assignment` provider.') + end + + def to_puppet_policy_assignment(json, subject_type) + case subject_type + when 'user' + subject = json.fetch('accessKey') + policies = json.fetch('policyName', '').split(',') + when 'group' + subject = json.fetch('groupName') + policies = json.fetch('groupPolicy', '').split(',') + else + raise Puppet::ExecutionFailure, "Unknown subject_type `#{subject_type}`. Supported values: user, group" + end + + { + ensure: 'present', + title: "#{subject_type}_#{subject}", + subject: subject, + subject_type: subject_type, + policies: policies, + } + end +end diff --git a/lib/puppet/provider/minio_user/minio_user.rb b/lib/puppet/provider/minio_user/minio_user.rb new file mode 100644 index 0000000..050362f --- /dev/null +++ b/lib/puppet/provider/minio_user/minio_user.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'puppet/resource_api/simple_provider' +require 'puppet_x/minio/client' +require 'puppet_x/minio/util' + +STATUS_MAP ||= { + 'enabled': true, + 'disabled': false +}.freeze + +# Implementation for the minio_user type using the Resource API. +class Puppet::Provider::MinioUser::MinioUser < Puppet::ResourceApi::SimpleProvider + include PuppetX::Minio::Util + + def get(context) + context.debug('Returning list of minio users') + return [] unless PuppetX::Minio::Client.installed? || PuppetX::Minio::Client.alias_set? + + @instances = [] + PuppetX::Minio::Client.execute("admin user list #{PuppetX::Minio::Client.alias}").each do |json_user| + # `mcli admin user info` returns an array + json_user_info = PuppetX::Minio::Client.execute("admin user info #{PuppetX::Minio::Client.alias} #{json_user['accessKey']}").first + @instances << to_puppet_user(json_user_info) + end + @instances + end + + def create(context, name, should) + context.notice("Creating '#{name}' with #{should.inspect}") + + operations = [] + operations << ["admin user add #{PuppetX::Minio::Client.alias} #{should[:access_key]} #{unwrap_maybe_sensitive(should[:secret_key])}", sensitive: true] + operations << ["admin user disable #{PuppetX::Minio::Client.alias} #{should[:access_key]}"] unless should[:enabled] + + operations.each do |op| + PuppetX::Minio::Client.execute(*op) + end + end + + def update(context, name, should) + context.notice("Updating '#{name}' with #{should.inspect}") + + operations = [] + operations << "admin user disable #{PuppetX::Minio::Client.alias} #{name}" unless should[:enabled] + + operations.each do |op| + PuppetX::Minio::Client.execute(op) + end + end + + def delete(context, name) + context.notice("Deleting '#{name}'") + PuppetX::Minio::Client.execute("admin user remove #{PuppetX::Minio::Client.alias} #{name}") + end + + def insync?(context, _name, property_name, is_hash, should_hash) + context.debug("Checking whether #{property_name} is out of sync") + case property_name + when :secret_key + # Let Puppet believe that the resource doesn't need updating, + # since we can't check a user's secret key + true + end + end + + def to_puppet_user(json) + { + ensure: 'present', + access_key: json['accessKey'], + enabled: STATUS_MAP[json['userStatus'].to_sym], + } + end +end diff --git a/lib/puppet/type/minio_bucket.rb b/lib/puppet/type/minio_bucket.rb new file mode 100644 index 0000000..1f19387 --- /dev/null +++ b/lib/puppet/type/minio_bucket.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'puppet/resource_api' + +Puppet::ResourceApi.register_type( + name: 'minio_bucket', + docs: <<-EOS, +@summary Manages local MinIO S3 buckets +@example + minio_bucket { 'my-bucket': + ensure => 'present', + } + +**Autorequires**: +* `File[/root/.minioclient]` +* `File[/root/.minio_default_alias]` +EOS + features: ['custom_insync'], + attributes: { + ensure: { + type: 'Enum[present, absent]', + desc: 'Whether this resource should be present or absent on the target system.', + default: 'present', + }, + name: { + type: 'String', + desc: 'The name of the resource you want to manage.', + behaviour: :namevar, + }, + region: { + type: 'Optional[String]', + desc: 'Region where to create the bucket.', + behaviour: :init_only, + default: 'us-east-1', + }, + enable_object_lock: { + type: 'Optional[Boolean]', + desc: 'Enables/Disables S3 object locking.', + default: false, + } + }, +) diff --git a/lib/puppet/type/minio_group.rb b/lib/puppet/type/minio_group.rb new file mode 100644 index 0000000..77e4bda --- /dev/null +++ b/lib/puppet/type/minio_group.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'puppet/resource_api' + +Puppet::ResourceApi.register_type( + name: 'minio_group', + docs: <<-EOS, +@summary Manages local MinIO groups +@example + minio_group { 'admins': + ensure => 'present', + members => ['userOne', 'userTwo'], + } +@example + minio_group { 'my-group': + ensure => 'present', + members => ['userThree', 'userFour'], + } + +**Autorequires**: +* `File[/root/.minioclient]` +* `File[/root/.minio_default_alias]` +EOS + features: [], + attributes: { + ensure: { + type: 'Enum[present, absent]', + desc: 'Whether this resource should be present or absent on the target system.', + default: 'present', + }, + name: { + type: 'String', + desc: 'The name of the resource you want to manage.', + behaviour: :namevar, + }, + members: { + type: 'Array[String, 1]', + desc: 'List of users that should be part of this group.', + }, + enabled: { + type: 'Optional[Boolean]', + desc: 'Set to false to disable this group. Defaults to true.', + default: true, + }, + }, +) diff --git a/lib/puppet/type/minio_policy.rb b/lib/puppet/type/minio_policy.rb new file mode 100644 index 0000000..b25cc30 --- /dev/null +++ b/lib/puppet/type/minio_policy.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'puppet/resource_api' + +Puppet::ResourceApi.register_type( + name: 'minio_policy', + docs: <<-EOS, +@summary Manages local MinIO policies +@example + minio_policy { 'custom-policy': + ensure => 'present', + statement => [ + { + 'Effect' => 'Allow', + 'Action' => ['s3:ListBucket'], + 'Resource' => ['arn:aws:s3:::my-bucket'] + }, + { + 'Effect' => 'Allow', + 'Action' => ['s3:GetObject', 's3:PutObject'], + 'Resource' => ['arn:aws:s3:::my-bucket'] + } + ], + } + +**Autorequires**: +* `File[/root/.minioclient]` +* `File[/root/.minio_default_alias]` +EOS + features: ['canonicalize'], + attributes: { + ensure: { + type: 'Enum[present, absent]', + desc: 'Whether this resource should be present or absent on the target system.', + default: 'present', + }, + name: { + type: 'String', + desc: 'The name of the resource you want to manage.', + behaviour: :namevar, + }, + version: { + type: 'Optional[String]', + desc: 'Specifies the language syntax rules that are to be used to process a policy.', + default: '2012-10-17', + }, + statement: { + type: 'Array[Hash]', + desc: 'List of statements describing the policy.', + } + }, +) diff --git a/lib/puppet/type/minio_policy_assignment.rb b/lib/puppet/type/minio_policy_assignment.rb new file mode 100644 index 0000000..706c481 --- /dev/null +++ b/lib/puppet/type/minio_policy_assignment.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'puppet/resource_api' + +Puppet::ResourceApi.register_type( + name: 'minio_policy_assignment', + docs: <<-EOS, +@summary Assigns MinIO policies to users or groups +@example +minio_policy_assignment { 'test': + ensure => 'present', +} + +**Autorequires**: +* `File[/root/.minioclient]` +* `File[/root/.minio_default_alias]` +EOS + features: [], + title_patterns: [ + { + pattern: %r{^(?.*)_(?.*)$}, + desc: 'Subject type and subject are both provided with a hyphen seperator', + }, + ], + attributes: { + ensure: { + type: 'Enum[present, absent]', + desc: 'Whether this resource should be present or absent on the target system.', + default: 'present', + }, + subject_type: { + type: 'Enum[user, group]', + desc: 'The type of subject to assign policie to. Should be user or group.', + behaviour: :namevar, + }, + subject: { + type: 'String', + desc: 'The user or group to assign policies to.', + behaviour: :namevar, + }, + policies: { + type: 'Array[String]', + desc: 'List of policies to assign to the subject.', + }, + }, +) diff --git a/lib/puppet/type/minio_user.rb b/lib/puppet/type/minio_user.rb new file mode 100644 index 0000000..245ed1f --- /dev/null +++ b/lib/puppet/type/minio_user.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'puppet/resource_api' + +Puppet::ResourceApi.register_type( + name: 'minio_user', + docs: <<-EOS, +@summary Manages local MinIO users +@example + minio_user { 'userOne': + ensure => 'present', + secret_key => Sensitive('password'), + policies => ['consoleAdmin', 'custom-policy'], + } + +**Autorequires**: +* `File[/root/.minioclient]` +* `File[/root/.minio_default_alias]` +EOS + features: ['custom_insync'], + attributes: { + ensure: { + type: 'Enum[present, absent]', + desc: 'Whether this resource should be present or absent on the target system.', + default: 'present', + }, + access_key: { + type: 'String', + desc: 'API access key. This can also be used as a username.', + behaviour: :namevar, + }, + secret_key: { + type: 'Variant[Sensitive[String[8, 40]], String[8, 40]]', + desc: 'API access secret or password.', + }, + enabled: { + type: 'Optional[Boolean]', + desc: 'Enables/Disables this user account', + default: true, + }, + }, +) diff --git a/lib/puppet_x/minio/client.rb b/lib/puppet_x/minio/client.rb index 161c5f0..9bb83fe 100644 --- a/lib/puppet_x/minio/client.rb +++ b/lib/puppet_x/minio/client.rb @@ -6,8 +6,10 @@ module PuppetX # rubocop:disable Style/ClassAndModuleChildren module Minio # rubocop:disable Style/ClassAndModuleChildren class Client # rubocop:disable Style/Documentation CLIENT_LINK_LOCATION = '/root/.minioclient' + DEFAULT_ALIAS_LOCATION = '/root/.minio_default_alias' @client_location = nil + @default_alias = nil def self.execute(args, **execute_args) ensure_client_installed @@ -19,6 +21,11 @@ def self.execute(args, **execute_args) end end + def self.alias + ensure_alias + @default_alias + end + def self.ensure_client_installed return if @client_location @@ -33,9 +40,27 @@ def self.ensure_client_installed @client_location = File.readlink(CLIENT_LINK_LOCATION) end + def self.ensure_alias + return if @default_alias + + unless alias_set? + errormsg = [ + "MinIO default alias file does not exist at #{DEFAULT_ALIAS_LOCATION}. ", + 'Make sure to specify an alias to be used with `minio::default_client_alias`.', + ] + raise Puppet::ExecutionFailure, errormsg.join + end + + @default_alias = File.read(DEFAULT_ALIAS_LOCATION) + end + def self.installed? File.exist?(CLIENT_LINK_LOCATION) end + + def self.alias_set? + File.exist?(DEFAULT_ALIAS_LOCATION) + end end end end diff --git a/lib/puppet_x/minio/util.rb b/lib/puppet_x/minio/util.rb new file mode 100644 index 0000000..703b95a --- /dev/null +++ b/lib/puppet_x/minio/util.rb @@ -0,0 +1,13 @@ +module PuppetX + module Minio + module Util + def unwrap_maybe_sensitive(param) + if param.is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive) + return param.unwrap + end + + param + end + end + end +end diff --git a/manifests/client.pp b/manifests/client.pp index e25309a..9ac357f 100644 --- a/manifests/client.pp +++ b/manifests/client.pp @@ -40,6 +40,8 @@ # Target directory to hold the minio client installation. Default: `/opt/minioclient` # @param [Hash] aliases # List of aliases to add to the minio client configuration. For parameter description see `minio_client_alias`. +# @param [String] default_client_alias +# The default client alias to use when interacting with MinIO's API. Required. # @param [Boolean] purge_unmanaged_aliases # Decides if puppet should purge unmanaged minio client aliases # @@ -56,6 +58,7 @@ Stdlib::Absolutepath $installation_directory = $minio::client_installation_directory, String $binary_name = $minio::client_binary_name, Hash $aliases = $minio::client_aliases, + String $default_client_alias = $minio::default_client_alias, Boolean $purge_unmanaged_aliases = $minio::purge_unmanaged_client_aliases, ) { if ($manage_client_installation) { diff --git a/manifests/client/config.pp b/manifests/client/config.pp index ad44e86..aeac5c6 100644 --- a/manifests/client/config.pp +++ b/manifests/client/config.pp @@ -18,6 +18,8 @@ # # @param [Hash] aliases # List of aliases to add to the minio client configuration. For parameter description see `minio_client_alias`. +# @param [String] default_client_alias +# The default client alias to use when interacting with MinIO's API. Required. # @param [Boolean] purge_unmanaged_aliases # Decides if puppet should purge unmanaged minio client aliases # @@ -26,6 +28,7 @@ # class minio::client::config( Hash $aliases = $minio::client::aliases, + String $default_client_alias = $minio::client::default_client_alias, Boolean $purge_unmanaged_aliases = $minio::client::purge_unmanaged_aliases, ) { if ($purge_unmanaged_aliases) { @@ -34,6 +37,14 @@ } } + file { '/root/.minio_default_alias': + ensure => file, + owner => 'root', + group => 'root', + mode => '0640', + content => $default_client_alias, + } + $aliases.each | $alias, $alias_values | { minio_client_alias {$alias: * => $alias_values, diff --git a/manifests/init.pp b/manifests/init.pp index acceb4a..eec75cc 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -114,6 +114,8 @@ # List of aliases to add to the minio client configuration. For parameter description see `minio_client_alias`. # @param [Boolean] purge_unmanaged_client_aliases # Decides if puppet should purge unmanaged minio client aliases +# @param [String] default_client_alias +# The default client alias to use when interacting with MinIO's API. Required. # @param [Enum['present', 'absent']] cert_ensure # Decides if minio certificates binary will be installed. # @param [Stdlib::Absolutepath] cert_directory @@ -178,6 +180,7 @@ String $client_binary_name, Hash $client_aliases, Boolean $purge_unmanaged_client_aliases, + String $default_client_alias, Enum['present', 'absent'] $cert_ensure, Stdlib::Absolutepath $cert_directory, Optional[String[1]] $default_cert_name, diff --git a/spec/unit/puppet/provider/minio_bucket/minio_bucket_spec.rb b/spec/unit/puppet/provider/minio_bucket/minio_bucket_spec.rb new file mode 100644 index 0000000..38c139e --- /dev/null +++ b/spec/unit/puppet/provider/minio_bucket/minio_bucket_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'spec_helper' + +ensure_module_defined('Puppet::Provider::MinioBucket') +require 'puppet/provider/minio_bucket/minio_bucket' + +RSpec.describe Puppet::Provider::MinioBucket::MinioBucket do + subject(:provider) { described_class.new } + + let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } + + before :each do + allow(context).to receive(:debug) + allow(context).to receive(:notice) + allow(context).to receive(:warning) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:readlink).and_return('/usr/local/sbin/minio-client') + Puppet::Util::ExecutionStub.set do |_command, _options| + '' + end + end + + describe '#get' do + it 'returns empty list when client is not installed' do + allow(File).to receive(:exist?).and_return(false) + expect(provider.get(context)).to eq [] + end + + it 'processes resources' do + Puppet::Util::ExecutionStub.set do |_command, _options| + <<-JSONSTRINGS + {"status": "success","type": "folder","lastModified": "1970-01-01T00:00:00.000+01:00","size": 0,"key": "bucket-one/","etag": "","url": "http://localhost:9200","versionOrdinal": 1} + {"status": "success","type": "folder","lastModified": "1970-01-01T00:00:00.000+01:00","size": 0,"key": "bucket-two/","etag": "","url": "http://localhost:9200","versionOrdinal": 1} + JSONSTRINGS + end + + expect(context).to receive(:debug).with('Returning list of minio buckets') + expect(provider.get(context)).to eq [ + { + name: 'bucket-one', + ensure: 'present', + }, + { + name: 'bucket-two', + ensure: 'present', + }, + ] + end + end + + describe 'create(context, name, should)' do + it 'creates the resource' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to eq '/usr/local/sbin/minio-client --json mb puppet/bucket-one' + + '' + end + + expect(context).to receive(:notice).with(%r{\ACreating 'bucket-one'}) + provider.create(context, 'bucket-one', name: 'bucket-one', ensure: 'present') + end + end + + describe 'update(context, name, should)' do + it 'updates the resource' do + expect(context).to receive(:warning).with('`update` method not implemented for `minio_bucket` provider') + provider.update(context, 'bucket-one', name: 'bucket-one-test', ensure: 'present') + end + end + + describe 'delete(context, name)' do + it 'deletes the resource' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to eq '/usr/local/sbin/minio-client --json rb --force puppet/bucket-one' + + '' + end + + expect(context).to receive(:notice).with(%r{\ADeleting 'bucket-one'}) + provider.delete(context, 'bucket-one') + end + end +end diff --git a/spec/unit/puppet/provider/minio_group/minio_group_spec.rb b/spec/unit/puppet/provider/minio_group/minio_group_spec.rb new file mode 100644 index 0000000..5ecf3fe --- /dev/null +++ b/spec/unit/puppet/provider/minio_group/minio_group_spec.rb @@ -0,0 +1,174 @@ +# frozen_string_literal: true + +require 'spec_helper' + +ensure_module_defined('Puppet::Provider::MinioGroup') +require 'puppet/provider/minio_group/minio_group' + +RSpec.describe Puppet::Provider::MinioGroup::MinioGroup do + subject(:provider) { described_class.new } + + let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } + + before :each do + allow(context).to receive(:debug) + allow(context).to receive(:notice) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:readlink).and_return('/usr/local/sbin/minio-client') + Puppet::Util::ExecutionStub.set do |_command, _options| + '' + end + end + + describe '#get' do + it 'returns empty list when client is not installed' do + allow(File).to receive(:exist?).and_return(false) + expect(provider.get(context)).to eq [] + end + + it 'processes resources' do + Puppet::Util::ExecutionStub.set do |command, _options| + case command + when /list/ + out = <<-JSONSTRINGS + {"status": "success","groups": ["test-one","test-two"]} + JSONSTRINGS + when /test-one/ + out = <<-JSONSTRINGS + {"status": "success","groupName": "test-one","members": ["user-one","user-two"],"groupStatus": "enabled","groupPolicy": "test-policy"} + JSONSTRINGS + else + out = <<-JSONSTRINGS + {"status": "success","groupName": "test-two","members": ["user-three","user-four"],"groupStatus": "enabled","groupPolicy": "consoleAdmin"} + JSONSTRINGS + end + + out + end + + expect(context).to receive(:debug).with('Returning list of minio groups') + expect(provider.get(context)).to eq [ + { + name: 'test-one', + ensure: 'present', + members: ['user-one', 'user-two'], + policies: ['test-policy'], + enabled: true, + }, + { + name: 'test-two', + ensure: 'present', + members: ['user-three', 'user-four'], + policies: ['consoleAdmin'], + enabled: true, + }, + ] + end + end + + describe 'create(context, name, should)' do + it 'with defaults' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to eq '/usr/local/sbin/minio-client --json admin group add puppet test-one user-one user-two' + + '' + end + expect(context).to receive(:notice).with(%r{\ACreating 'test-one'}) + + provider.create(context, 'test-one', + name: 'test-one', + ensure: 'present', + members: ['user-one', 'user-two'], + enabled: true) + end + + it 'with policies set' do + Puppet::Util::ExecutionStub.set do |command, _options| + case command + when /policy/ + expected_command = '/usr/local/sbin/minio-client --json admin policy set puppet custom-policy-one,custom-policy-two group=test-one' + else + expected_command = '/usr/local/sbin/minio-client --json admin group add puppet test-one user-one user-two' + end + expect(command).to eq(expected_command) + + '' + end + expect(context).to receive(:notice).with (%r{\ACreating 'test-one'}) + + provider.create(context, 'test-one', + name: 'test-one', + ensure: 'present', + members: ['user-one', 'user-two'], + policies: ['custom-policy-one', 'custom-policy-two'], + enabled: true) + end + + it 'with group disabled' do + Puppet::Util::ExecutionStub.set do |command, _options| + case command + when /disable/ + expected_command = '/usr/local/sbin/minio-client --json admin group disable puppet test-one' + else + expected_command = '/usr/local/sbin/minio-client --json admin group add puppet test-one user-one user-two' + end + expect(command).to eq(expected_command) + + '' + end + expect(context).to receive(:notice).with (%r{\ACreating 'test-one'}) + + provider.create(context, 'test-one', + name: 'test-one', + ensure: 'present', + members: ['user-one', 'user-two'], + enabled: false) + + end + end + + describe 'update(context, name, should)' do + it 'updates the resource' do + expect(context).to receive(:notice).with(%r{\AUpdating 'test-one'}) + + expect(subject).to receive(:delete).with(context, 'test-one') + expect(subject).to receive(:create).with(context, 'test-one', + name: 'test-one', + ensure: 'present', + members: ['user-one'], + enabled: false) + + provider.update(context, 'test-one', + name: 'test-one', + ensure: 'present', + members: ['user-one'], + enabled: false) + end + end + + describe 'delete(context, name)' do + it 'deletes the resource' do + Puppet::Util::ExecutionStub.set do |command, _options| + case command + when /info/ + expected_command = '/usr/local/sbin/minio-client --json admin group info puppet test-one' + out = <<-JSONSTRING + {"status": "success","groupName": "test-one","members": ["user-one","user-two"],"groupStatus": "enabled","groupPolicy": "test-policy"} + JSONSTRING + when /user/ + expected_command = '/usr/local/sbin/minio-client --json admin group remove puppet test-one user-one user-two' + out = '' + else + expected_command = '/usr/local/sbin/minio-client --json admin group remove puppet test-one' + out = '' + end + + expect(command).to eq(expected_command) + out + end + expect(context).to receive(:notice).with(%r{\ADeleting 'test-one'}) + + provider.delete(context, 'test-one') + end + end +end diff --git a/spec/unit/puppet/provider/minio_policy/minio_policy_spec.rb b/spec/unit/puppet/provider/minio_policy/minio_policy_spec.rb new file mode 100644 index 0000000..828bbe3 --- /dev/null +++ b/spec/unit/puppet/provider/minio_policy/minio_policy_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +require 'spec_helper' + +ensure_module_defined('Puppet::Provider::MinioPolicy') +require 'puppet/provider/minio_policy/minio_policy' + +RSpec.describe Puppet::Provider::MinioPolicy::MinioPolicy do + subject(:provider) { described_class.new } + + let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } + + before :each do + allow(context).to receive(:debug) + allow(context).to receive(:notice) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:readlink).and_return('/usr/local/sbin/minio-client') + Puppet::Util::ExecutionStub.set do |_command, _options| + '' + end + end + + describe '#get' do + it 'returns empty list when client is not installed' do + allow(File).to receive(:exist?).and_return(false) + expect(provider.get(context)).to eq [] + end + + it 'processes resources' do + Puppet::Util::ExecutionStub.set do |command, _options| + case command + when /list/ + out = <<-JSONSTRINGS + {"status": "success","policy": "test-one","isGroup": false} + JSONSTRINGS + else + out = <<-JSONSTRINGS + {"status": "success","policy": "test-one","isGroup": false,"policyJSON": {"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["s3:ListBucket"],"Resource": ["arn:aws:s3:::test-one-bucket-*"]},{"Effect": "Allow","Action": ["s3:GetObject","s3:PutObject"],"Resource": ["arn:aws:s3:::test-one-*"]}]}} + JSONSTRINGS + end + + out + end + + expect(context).to receive(:debug).with('Returning list of minio policies') + expect(provider.get(context)).to eq [ + { + name: 'test-one', + ensure: 'present', + version: '2012-10-17', + statement: [ # Using hash rockets, since Puppet returns strings as hash keys + { + 'Effect' => 'Allow', + 'Action' => ['s3:ListBucket'], + 'Resource' => ['arn:aws:s3:::test-one-bucket-*'], + }, + { + 'Effect' => 'Allow', + 'Action' => ['s3:GetObject','s3:PutObject'], + 'Resource' => ['arn:aws:s3:::test-one-*'], + }, + ], + }, + ] + end + end + + describe 'create(context, name, should)' do + it 'creates the resource' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to include '/usr/local/sbin/minio-client --json admin policy add puppet test-one' + + '' + end + expect(context).to receive(:notice).with(%r{\ACreating 'test-one'}) + + provider.create(context, 'test-one', + name: 'test-one', + ensure: 'present', + statement: [ + { + Effect: 'Allow', + Action:['s3:ListBucket'], + Resource: ['arn:aws:s3:::test-one-bucket-*'], + } + ]) + end + end + + describe 'update(context, name, should)' do + it 'updates the resource' do + expect(context).to receive(:notice).with(%r{\AUpdating 'test-one'}) + + expect(subject).to receive(:delete).with(context, 'test-one') + expect(subject).to receive(:create).with(context, 'test-one', + name: 'test-one', + ensure: 'present', + statement: [ + { + Effect: 'Allow', + Action:['s3:ListBucket'], + Resource: ['arn:aws:s3:::test-one-bucket-*'], + } + ]) + + provider.update(context, 'test-one', + name: 'test-one', + ensure: 'present', + statement: [ + { + Effect: 'Allow', + Action:['s3:ListBucket'], + Resource: ['arn:aws:s3:::test-one-bucket-*'], + } + ]) + end + end + + describe 'delete(context, name)' do + it 'deletes the resource' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to eq '/usr/local/sbin/minio-client --json admin policy remove puppet test-one' + + '' + end + expect(context).to receive(:notice).with(%r{\ADeleting 'test-one'}) + + provider.delete(context, 'test-one') + end + end + + describe 'sanitize_statement(statement)' do + def self.test_sanitize_statement(desc, input) + expected = {:Effect => 'Allow', :Action => ['s3:GetObject', 's3:PutObject'], :Resource => ['arn:aws:s3:::test-one-bucket-*']} + + it desc do + expect(provider.sanitize_statement(input)).to eq expected + end + end + + test_sanitize_statement 'capitalizes keys', {'effect': 'Allow', 'action': ['s3:GetObject', 's3:PutObject'], 'resource': ['arn:aws:s3:::test-one-bucket-*']} + test_sanitize_statement 'sorts Action and Resource arrays', {'Effect': 'Allow', 'Action': ['s3:PutObject', 's3:GetObject'], 'Resource': ['arn:aws:s3:::test-one-bucket-*']} + end +end diff --git a/spec/unit/puppet/provider/minio_policy_assignment/minio_policy_assignment_spec.rb b/spec/unit/puppet/provider/minio_policy_assignment/minio_policy_assignment_spec.rb new file mode 100644 index 0000000..2825a99 --- /dev/null +++ b/spec/unit/puppet/provider/minio_policy_assignment/minio_policy_assignment_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +ensure_module_defined('Puppet::Provider::MinioPolicyAssignment') +require 'puppet/provider/minio_policy_assignment/minio_policy_assignment' + +RSpec.describe Puppet::Provider::MinioPolicyAssignment::MinioPolicyAssignment do + subject(:provider) { described_class.new } + + let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } + + describe '#get' do + it 'processes resources' do + expect(context).to receive(:debug).with('Returning pre-canned example data') + expect(provider.get(context)).to eq [ + { + name: 'foo', + ensure: 'present', + }, + { + name: 'bar', + ensure: 'present', + }, + ] + end + end + + describe 'create(context, name, should)' do + it 'creates the resource' do + expect(context).to receive(:notice).with(%r{\ACreating 'a'}) + + provider.create(context, 'a', name: 'a', ensure: 'present') + end + end + + describe 'update(context, name, should)' do + it 'updates the resource' do + expect(context).to receive(:notice).with(%r{\AUpdating 'foo'}) + + provider.update(context, 'foo', name: 'foo', ensure: 'present') + end + end + + describe 'delete(context, name)' do + it 'deletes the resource' do + expect(context).to receive(:notice).with(%r{\ADeleting 'foo'}) + + provider.delete(context, 'foo') + end + end +end diff --git a/spec/unit/puppet/provider/minio_user/minio_user_spec.rb b/spec/unit/puppet/provider/minio_user/minio_user_spec.rb new file mode 100644 index 0000000..c5f63ff --- /dev/null +++ b/spec/unit/puppet/provider/minio_user/minio_user_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +ensure_module_defined('Puppet::Provider::MinioUser') +require 'puppet/provider/minio_user/minio_user' + +RSpec.describe Puppet::Provider::MinioUser::MinioUser do + subject(:provider) { described_class.new } + + let(:context) { instance_double('Puppet::ResourceApi::BaseContext', 'context') } + + before :each do + allow(context).to receive(:debug) + allow(context).to receive(:notice) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:readlink).and_return('/usr/local/sbin/minio-client') + Puppet::Util::ExecutionStub.set do |_command, _options| + '' + end + end + + describe '#get' do + it 'returns empty list when client is not installed' do + allow(File).to receive(:exist?).and_return(false) + expect(provider.get(context)).to eq [] + end + + it 'processes resources' do + Puppet::Util::ExecutionStub.set do |command, _options| + case command + when /list/ + out = <<-JSONSTRINGS + {"status": "success","accessKey": "user-one","userStatus": "enabled"} + {"status": "success","accessKey": "user-two","userStatus": "enabled"} + JSONSTRINGS + when /user-one/ + out = <<-JSONSTRINGS + {"status": "success","accessKey": "user-one","userStatus": "enabled","memberOf": ["group-one"]} + JSONSTRINGS + else + out = <<-JSONSTRINGS + {"status": "success","accessKey": "user-two","policyName": "readonly,custom-policy","userStatus": "enabled"} + JSONSTRINGS + end + + out + end + + expect(context).to receive(:debug).with('Returning list of minio users') + expect(provider.get(context)).to eq [ + { + access_key: 'user-one', + ensure: 'present', + policies: nil, + member_of: ['group-one'], + }, + { + access_key: 'user-two', + ensure: 'present', + policies: ['readonly','custom-policy'], + member_of: [], + }, + ] + end + end + + describe 'create(context, name, should)' do + it 'with defaults' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to eq '/usr/local/sbin/minio-client --json admin user add puppet user-one MySecretPass' + + '' + end + expect(context).to receive(:notice).with(%r{\ACreating 'user-one'}) + + provider.create(context, 'user-one', + ensure: 'present', + access_key: 'user-one', + secret_key: Puppet::Pops::Types::PSensitiveType::Sensitive.new('MySecretPass')) + end + + it 'with policies' do + Puppet::Util::ExecutionStub.set do |command, _options| + case command + when /policy/ + expected_command = '/usr/local/sbin/minio-client --json admin policy set puppet readonly custom-policy user=user-one' + else + expected_command = '/usr/local/sbin/minio-client --json admin user add puppet user-one MySecretPass' + end + expect(command).to eq expected_command + + '' + end + expect(context).to receive(:notice).with(%r{\ACreating 'user-one'}) + + provider.create(context, 'user-one', + ensure: 'present', + access_key: 'user-one', + secret_key: Puppet::Pops::Types::PSensitiveType::Sensitive.new('MySecretPass'), + policies: ['readonly', 'custom-policy']) + end + end + + describe 'update(context, name, should)' do + it 'updates the resource' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to eq '/usr/local/sbin/minio-client --json admin policy set puppet consoleAdmin user=user-one' + + '' + end + expect(context).to receive(:notice).with(%r{\AUpdating 'user-one'}) + + provider.update(context, 'user-one', + ensure: 'present', + access_key: 'user-one', + policies: ['consoleAdmin']) + end + end + + describe 'delete(context, name)' do + it 'deletes the resource' do + Puppet::Util::ExecutionStub.set do |command, _options| + expect(command).to eq '/usr/local/sbin/minio-client --json admin user remove puppet user-one' + + '' + end + expect(context).to receive(:notice).with(%r{\ADeleting 'user-one'}) + + provider.delete(context, 'user-one') + end + end +end diff --git a/spec/unit/puppet/type/minio_bucket_spec.rb b/spec/unit/puppet/type/minio_bucket_spec.rb new file mode 100644 index 0000000..f349ba7 --- /dev/null +++ b/spec/unit/puppet/type/minio_bucket_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/type/minio_bucket' + +RSpec.describe 'the minio_bucket type' do + it 'loads' do + expect(Puppet::Type.type(:minio_bucket)).not_to be_nil + end +end diff --git a/spec/unit/puppet/type/minio_group_spec.rb b/spec/unit/puppet/type/minio_group_spec.rb new file mode 100644 index 0000000..2e6822f --- /dev/null +++ b/spec/unit/puppet/type/minio_group_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/type/minio_group' + +RSpec.describe 'the minio_group type' do + it 'loads' do + expect(Puppet::Type.type(:minio_group)).not_to be_nil + end +end diff --git a/spec/unit/puppet/type/minio_policy_assignment_spec.rb b/spec/unit/puppet/type/minio_policy_assignment_spec.rb new file mode 100644 index 0000000..a5ee0ec --- /dev/null +++ b/spec/unit/puppet/type/minio_policy_assignment_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/type/minio_policy_assignment' + +RSpec.describe 'the minio_policy_assignment type' do + it 'loads' do + expect(Puppet::Type.type(:minio_policy_assignment)).not_to be_nil + end +end diff --git a/spec/unit/puppet/type/minio_policy_spec.rb b/spec/unit/puppet/type/minio_policy_spec.rb new file mode 100644 index 0000000..88880e9 --- /dev/null +++ b/spec/unit/puppet/type/minio_policy_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/type/minio_policy' + +RSpec.describe 'the minio_policy type' do + it 'loads' do + expect(Puppet::Type.type(:minio_policy)).not_to be_nil + end +end diff --git a/spec/unit/puppet/type/minio_user_spec.rb b/spec/unit/puppet/type/minio_user_spec.rb new file mode 100644 index 0000000..fb7714e --- /dev/null +++ b/spec/unit/puppet/type/minio_user_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'puppet/type/minio_user' + +RSpec.describe 'the minio_user type' do + it 'loads' do + expect(Puppet::Type.type(:minio_user)).not_to be_nil + end +end