aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.fixtures.yml6
-rw-r--r--.gitattributes4
-rw-r--r--.gitignore24
-rw-r--r--.pdkignore24
-rw-r--r--.rspec2
-rw-r--r--.rubocop.yml120
-rw-r--r--.sync.yml41
-rw-r--r--.travis.yml41
-rw-r--r--.yardopts1
-rw-r--r--CHANGELOG.md11
-rw-r--r--Gemfile88
-rw-r--r--README.md43
-rw-r--r--REFERENCE.md180
-rw-r--r--Rakefile75
-rw-r--r--lib/puppet/provider/cron/crontab.rb298
-rw-r--r--lib/puppet/type/cron.rb469
-rw-r--r--locales/config.yaml24
-rw-r--r--locales/puppetlabs-cron_core.pot43
-rw-r--r--metadata.json83
-rw-r--r--spec/acceptance/nodesets/default.yml19
-rw-r--r--spec/acceptance/tests/resource/cron/should_allow_changing_parameters_spec.rb63
-rw-r--r--spec/acceptance/tests/resource/cron/should_be_idempotent_spec.rb32
-rw-r--r--spec/acceptance/tests/resource/cron/should_create_cron_spec.rb31
-rw-r--r--spec/acceptance/tests/resource/cron/should_match_existing_spec.rb33
-rw-r--r--spec/acceptance/tests/resource/cron/should_remove_cron_spec.rb35
-rw-r--r--spec/acceptance/tests/resource/cron/should_remove_leading_and_trailing_whitespace_spec.rb41
-rw-r--r--spec/acceptance/tests/resource/cron/should_remove_matching_spec.rb34
-rw-r--r--spec/acceptance/tests/resource/cron/should_update_existing_spec.rb39
-rw-r--r--spec/default_facts.yml8
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/create_normal_entry19
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/create_special_entry18
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/crontab_user115
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/crontab_user24
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/crontab_user317
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/modify_entry13
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input115
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input26
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/purged8
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/remove_named_resource12
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/remove_unnamed_resource14
-rw-r--r--spec/fixtures/integration/provider/cron/crontab/unspecialized15
-rw-r--r--spec/fixtures/unit/provider/cron/crontab/single_line.yaml272
-rw-r--r--spec/fixtures/unit/provider/cron/crontab/vixie_header.txt3
-rw-r--r--spec/fixtures/unit/provider/cron/parsed/managed6
-rw-r--r--spec/fixtures/unit/provider/cron/parsed/simple9
-rw-r--r--spec/integration/provider/cron/crontab_spec.rb237
-rw-r--r--spec/lib/puppet_spec/compiler.rb112
-rw-r--r--spec/lib/puppet_spec/files.rb112
-rw-r--r--spec/spec_helper.rb44
-rw-r--r--spec/spec_helper_acceptance.rb40
-rw-r--r--spec/spec_helper_local.rb17
-rw-r--r--spec/unit/provider/cron/crontab_spec.rb205
-rw-r--r--spec/unit/provider/cron/parsed_spec.rb335
-rw-r--r--spec/unit/type/cron_spec.rb536
54 files changed, 3995 insertions, 1 deletions
diff --git a/.fixtures.yml b/.fixtures.yml
new file mode 100644
index 0000000..2296adb
--- /dev/null
+++ b/.fixtures.yml
@@ -0,0 +1,6 @@
+# This file can be used to install module dependencies for unit testing
+# See https://github.com/puppetlabs/puppetlabs_spec_helper#using-fixtures for details
+---
+fixtures:
+ forge_modules:
+# stdlib: "puppetlabs/stdlib"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..543dd6a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,4 @@
+*.rb eol=lf
+*.erb eol=lf
+*.pp eol=lf
+*.sh eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..650022e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+.git/
+.*.sw[op]
+.metadata
+.yardoc
+.yardwarns
+*.iml
+/.bundle/
+/.idea/
+/.vagrant/
+/coverage/
+/bin/
+/doc/
+/Gemfile.local
+/Gemfile.lock
+/junit/
+/log/
+/pkg/
+/spec/fixtures/manifests/
+/spec/fixtures/modules/
+/tmp/
+/vendor/
+/convert_report.txt
+/update_report.txt
+.DS_Store
diff --git a/.pdkignore b/.pdkignore
new file mode 100644
index 0000000..650022e
--- /dev/null
+++ b/.pdkignore
@@ -0,0 +1,24 @@
+.git/
+.*.sw[op]
+.metadata
+.yardoc
+.yardwarns
+*.iml
+/.bundle/
+/.idea/
+/.vagrant/
+/coverage/
+/bin/
+/doc/
+/Gemfile.local
+/Gemfile.lock
+/junit/
+/log/
+/pkg/
+/spec/fixtures/manifests/
+/spec/fixtures/modules/
+/tmp/
+/vendor/
+/convert_report.txt
+/update_report.txt
+.DS_Store
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..16f9cdb
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,2 @@
+--color
+--format documentation
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..4b905f3
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,120 @@
+---
+require: rubocop-rspec
+AllCops:
+ DisplayCopNames: true
+ TargetRubyVersion: '2.1'
+ Include:
+ - "./**/*.rb"
+ Exclude:
+ - bin/*
+ - ".vendor/**/*"
+ - "**/Gemfile"
+ - "**/Rakefile"
+ - pkg/**/*
+ - spec/fixtures/**/*
+ - vendor/**/*
+ - "**/Puppetfile"
+ - "**/Vagrantfile"
+ - "**/Guardfile"
+Metrics/LineLength:
+ Description: People have wide screens, use them.
+ Max: 200
+RSpec/BeforeAfterAll:
+ Description: Beware of using after(:all) as it may cause state to leak between tests.
+ A necessary evil in acceptance testing.
+ Exclude:
+ - spec/acceptance/**/*.rb
+RSpec/HookArgument:
+ Description: Prefer explicit :each argument, matching existing module's style
+ EnforcedStyle: each
+Style/BlockDelimiters:
+ Description: Prefer braces for chaining. Mostly an aesthetical choice. Better to
+ be consistent then.
+ EnforcedStyle: braces_for_chaining
+Style/ClassAndModuleChildren:
+ Description: Compact style reduces the required amount of indentation.
+ EnforcedStyle: compact
+Style/EmptyElse:
+ Description: Enforce against empty else clauses, but allow `nil` for clarity.
+ EnforcedStyle: empty
+Style/FormatString:
+ Description: Following the main puppet project's style, prefer the % format format.
+ EnforcedStyle: percent
+Style/FormatStringToken:
+ Description: Following the main puppet project's style, prefer the simpler template
+ tokens over annotated ones.
+ EnforcedStyle: template
+Style/Lambda:
+ Description: Prefer the keyword for easier discoverability.
+ EnforcedStyle: literal
+Style/RegexpLiteral:
+ Description: Community preference. See https://github.com/voxpupuli/modulesync_config/issues/168
+ EnforcedStyle: percent_r
+Style/TernaryParentheses:
+ Description: Checks for use of parentheses around ternary conditions. Enforce parentheses
+ on complex expressions for better readability, but seriously consider breaking
+ it up.
+ EnforcedStyle: require_parentheses_when_complex
+Style/TrailingCommaInArguments:
+ Description: Prefer always trailing comma on multiline argument lists. This makes
+ diffs, and re-ordering nicer.
+ EnforcedStyleForMultiline: comma
+Style/TrailingCommaInLiteral:
+ Description: Prefer always trailing comma on multiline literals. This makes diffs,
+ and re-ordering nicer.
+ EnforcedStyleForMultiline: comma
+Style/SymbolArray:
+ Description: Using percent style obscures symbolic intent of array's contents.
+ EnforcedStyle: brackets
+RSpec/NamedSubject:
+ Enabled: false
+RSpec/MessageSpies:
+ EnforcedStyle: receive
+Style/Documentation:
+ Exclude:
+ - lib/puppet/parser/functions/**/*
+ - spec/**/*
+Style/WordArray:
+ EnforcedStyle: brackets
+Style/CollectionMethods:
+ Enabled: true
+Style/MethodCalledOnDoEndBlock:
+ Enabled: true
+Style/StringMethods:
+ Enabled: true
+Layout/EndOfLine:
+ Enabled: false
+Layout/IndentHeredoc:
+ Enabled: false
+Metrics/AbcSize:
+ Enabled: false
+Metrics/BlockLength:
+ Enabled: false
+Metrics/ClassLength:
+ Enabled: false
+Metrics/CyclomaticComplexity:
+ Enabled: false
+Metrics/MethodLength:
+ Enabled: false
+Metrics/ModuleLength:
+ Enabled: false
+Metrics/ParameterLists:
+ Enabled: false
+Metrics/PerceivedComplexity:
+ Enabled: false
+RSpec/DescribeClass:
+ Enabled: false
+RSpec/ExampleLength:
+ Enabled: false
+RSpec/MessageExpectation:
+ Enabled: false
+RSpec/MultipleExpectations:
+ Enabled: false
+RSpec/NestedGroups:
+ Enabled: false
+Style/AsciiComments:
+ Enabled: false
+Style/IfUnlessModifier:
+ Enabled: false
+Style/SymbolProc:
+ Enabled: false
diff --git a/.sync.yml b/.sync.yml
new file mode 100644
index 0000000..1b0f8ac
--- /dev/null
+++ b/.sync.yml
@@ -0,0 +1,41 @@
+---
+.rubocop.yml:
+ default_configs:
+ RSpec/NamedSubject:
+ Enabled: false
+
+Gemfile:
+ required:
+ ':system_tests':
+ - gem: 'puppet-module-posix-system-r#{minor_version}'
+ platforms: ruby
+ - gem: 'puppet-module-win-system-r#{minor_version}'
+ platforms:
+ - mswin
+ - mingw
+ - x64_mingw
+ - gem: beaker
+ version: '~> 3.34'
+ from_env: BEAKER_VERSION
+ - gem: beaker-abs
+ from_env: BEAKER_ABS_VERSION
+ version: '~> 0.5'
+ - gem: beaker-pe
+ - gem: beaker-hostgenerator
+ from_env: BEAKER_HOSTGENERATOR_VERSION
+ - gem: beaker-rspec
+ from_env: BEAKER_RSPEC_VERSION
+ - gem: beaker-puppet
+ from_env: BEAKER_PUPPET_VERSION
+ version: '~> 0.14'
+
+.gitlab-ci.yml:
+ delete: true
+
+appveyor.yml:
+ delete: true
+
+.travis.yml:
+ remove_includes:
+ - env: PUPPET_GEM_VERSION="~> 4.0" CHECK=parallel_spec
+ rvm: 2.1.9 \ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e36fa84
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,41 @@
+---
+sudo: false
+dist: trusty
+language: ruby
+cache: bundler
+before_install:
+ - bundle -v
+ - rm -f Gemfile.lock
+ - gem update --system
+ - gem --version
+ - bundle -v
+script:
+ - 'bundle exec rake $CHECK'
+bundler_args: --without system_tests
+rvm:
+ - 2.4.4
+env:
+ global:
+ - BEAKER_PUPPET_COLLECTION=puppet5 PUPPET_GEM_VERSION="~> 5.0"
+matrix:
+ fast_finish: true
+ include:
+ -
+ env: CHECK="syntax lint metadata_lint check:symlinks check:git_ignore check:dot_underscore check:test_file rubocop"
+ -
+ env: CHECK=parallel_spec
+branches:
+ only:
+ - master
+ - /^v\d/
+notifications:
+ email: false
+deploy:
+ provider: puppetforge
+ user: puppet
+ password:
+ secure: ""
+ on:
+ tags: true
+ all_branches: true
+ condition: "$DEPLOY_TO_FORGE = yes"
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 0000000..29c933b
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1 @@
+--markup markdown
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..4c954cd
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## Release 0.1.0
+
+**Features**
+
+**Bugfixes**
+
+**Known Issues**
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..b7a0fce
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,88 @@
+source ENV['GEM_SOURCE'] || 'https://rubygems.org'
+
+def location_for(place_or_version, fake_version = nil)
+ if place_or_version =~ %r{\A(git[:@][^#]*)#(.*)}
+ [fake_version, { git: Regexp.last_match(1), branch: Regexp.last_match(2), require: false }].compact
+ elsif place_or_version =~ %r{\Afile:\/\/(.*)}
+ ['>= 0', { path: File.expand_path(Regexp.last_match(1)), require: false }]
+ else
+ [place_or_version, { require: false }]
+ end
+end
+
+def gem_type(place_or_version)
+ if place_or_version =~ %r{\Agit[:@]}
+ :git
+ elsif !place_or_version.nil? && place_or_version.start_with?('file:')
+ :file
+ else
+ :gem
+ end
+end
+
+ruby_version_segments = Gem::Version.new(RUBY_VERSION.dup).segments
+minor_version = ruby_version_segments[0..1].join('.')
+
+group :development do
+ gem "fast_gettext", '1.1.0', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.1.0')
+ gem "fast_gettext", require: false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0')
+ gem "json_pure", '<= 2.0.1', require: false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0')
+ gem "json", '= 1.8.1', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.1.9')
+ gem "json", '<= 2.0.4', require: false if Gem::Version.new(RUBY_VERSION.dup) == Gem::Version.new('2.4.4')
+ gem "puppet-module-posix-default-r#{minor_version}", require: false, platforms: [:ruby]
+ gem "puppet-module-posix-dev-r#{minor_version}", require: false, platforms: [:ruby]
+ gem "puppet-module-win-default-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw]
+ gem "puppet-module-win-dev-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw]
+end
+group :system_tests do
+ gem "puppet-module-posix-system-r#{minor_version}", require: false, platforms: [:ruby]
+ gem "puppet-module-win-system-r#{minor_version}", require: false, platforms: [:mswin, :mingw, :x64_mingw]
+ gem "beaker", *location_for(ENV['BEAKER_VERSION'] || '~> 3.34')
+ gem "beaker-abs", *location_for(ENV['BEAKER_ABS_VERSION'] || '~> 0.5')
+ gem "beaker-pe", require: false
+ gem "beaker-hostgenerator"
+ gem "beaker-rspec"
+ gem "beaker-puppet", *location_for(ENV['BEAKER_PUPPET_VERSION'] || '~> 0.14')
+end
+
+puppet_version = ENV['PUPPET_GEM_VERSION']
+puppet_type = gem_type(puppet_version)
+facter_version = ENV['FACTER_GEM_VERSION']
+hiera_version = ENV['HIERA_GEM_VERSION']
+
+gems = {}
+
+gems['puppet'] = location_for(puppet_version)
+
+# If facter or hiera versions have been specified via the environment
+# variables
+
+gems['facter'] = location_for(facter_version) if facter_version
+gems['hiera'] = location_for(hiera_version) if hiera_version
+
+if Gem.win_platform? && puppet_version =~ %r{^(file:///|git://)}
+ # If we're using a Puppet gem on Windows which handles its own win32-xxx gem
+ # dependencies (>= 3.5.0), set the maximum versions (see PUP-6445).
+ gems['win32-dir'] = ['<= 0.4.9', require: false]
+ gems['win32-eventlog'] = ['<= 0.6.5', require: false]
+ gems['win32-process'] = ['<= 0.7.5', require: false]
+ gems['win32-security'] = ['<= 0.2.5', require: false]
+ gems['win32-service'] = ['0.8.8', require: false]
+end
+
+gems.each do |gem_name, gem_params|
+ gem gem_name, *gem_params
+end
+
+# Evaluate Gemfile.local and ~/.gemfile if they exist
+extra_gemfiles = [
+ "#{__FILE__}.local",
+ File.join(Dir.home, '.gemfile'),
+]
+
+extra_gemfiles.each do |gemfile|
+ if File.file?(gemfile) && File.readable?(gemfile)
+ eval(File.read(gemfile), binding)
+ end
+end
+# vim: syntax=ruby
diff --git a/README.md b/README.md
index 0ae356e..14235c0 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,42 @@
-# puppetlabs-cron_core \ No newline at end of file
+
+# cron_core
+
+#### Table of Contents
+
+1. [Description](#description)
+3. [Reference](#reference)
+4. [Limitations - OS compatibility, etc.](#limitations)
+5. [Development - Guide for contributing to the module](#development)
+
+## Description
+
+Install and manage `cron` resources.
+
+## Reference
+
+Please see `REFERENCE.md` for the reference documentation.
+
+This module is documented using Puppet Strings.
+
+For a quick primer on how Strings works, please see [this blog post](https://puppet.com/blog/using-puppet-strings-generate-great-documentation-puppet-modules) or the [README.md](https://github.com/puppetlabs/puppet-strings/blob/master/README.md) for Puppet Strings.
+
+To generate documentation locally, run:
+
+```
+bundle install
+bundle exec puppet strings generate ./lib/**/*.rb
+```
+
+This command will create a browsable \_index.html file in the doc directory. The references available here are all generated from YARD-style comments embedded in the code base. When any development happens on this module, the impacted documentation should also be updated.
+
+## Limitations
+
+`cron` is not compatible Windows or Fedora 28.
+
+## Development
+
+Puppet Labs modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can't access the huge number of platforms and myriad of hardware, software, and deployment configurations that Puppet is intended to serve.
+
+We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things.
+
+For more information, see our [module contribution guide](https://docs.puppetlabs.com/forge/contributing.html). \ No newline at end of file
diff --git a/REFERENCE.md b/REFERENCE.md
new file mode 100644
index 0000000..79c1e42
--- /dev/null
+++ b/REFERENCE.md
@@ -0,0 +1,180 @@
+# Reference
+<!-- DO NOT EDIT: This document was generated by Puppet Strings -->
+
+## Table of Contents
+
+**Resource types**
+
+* [`cron`](#cron): Installs and manages cron jobs. Every cron resource created by Puppet requires a command and at least one periodic attribute (hour, minute, m
+
+## Resource types
+
+### cron
+
+Installs and manages cron jobs. Every cron resource created by Puppet
+requires a command and at least one periodic attribute (hour, minute,
+month, monthday, weekday, or special). While the name of the cron job is
+not part of the actual job, the name is stored in a comment beginning with
+`# Puppet Name: `. These comments are used to match crontab entries created
+by Puppet with cron resources.
+
+If an existing crontab entry happens to match the scheduling and command of a
+cron resource that has never been synced, Puppet defers to the existing
+crontab entry and does not create a new entry tagged with the `# Puppet Name: `
+comment.
+
+Example:
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => 2,
+ minute => 0,
+ }
+
+Note that all periodic attributes can be specified as an array of values:
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => [2, 4],
+ }
+
+...or using ranges or the step syntax `*/2` (although there's no guarantee
+that your `cron` daemon supports these):
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => ['2-4'],
+ minute => '*/10',
+ }
+
+**Important:** _The Cron type will not reset parameters that are
+removed from a manifest_. For example, removing a `minute => 10` parameter
+will not reset the minute component of the associated cronjob to `*`.
+These changes must be expressed by setting the parameter to
+`minute => absent` because Puppet only manages parameters that are out of
+sync with manifest entries.
+
+**Autorequires:** If Puppet is managing the user account specified by the
+`user` property of a cron resource, then the cron resource will autorequire
+that user.
+
+#### Properties
+
+The following properties are available in the `cron` type.
+
+##### `ensure`
+
+Valid values: present, absent
+
+The basic property that the resource should be in.
+
+Default value: present
+
+##### `command`
+
+The command to execute in the cron job. The environment
+provided to the command varies by local system rules, and it is
+best to always provide a fully qualified command. The user's
+profile is not sourced when the command is run, so if the
+user's environment is desired it should be sourced manually.
+
+All cron parameters support `absent` as a value; this will
+remove any existing values for that field.
+
+##### `special`
+
+A special value such as 'reboot' or 'annually'.
+Only available on supported systems such as Vixie Cron.
+Overrides more specific time of day/week settings.
+Set to 'absent' to make puppet revert to a plain numeric schedule.
+
+##### `minute`
+
+The minute at which to run the cron job.
+Optional; if specified, must be between 0 and 59, inclusive.
+
+##### `hour`
+
+The hour at which to run the cron job. Optional;
+if specified, must be between 0 and 23, inclusive.
+
+##### `weekday`
+
+The weekday on which to run the command. Optional; if specified,
+must be either:
+
+- A number between 0 and 7, inclusive, with 0 or 7 being Sunday
+- The name of the day, such as 'Tuesday'.
+
+##### `month`
+
+The month of the year. Optional; if specified,
+must be either:
+
+- A number between 1 and 12, inclusive, with 1 being January
+- The name of the month, such as 'December'.
+
+##### `monthday`
+
+The day of the month on which to run the
+command. Optional; if specified, must be between 1 and 31.
+
+##### `environment`
+
+Any environment settings associated with this cron job. They
+will be stored between the header and the job in the crontab. There
+can be no guarantees that other, earlier settings will not also
+affect a given cron job.
+
+
+Also, Puppet cannot automatically determine whether an existing,
+unmanaged environment setting is associated with a given cron
+job. If you already have cron jobs with environment settings,
+then Puppet will keep those settings in the same place in the file,
+but will not associate them with a specific job.
+
+Settings should be specified exactly as they should appear in
+the crontab, like `PATH=/bin:/usr/bin:/usr/sbin`.
+
+##### `user`
+
+The user who owns the cron job. This user must
+be allowed to run cron jobs, which is not currently checked by
+Puppet.
+
+This property defaults to the user running Puppet or `root`.
+
+The default crontab provider executes the system `crontab` using
+the user account specified by this property.
+
+##### `target`
+
+The name of the crontab file in which the cron job should be stored.
+
+This property defaults to the value of the `user` property if set, the
+user running Puppet or `root`.
+
+For the default crontab provider, this property is functionally
+equivalent to the `user` property and should be avoided. In particular,
+setting both `user` and `target` to different values will result in
+undefined behavior.
+
+#### Parameters
+
+The following parameters are available in the `cron` type.
+
+##### `name`
+
+namevar
+
+The symbolic name of the cron job. This name
+is used for human reference only and is generated automatically
+for cron jobs found on the system. This generally won't
+matter, as Puppet will do its best to match existing cron jobs
+against specified jobs (and Puppet adds a comment to cron jobs it adds),
+but it is at least possible that converting from unmanaged jobs to
+managed jobs might require manual intervention.
+
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..204fb18
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,75 @@
+require 'puppetlabs_spec_helper/rake_tasks'
+require 'puppet-syntax/tasks/puppet-syntax'
+require 'puppet_blacksmith/rake_tasks' if Bundler.rubygems.find_name('puppet-blacksmith').any?
+require 'github_changelog_generator/task' if Bundler.rubygems.find_name('github_changelog_generator').any?
+
+def changelog_user
+ return unless Rake.application.top_level_tasks.include? "changelog"
+ returnVal = nil || JSON.load(File.read('metadata.json'))['author']
+ raise "unable to find the changelog_user in .sync.yml, or the author in metadata.json" if returnVal.nil?
+ puts "GitHubChangelogGenerator user:#{returnVal}"
+ returnVal
+end
+
+def changelog_project
+ return unless Rake.application.top_level_tasks.include? "changelog"
+ returnVal = nil || JSON.load(File.read('metadata.json'))['name']
+ raise "unable to find the changelog_project in .sync.yml or the name in metadata.json" if returnVal.nil?
+ puts "GitHubChangelogGenerator project:#{returnVal}"
+ returnVal
+end
+
+def changelog_future_release
+ return unless Rake.application.top_level_tasks.include? "changelog"
+ returnVal = JSON.load(File.read('metadata.json'))['version']
+ raise "unable to find the future_release (version) in metadata.json" if returnVal.nil?
+ puts "GitHubChangelogGenerator future_release:#{returnVal}"
+ returnVal
+end
+
+PuppetLint.configuration.send('disable_relative')
+
+if Bundler.rubygems.find_name('github_changelog_generator').any?
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
+ raise "Set CHANGELOG_GITHUB_TOKEN environment variable eg 'export CHANGELOG_GITHUB_TOKEN=valid_token_here'" if Rake.application.top_level_tasks.include? "changelog" and ENV['CHANGELOG_GITHUB_TOKEN'].nil?
+ config.user = "#{changelog_user}"
+ config.project = "#{changelog_project}"
+ config.future_release = "#{changelog_future_release}"
+ config.exclude_labels = ['maintenance']
+ config.header = "# Change log\n\nAll notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org)."
+ config.add_pr_wo_labels = true
+ config.issues = false
+ config.merge_prefix = "### UNCATEGORIZED PRS; GO LABEL THEM"
+ config.configure_sections = {
+ "Changed" => {
+ "prefix" => "### Changed",
+ "labels" => ["backwards-incompatible"],
+ },
+ "Added" => {
+ "prefix" => "### Added",
+ "labels" => ["feature", "enhancement"],
+ },
+ "Fixed" => {
+ "prefix" => "### Fixed",
+ "labels" => ["bugfix"],
+ },
+ }
+ end
+else
+ desc 'Generate a Changelog from GitHub'
+ task :changelog do
+ raise <<EOM
+The changelog tasks depends on unreleased features of the github_changelog_generator gem.
+Please manually add it to your .sync.yml for now, and run `pdk update`:
+---
+Gemfile:
+ optional:
+ ':development':
+ - gem: 'github_changelog_generator'
+ git: 'https://github.com/skywinder/github-changelog-generator'
+ ref: '20ee04ba1234e9e83eb2ffb5056e23d641c7a018'
+ condition: "Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.2.2')"
+EOM
+ end
+end
+
diff --git a/lib/puppet/provider/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb
new file mode 100644
index 0000000..fec5511
--- /dev/null
+++ b/lib/puppet/provider/cron/crontab.rb
@@ -0,0 +1,298 @@
+require 'puppet/provider/parsedfile'
+
+Puppet::Type.type(:cron).provide(:crontab, parent: Puppet::Provider::ParsedFile, default_target: ENV['USER'] || 'root') do
+ commands crontab: 'crontab'
+
+ text_line :comment, match: %r{^\s*#}, post_parse: proc { |record|
+ record[:name] = Regexp.last_match(1) if record[:line] =~ %r{Puppet Name: (.+)\s*$}
+ }
+
+ text_line :blank, match: %r{^\s*$}
+
+ text_line :environment, match: %r{^\s*\w+\s*=}
+
+ def self.filetype
+ tabname = case Facter.value(:osfamily)
+ when 'Solaris'
+ :suntab
+ when 'AIX'
+ :aixtab
+ else
+ :crontab
+ end
+
+ Puppet::Util::FileType.filetype(tabname)
+ end
+
+ self::TIME_FIELDS = [:minute, :hour, :monthday, :month, :weekday].freeze
+
+ record_line :crontab,
+ fields: ['time', 'command'],
+ match: %r{^\s*(@\w+|\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$},
+ absent: '*',
+ block_eval: :instance do
+
+ def post_parse(record)
+ time = record.delete(:time)
+ match = %r{@(\S+)}.match(time)
+ if match
+ # is there another way to access the constant?
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each { |f| record[f] = :absent }
+ record[:special] = match.captures[0]
+ return record
+ end
+
+ match = %r{(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)}.match(time)
+ if match
+ record[:special] = :absent
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.zip(match.captures).each do |field, value|
+ record[field] = if value == absent
+ :absent
+ else
+ value.split(',')
+ end
+ end
+
+ return record
+ end
+
+ raise Puppet::Error, _('Line got parsed as a crontab entry but cannot be handled. Please file a bug with the contents of your crontab')
+ end
+
+ def pre_gen(record)
+ if record[:special] && record[:special] != :absent
+ record[:special] = "@#{record[:special]}"
+ end
+
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each do |field|
+ if (vals = record[field]) && vals.is_a?(Array)
+ record[field] = vals.join(',')
+ end
+ end
+ record
+ end
+
+ def to_line(record)
+ str = ''
+ record[:name] = nil if record[:unmanaged]
+ str = "# Puppet Name: #{record[:name]}\n" if record[:name]
+ if record[:environment] && record[:environment] != :absent
+ str += record[:environment].map { |line| "#{line}\n" }.join('')
+ end
+ fields = if record[:special] && record[:special] != :absent
+ [:special, :command]
+ else
+ Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS + [:command]
+ end
+ str += record.values_at(*fields).map { |field|
+ if field.nil? || field == :absent
+ absent
+ else
+ field
+ end
+ }.join(joiner)
+ str
+ end
+ end
+
+ def create
+ if resource.should(:command)
+ super
+ else
+ resource.err _('no command specified, cannot create')
+ end
+ end
+
+ # Look up a resource with a given name whose user matches a record target
+ #
+ # @api private
+ #
+ # @note This overrides the ParsedFile method for finding resources by name,
+ # so that only records for a given user are matched to resources of the
+ # same user so that orphaned records in other crontabs don't get falsely
+ # matched (#2251)
+ #
+ # @param [Hash<Symbol, Object>] record
+ # @param [Array<Puppet::Resource>] resources
+ #
+ # @return [Puppet::Resource, nil] The resource if found, else nil
+ def self.resource_for_record(record, resources)
+ resource = super
+
+ target = resource[:target] || resource[:user] if resource
+ return resource if record[:target] == target
+ end
+
+ # Return the header placed at the top of each generated file, warning
+ # users that modifying this file manually is probably a bad idea.
+ def self.header
+ %(# HEADER: This file was autogenerated at #{Time.now} by puppet.
+# HEADER: While it can still be managed manually, it is definitely not recommended.
+# HEADER: Note particularly that the comments starting with 'Puppet Name' should
+# HEADER: not be deleted, as doing so could cause duplicate cron jobs.\n)
+ end
+
+ # Regex for finding one vixie cron header.
+ def self.native_header_regex
+ %r{# DO NOT EDIT THIS FILE.*?Cron version.*?vixie.*?\n}m
+ end
+
+ # If a vixie cron header is found, it should be dropped, cron will insert
+ # a new one in any case, so we need to avoid duplicates.
+ def self.drop_native_header
+ true
+ end
+
+ # See if we can match the record against an existing cron job.
+ def self.match(record, resources)
+ # if the record is named, do not even bother (#19876)
+ # except the resource name was implicitly generated (#3220)
+ return false if record[:name] && !record[:unmanaged]
+ resources.each do |_name, resource|
+ # Match the command first, since it's the most important one.
+ next unless record[:target] == resource[:target]
+ next unless record[:command] == resource.value(:command)
+
+ # Now check the time fields
+ compare_fields = self::TIME_FIELDS + [:special]
+
+ matched = true
+ compare_fields.each do |field|
+ # If the resource does not manage a property (say monthday) it should
+ # always match. If it is the other way around (e.g. resource defines
+ # a should value for :special but the record does not have it, we do
+ # not match
+ next unless resource[field]
+ unless record.include?(field)
+ matched = false
+ break
+ end
+
+ if (record_value = record[field]) && (resource_value = resource.value(field))
+ # The record translates '*' into absent in the post_parse hook and
+ # the resource type does exactly the opposite (alias :absent to *)
+ next if resource_value == '*' && record_value == :absent
+ next if resource_value == record_value
+ end
+ matched = false
+ break
+ end
+ return resource if matched
+ end
+ false
+ end
+
+ @name_index = 0
+
+ # Collapse name and env records.
+ def self.prefetch_hook(records)
+ name = nil
+ envs = nil
+ result = []
+ records.each do |record|
+ case record[:record_type]
+ when :comment
+ if record[:name]
+ name = record[:name]
+ record[:skip] = true
+
+ # Start collecting env values
+ envs = []
+ end
+ when :environment
+ # If we're collecting env values (meaning we're in a named cronjob),
+ # store the line and skip the record.
+ if envs
+ envs << record[:line]
+ record[:skip] = true
+ end
+ when :blank # rubocop: disable Lint/EmptyWhen
+ # nothing
+ else
+ if name
+ record[:name] = name
+ name = nil
+ else
+ cmd_string = record[:command].gsub(%r{\s+}, '_')
+ index = (@name_index += 1)
+ record[:name] = "unmanaged:#{cmd_string}-#{index}"
+ record[:unmanaged] = true
+ end
+ if envs.nil? || envs.empty?
+ record[:environment] = :absent
+ else
+ # Collect all of the environment lines, and mark the records to be skipped,
+ # since their data is included in our crontab record.
+ record[:environment] = envs
+
+ # And turn off env collection again
+ envs = nil
+ end
+ end
+ result << record unless record[:skip]
+ end
+ result
+ end
+
+ def self.to_file(records)
+ text = super
+ # Apparently Freebsd will "helpfully" add a new TZ line to every
+ # single cron line, but not in all cases (e.g., it doesn't do it
+ # on my machine). This is my attempt to fix it so the TZ lines don't
+ # multiply.
+ if text =~ %r{(^TZ=.+\n)}
+ tz = Regexp.last_match(1)
+ text.sub!(tz, '')
+ text = tz + text
+ end
+ text
+ end
+
+ def user=(user)
+ # we have to mark the target as modified first, to make sure that if
+ # we move a cronjob from userA to userB, userA's crontab will also
+ # be rewritten
+ mark_target_modified
+ @property_hash[:user] = user
+ @property_hash[:target] = user
+ end
+
+ def user
+ @property_hash[:user] || @property_hash[:target]
+ end
+
+ CRONTAB_DIR = case Facter.value('osfamily')
+ when 'Debian', 'HP-UX'
+ '/var/spool/cron/crontabs'
+ when %r{BSD}
+ '/var/cron/tabs'
+ when 'Darwin'
+ '/usr/lib/cron/tabs/'
+ else
+ '/var/spool/cron'
+ end
+
+ # Yield the names of all crontab files stored on the local system.
+ #
+ # @note Ignores files that are not writable for the puppet process.
+ #
+ # @api private
+ def self.enumerate_crontabs
+ Puppet.debug "looking for crontabs in #{CRONTAB_DIR}"
+ return unless File.readable?(CRONTAB_DIR)
+ Dir.foreach(CRONTAB_DIR) do |file|
+ path = "#{CRONTAB_DIR}/#{file}"
+ yield(file) if File.file?(path) && File.writable?(path)
+ end
+ end
+
+ # Include all plausible crontab files on the system
+ # in the list of targets (#11383 / PUP-1381)
+ def self.targets(resources = nil)
+ targets = super(resources)
+ enumerate_crontabs do |target|
+ targets << target
+ end
+ targets.uniq
+ end
+end
diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb
new file mode 100644
index 0000000..e07ce70
--- /dev/null
+++ b/lib/puppet/type/cron.rb
@@ -0,0 +1,469 @@
+require 'etc'
+require 'facter'
+require 'puppet/util/filetype'
+
+Puppet::Type.newtype(:cron) do
+ @doc = <<-'EOT'
+ Installs and manages cron jobs. Every cron resource created by Puppet
+ requires a command and at least one periodic attribute (hour, minute,
+ month, monthday, weekday, or special). While the name of the cron job is
+ not part of the actual job, the name is stored in a comment beginning with
+ `# Puppet Name: `. These comments are used to match crontab entries created
+ by Puppet with cron resources.
+
+ If an existing crontab entry happens to match the scheduling and command of a
+ cron resource that has never been synced, Puppet defers to the existing
+ crontab entry and does not create a new entry tagged with the `# Puppet Name: `
+ comment.
+
+ Example:
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => 2,
+ minute => 0,
+ }
+
+ Note that all periodic attributes can be specified as an array of values:
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => [2, 4],
+ }
+
+ ...or using ranges or the step syntax `*/2` (although there's no guarantee
+ that your `cron` daemon supports these):
+
+ cron { 'logrotate':
+ command => '/usr/sbin/logrotate',
+ user => 'root',
+ hour => ['2-4'],
+ minute => '*/10',
+ }
+
+ **Important:** _The Cron type will not reset parameters that are
+ removed from a manifest_. For example, removing a `minute => 10` parameter
+ will not reset the minute component of the associated cronjob to `*`.
+ These changes must be expressed by setting the parameter to
+ `minute => absent` because Puppet only manages parameters that are out of
+ sync with manifest entries.
+
+ **Autorequires:** If Puppet is managing the user account specified by the
+ `user` property of a cron resource, then the cron resource will autorequire
+ that user.
+ EOT
+ ensurable
+
+ # A base class for all of the Cron parameters, since they all have
+ # similar argument checking going on.
+ class CronParam < Puppet::Property
+ class << self
+ attr_accessor :boundaries, :default
+ end
+
+ # We have to override the parent method, because we consume the entire
+ # "should" array
+ def insync?(is)
+ is_to_s(is) == should_to_s
+ end
+
+ # A method used to do parameter input handling. Converts integers
+ # in string form to actual integers, and returns the value if it's
+ # an integer or false if it's just a normal string.
+ def numfix(num)
+ if num =~ %r{^\d+$}
+ num.to_i
+ elsif num.is_a?(Integer)
+ num
+ else
+ false
+ end
+ end
+
+ # Verify that a number is within the specified limits. Return the
+ # number if it is, or false if it is not.
+ def limitcheck(num, lower, upper)
+ (num >= lower && num <= upper) && num
+ end
+
+ # Verify that a value falls within the specified array. Does case
+ # insensitive matching, and supports matching either the entire word
+ # or the first three letters of the word.
+ def alphacheck(value, ary)
+ tmp = value.downcase
+
+ # If they specified a shortened version of the name, then see
+ # if we can lengthen it (e.g., mon => monday).
+ if tmp.length == 3
+ ary.each_with_index do |name, index|
+ if tmp.casecmp(name[0..2]).zero?
+ return index
+ end
+ end
+ elsif ary.include?(tmp)
+ return ary.index(tmp)
+ end
+
+ false
+ end
+
+ def should_to_s(value = @should)
+ if value
+ if value.is_a?(Array) && (name == :command || value[0].is_a?(Symbol))
+ value = value[0]
+ end
+ super(value)
+ else
+ nil
+ end
+ end
+
+ def is_to_s(value = @is) # rubocop: disable Style/PredicateName
+ if value
+ if value.is_a?(Array) && (name == :command || value[0].is_a?(Symbol))
+ value = value[0]
+ end
+ super(value)
+ else
+ nil
+ end
+ end
+
+ def should
+ if @should && @should[0] == :absent
+ :absent
+ else
+ @should
+ end
+ end
+
+ def should=(ary)
+ super
+ @should.flatten!
+ end
+
+ # The method that does all of the actual parameter value
+ # checking; called by all of the +param<name>=+ methods.
+ # Requires the value, type, and bounds, and optionally supports
+ # a boolean of whether to do alpha checking, and if so requires
+ # the ary against which to do the checking.
+ munge do |value|
+ # Support 'absent' as a value, so that they can remove
+ # a value
+ if ['absent', :absent].include? value
+ return :absent
+ end
+
+ # Allow step syntax
+ if value =~ %r{^\*/[0-9]+$}
+ return value
+ end
+
+ # Allow ranges
+ if value =~ %r{^[0-9]+-[0-9]+$}
+ return value
+ end
+
+ # Allow ranges with step
+ if value =~ %r{^[0-9]+-[0-9]+/[0-9]+$}
+ return value
+ end
+
+ if value == '*'
+ return :absent
+ end
+
+ return value unless self.class.boundaries
+ lower, upper = self.class.boundaries
+ retval = nil
+ num = numfix(value)
+ if num
+ retval = limitcheck(num, lower, upper)
+ elsif respond_to?(:alpha)
+ # If it has an alpha method defined, then we check
+ # to see if our value is in that list and if so we turn
+ # it into a number
+ retval = alphacheck(value, alpha)
+ end
+
+ raise _('%{value} is not a valid %{name}') % { value: value, name: self.class.name } unless retval
+ return retval.to_s if retval
+ end
+ end
+
+ # Somewhat uniquely, this property does not actually change anything -- it
+ # just calls +@resource.sync+, which writes out the whole cron tab for
+ # the user in question. There is no real way to change individual cron
+ # jobs without rewriting the entire cron file.
+ #
+ # Note that this means that managing many cron jobs for a given user
+ # could currently result in multiple write sessions for that user.
+ newproperty(:command, parent: CronParam) do
+ desc "The command to execute in the cron job. The environment
+ provided to the command varies by local system rules, and it is
+ best to always provide a fully qualified command. The user's
+ profile is not sourced when the command is run, so if the
+ user's environment is desired it should be sourced manually.
+
+ All cron parameters support `absent` as a value; this will
+ remove any existing values for that field."
+
+ def retrieve
+ return_value = super
+ return_value = return_value[0] if return_value && return_value.is_a?(Array)
+
+ return_value
+ end
+
+ def should
+ if @should
+ if @should.is_a? Array
+ @should[0]
+ else
+ devfail 'command is not an array'
+ end
+ else
+ nil
+ end
+ end
+
+ def munge(value)
+ value.strip
+ end
+ end
+
+ newproperty(:special) do
+ desc "A special value such as 'reboot' or 'annually'.
+ Only available on supported systems such as Vixie Cron.
+ Overrides more specific time of day/week settings.
+ Set to 'absent' to make puppet revert to a plain numeric schedule."
+
+ def specials
+ ['reboot', 'yearly', 'annually', 'monthly', 'weekly', 'daily', 'midnight', 'hourly', 'absent'] +
+ [:absent]
+ end
+
+ validate do |value|
+ raise ArgumentError, _('Invalid special schedule %{value}') % { value: value.inspect } unless specials.include?(value)
+ end
+
+ def munge(value)
+ # Support value absent so that a schedule can be
+ # forced to change to numeric.
+ if ['absent', :absent].include? value
+ return :absent
+ end
+ value
+ end
+ end
+
+ newproperty(:minute, parent: CronParam) do
+ self.boundaries = [0, 59]
+ desc "The minute at which to run the cron job.
+ Optional; if specified, must be between 0 and 59, inclusive."
+ end
+
+ newproperty(:hour, parent: CronParam) do
+ self.boundaries = [0, 23]
+ desc "The hour at which to run the cron job. Optional;
+ if specified, must be between 0 and 23, inclusive."
+ end
+
+ newproperty(:weekday, parent: CronParam) do
+ def alpha
+ ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']
+ end
+ self.boundaries = [0, 7]
+ desc "The weekday on which to run the command. Optional; if specified,
+ must be either:
+
+ - A number between 0 and 7, inclusive, with 0 or 7 being Sunday
+ - The name of the day, such as 'Tuesday'."
+ end
+
+ newproperty(:month, parent: CronParam) do
+ def alpha
+ # The ___placeholder accounts for the fact that month is unique among
+ # "nameable" crontab entries in that it does not use 0-based indexing.
+ # Padding the array with a placeholder introduces the appropriate shift
+ # in indices.
+ ['___placeholder', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december']
+ end
+ self.boundaries = [1, 12]
+ desc "The month of the year. Optional; if specified,
+ must be either:
+
+ - A number between 1 and 12, inclusive, with 1 being January
+ - The name of the month, such as 'December'."
+ end
+
+ newproperty(:monthday, parent: CronParam) do
+ self.boundaries = [1, 31]
+ desc "The day of the month on which to run the
+ command. Optional; if specified, must be between 1 and 31."
+ end
+
+ newproperty(:environment) do
+ desc "Any environment settings associated with this cron job. They
+ will be stored between the header and the job in the crontab. There
+ can be no guarantees that other, earlier settings will not also
+ affect a given cron job.
+
+
+ Also, Puppet cannot automatically determine whether an existing,
+ unmanaged environment setting is associated with a given cron
+ job. If you already have cron jobs with environment settings,
+ then Puppet will keep those settings in the same place in the file,
+ but will not associate them with a specific job.
+
+ Settings should be specified exactly as they should appear in
+ the crontab, like `PATH=/bin:/usr/bin:/usr/sbin`."
+
+ validate do |value|
+ unless value =~ %r{^\s*(\w+)\s*=\s*(.*)\s*$} || value == :absent || value == 'absent'
+ raise ArgumentError, _('Invalid environment setting %{value}') % { value: value.inspect }
+ end
+ end
+
+ def insync?(is)
+ if is.is_a? Array
+ is.sort == @should.sort
+ else
+ is == @should
+ end
+ end
+
+ attr_reader :should
+
+ def should_to_s(newvalue = @should)
+ if newvalue
+ newvalue.join(',')
+ else
+ nil
+ end
+ end
+ end
+
+ newparam(:name) do
+ desc "The symbolic name of the cron job. This name
+ is used for human reference only and is generated automatically
+ for cron jobs found on the system. This generally won't
+ matter, as Puppet will do its best to match existing cron jobs
+ against specified jobs (and Puppet adds a comment to cron jobs it adds),
+ but it is at least possible that converting from unmanaged jobs to
+ managed jobs might require manual intervention."
+
+ isnamevar
+ end
+
+ newproperty(:user) do
+ desc "The user who owns the cron job. This user must
+ be allowed to run cron jobs, which is not currently checked by
+ Puppet.
+
+ This property defaults to the user running Puppet or `root`.
+
+ The default crontab provider executes the system `crontab` using
+ the user account specified by this property."
+
+ defaultto do
+ unless provider.is_a?(@resource.class.provider(:crontab))
+ struct = Etc.getpwuid(Process.uid)
+ struct.respond_to?(:name) && struct.name || 'root'
+ end
+ end
+ end
+
+ # Autorequire the owner of the crontab entry.
+ autorequire(:user) do
+ self[:user]
+ end
+
+ newproperty(:target) do
+ desc "The name of the crontab file in which the cron job should be stored.
+
+ This property defaults to the value of the `user` property if set, the
+ user running Puppet or `root`.
+
+ For the default crontab provider, this property is functionally
+ equivalent to the `user` property and should be avoided. In particular,
+ setting both `user` and `target` to different values will result in
+ undefined behavior."
+
+ defaultto do
+ if provider.is_a?(@resource.class.provider(:crontab))
+ val = @resource.should(:user)
+ if val
+ val
+ else
+ struct = Etc.getpwuid(Process.uid)
+ struct.respond_to?(:name) && struct.name || 'root'
+ end
+ elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile)
+ provider.class.default_target
+ else
+ nil
+ end
+ end
+ end
+
+ validate do
+ return true unless self[:special]
+ return true if self[:special] == :absent
+ # there is a special schedule in @should, so we don't want to see
+ # any numeric should values
+ [:minute, :hour, :weekday, :monthday, :month].each do |field|
+ next unless self[field]
+ next if self[field] == :absent
+ raise ArgumentError, _('%{cron} cannot specify both a special schedule and a value for %{field}') % { cron: ref, field: field }
+ end
+ end
+
+ # We have to reorder things so that :provide is before :target
+
+ attr_accessor :uid
+
+ # Marks the resource as "being purged".
+ #
+ # @api public
+ #
+ # @note This overrides the Puppet::Type method in order to handle
+ # an edge case that has so far been observed during testing only.
+ # Without forcing the should-value for the user property to be
+ # identical to the original cron file, purging from a fixture
+ # will not work, because the user property defaults to the user
+ # running the test. It is not clear whether this scenario can apply
+ # during normal operation.
+ #
+ # @note Also, when not forcing the should-value for the target
+ # property, unpurged file content (such as comments) can end up
+ # being written to the default target (i.e. the current login name).
+ def purging
+ self[:target] = provider.target
+ self[:user] = provider.target
+ super
+ end
+
+ def value(name)
+ name = name.to_sym
+ ret = nil
+ obj = @parameters[name]
+ if obj
+ ret = obj.should || obj.retrieve
+
+ if ret == :absent
+ ret = nil
+ end
+ end
+
+ unless ret
+ unless [:command, :special].include? name
+ ret = '*'
+ end
+ end
+
+ ret
+ end
+end
diff --git a/locales/config.yaml b/locales/config.yaml
new file mode 100644
index 0000000..73037b7
--- /dev/null
+++ b/locales/config.yaml
@@ -0,0 +1,24 @@
+# This is the project-specific configuration file for setting up
+# fast_gettext for your project.
+gettext:
+ # This is used for the name of the .pot and .po files; they will be
+ # called <project_name>.pot?
+ project_name: puppetlabs-cron_core
+ # This is used in comments in the .pot and .po files to indicate what
+ # project the files belong to and should bea little more desctiptive than
+ # <project_name>
+ package_name: puppetlabs-cron_core
+ # The locale that the default messages in the .pot file are in
+ default_locale: en
+ # The email used for sending bug reports.
+ bugs_address: docs@puppet.com
+ # The holder of the copyright.
+ copyright_holder: Puppet, Inc.
+ # This determines which comments in code should be eligible for translation.
+ # Any comments that start with this string will be externalized. (Leave
+ # empty to include all.)
+ comments_tag: TRANSLATOR
+ # Patterns for +Dir.glob+ used to find all files that might contain
+ # translatable content, relative to the project root directory
+ source_files:
+ - './lib/**/*.rb' \ No newline at end of file
diff --git a/locales/puppetlabs-cron_core.pot b/locales/puppetlabs-cron_core.pot
new file mode 100644
index 0000000..dcc12a2
--- /dev/null
+++ b/locales/puppetlabs-cron_core.pot
@@ -0,0 +1,43 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) 2018 Puppet, Inc.
+# This file is distributed under the same license as the puppetlabs-cron_core package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2018.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: puppetlabs-cron_core \n"
+"Report-Msgid-Bugs-To: docs@puppet.com\n"
+"POT-Creation-Date: 2018-08-23 13:54-0700\n"
+"PO-Revision-Date: 2018-08-23 13:54-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+
+#: ../lib/puppet/provider/cron/crontab.rb:59
+msgid "Line got parsed as a crontab entry but cannot be handled. Please file a bug with the contents of your crontab"
+msgstr ""
+
+#: ../lib/puppet/provider/cron/crontab.rb:102
+msgid "no command specified, cannot create"
+msgstr ""
+
+#: ../lib/puppet/type/cron.rb:191
+msgid "%{value} is not a valid %{name}"
+msgstr ""
+
+#: ../lib/puppet/type/cron.rb:249
+msgid "Invalid special schedule %{value}"
+msgstr ""
+
+#: ../lib/puppet/type/cron.rb:326
+msgid "Invalid environment setting %{value}"
+msgstr ""
+
+#: ../lib/puppet/type/cron.rb:420
+msgid "%{cron} cannot specify both a special schedule and a value for %{field}"
+msgstr ""
diff --git a/metadata.json b/metadata.json
new file mode 100644
index 0000000..d10f354
--- /dev/null
+++ b/metadata.json
@@ -0,0 +1,83 @@
+{
+ "name": "puppetlabs-cron_core",
+ "version": "0.1.0",
+ "author": "Puppet Labs",
+ "summary": "Install and manage cron resources.",
+ "license": "Apache-2.0",
+ "source": "https://github.com/puppetlabs/puppetlabs-cron_core",
+ "issues_url": "https://tickets.puppetlabs.com/projects/MODULES",
+ "dependencies": [
+
+ ],
+ "operatingsystem_support": [
+ {
+ "operatingsystem": "CentOS",
+ "operatingsystemrelease": [
+ "7"
+ ]
+ },
+ {
+ "operatingsystem": "OracleLinux",
+ "operatingsystemrelease": [
+ "7"
+ ]
+ },
+ {
+ "operatingsystem": "RedHat",
+ "operatingsystemrelease": [
+ "7"
+ ]
+ },
+ {
+ "operatingsystem": "Scientific",
+ "operatingsystemrelease": [
+ "7"
+ ]
+ },
+ {
+ "operatingsystem": "Debian",
+ "operatingsystemrelease": [
+ "8"
+ ]
+ },
+ {
+ "operatingsystem": "Ubuntu",
+ "operatingsystemrelease": [
+ "16.04"
+ ]
+ },
+ {
+ "operatingsystem": "Fedora",
+ "operatingsystemrelease": [
+ "25"
+ ]
+ },
+ {
+ "operatingsystem": "Darwin",
+ "operatingsystemrelease": [
+ "16"
+ ]
+ },
+ {
+ "operatingsystem": "SLES",
+ "operatingsystemrelease": [
+ "12"
+ ]
+ },
+ {
+ "operatingsystem": "Solaris",
+ "operatingsystemrelease": [
+ "11"
+ ]
+ }
+ ],
+ "requirements": [
+ {
+ "name": "puppet",
+ "version_requirement": ">= 6.0.0 < 7.0.0"
+ }
+ ],
+ "pdk-version": "1.6.0",
+ "template-url": "https://github.com/puppetlabs/pdk-templates.git",
+ "template-ref": "1.7.0-0-g57412ed"
+}
diff --git a/spec/acceptance/nodesets/default.yml b/spec/acceptance/nodesets/default.yml
new file mode 100644
index 0000000..2cd2823
--- /dev/null
+++ b/spec/acceptance/nodesets/default.yml
@@ -0,0 +1,19 @@
+---
+HOSTS:
+ centos7-64-1:
+ pe_dir:
+ pe_ver:
+ pe_upgrade_dir:
+ pe_upgrade_ver:
+ hypervisor: vmpooler
+ platform: centos-7-x86_64
+ packaging_platform: el-7-x86_64
+ template: centos-7-x86_64
+ roles:
+ - agent
+ - default
+CONFIG:
+ type: agent
+ nfs_server: none
+ consoleport: 443
+ pooling_api: http://vmpooler.delivery.puppetlabs.net/ \ No newline at end of file
diff --git a/spec/acceptance/tests/resource/cron/should_allow_changing_parameters_spec.rb b/spec/acceptance/tests/resource/cron/should_allow_changing_parameters_spec.rb
new file mode 100644
index 0000000..615d617
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_allow_changing_parameters_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when changing parameters' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup agent
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean agent
+ end
+ end
+
+ compatible_agents.each do |agent|
+ it "manages cron entries on #{agent}" do
+ step 'Cron: basic - verify that it can be created'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/false", user => "tstuser", hour => "*", minute => [1], ensure => present,}')
+ expect(result.stdout).to match(%r{ensure: created})
+ result = run_cron_on(agent, :list, 'tstuser')
+ expect(result.stdout).to match(%r{.bin.false})
+
+ step 'Cron: allow changing command'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/true", user => "tstuser", hour => "*", minute => [1], ensure => present,}')
+ expect(result.stdout).to match(%r{command changed '.bin.false'.* to '.bin.true'})
+
+ result = run_cron_on(agent, :list, 'tstuser')
+ expect(result.stdout).to match(%r{1 . . . . .bin.true})
+
+ step 'Cron: allow changing time'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/true", user => "tstuser", hour => "1", minute => [1], ensure => present,}')
+ expect(result.stdout).to match(%r{hour: defined 'hour' as \['1'\]})
+
+ result = run_cron_on(agent, :list, 'tstuser')
+ expect(result.stdout).to match(%r{1 1 . . . .bin.true})
+
+ step 'Cron: allow changing time(array)'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/true", user => "tstuser", hour => ["1","2"], minute => [1], ensure => present,}')
+ expect(result.stdout).to match(%r{hour: hour changed \['1'\].* to \['1', '2'\]})
+
+ result = run_cron_on(agent, :list, 'tstuser')
+ expect(result.stdout).to match(%r{1 1,2 . . . .bin.true})
+
+ step 'Cron: allow changing time(array modification)'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/true", user => "tstuser", hour => ["3","2"], minute => [1], ensure => present,}')
+ expect(result.stdout).to match(%r{hour: hour changed \['1', '2'\].* to \['3', '2'\]})
+
+ result = run_cron_on(agent, :list, 'tstuser')
+ expect(result.stdout).to match(%r{1 3,2 . . . .bin.true})
+
+ step 'Cron: allow changing time(array modification to *)'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/true", user => "tstuser", hour => "*", minute => "*", ensure => present,}')
+ expect(result.stdout).to match(%r{minute: undefined 'minute' from \['1'\]})
+ expect(result.stdout).to match(%r{hour: undefined 'hour' from \['3', '2'\]})
+
+ result = run_cron_on(agent, :list, 'tstuser')
+ expect(result.stdout).to match(%r{\* \* . . . .bin.true})
+ end
+ end
+end
diff --git a/spec/acceptance/tests/resource/cron/should_be_idempotent_spec.rb b/spec/acceptance/tests/resource/cron/should_be_idempotent_spec.rb
new file mode 100644
index 0000000..35a91b7
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_be_idempotent_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when checking idempotency' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup(agent)
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean(agent)
+ end
+ end
+
+ compatible_agents.each do |agent|
+ it "ensures idempotency on #{agent}" do
+ step 'Cron: basic - verify that it can be created'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/true", user => "tstuser", hour => "*", minute => [1], ensure => present,}')
+ expect(result.stdout).to match(%r{ensure: created})
+
+ result = run_cron_on(agent, :list, 'tstuser')
+ expect(result.stdout).to match(%r{. . . . . .bin.true})
+
+ step 'Cron: basic - should not create again'
+ result = apply_manifest_on(agent, 'cron { "myjob": command => "/bin/true", user => "tstuser", hour => "*", minute => [1], ensure => present,}')
+ expect(result.stdout).not_to match(%r{ensure: created})
+ end
+ end
+end
diff --git a/spec/acceptance/tests/resource/cron/should_create_cron_spec.rb b/spec/acceptance/tests/resource/cron/should_create_cron_spec.rb
new file mode 100644
index 0000000..05ae5e5
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_create_cron_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when creating cron' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup(agent)
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean(agent)
+ end
+ end
+
+ compatible_agents.each do |host|
+ it 'creates a new cron job' do
+ step 'apply the resource on the host using puppet resource'
+ on(host, puppet_resource('cron', 'crontest', 'user=tstuser', 'command=/bin/true', 'ensure=present')) do
+ expect(stdout).to match(%r{created})
+ end
+
+ step 'verify that crontab -l contains what you expected'
+ run_cron_on(host, :list, 'tstuser') do
+ expect(stdout).to match(%r{\* \* \* \* \* /bin/true})
+ end
+ end
+ end
+end
diff --git a/spec/acceptance/tests/resource/cron/should_match_existing_spec.rb b/spec/acceptance/tests/resource/cron/should_match_existing_spec.rb
new file mode 100644
index 0000000..ce25be7
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_match_existing_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when matching cron' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup(agent)
+ step 'Create the existing cron job by hand...'
+ run_cron_on(agent, :add, 'tstuser', '* * * * * /bin/true')
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean(agent)
+ end
+ end
+
+ compatible_agents.each do |host|
+ it 'matches existing cron jobs' do
+ step 'Apply the resource on the host using puppet resource'
+ on(host, puppet_resource('cron', 'crontest', 'user=tstuser', 'command=/bin/true', 'ensure=present')) do
+ expect(stdout).to match(%r{present})
+ end
+
+ step 'Verify that crontab -l contains what you expected'
+ run_cron_on(host, :list, 'tstuser') do
+ expect(stdout).to match(%r{\* \* \* \* \* /bin/true})
+ end
+ end
+ end
+end
diff --git a/spec/acceptance/tests/resource/cron/should_remove_cron_spec.rb b/spec/acceptance/tests/resource/cron/should_remove_cron_spec.rb
new file mode 100644
index 0000000..d23cded
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_remove_cron_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when removing crontab' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup(agent)
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean(agent)
+ end
+ end
+
+ compatible_agents.each do |host|
+ it 'removes existing crontabs' do
+ step 'create the existing job by hand...'
+ run_cron_on(host, :add, 'tstuser', '* * * * * /bin/true')
+
+ step 'apply the resource on the host using puppet resource'
+ on(host, puppet_resource('cron', 'crontest', 'user=tstuser',
+ 'command=/bin/true', 'ensure=absent')) do
+ expect(stdout).to match(%r{crontest\D+ensure:\s+removed})
+ end
+
+ step ' contains what you expected'
+ run_cron_on(host, :list, 'tstuser') do
+ expect(stderr).not_to match(%r{/bin/true})
+ end
+ end
+ end
+end
diff --git a/spec/acceptance/tests/resource/cron/should_remove_leading_and_trailing_whitespace_spec.rb b/spec/acceptance/tests/resource/cron/should_remove_leading_and_trailing_whitespace_spec.rb
new file mode 100644
index 0000000..da04daa
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_remove_leading_and_trailing_whitespace_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when stripping whitespace from cron jobs' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup(agent)
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean(agent)
+ end
+ end
+
+ agents.each do |host|
+ it 'removes leading and trailing whitespace from cron jobs' do
+ step 'apply the resource on the host using puppet resource'
+ on(host, puppet_resource('cron', 'crontest', 'user=tstuser', "command=' date > /dev/null '", 'ensure=present')) do
+ expect(stdout).to match(%r{created})
+ end
+
+ step 'verify the added crontab entry has stripped whitespace'
+ run_cron_on(host, :list, 'tstuser') do
+ expect(stdout).to match(%r{\* \* \* \* \* date > .dev.null})
+ end
+
+ step 'apply the resource with trailing whitespace and check nothing happened'
+ on(host, puppet_resource('cron', 'crontest', 'user=tstuser', "command='date > /dev/null '", 'ensure=present')) do
+ expect(stdout).not_to match(%r{ensure: created})
+ end
+
+ step 'apply the resource with leading whitespace and check nothing happened'
+ on(host, puppet_resource('cron', 'crontest', 'user=tstuser', "command=' date > /dev/null'", 'ensure=present')) do
+ expect(stdout).not_to match(%r{ensure: created})
+ end
+ end
+ end
+end
diff --git a/spec/acceptance/tests/resource/cron/should_remove_matching_spec.rb b/spec/acceptance/tests/resource/cron/should_remove_matching_spec.rb
new file mode 100644
index 0000000..652d8c5
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_remove_matching_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when removing crontabs' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup(agent)
+
+ step 'create the existing job by hand...'
+ run_cron_on(agent, :add, 'tstuser', '* * * * * /bin/true')
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean(agent)
+ end
+ end
+
+ compatible_agents.each do |host|
+ it 'removes crontabs based on matching' do
+ step 'Remove cron resource'
+ on(host, puppet_resource('cron', 'bogus', 'user=tstuser', 'command=/bin/true', 'ensure=absent')) do
+ expect(stdout).to match(%r{bogus\D+ensure: removed})
+ end
+
+ step 'verify that crontab -l contains what you expected'
+ run_cron_on(host, :list, 'tstuser') do
+ expect(stdout.scan('/bin/true').length).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/acceptance/tests/resource/cron/should_update_existing_spec.rb b/spec/acceptance/tests/resource/cron/should_update_existing_spec.rb
new file mode 100644
index 0000000..3b226e7
--- /dev/null
+++ b/spec/acceptance/tests/resource/cron/should_update_existing_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper_acceptance'
+
+RSpec.context 'when updating cron jobs' do
+ before(:each) do
+ compatible_agents.each do |agent|
+ step 'ensure the user exists via puppet'
+ setup(agent)
+
+ step 'create the existing job by hand...'
+ run_cron_on(agent, :add, 'tstuser', '* * * * * /bin/true')
+ end
+ end
+
+ after(:each) do
+ compatible_agents.each do |agent|
+ step 'Cron: cleanup'
+ clean(agent)
+ end
+ end
+
+ compatible_agents.each do |host|
+ it 'updates existing cron entries' do
+ step 'verify that crontab -l contains what you expected'
+ run_cron_on(host, :list, 'tstuser') do
+ expect(stdout).to match(%r{\* \* \* \* \* /bin/true})
+ end
+
+ step 'apply the resource change on the host'
+ on(host, puppet_resource('cron', 'crontest', 'user=tstuser', 'command=/bin/true', 'ensure=present', "hour='0-6'")) do
+ expect(stdout).to match(%r{hour\s+=>\s+\['0-6'\]})
+ end
+
+ step 'verify that crontab -l contains what you expected'
+ run_cron_on(host, :list, 'tstuser') do
+ expect(stdout).to match(%r{\* 0-6 \* \* \* /bin/true})
+ end
+ end
+ end
+end
diff --git a/spec/default_facts.yml b/spec/default_facts.yml
new file mode 100644
index 0000000..3248be5
--- /dev/null
+++ b/spec/default_facts.yml
@@ -0,0 +1,8 @@
+# Use default_module_facts.yml for module specific facts.
+#
+# Facts specified here will override the values provided by rspec-puppet-facts.
+---
+concat_basedir: "/tmp"
+ipaddress: "172.16.254.254"
+is_pe: false
+macaddress: "AA:AA:AA:AA:AA:AA"
diff --git a/spec/fixtures/integration/provider/cron/crontab/create_normal_entry b/spec/fixtures/integration/provider/cron/crontab/create_normal_entry
new file mode 100644
index 0000000..e3e2c04
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/create_normal_entry
@@ -0,0 +1,19 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: My daily failure
+MAILTO=""
+@daily /bin/false
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
+# Puppet Name: new entry
+MAILTO=""
+SHELL=/bin/bash
+12 * * * 2 /bin/new
diff --git a/spec/fixtures/integration/provider/cron/crontab/create_special_entry b/spec/fixtures/integration/provider/cron/crontab/create_special_entry
new file mode 100644
index 0000000..ee25954
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/create_special_entry
@@ -0,0 +1,18 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: My daily failure
+MAILTO=""
+@daily /bin/false
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
+# Puppet Name: new special entry
+MAILTO=bob@company.com
+@reboot echo "Booted" 1>&2
diff --git a/spec/fixtures/integration/provider/cron/crontab/crontab_user1 b/spec/fixtures/integration/provider/cron/crontab/crontab_user1
new file mode 100644
index 0000000..2c7d542
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/crontab_user1
@@ -0,0 +1,15 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: My daily failure
+MAILTO=""
+@daily /bin/false
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
diff --git a/spec/fixtures/integration/provider/cron/crontab/crontab_user2 b/spec/fixtures/integration/provider/cron/crontab/crontab_user2
new file mode 100644
index 0000000..267e643
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/crontab_user2
@@ -0,0 +1,4 @@
+# HEADER: some simple
+# HEADER: header
+# Puppet Name: some_unrelevant job
+* * * * * /bin/true
diff --git a/spec/fixtures/integration/provider/cron/crontab/crontab_user3 b/spec/fixtures/integration/provider/cron/crontab/crontab_user3
new file mode 100644
index 0000000..ae314ae
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/crontab_user3
@@ -0,0 +1,17 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: My daily failure
+MAILTO=""
+@daily /bin/false
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
+# Puppet Name: My weekly failure
+@weekly /bin/false
diff --git a/spec/fixtures/integration/provider/cron/crontab/modify_entry b/spec/fixtures/integration/provider/cron/crontab/modify_entry
new file mode 100644
index 0000000..ed06fd4
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/modify_entry
@@ -0,0 +1,13 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: My daily failure
+MAILTO=""
+@daily /bin/false
+# Puppet Name: Monthly job
+@monthly /usr/bin/monthly
diff --git a/spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input1 b/spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input1
new file mode 100644
index 0000000..2c7d542
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input1
@@ -0,0 +1,15 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: My daily failure
+MAILTO=""
+@daily /bin/false
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
diff --git a/spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input2 b/spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input2
new file mode 100644
index 0000000..0b68287
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/moved_cronjob_input2
@@ -0,0 +1,6 @@
+# HEADER: some simple
+# HEADER: header
+# Puppet Name: some_unrelevant job
+* * * * * /bin/true
+# Puppet Name: My daily failure
+@daily /bin/false
diff --git a/spec/fixtures/integration/provider/cron/crontab/purged b/spec/fixtures/integration/provider/cron/crontab/purged
new file mode 100644
index 0000000..b302836
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/purged
@@ -0,0 +1,8 @@
+# HEADER: some simple
+# HEADER: header
+
+# commend with blankline above and below
+
+
+# Puppet Name: only managed entry
+* * * * * /bin/true
diff --git a/spec/fixtures/integration/provider/cron/crontab/remove_named_resource b/spec/fixtures/integration/provider/cron/crontab/remove_named_resource
new file mode 100644
index 0000000..e1c1716
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/remove_named_resource
@@ -0,0 +1,12 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
diff --git a/spec/fixtures/integration/provider/cron/crontab/remove_unnamed_resource b/spec/fixtures/integration/provider/cron/crontab/remove_unnamed_resource
new file mode 100644
index 0000000..2dcbfe2
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/remove_unnamed_resource
@@ -0,0 +1,14 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+
+# Puppet Name: My daily failure
+MAILTO=""
+@daily /bin/false
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
diff --git a/spec/fixtures/integration/provider/cron/crontab/unspecialized b/spec/fixtures/integration/provider/cron/crontab/unspecialized
new file mode 100644
index 0000000..e6a4082
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/unspecialized
@@ -0,0 +1,15 @@
+# HEADER: some simple
+# HEADER: header
+@daily /bin/unnamed_special_command >> /dev/null 2>&1
+
+# commend with blankline above and below
+
+17-19,22 0-23/2 * * 2 /bin/unnamed_regular_command
+
+# Puppet Name: My daily failure
+MAILTO=""
+* * * * * /bin/false
+# Puppet Name: Monthly job
+SHELL=/bin/sh
+MAILTO=mail@company.com
+15 14 1 * * $HOME/bin/monthly
diff --git a/spec/fixtures/unit/provider/cron/crontab/single_line.yaml b/spec/fixtures/unit/provider/cron/crontab/single_line.yaml
new file mode 100644
index 0000000..da2853e
--- /dev/null
+++ b/spec/fixtures/unit/provider/cron/crontab/single_line.yaml
@@ -0,0 +1,272 @@
+---
+:longcommment:
+ :text: "# This is a comment"
+ :record:
+ :line: "# This is a comment"
+ :record_type: :comment
+:special:
+ :text: "@hourly /bin/date"
+ :record:
+ :special: hourly
+ :command: /bin/date
+ :record_type: :crontab
+:long_name:
+ :text: "# Puppet Name: long_name"
+ :record:
+ :line: "# Puppet Name: long_name"
+ :name: long_name
+ :record_type: :comment
+:multiple_minutes:
+ :text: 5,15 * * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ - "15"
+ :command: /bin/date
+ :record_type: :crontab
+:environment:
+ :text: ONE=TWO
+ :record:
+ :line: ONE=TWO
+ :record_type: :environment
+:empty:
+ :text: ""
+ :record:
+ :line: ""
+ :record_type: :blank
+:simple:
+ :text: "* * * * * /bin/date"
+ :record:
+ :command: /bin/date
+ :record_type: :crontab
+:whitespace:
+ :text: " "
+ :record:
+ :line: " "
+ :record_type: :blank
+:minute_and_hour:
+ :text: 5 15 * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ :hour:
+ - "15"
+ :command: /bin/date
+ :record_type: :crontab
+:lowercase_environment:
+ :text: a=b
+ :record:
+ :line: a=b
+ :record_type: :environment
+:special_with_spaces:
+ :text: "@daily /bin/echo testing"
+ :record:
+ :special: daily
+ :command: /bin/echo testing
+ :record_type: :crontab
+:tabs:
+ :text: !binary |
+ CQ==
+
+ :record:
+ :line: !binary |
+ CQ==
+
+ :record_type: :blank
+:multiple_minute_and_hour:
+ :text: 5,10 15,20 * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ - "10"
+ :hour:
+ - "15"
+ - "20"
+ :command: /bin/date
+ :record_type: :crontab
+:name:
+ :text: "# Puppet Name: testing"
+ :record:
+ :line: "# Puppet Name: testing"
+ :name: testing
+ :record_type: :comment
+:another_env:
+ :text: Testing=True
+ :record:
+ :line: Testing=True
+ :record_type: :environment
+:shortcomment:
+ :text: "#"
+ :record:
+ :line: "#"
+ :record_type: :comment
+:spaces_in_command:
+ :text: "* * * * * /bin/echo testing"
+ :record:
+ :command: /bin/echo testing
+ :record_type: :crontab
+:fourth_env:
+ :text: True=False
+ :record:
+ :line: True=False
+ :record_type: :environment
+:simple_with_minute:
+ :text: 5 * * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ :command: /bin/date
+ :record_type: :crontab
+:spaces_in_command_with_times:
+ :text: 5,10 15,20 * * * /bin/echo testing
+ :record:
+ :minute:
+ - "5"
+ - "10"
+ :hour:
+ - "15"
+ - "20"
+ :command: /bin/echo testing
+ :record_type: :crontab
+:name_with_spaces:
+ :text: "# Puppet Name: another name"
+ :record:
+ :line: "# Puppet Name: another name"
+ :name: another name
+ :record_type: :comment
+---
+:longcommment:
+ :text: "# This is a comment"
+ :record:
+ :line: "# This is a comment"
+ :record_type: :comment
+:special:
+ :text: "@hourly /bin/date"
+ :record:
+ :special: hourly
+ :command: /bin/date
+ :record_type: :crontab
+:long_name:
+ :text: "# Puppet Name: long_name"
+ :record:
+ :line: "# Puppet Name: long_name"
+ :name: long_name
+ :record_type: :comment
+:multiple_minutes:
+ :text: 5,15 * * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ - "15"
+ :command: /bin/date
+ :record_type: :crontab
+:environment:
+ :text: ONE=TWO
+ :record:
+ :line: ONE=TWO
+ :record_type: :environment
+:empty:
+ :text: ""
+ :record:
+ :line: ""
+ :record_type: :blank
+:simple:
+ :text: "* * * * * /bin/date"
+ :record:
+ :command: /bin/date
+ :record_type: :crontab
+:whitespace:
+ :text: " "
+ :record:
+ :line: " "
+ :record_type: :blank
+:minute_and_hour:
+ :text: 5 15 * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ :hour:
+ - "15"
+ :command: /bin/date
+ :record_type: :crontab
+:lowercase_environment:
+ :text: a=b
+ :record:
+ :line: a=b
+ :record_type: :environment
+:special_with_spaces:
+ :text: "@daily /bin/echo testing"
+ :record:
+ :special: daily
+ :command: /bin/echo testing
+ :record_type: :crontab
+:tabs:
+ :text: !binary |
+ CQ==
+
+ :record:
+ :line: !binary |
+ CQ==
+
+ :record_type: :blank
+:multiple_minute_and_hour:
+ :text: 5,10 15,20 * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ - "10"
+ :hour:
+ - "15"
+ - "20"
+ :command: /bin/date
+ :record_type: :crontab
+:name:
+ :text: "# Puppet Name: testing"
+ :record:
+ :line: "# Puppet Name: testing"
+ :name: testing
+ :record_type: :comment
+:another_env:
+ :text: Testing=True
+ :record:
+ :line: Testing=True
+ :record_type: :environment
+:shortcomment:
+ :text: "#"
+ :record:
+ :line: "#"
+ :record_type: :comment
+:spaces_in_command:
+ :text: "* * * * * /bin/echo testing"
+ :record:
+ :command: /bin/echo testing
+ :record_type: :crontab
+:fourth_env:
+ :text: True=False
+ :record:
+ :line: True=False
+ :record_type: :environment
+:simple_with_minute:
+ :text: 5 * * * * /bin/date
+ :record:
+ :minute:
+ - "5"
+ :command: /bin/date
+ :record_type: :crontab
+:spaces_in_command_with_times:
+ :text: 5,10 15,20 * * * /bin/echo testing
+ :record:
+ :minute:
+ - "5"
+ - "10"
+ :hour:
+ - "15"
+ - "20"
+ :command: /bin/echo testing
+ :record_type: :crontab
+:name_with_spaces:
+ :text: "# Puppet Name: another name"
+ :record:
+ :line: "# Puppet Name: another name"
+ :name: another name
+ :record_type: :comment
diff --git a/spec/fixtures/unit/provider/cron/crontab/vixie_header.txt b/spec/fixtures/unit/provider/cron/crontab/vixie_header.txt
new file mode 100644
index 0000000..7ccfc37
--- /dev/null
+++ b/spec/fixtures/unit/provider/cron/crontab/vixie_header.txt
@@ -0,0 +1,3 @@
+# DO NOT EDIT THIS FILE - edit the master and reinstall.
+# (- installed on Thu Apr 12 12:16:01 2007)
+# (Cron version V5.0 -- $Id: crontab.c,v 1.12 2004/01/23 18:56:42 vixie Exp $)
diff --git a/spec/fixtures/unit/provider/cron/parsed/managed b/spec/fixtures/unit/provider/cron/parsed/managed
new file mode 100644
index 0000000..c48d20a
--- /dev/null
+++ b/spec/fixtures/unit/provider/cron/parsed/managed
@@ -0,0 +1,6 @@
+# Puppet Name: real_job
+* * * * * /bin/true
+# Puppet Name: complex_job
+MAILTO=foo@example.com
+SHELL=/bin/sh
+@reboot /bin/true >> /dev/null 2>&1
diff --git a/spec/fixtures/unit/provider/cron/parsed/simple b/spec/fixtures/unit/provider/cron/parsed/simple
new file mode 100644
index 0000000..477553e
--- /dev/null
+++ b/spec/fixtures/unit/provider/cron/parsed/simple
@@ -0,0 +1,9 @@
+# use /bin/sh to run commands, no matter what /etc/passwd says
+SHELL=/bin/sh
+# mail any output to `paul', no matter whose crontab this is
+MAILTO=paul
+#
+# run five minutes after midnight, every day
+5 0 * * * $HOME/bin/daily.job >> $HOME/tmp/out 2>&1
+# run at 2:15pm on the first of every month -- output mailed to paul
+15 14 1 * * $HOME/bin/monthly
diff --git a/spec/integration/provider/cron/crontab_spec.rb b/spec/integration/provider/cron/crontab_spec.rb
new file mode 100644
index 0000000..989eae6
--- /dev/null
+++ b/spec/integration/provider/cron/crontab_spec.rb
@@ -0,0 +1,237 @@
+require 'spec_helper'
+require 'puppet/file_bucket/dipper'
+require 'puppet_spec/compiler'
+
+describe Puppet::Type.type(:cron).provider(:crontab), unless: Puppet.features.microsoft_windows? do
+ include PuppetSpec::Files
+ include PuppetSpec::Compiler
+
+ before :each do
+ Puppet::Type.type(:cron).stubs(:defaultprovider).returns described_class
+ described_class.stubs(:suitable?).returns true
+ Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # rubocop:disable RSpec/AnyInstance
+
+ # I don't want to execute anything
+ described_class.stubs(:filetype).returns Puppet::Util::FileType::FileTypeFlat
+ described_class.stubs(:default_target).returns crontab_user1
+
+ # I don't want to stub Time.now to get a static header because I don't know
+ # where Time.now is used elsewhere, so just go with a very simple header
+ described_class.stubs(:header).returns "# HEADER: some simple\n# HEADER: header\n"
+ FileUtils.cp(my_fixture('crontab_user1'), crontab_user1)
+ FileUtils.cp(my_fixture('crontab_user2'), crontab_user2)
+ end
+
+ after :each do
+ described_class.clear
+ end
+
+ let :crontab_user1 do
+ tmpfile('cron_integration_specs')
+ end
+
+ let :crontab_user2 do
+ tmpfile('cron_integration_specs')
+ end
+
+ def expect_output(fixture_name)
+ expect(File.read(crontab_user1)).to eq(File.read(my_fixture(fixture_name)))
+ end
+
+ describe 'when managing a cron entry' do
+ it 'is able to purge unmanaged entries' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'only managed entry':
+ ensure => 'present',
+ command => '/bin/true',
+ target => '#{crontab_user1}',
+ }
+ resources { 'cron': purge => 'true' }
+ MANIFEST
+ expect_output('purged')
+ end
+
+ describe 'with ensure absent' do
+ it 'does nothing if entry already absent' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'no_such_entry':
+ ensure => 'absent',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('crontab_user1')
+ end
+
+ it 'removes the resource from crontab if present' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'My daily failure':
+ ensure => 'absent',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('remove_named_resource')
+ end
+
+ it 'removes a matching cronentry if present' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'no_such_named_resource_in_crontab':
+ ensure => absent,
+ minute => [ '17-19', '22' ],
+ hour => [ '0-23/2' ],
+ weekday => 'Tue',
+ command => '/bin/unnamed_regular_command',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('remove_unnamed_resource')
+ end
+ end
+
+ describe 'with ensure present' do
+ context 'and no command specified' do
+ it 'works if the resource is already present' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'My daily failure':
+ special => 'daily',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('crontab_user1')
+ end
+ it 'fails if the resource needs creating' do
+ manifest = <<-MANIFEST
+ cron {
+ 'Entirely new resource':
+ special => 'daily',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ apply_compiled_manifest(manifest) do |res|
+ if res.ref == 'Cron[Entirely new resource]'
+ res.expects(:err).with(regexp_matches(%r{no command}))
+ else
+ res.expects(:err).never
+ end
+ end
+ end
+ end
+
+ it 'does nothing if entry already present' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'My daily failure':
+ special => 'daily',
+ command => '/bin/false',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('crontab_user1')
+ end
+
+ it "works correctly when managing 'target' but not 'user'" do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'My weekly failure':
+ special => 'weekly',
+ command => '/bin/false',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('crontab_user3')
+ end
+
+ it 'does nothing if a matching entry already present' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'no_such_named_resource_in_crontab':
+ ensure => present,
+ minute => [ '17-19', '22' ],
+ hour => [ '0-23/2' ],
+ command => '/bin/unnamed_regular_command',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('crontab_user1')
+ end
+
+ it 'adds a new normal entry if currently absent' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'new entry':
+ ensure => present,
+ minute => '12',
+ weekday => 'Tue',
+ command => '/bin/new',
+ environment => [
+ 'MAILTO=""',
+ 'SHELL=/bin/bash'
+ ],
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('create_normal_entry')
+ end
+
+ it 'adds a new special entry if currently absent' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'new special entry':
+ ensure => present,
+ special => 'reboot',
+ command => 'echo "Booted" 1>&2',
+ environment => 'MAILTO=bob@company.com',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('create_special_entry')
+ end
+
+ it 'changes existing entry if out of sync' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'Monthly job':
+ ensure => present,
+ special => 'monthly',
+ #minute => ['22'],
+ command => '/usr/bin/monthly',
+ environment => [],
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('modify_entry')
+ end
+ it 'changes a special schedule to numeric if requested' do
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'My daily failure':
+ special => 'absent',
+ command => '/bin/false',
+ target => '#{crontab_user1}',
+ }
+ MANIFEST
+ expect_output('unspecialized')
+ end
+ it 'does not try to move an entry from one file to another' do
+ # force the parsedfile provider to also parse user1's crontab
+ apply_with_error_check(<<-MANIFEST)
+ cron {
+ 'foo':
+ ensure => absent,
+ target => '#{crontab_user1}';
+ 'My daily failure':
+ special => 'daily',
+ command => "/bin/false",
+ target => '#{crontab_user2}',
+ }
+ MANIFEST
+ expect(File.read(crontab_user1)).to eq(File.read(my_fixture('moved_cronjob_input1')))
+ expect(File.read(crontab_user2)).to eq(File.read(my_fixture('moved_cronjob_input2')))
+ end
+ end
+ end
+end
diff --git a/spec/lib/puppet_spec/compiler.rb b/spec/lib/puppet_spec/compiler.rb
new file mode 100644
index 0000000..89c97a5
--- /dev/null
+++ b/spec/lib/puppet_spec/compiler.rb
@@ -0,0 +1,112 @@
+module PuppetSpec::Compiler
+ module_function
+
+ def compile_to_catalog(string, node = Puppet::Node.new('test'))
+ Puppet[:code] = string
+ # see lib/puppet/indirector/catalog/compiler.rb#filter
+ Puppet::Parser::Compiler.compile(node).filter { |r| r.virtual? }
+ end
+
+ # Does not removed virtual resources in compiled catalog (i.e. keeps unrealized)
+ def compile_to_catalog_unfiltered(string, node = Puppet::Node.new('test'))
+ Puppet[:code] = string
+ # see lib/puppet/indirector/catalog/compiler.rb#filter
+ Puppet::Parser::Compiler.compile(node)
+ end
+
+ def compile_to_ral(manifest, node = Puppet::Node.new('test'))
+ catalog = compile_to_catalog(manifest, node)
+ ral = catalog.to_ral
+ ral.finalize
+ ral
+ end
+
+ def compile_to_relationship_graph(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new)
+ ral = compile_to_ral(manifest)
+ graph = Puppet::Graph::RelationshipGraph.new(prioritizer)
+ graph.populate_from(ral)
+ graph
+ end
+
+ def apply_compiled_manifest(manifest, prioritizer = Puppet::Graph::SequentialPrioritizer.new)
+ catalog = compile_to_ral(manifest)
+ if block_given?
+ catalog.resources.each { |res| yield res }
+ end
+ transaction = Puppet::Transaction.new(catalog,
+ Puppet::Transaction::Report.new,
+ prioritizer)
+ transaction.evaluate
+ transaction.report.finalize_report
+
+ transaction
+ end
+
+ def apply_with_error_check(manifest)
+ apply_compiled_manifest(manifest) do |res|
+ res.expects(:err).never
+ end
+ end
+
+ def order_resources_traversed_in(relationships)
+ order_seen = []
+ relationships.traverse { |resource| order_seen << resource.ref }
+ order_seen
+ end
+
+ def collect_notices(code, node = Puppet::Node.new('foonode'))
+ Puppet[:code] = code
+ compiler = Puppet::Parser::Compiler.new(node)
+ node.environment.check_for_reparse
+ logs = []
+ Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do
+ yield(compiler)
+ end
+ logs = logs.select { |log| log.level == :notice }.map { |log| log.message }
+ logs
+ end
+
+ def eval_and_collect_notices(code, node = Puppet::Node.new('foonode'), topscope_vars = {})
+ collect_notices(code, node) do |compiler|
+ unless topscope_vars.empty?
+ scope = compiler.topscope
+ topscope_vars.each { |k, v| scope.setvar(k, v) }
+ end
+ if block_given?
+ compiler.compile do |catalog|
+ yield(compiler.topscope, catalog)
+ catalog
+ end
+ else
+ compiler.compile
+ end
+ end
+ end
+
+ # Compiles a catalog, and if source is given evaluates it and returns its result.
+ # The catalog is returned if no source is given.
+ # Topscope variables are set before compilation
+ # Uses a created node 'testnode' if none is given.
+ # (Parameters given by name)
+ #
+ def evaluate(code: 'undef', source: nil, node: Puppet::Node.new('testnode'), variables: {})
+ source_location = caller(0..0)
+ Puppet[:code] = code
+ compiler = Puppet::Parser::Compiler.new(node)
+ unless variables.empty?
+ scope = compiler.topscope
+ variables.each { |k, v| scope.setvar(k, v) }
+ end
+
+ if source.nil?
+ compiler.compile
+ # see lib/puppet/indirector/catalog/compiler.rb#filter
+ return compiler.filter { |r| r.virtual? }
+ end
+
+ # evaluate given source is the context of the compiled state and return its result
+ compiler.compile do |_catalog|
+ Puppet::Pops::Parser::EvaluatingParser.singleton.evaluate_string(compiler.topscope, source, source_location)
+ end
+ end
+end
diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb
new file mode 100644
index 0000000..c8315c9
--- /dev/null
+++ b/spec/lib/puppet_spec/files.rb
@@ -0,0 +1,112 @@
+require 'fileutils'
+require 'tempfile'
+require 'tmpdir'
+require 'pathname'
+
+# A support module for testing files.
+module PuppetSpec::Files
+ def self.cleanup
+ # rubocop:disable Style/GlobalVars
+ $global_tempfiles ||= []
+ $global_tempfiles.each do |path|
+ begin
+ Dir.unstub(:entries)
+ FileUtils.rm_rf path, secure: true
+ rescue Errno::ENOENT # rubocop:disable Lint/HandleExceptions
+ # nothing to do
+ end
+ end
+ $global_tempfiles = []
+ # rubocop:enable Style/GlobalVars
+ end
+
+ module_function
+
+ def make_absolute(path)
+ path = File.expand_path(path)
+ path[0] = 'c' if Puppet.features.microsoft_windows?
+ path
+ end
+
+ def tmpfile(name, dir = nil)
+ dir ||= Dir.tmpdir
+ path = Puppet::FileSystem.expand_path(make_tmpname(name, nil).encode(Encoding::UTF_8), dir)
+ record_tmp(File.expand_path(path))
+
+ path
+ end
+
+ def file_containing(name, contents)
+ file = tmpfile(name)
+ File.open(file, 'wb') { |f| f.write(contents) }
+ file
+ end
+
+ def script_containing(name, contents)
+ file = tmpfile(name)
+ if Puppet.features.microsoft_windows?
+ file += '.bat'
+ text = contents[:windows]
+ else
+ text = contents[:posix]
+ end
+ File.open(file, 'wb') { |f| f.write(text) }
+ Puppet::FileSystem.chmod(0o755, file)
+ file
+ end
+
+ def tmpdir(name)
+ dir = Puppet::FileSystem.expand_path(Dir.mktmpdir(name).encode!(Encoding::UTF_8))
+
+ record_tmp(dir)
+
+ dir
+ end
+
+ # Copied from ruby 2.4 source
+ def make_tmpname((prefix, suffix), n)
+ prefix = (String.try_convert(prefix) ||
+ raise(ArgumentError, "unexpected prefix: #{prefix.inspect}"))
+ suffix &&= (String.try_convert(suffix) ||
+ raise(ArgumentError, "unexpected suffix: #{suffix.inspect}"))
+ t = Time.now.strftime('%Y%m%d')
+ path = "#{prefix}#{t}-#{$PROCESS_ID}-#{rand(0x100000000).to_s(36)}".dup
+ path << "-#{n}" if n
+ path << suffix if suffix
+ path
+ end
+
+ def dir_containing(name, contents_hash)
+ dir_contained_in(tmpdir(name), contents_hash)
+ end
+
+ def dir_contained_in(dir, contents_hash)
+ contents_hash.each do |k, v|
+ if v.is_a?(Hash)
+ Dir.mkdir(tmp = File.join(dir, k))
+ dir_contained_in(tmp, v)
+ else
+ file = File.join(dir, k)
+ File.open(file, 'wb') { |f| f.write(v) }
+ end
+ end
+ dir
+ end
+
+ def record_tmp(tmp)
+ # rubocop:disable Style/GlobalVars
+ $global_tempfiles ||= []
+ $global_tempfiles << tmp
+ # rubocop:enable Style/GlobalVars
+ end
+
+ def expect_file_mode(file, mode)
+ actual_mode = '%o' % Puppet::FileSystem.stat(file).mode
+ target_mode = if Puppet.features.microsoft_windows?
+ mode
+ else
+ '10' + '%04i' % mode.to_i
+ end
+ expect(actual_mode).to eq(target_mode)
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..5e721b7
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,44 @@
+require 'puppetlabs_spec_helper/module_spec_helper'
+require 'rspec-puppet-facts'
+
+begin
+ require 'spec_helper_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_local.rb'))
+rescue LoadError => loaderror
+ warn "Could not require spec_helper_local: #{loaderror.message}"
+end
+
+include RspecPuppetFacts
+
+default_facts = {
+ puppetversion: Puppet.version,
+ facterversion: Facter.version,
+}
+
+default_facts_path = File.expand_path(File.join(File.dirname(__FILE__), 'default_facts.yml'))
+default_module_facts_path = File.expand_path(File.join(File.dirname(__FILE__), 'default_module_facts.yml'))
+
+if File.exist?(default_facts_path) && File.readable?(default_facts_path)
+ default_facts.merge!(YAML.safe_load(File.read(default_facts_path)))
+end
+
+if File.exist?(default_module_facts_path) && File.readable?(default_module_facts_path)
+ default_facts.merge!(YAML.safe_load(File.read(default_module_facts_path)))
+end
+
+RSpec.configure do |c|
+ c.default_facts = default_facts
+ c.before :each do
+ # set to strictest setting for testing
+ # by default Puppet runs at warning level
+ Puppet.settings[:strict] = :warning
+ end
+end
+
+def ensure_module_defined(module_name)
+ module_name.split('::').reduce(Object) do |last_module, next_module|
+ last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module)
+ last_module.const_get(next_module)
+ end
+end
+
+# 'spec_overrides' from sync.yml will appear below this line
diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb
new file mode 100644
index 0000000..4759227
--- /dev/null
+++ b/spec/spec_helper_acceptance.rb
@@ -0,0 +1,40 @@
+require 'beaker-rspec'
+require 'beaker/module_install_helper'
+require 'beaker/puppet_install_helper'
+
+$LOAD_PATH << File.join(__dir__, 'acceptance/lib')
+
+def beaker_opts
+ { debug: true, trace: true, expect_failures: true, acceptable_exit_codes: (0...256) }
+ # { expect_failures: true, acceptable_exit_codes: (0...256) }
+end
+
+def compatible_agents
+ agents.reject { |agent| agent['platform'].include?('windows') || agent['platform'].include?('eos-') || agent['platform'].include?('fedora-28') }
+end
+
+def clean(agent, o = {})
+ o = { user: 'tstuser' }.merge(o)
+ run_cron_on(agent, :remove, o[:user])
+ apply_manifest_on(agent, %([user{'%s': ensure => absent, managehome => false }]) % o[:user])
+end
+
+def setup(agent, o = {})
+ o = { user: 'tstuser' }.merge(o)
+ apply_manifest_on(agent, %(user { '%s': ensure => present, managehome => false }) % o[:user])
+ apply_manifest_on(agent, %(case $operatingsystem {
+ centos, redhat: {$cron = 'cronie'}
+ solaris: { $cron = 'core-os' }
+ default: {$cron ='cron'} }
+ package {'cron': name=> $cron, ensure=>present, }))
+end
+
+RSpec.configure do |c|
+ c.before :suite do
+ unless ENV['BEAKER_provision'] == 'no'
+ run_puppet_install_helper
+ install_module_on(hosts_as('default'))
+ install_module_dependencies_on(hosts)
+ end
+ end
+end
diff --git a/spec/spec_helper_local.rb b/spec/spec_helper_local.rb
new file mode 100644
index 0000000..f06b4bb
--- /dev/null
+++ b/spec/spec_helper_local.rb
@@ -0,0 +1,17 @@
+dir = File.expand_path(File.dirname(__FILE__))
+$LOAD_PATH.unshift File.join(dir, 'lib')
+
+# Container for various Puppet-specific RSpec helpers.
+module PuppetSpec
+end
+
+require 'puppet_spec/files'
+
+RSpec.configure do |config|
+ config.before :each do |_test|
+ base = PuppetSpec::Files.tmpdir('tmp_settings')
+ Puppet[:vardir] = File.join(base, 'var')
+
+ FileUtils.mkdir_p Puppet[:statedir]
+ end
+end
diff --git a/spec/unit/provider/cron/crontab_spec.rb b/spec/unit/provider/cron/crontab_spec.rb
new file mode 100644
index 0000000..031b3ae
--- /dev/null
+++ b/spec/unit/provider/cron/crontab_spec.rb
@@ -0,0 +1,205 @@
+require 'spec_helper'
+
+describe Puppet::Type.type(:cron).provider(:crontab) do
+ subject do
+ provider = Puppet::Type.type(:cron).provider(:crontab)
+ provider.initvars
+ provider
+ end
+
+ def compare_crontab_text(have, want)
+ # We should have four header lines, and then the text...
+ expect(have.lines.to_a[0..3]).to(be_all { |x| x =~ %r{^# } })
+ expect(have.lines.to_a[4..-1].join('')).to eq(want)
+ end
+
+ context 'with the simple samples' do
+ FIELDS = {
+ crontab: ['command', 'minute', 'hour', 'month', 'monthday', 'weekday'].map { |o| o.to_sym },
+ environment: [:line],
+ blank: [:line],
+ comment: [:line],
+ }.freeze
+
+ def compare_crontab_record(have, want)
+ want.each do |param, value|
+ expect(have).to be_key param
+ expect(have[param]).to eq(value)
+ end
+
+ (FIELDS[have[:record_type]] - want.keys).each do |name|
+ expect(have[name]).to eq(:absent)
+ end
+ end
+
+ ########################################################################
+ # Simple input fixtures for testing.
+ samples = YAML.load(File.read(my_fixture('single_line.yaml'))) # rubocop:disable Security/YAMLLoad
+
+ samples.each do |name, data|
+ it "should parse crontab line #{name} correctly" do
+ compare_crontab_record subject.parse_line(data[:text]), data[:record]
+ end
+
+ it "should reconstruct the crontab line #{name} from the record" do
+ expect(subject.to_line(data[:record])).to eq(data[:text])
+ end
+ end
+
+ records = []
+ text = ''
+
+ # Sorting is from the original, and avoids :empty being the last line,
+ # since the provider will ignore that and cause this to fail.
+ samples.sort_by { |x| x.first.to_s }.each do |_name, data|
+ records << data[:record]
+ text << data[:text] + "\n"
+ end
+
+ it 'parses all sample records at once' do
+ subject.parse(text).zip(records).each do |round|
+ compare_crontab_record(*round)
+ end
+ end
+
+ it 'reconstitutes the file from the records' do
+ compare_crontab_text subject.to_file(records), text
+ end
+
+ context 'multi-line crontabs' do
+ tests = { simple: [:spaces_in_command_with_times],
+ with_name: [:name, :spaces_in_command_with_times],
+ with_env: [:environment, :spaces_in_command_with_times],
+ with_multiple_envs: [:environment, :lowercase_environment, :spaces_in_command_with_times],
+ with_name_and_env: [:name_with_spaces, :another_env, :spaces_in_command_with_times],
+ with_name_and_multiple_envs: [:long_name, :another_env, :fourth_env, :spaces_in_command_with_times] }
+
+ all_records = []
+ all_text = ''
+
+ tests.each do |name, content|
+ data = content.map { |x| samples[x] || raise("missing sample data #{x}") }
+ text = data.map { |x| x[:text] }.join("\n") + "\n"
+ records = data.map { |x| x[:record] }
+
+ # Capture the whole thing for later, too...
+ all_records += records
+ all_text += text
+
+ context name.to_s.tr('_', ' ') do
+ it 'regenerates the text from the record' do
+ compare_crontab_text subject.to_file(records), text
+ end
+
+ it 'parses the records from the text' do
+ subject.parse(text).zip(records).each do |round|
+ compare_crontab_record(*round)
+ end
+ end
+ end
+ end
+
+ it 'parses the whole set of records from the text' do
+ subject.parse(all_text).zip(all_records).each do |round|
+ compare_crontab_record(*round)
+ end
+ end
+
+ it 'regenerates the whole text from the set of all records' do
+ compare_crontab_text subject.to_file(all_records), all_text
+ end
+ end
+ end
+
+ context 'when receiving a vixie cron header from the cron interface' do
+ it 'does not write that header back to disk' do
+ vixie_header = File.read(my_fixture('vixie_header.txt'))
+ vixie_records = subject.parse(vixie_header)
+ compare_crontab_text subject.to_file(vixie_records), ''
+ end
+ end
+
+ context 'when adding a cronjob with the same command as an existing job' do
+ let(:record) { { name: 'existing', user: 'root', command: '/bin/true', record_type: :crontab } }
+ let(:resource) { Puppet::Type::Cron.new(name: 'test', user: 'root', command: '/bin/true') }
+ let(:resources) { { 'test' => resource } }
+
+ before :each do
+ subject.stubs(:prefetch_all_targets).returns([record])
+ end
+
+ # this would be a more fitting test, but I haven't yet
+ # figured out how to get it working
+ # it "should include both jobs in the output" do
+ # subject.prefetch(resources)
+ # class Puppet::Provider::ParsedFile
+ # def self.records
+ # @records
+ # end
+ # end
+ # subject.to_file(subject.records).should match /Puppet name: test/
+ # end
+
+ it "does not base the new resource's provider on the existing record" do
+ subject.expects(:new).with(record).never
+ subject.stubs(:new)
+ subject.prefetch(resources)
+ end
+ end
+
+ context 'when prefetching an entry now managed for another user' do
+ let(:resource) do
+ s = stub(:resource)
+ s.stubs(:[]).with(:user).returns 'root'
+ s.stubs(:[]).with(:target).returns 'root'
+ s
+ end
+
+ let(:record) { { name: 'test', user: 'nobody', command: '/bin/true', record_type: :crontab } }
+ let(:resources) { { 'test' => resource } }
+
+ before :each do
+ subject.stubs(:prefetch_all_targets).returns([record])
+ end
+
+ it 'tries and use the match method to find a more fitting record' do
+ subject.expects(:match).with(record, resources)
+ subject.prefetch(resources)
+ end
+
+ it 'does not match a provider to the resource' do
+ resource.expects(:provider=).never
+ subject.prefetch(resources)
+ end
+
+ it 'does not find the resource when looking up the on-disk record' do
+ subject.prefetch(resources)
+ expect(subject.resource_for_record(record, resources)).to be_nil
+ end
+ end
+
+ context 'when matching resources to existing crontab entries' do
+ let(:first_resource) { Puppet::Type::Cron.new(name: :one, user: 'root', command: '/bin/true') }
+ let(:second_resource) { Puppet::Type::Cron.new(name: :two, user: 'nobody', command: '/bin/false') }
+
+ let(:resources) { { one: first_resource, two: second_resource } }
+
+ describe 'with a record with a matching name and mismatching user (#2251)' do
+ # Puppet::Resource objects have #should defined on them, so in these
+ # examples we have to use the monkey patched `must` alias for the rspec
+ # `should` method.
+
+ it "doesn't match the record to the resource" do
+ record = { name: :one, user: 'notroot', record_type: :crontab }
+ expect(subject.resource_for_record(record, resources)).to be_nil
+ end
+ end
+
+ describe 'with a record with a matching name and matching user' do
+ it 'matches the record to the resource' do
+ record = { name: :two, target: 'nobody', command: '/bin/false' }
+ expect(subject.resource_for_record(record, resources)).to eq(second_resource)
+ end
+ end
+ end
+end
diff --git a/spec/unit/provider/cron/parsed_spec.rb b/spec/unit/provider/cron/parsed_spec.rb
new file mode 100644
index 0000000..d4460f7
--- /dev/null
+++ b/spec/unit/provider/cron/parsed_spec.rb
@@ -0,0 +1,335 @@
+require 'spec_helper'
+
+describe Puppet::Type.type(:cron).provider(:crontab) do
+ let :provider do
+ described_class.new(command: '/bin/true')
+ end
+
+ let :resource do
+ Puppet::Type.type(:cron).new(
+ minute: ['0', '15', '30', '45'],
+ hour: ['8-18', '20-22'],
+ monthday: ['31'],
+ month: ['12'],
+ weekday: ['7'],
+ name: 'basic',
+ command: '/bin/true',
+ target: 'root',
+ provider: provider,
+ )
+ end
+
+ let :resource_special do
+ Puppet::Type.type(:cron).new(
+ special: 'reboot',
+ name: 'special',
+ command: '/bin/true',
+ target: 'nobody',
+ )
+ end
+
+ let :resource_sparse do
+ Puppet::Type.type(:cron).new(
+ minute: ['42'],
+ target: 'root',
+ name: 'sparse',
+ )
+ end
+
+ let :record_special do
+ {
+ record_type: :crontab,
+ special: 'reboot',
+ command: '/bin/true',
+ on_disk: true,
+ target: 'nobody',
+ }
+ end
+
+ let :record do
+ {
+ record_type: :crontab,
+ minute: ['0', '15', '30', '45'],
+ hour: ['8-18', '20-22'],
+ monthday: ['31'],
+ month: ['12'],
+ weekday: ['7'],
+ special: :absent,
+ command: '/bin/true',
+ on_disk: true,
+ target: 'root',
+ }
+ end
+
+ describe 'when determining the correct filetype' do
+ it 'uses the suntab filetype on Solaris' do
+ Facter.stubs(:value).with(:osfamily).returns 'Solaris'
+ expect(described_class.filetype).to eq(Puppet::Util::FileType::FileTypeSuntab)
+ end
+
+ it 'uses the aixtab filetype on AIX' do
+ Facter.stubs(:value).with(:osfamily).returns 'AIX'
+ expect(described_class.filetype).to eq(Puppet::Util::FileType::FileTypeAixtab)
+ end
+
+ it 'uses the crontab filetype on other platforms' do
+ Facter.stubs(:value).with(:osfamily).returns 'Not a real operating system family'
+ expect(described_class.filetype).to eq(Puppet::Util::FileType::FileTypeCrontab)
+ end
+ end
+
+ # I'd use ENV.expects(:[]).with('USER') but this does not work because
+ # ENV["USER"] is evaluated at load time.
+ describe 'when determining the default target' do
+ it "should use the current user #{ENV['USER']}", if: ENV['USER'] do
+ expect(described_class.default_target).to eq(ENV['USER'])
+ end
+
+ it 'fallbacks to root', unless: ENV['USER'] do
+ expect(described_class.default_target).to eq('root')
+ end
+ end
+
+ describe '.targets' do
+ let(:tabs) { [described_class.default_target] + ['foo', 'bar'] }
+
+ before(:each) do
+ File.expects(:readable?).returns true
+ File.stubs(:file?).returns true
+ File.stubs(:writable?).returns true
+ end
+ after(:each) do
+ File.unstub :readable?, :file?, :writable?
+ Dir.unstub :foreach
+ end
+ it 'adds all crontabs as targets' do
+ Dir.expects(:foreach).multiple_yields(*tabs)
+ expect(described_class.targets).to eq(tabs)
+ end
+ end
+
+ describe 'when parsing a record' do
+ it 'parses a comment' do
+ expect(described_class.parse_line('# This is a test')).to eq(record_type: :comment,
+ line: '# This is a test')
+ end
+
+ it 'gets the resource name of a PUPPET NAME comment' do
+ expect(described_class.parse_line('# Puppet Name: My Fancy Cronjob')).to eq(record_type: :comment,
+ name: 'My Fancy Cronjob',
+ line: '# Puppet Name: My Fancy Cronjob')
+ end
+
+ it 'ignores blank lines' do
+ expect(described_class.parse_line('')).to eq(record_type: :blank, line: '')
+ expect(described_class.parse_line(' ')).to eq(record_type: :blank, line: ' ')
+ expect(described_class.parse_line("\t")).to eq(record_type: :blank, line: "\t")
+ expect(described_class.parse_line(" \t ")).to eq(record_type: :blank, line: " \t ")
+ end
+
+ it 'extracts environment assignments' do
+ # man 5 crontab: MAILTO="" with no value can be used to surpress sending
+ # mails at all
+ expect(described_class.parse_line('MAILTO=""')).to eq(record_type: :environment, line: 'MAILTO=""')
+ expect(described_class.parse_line('FOO=BAR')).to eq(record_type: :environment, line: 'FOO=BAR')
+ expect(described_class.parse_line('FOO_BAR=BAR')).to eq(record_type: :environment, line: 'FOO_BAR=BAR')
+ expect(described_class.parse_line('SPACE = BAR')).to eq(record_type: :environment, line: 'SPACE = BAR')
+ end
+
+ it 'extracts a cron entry' do
+ expect(described_class.parse_line('* * * * * /bin/true')).to eq(record_type: :crontab,
+ hour: :absent,
+ minute: :absent,
+ month: :absent,
+ weekday: :absent,
+ monthday: :absent,
+ special: :absent,
+ command: '/bin/true')
+ expect(described_class.parse_line('0,15,30,45 8-18,20-22 31 12 7 /bin/true')).to eq(record_type: :crontab,
+ minute: ['0', '15', '30', '45'],
+ hour: ['8-18', '20-22'],
+ monthday: ['31'],
+ month: ['12'],
+ weekday: ['7'],
+ special: :absent,
+ command: '/bin/true')
+ # A percent sign will cause the rest of the string to be passed as
+ # standard input and will also act as a newline character. Not sure
+ # if puppet should convert % to a \n as the command property so the
+ # test covers the current behaviour: Do not do any conversions
+ expect(described_class.parse_line('0 22 * * 1-5 mail -s "It\'s 10pm" joe%Joe,%%Where are your kids?%')).to eq(record_type: :crontab,
+ minute: ['0'],
+ hour: ['22'],
+ monthday: :absent,
+ month: :absent,
+ weekday: ['1-5'],
+ special: :absent,
+ command: 'mail -s "It\'s 10pm" joe%Joe,%%Where are your kids?%')
+ end
+
+ describe 'it should support special strings' do
+ ['reboot', 'yearly', 'anually', 'monthly', 'weekly', 'daily', 'midnight', 'hourly'].each do |special|
+ it "should support @#{special}" do
+ expect(described_class.parse_line("@#{special} /bin/true")).to eq(record_type: :crontab,
+ hour: :absent,
+ minute: :absent,
+ month: :absent,
+ weekday: :absent,
+ monthday: :absent,
+ special: special,
+ command: '/bin/true')
+ end
+ end
+ end
+ end
+
+ describe '.instances' do
+ before :each do
+ described_class.stubs(:default_target).returns 'foobar'
+ end
+
+ describe 'on linux' do
+ before(:each) do
+ Facter.stubs(:value).with(:osfamily).returns 'Linux'
+ Facter.stubs(:value).with(:operatingsystem)
+ end
+
+ it 'contains no resources for a user who has no crontab, or for a user that is absent' do
+ # `crontab...` does only capture stdout here. On vixie-cron-4.1
+ # STDERR shows "no crontab for foobar" but stderr is ignored as
+ # well as the exitcode.
+ # STDERR shows "crontab: user `foobar' unknown" but stderr is
+ # ignored as well as the exitcode
+ described_class.target_object('foobar').expects(:`).with('crontab -u foobar -l 2>/dev/null').returns ''
+ expect(described_class.instances.select do |resource|
+ resource.get('target') == 'foobar'
+ end).to be_empty
+ end
+
+ it 'is able to create records from not-managed records' do
+ described_class.stubs(:target_object).returns File.new(my_fixture('simple'))
+ parameters = described_class.instances.map do |p|
+ h = { name: p.get(:name) }
+ Puppet::Type.type(:cron).validproperties.each do |property|
+ h[property] = p.get(property)
+ end
+ h
+ end
+
+ expect(parameters[0][:name]).to match(%r{unmanaged:\$HOME/bin/daily.job_>>_\$HOME/tmp/out_2>&1-\d+})
+ expect(parameters[0][:minute]).to eq(['5'])
+ expect(parameters[0][:hour]).to eq(['0'])
+ expect(parameters[0][:weekday]).to eq(:absent)
+ expect(parameters[0][:month]).to eq(:absent)
+ expect(parameters[0][:monthday]).to eq(:absent)
+ expect(parameters[0][:special]).to eq(:absent)
+ expect(parameters[0][:command]).to match(%r{\$HOME/bin/daily.job >> \$HOME/tmp/out 2>&1})
+ expect(parameters[0][:ensure]).to eq(:present)
+ expect(parameters[0][:environment]).to eq(:absent)
+ expect(parameters[0][:user]).to eq(:absent)
+
+ expect(parameters[1][:name]).to match(%r{unmanaged:\$HOME/bin/monthly-\d+})
+ expect(parameters[1][:minute]).to eq(['15'])
+ expect(parameters[1][:hour]).to eq(['14'])
+ expect(parameters[1][:weekday]).to eq(:absent)
+ expect(parameters[1][:month]).to eq(:absent)
+ expect(parameters[1][:monthday]).to eq(['1'])
+ expect(parameters[1][:special]).to eq(:absent)
+ expect(parameters[1][:command]).to match(%r{\$HOME/bin/monthly})
+ expect(parameters[1][:ensure]).to eq(:present)
+ expect(parameters[1][:environment]).to eq(:absent)
+ expect(parameters[1][:user]).to eq(:absent)
+ expect(parameters[1][:target]).to eq('foobar')
+ end
+
+ it 'is able to parse puppet managed cronjobs' do
+ described_class.stubs(:target_object).returns File.new(my_fixture('managed'))
+ expect(described_class.instances.map do |p|
+ h = { name: p.get(:name) }
+ Puppet::Type.type(:cron).validproperties.each do |property|
+ h[property] = p.get(property)
+ end
+ h
+ end).to eq([
+ {
+ name: 'real_job',
+ minute: :absent,
+ hour: :absent,
+ weekday: :absent,
+ month: :absent,
+ monthday: :absent,
+ special: :absent,
+ command: '/bin/true',
+ ensure: :present,
+ environment: :absent,
+ user: :absent,
+ target: 'foobar',
+ },
+ {
+ name: 'complex_job',
+ minute: :absent,
+ hour: :absent,
+ weekday: :absent,
+ month: :absent,
+ monthday: :absent,
+ special: 'reboot',
+ command: '/bin/true >> /dev/null 2>&1',
+ ensure: :present,
+ environment: [
+ 'MAILTO=foo@example.com',
+ 'SHELL=/bin/sh',
+ ],
+ user: :absent,
+ target: 'foobar',
+ },
+ ])
+ end
+ end
+ end
+
+ describe '.match' do
+ describe 'normal records' do
+ it 'matches when all fields are the same' do
+ expect(described_class.match(record, resource[:name] => resource)).to eq(resource)
+ end
+
+ {
+ minute: ['0', '15', '31', '45'],
+ hour: ['8-18'],
+ monthday: ['30', '31'],
+ month: ['12', '23'],
+ weekday: ['4'],
+ command: '/bin/false',
+ target: 'nobody',
+ }.each_pair do |field, new_value|
+ it "should not match a record when #{field} does not match" do
+ record[field] = new_value
+ expect(described_class.match(record, resource[:name] => resource)).to be_falsey
+ end
+ end
+ end
+
+ describe 'special records' do
+ it 'matches when all fields are the same' do
+ expect(described_class.match(record_special, resource_special[:name] => resource_special)).to eq(resource_special)
+ end
+
+ {
+ special: 'monthly',
+ command: '/bin/false',
+ target: 'root',
+ }.each_pair do |field, new_value|
+ it "should not match a record when #{field} does not match" do
+ record_special[field] = new_value
+ expect(described_class.match(record_special, resource_special[:name] => resource_special)).to be_falsey
+ end
+ end
+ end
+
+ describe 'with a resource without a command' do
+ it 'does not raise an error' do
+ expect { described_class.match(record, resource_sparse[:name] => resource_sparse) }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/unit/type/cron_spec.rb b/spec/unit/type/cron_spec.rb
new file mode 100644
index 0000000..32bde11
--- /dev/null
+++ b/spec/unit/type/cron_spec.rb
@@ -0,0 +1,536 @@
+require 'spec_helper'
+
+describe Puppet::Type.type(:cron), unless: Puppet.features.microsoft_windows? do
+ let(:simple_provider) do
+ provider_class = described_class.provide(:simple) { mk_resource_methods }
+ provider_class.stubs(:suitable?).returns true
+ provider_class
+ end
+
+ before :each do
+ described_class.stubs(:defaultprovider).returns simple_provider
+ end
+
+ after :each do
+ described_class.unprovide(:simple)
+ end
+
+ it 'has :name be its namevar' do
+ expect(described_class.key_attributes).to eq([:name])
+ end
+
+ describe 'when validating attributes' do
+ [:name, :provider].each do |param|
+ it "should have a #{param} parameter" do
+ expect(described_class.attrtype(param)).to eq(:param)
+ end
+ end
+
+ [:command, :special, :minute, :hour, :weekday, :month, :monthday, :environment, :user, :target].each do |property|
+ it "should have a #{property} property" do
+ expect(described_class.attrtype(property)).to eq(:property)
+ end
+ end
+
+ [:command, :minute, :hour, :weekday, :month, :monthday].each do |cronparam|
+ it "should have #{cronparam} of type CronParam" do
+ expect(described_class.attrclass(cronparam).ancestors).to include CronParam
+ end
+ end
+ end
+
+ describe 'when validating values' do
+ describe 'ensure' do
+ it 'supports present as a value for ensure' do
+ expect { described_class.new(name: 'foo', ensure: :present) }.not_to raise_error
+ end
+
+ it 'supports absent as a value for ensure' do
+ expect { described_class.new(name: 'foo', ensure: :absent) }.not_to raise_error
+ end
+
+ it 'does not support other values' do
+ expect { described_class.new(name: 'foo', ensure: :foo) }.to raise_error(Puppet::Error, %r{Invalid value})
+ end
+ end
+
+ describe 'command' do
+ it 'discards leading spaces' do
+ expect(described_class.new(name: 'foo', command: ' /bin/true')[:command]).not_to match Regexp.new(' ')
+ end
+ it 'discards trailing spaces' do
+ expect(described_class.new(name: 'foo', command: '/bin/true ')[:command]).not_to match Regexp.new(' ')
+ end
+ end
+
+ describe 'minute' do
+ it 'supports absent' do
+ expect { described_class.new(name: 'foo', minute: 'absent') }.not_to raise_error
+ end
+
+ it 'supports *' do
+ expect { described_class.new(name: 'foo', minute: '*') }.not_to raise_error
+ end
+
+ it 'translates absent to :absent' do
+ expect(described_class.new(name: 'foo', minute: 'absent')[:minute]).to eq(:absent)
+ end
+
+ it 'translates * to :absent' do
+ expect(described_class.new(name: 'foo', minute: '*')[:minute]).to eq(:absent)
+ end
+
+ it 'supports valid single values' do
+ expect { described_class.new(name: 'foo', minute: '0') }.not_to raise_error
+ expect { described_class.new(name: 'foo', minute: '1') }.not_to raise_error
+ expect { described_class.new(name: 'foo', minute: '59') }.not_to raise_error
+ end
+
+ it 'does not support non numeric characters' do
+ expect { described_class.new(name: 'foo', minute: 'z59') }.to raise_error(Puppet::Error, %r{z59 is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: '5z9') }.to raise_error(Puppet::Error, %r{5z9 is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: '59z') }.to raise_error(Puppet::Error, %r{59z is not a valid minute})
+ end
+
+ it 'does not support single values out of range' do
+ expect { described_class.new(name: 'foo', minute: '-1') }.to raise_error(Puppet::Error, %r{-1 is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: '60') }.to raise_error(Puppet::Error, %r{60 is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: '61') }.to raise_error(Puppet::Error, %r{61 is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: '120') }.to raise_error(Puppet::Error, %r{120 is not a valid minute})
+ end
+
+ it 'supports valid multiple values' do
+ expect { described_class.new(name: 'foo', minute: ['0', '1', '59']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', minute: ['40', '30', '20']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', minute: ['10', '30', '20']) }.not_to raise_error
+ end
+
+ it 'does not support multiple values if at least one is invalid' do
+ # one invalid
+ expect { described_class.new(name: 'foo', minute: ['0', '1', '60']) }.to raise_error(Puppet::Error, %r{60 is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: ['0', '120', '59']) }.to raise_error(Puppet::Error, %r{120 is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: ['-1', '1', '59']) }.to raise_error(Puppet::Error, %r{-1 is not a valid minute})
+ # two invalid
+ expect { described_class.new(name: 'foo', minute: ['0', '61', '62']) }.to raise_error(Puppet::Error, %r{(61|62) is not a valid minute})
+ # all invalid
+ expect { described_class.new(name: 'foo', minute: ['-1', '61', '62']) }.to raise_error(Puppet::Error, %r{(-1|61|62) is not a valid minute})
+ end
+
+ it 'supports valid step syntax' do
+ expect { described_class.new(name: 'foo', minute: '*/2') }.not_to raise_error
+ expect { described_class.new(name: 'foo', minute: '10-16/2') }.not_to raise_error
+ end
+
+ it 'does not support invalid steps' do
+ expect { described_class.new(name: 'foo', minute: '*/A') }.to raise_error(Puppet::Error, %r{\*/A is not a valid minute})
+ expect { described_class.new(name: 'foo', minute: '*/2A') }.to raise_error(Puppet::Error, %r{\*/2A is not a valid minute})
+ # As it turns out cron does not complaining about steps that exceed the valid range
+ # expect { described_class.new(:name => 'foo', :minute => '*/120' ) }.to raise_error(Puppet::Error, /is not a valid minute/)
+ end
+ end
+
+ describe 'hour' do
+ it 'supports absent' do
+ expect { described_class.new(name: 'foo', hour: 'absent') }.not_to raise_error
+ end
+
+ it 'supports *' do
+ expect { described_class.new(name: 'foo', hour: '*') }.not_to raise_error
+ end
+
+ it 'translates absent to :absent' do
+ expect(described_class.new(name: 'foo', hour: 'absent')[:hour]).to eq(:absent)
+ end
+
+ it 'translates * to :absent' do
+ expect(described_class.new(name: 'foo', hour: '*')[:hour]).to eq(:absent)
+ end
+
+ it 'supports valid single values' do
+ expect { described_class.new(name: 'foo', hour: '0') }.not_to raise_error
+ expect { described_class.new(name: 'foo', hour: '11') }.not_to raise_error
+ expect { described_class.new(name: 'foo', hour: '12') }.not_to raise_error
+ expect { described_class.new(name: 'foo', hour: '13') }.not_to raise_error
+ expect { described_class.new(name: 'foo', hour: '23') }.not_to raise_error
+ end
+
+ it 'does not support non numeric characters' do
+ expect { described_class.new(name: 'foo', hour: 'z15') }.to raise_error(Puppet::Error, %r{z15 is not a valid hour})
+ expect { described_class.new(name: 'foo', hour: '1z5') }.to raise_error(Puppet::Error, %r{1z5 is not a valid hour})
+ expect { described_class.new(name: 'foo', hour: '15z') }.to raise_error(Puppet::Error, %r{15z is not a valid hour})
+ end
+
+ it 'does not support single values out of range' do
+ expect { described_class.new(name: 'foo', hour: '-1') }.to raise_error(Puppet::Error, %r{-1 is not a valid hour})
+ expect { described_class.new(name: 'foo', hour: '24') }.to raise_error(Puppet::Error, %r{24 is not a valid hour})
+ expect { described_class.new(name: 'foo', hour: '120') }.to raise_error(Puppet::Error, %r{120 is not a valid hour})
+ end
+
+ it 'supports valid multiple values' do
+ expect { described_class.new(name: 'foo', hour: ['0', '1', '23']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', hour: ['5', '16', '14']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', hour: ['16', '13', '9']) }.not_to raise_error
+ end
+
+ it 'does not support multiple values if at least one is invalid' do
+ # one invalid
+ expect { described_class.new(name: 'foo', hour: ['0', '1', '24']) }.to raise_error(Puppet::Error, %r{24 is not a valid hour})
+ expect { described_class.new(name: 'foo', hour: ['0', '-1', '5']) }.to raise_error(Puppet::Error, %r{-1 is not a valid hour})
+ expect { described_class.new(name: 'foo', hour: ['-1', '1', '23']) }.to raise_error(Puppet::Error, %r{-1 is not a valid hour})
+ # two invalid
+ expect { described_class.new(name: 'foo', hour: ['0', '25', '26']) }.to raise_error(Puppet::Error, %r{(25|26) is not a valid hour})
+ # all invalid
+ expect { described_class.new(name: 'foo', hour: ['-1', '24', '120']) }.to raise_error(Puppet::Error, %r{(-1|24|120) is not a valid hour})
+ end
+
+ it 'supports valid step syntax' do
+ expect { described_class.new(name: 'foo', hour: '*/2') }.not_to raise_error
+ expect { described_class.new(name: 'foo', hour: '10-18/4') }.not_to raise_error
+ end
+
+ it 'does not support invalid steps' do
+ expect { described_class.new(name: 'foo', hour: '*/A') }.to raise_error(Puppet::Error, %r{\*/A is not a valid hour})
+ expect { described_class.new(name: 'foo', hour: '*/2A') }.to raise_error(Puppet::Error, %r{\*/2A is not a valid hour})
+ # As it turns out cron does not complaining about steps that exceed the valid range
+ # expect { described_class.new(:name => 'foo', :hour => '*/26' ) }.to raise_error(Puppet::Error, /is not a valid hour/)
+ end
+ end
+
+ describe 'weekday' do
+ it 'supports absent' do
+ expect { described_class.new(name: 'foo', weekday: 'absent') }.not_to raise_error
+ end
+
+ it 'supports *' do
+ expect { described_class.new(name: 'foo', weekday: '*') }.not_to raise_error
+ end
+
+ it 'translates absent to :absent' do
+ expect(described_class.new(name: 'foo', weekday: 'absent')[:weekday]).to eq(:absent)
+ end
+
+ it 'translates * to :absent' do
+ expect(described_class.new(name: 'foo', weekday: '*')[:weekday]).to eq(:absent)
+ end
+
+ it 'supports valid numeric weekdays' do
+ expect { described_class.new(name: 'foo', weekday: '0') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: '1') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: '6') }.not_to raise_error
+ # According to http://www.manpagez.com/man/5/crontab 7 is also valid (Sunday)
+ expect { described_class.new(name: 'foo', weekday: '7') }.not_to raise_error
+ end
+
+ it 'supports valid weekdays as words (long version)' do
+ expect { described_class.new(name: 'foo', weekday: 'Monday') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Tuesday') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Wednesday') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Thursday') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Friday') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Saturday') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Sunday') }.not_to raise_error
+ end
+
+ it 'supports valid weekdays as words (3 character version)' do
+ expect { described_class.new(name: 'foo', weekday: 'Mon') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Tue') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Wed') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Thu') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Fri') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Sat') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: 'Sun') }.not_to raise_error
+ end
+
+ it 'does not support numeric values out of range' do
+ expect { described_class.new(name: 'foo', weekday: '-1') }.to raise_error(Puppet::Error, %r{-1 is not a valid weekday})
+ expect { described_class.new(name: 'foo', weekday: '8') }.to raise_error(Puppet::Error, %r{8 is not a valid weekday})
+ end
+
+ it 'does not support invalid weekday names' do
+ expect { described_class.new(name: 'foo', weekday: 'Sar') }.to raise_error(Puppet::Error, %r{Sar is not a valid weekday})
+ end
+
+ it 'supports valid multiple values' do
+ expect { described_class.new(name: 'foo', weekday: ['0', '1', '6']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: ['Mon', 'Wed', 'Friday']) }.not_to raise_error
+ end
+
+ it 'does not support multiple values if at least one is invalid' do
+ # one invalid
+ expect { described_class.new(name: 'foo', weekday: ['0', '1', '8']) }.to raise_error(Puppet::Error, %r{8 is not a valid weekday})
+ expect { described_class.new(name: 'foo', weekday: ['Mon', 'Fii', 'Sat']) }.to raise_error(Puppet::Error, %r{Fii is not a valid weekday})
+ # two invalid
+ expect { described_class.new(name: 'foo', weekday: ['Mos', 'Fii', 'Sat']) }.to raise_error(Puppet::Error, %r{(Mos|Fii) is not a valid weekday})
+ # all invalid
+ expect { described_class.new(name: 'foo', weekday: ['Mos', 'Fii', 'Saa']) }.to raise_error(Puppet::Error, %r{(Mos|Fii|Saa) is not a valid weekday})
+ expect { described_class.new(name: 'foo', weekday: ['-1', '8', '11']) }.to raise_error(Puppet::Error, %r{(-1|8|11) is not a valid weekday})
+ end
+
+ it 'supports valid step syntax' do
+ expect { described_class.new(name: 'foo', weekday: '*/2') }.not_to raise_error
+ expect { described_class.new(name: 'foo', weekday: '0-4/2') }.not_to raise_error
+ end
+
+ it 'does not support invalid steps' do
+ expect { described_class.new(name: 'foo', weekday: '*/A') }.to raise_error(Puppet::Error, %r{\*/A is not a valid weekday})
+ expect { described_class.new(name: 'foo', weekday: '*/2A') }.to raise_error(Puppet::Error, %r{\*/2A is not a valid weekday})
+ # As it turns out cron does not complaining about steps that exceed the valid range
+ # expect { described_class.new(:name => 'foo', :weekday => '*/9' ) }.to raise_error(Puppet::Error, /is not a valid weekday/)
+ end
+ end
+
+ describe 'month' do
+ it 'supports absent' do
+ expect { described_class.new(name: 'foo', month: 'absent') }.not_to raise_error
+ end
+
+ it 'supports *' do
+ expect { described_class.new(name: 'foo', month: '*') }.not_to raise_error
+ end
+
+ it 'translates absent to :absent' do
+ expect(described_class.new(name: 'foo', month: 'absent')[:month]).to eq(:absent)
+ end
+
+ it 'translates * to :absent' do
+ expect(described_class.new(name: 'foo', month: '*')[:month]).to eq(:absent)
+ end
+
+ it 'supports valid numeric values' do
+ expect { described_class.new(name: 'foo', month: '1') }.not_to raise_error
+ expect { described_class.new(name: 'foo', month: '12') }.not_to raise_error
+ end
+
+ it 'supports valid months as words' do
+ expect(described_class.new(name: 'foo', month: 'January')[:month]).to eq(['1'])
+ expect(described_class.new(name: 'foo', month: 'February')[:month]).to eq(['2'])
+ expect(described_class.new(name: 'foo', month: 'March')[:month]).to eq(['3'])
+ expect(described_class.new(name: 'foo', month: 'April')[:month]).to eq(['4'])
+ expect(described_class.new(name: 'foo', month: 'May')[:month]).to eq(['5'])
+ expect(described_class.new(name: 'foo', month: 'June')[:month]).to eq(['6'])
+ expect(described_class.new(name: 'foo', month: 'July')[:month]).to eq(['7'])
+ expect(described_class.new(name: 'foo', month: 'August')[:month]).to eq(['8'])
+ expect(described_class.new(name: 'foo', month: 'September')[:month]).to eq(['9'])
+ expect(described_class.new(name: 'foo', month: 'October')[:month]).to eq(['10'])
+ expect(described_class.new(name: 'foo', month: 'November')[:month]).to eq(['11'])
+ expect(described_class.new(name: 'foo', month: 'December')[:month]).to eq(['12'])
+ end
+
+ it 'supports valid months as words (3 character short version)' do
+ expect(described_class.new(name: 'foo', month: 'Jan')[:month]).to eq(['1'])
+ expect(described_class.new(name: 'foo', month: 'Feb')[:month]).to eq(['2'])
+ expect(described_class.new(name: 'foo', month: 'Mar')[:month]).to eq(['3'])
+ expect(described_class.new(name: 'foo', month: 'Apr')[:month]).to eq(['4'])
+ expect(described_class.new(name: 'foo', month: 'May')[:month]).to eq(['5'])
+ expect(described_class.new(name: 'foo', month: 'Jun')[:month]).to eq(['6'])
+ expect(described_class.new(name: 'foo', month: 'Jul')[:month]).to eq(['7'])
+ expect(described_class.new(name: 'foo', month: 'Aug')[:month]).to eq(['8'])
+ expect(described_class.new(name: 'foo', month: 'Sep')[:month]).to eq(['9'])
+ expect(described_class.new(name: 'foo', month: 'Oct')[:month]).to eq(['10'])
+ expect(described_class.new(name: 'foo', month: 'Nov')[:month]).to eq(['11'])
+ expect(described_class.new(name: 'foo', month: 'Dec')[:month]).to eq(['12'])
+ end
+
+ it 'does not support numeric values out of range' do
+ expect { described_class.new(name: 'foo', month: '-1') }.to raise_error(Puppet::Error, %r{-1 is not a valid month})
+ expect { described_class.new(name: 'foo', month: '0') }.to raise_error(Puppet::Error, %r{0 is not a valid month})
+ expect { described_class.new(name: 'foo', month: '13') }.to raise_error(Puppet::Error, %r{13 is not a valid month})
+ end
+
+ it 'does not support words that are not valid months' do
+ expect { described_class.new(name: 'foo', month: 'Jal') }.to raise_error(Puppet::Error, %r{Jal is not a valid month})
+ end
+
+ it 'does not support single values out of range' do
+ expect { described_class.new(name: 'foo', month: '-1') }.to raise_error(Puppet::Error, %r{-1 is not a valid month})
+ expect { described_class.new(name: 'foo', month: '60') }.to raise_error(Puppet::Error, %r{60 is not a valid month})
+ expect { described_class.new(name: 'foo', month: '61') }.to raise_error(Puppet::Error, %r{61 is not a valid month})
+ expect { described_class.new(name: 'foo', month: '120') }.to raise_error(Puppet::Error, %r{120 is not a valid month})
+ end
+
+ it 'supports valid multiple values' do
+ expect { described_class.new(name: 'foo', month: ['1', '9', '12']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', month: ['Jan', 'March', 'Jul']) }.not_to raise_error
+ end
+
+ it 'does not support multiple values if at least one is invalid' do
+ # one invalid
+ expect { described_class.new(name: 'foo', month: ['0', '1', '12']) }.to raise_error(Puppet::Error, %r{0 is not a valid month})
+ expect { described_class.new(name: 'foo', month: ['1', '13', '10']) }.to raise_error(Puppet::Error, %r{13 is not a valid month})
+ expect { described_class.new(name: 'foo', month: ['Jan', 'Feb', 'Jxx']) }.to raise_error(Puppet::Error, %r{Jxx is not a valid month})
+ # two invalid
+ expect { described_class.new(name: 'foo', month: ['Jan', 'Fex', 'Jux']) }.to raise_error(Puppet::Error, %r{(Fex|Jux) is not a valid month})
+ # all invalid
+ expect { described_class.new(name: 'foo', month: ['-1', '0', '13']) }.to raise_error(Puppet::Error, %r{(-1|0|13) is not a valid month})
+ expect { described_class.new(name: 'foo', month: ['Jax', 'Fex', 'Aux']) }.to raise_error(Puppet::Error, %r{(Jax|Fex|Aux) is not a valid month})
+ end
+
+ it 'supports valid step syntax' do
+ expect { described_class.new(name: 'foo', month: '*/2') }.not_to raise_error
+ expect { described_class.new(name: 'foo', month: '1-12/3') }.not_to raise_error
+ end
+
+ it 'does not support invalid steps' do
+ expect { described_class.new(name: 'foo', month: '*/A') }.to raise_error(Puppet::Error, %r{\*/A is not a valid month})
+ expect { described_class.new(name: 'foo', month: '*/2A') }.to raise_error(Puppet::Error, %r{\*/2A is not a valid month})
+ # As it turns out cron does not complaining about steps that exceed the valid range
+ # expect { described_class.new(:name => 'foo', :month => '*/13' ) }.to raise_error(Puppet::Error, /is not a valid month/)
+ end
+ end
+
+ describe 'monthday' do
+ it 'supports absent' do
+ expect { described_class.new(name: 'foo', monthday: 'absent') }.not_to raise_error
+ end
+
+ it 'supports *' do
+ expect { described_class.new(name: 'foo', monthday: '*') }.not_to raise_error
+ end
+
+ it 'translates absent to :absent' do
+ expect(described_class.new(name: 'foo', monthday: 'absent')[:monthday]).to eq(:absent)
+ end
+
+ it 'translates * to :absent' do
+ expect(described_class.new(name: 'foo', monthday: '*')[:monthday]).to eq(:absent)
+ end
+
+ it 'supports valid single values' do
+ expect { described_class.new(name: 'foo', monthday: '1') }.not_to raise_error
+ expect { described_class.new(name: 'foo', monthday: '30') }.not_to raise_error
+ expect { described_class.new(name: 'foo', monthday: '31') }.not_to raise_error
+ end
+
+ it 'does not support non numeric characters' do
+ expect { described_class.new(name: 'foo', monthday: 'z23') }.to raise_error(Puppet::Error, %r{z23 is not a valid monthday})
+ expect { described_class.new(name: 'foo', monthday: '2z3') }.to raise_error(Puppet::Error, %r{2z3 is not a valid monthday})
+ expect { described_class.new(name: 'foo', monthday: '23z') }.to raise_error(Puppet::Error, %r{23z is not a valid monthday})
+ end
+
+ it 'does not support single values out of range' do
+ expect { described_class.new(name: 'foo', monthday: '-1') }.to raise_error(Puppet::Error, %r{-1 is not a valid monthday})
+ expect { described_class.new(name: 'foo', monthday: '0') }.to raise_error(Puppet::Error, %r{0 is not a valid monthday})
+ expect { described_class.new(name: 'foo', monthday: '32') }.to raise_error(Puppet::Error, %r{32 is not a valid monthday})
+ end
+
+ it 'supports valid multiple values' do
+ expect { described_class.new(name: 'foo', monthday: ['1', '23', '31']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', monthday: ['31', '23', '1']) }.not_to raise_error
+ expect { described_class.new(name: 'foo', monthday: ['1', '31', '23']) }.not_to raise_error
+ end
+
+ it 'does not support multiple values if at least one is invalid' do
+ # one invalid
+ expect { described_class.new(name: 'foo', monthday: ['1', '23', '32']) }.to raise_error(Puppet::Error, %r{32 is not a valid monthday})
+ expect { described_class.new(name: 'foo', monthday: ['-1', '12', '23']) }.to raise_error(Puppet::Error, %r{-1 is not a valid monthday})
+ expect { described_class.new(name: 'foo', monthday: ['13', '32', '30']) }.to raise_error(Puppet::Error, %r{32 is not a valid monthday})
+ # two invalid
+ expect { described_class.new(name: 'foo', monthday: ['-1', '0', '23']) }.to raise_error(Puppet::Error, %r{(-1|0) is not a valid monthday})
+ # all invalid
+ expect { described_class.new(name: 'foo', monthday: ['-1', '0', '32']) }.to raise_error(Puppet::Error, %r{(-1|0|32) is not a valid monthday})
+ end
+
+ it 'supports valid step syntax' do
+ expect { described_class.new(name: 'foo', monthday: '*/2') }.not_to raise_error
+ expect { described_class.new(name: 'foo', monthday: '10-16/2') }.not_to raise_error
+ end
+
+ it 'does not support invalid steps' do
+ expect { described_class.new(name: 'foo', monthday: '*/A') }.to raise_error(Puppet::Error, %r{\*/A is not a valid monthday})
+ expect { described_class.new(name: 'foo', monthday: '*/2A') }.to raise_error(Puppet::Error, %r{\*/2A is not a valid monthday})
+ # As it turns out cron does not complaining about steps that exceed the valid range
+ # expect { described_class.new(:name => 'foo', :monthday => '*/32' ) }.to raise_error(Puppet::Error, /is not a valid monthday/)
+ end
+ end
+
+ describe 'special' do
+ ['reboot', 'yearly', 'annually', 'monthly', 'weekly', 'daily', 'midnight', 'hourly'].each do |value|
+ it "should support the value '#{value}'" do
+ expect { described_class.new(name: 'foo', special: value) }.not_to raise_error
+ end
+ end
+
+ context 'when combined with numeric schedule fields' do
+ context "which are 'absent'" do
+ [['reboot', 'yearly', 'annually', 'monthly', 'weekly', 'daily', 'midnight', 'hourly'], :absent].flatten.each do |value|
+ it "should accept the value '#{value}' for special" do
+ expect {
+ described_class.new(name: 'foo', minute: :absent, special: value)
+ }.not_to raise_error
+ end
+ end
+ end
+ context 'which are not absent' do
+ ['reboot', 'yearly', 'annually', 'monthly', 'weekly', 'daily', 'midnight', 'hourly'].each do |value|
+ it "should not accept the value '#{value}' for special" do
+ expect {
+ described_class.new(name: 'foo', minute: '1', special: value)
+ }.to raise_error(Puppet::Error, %r{cannot specify both a special schedule and a value})
+ end
+ end
+ it "accepts the 'absent' value for special" do
+ expect {
+ described_class.new(name: 'foo', minute: '1', special: :absent)
+ }.not_to raise_error
+ end
+ end
+ end
+ end
+
+ describe 'environment' do
+ it 'accepts an :environment that looks like a path' do
+ expect {
+ described_class.new(name: 'foo', environment: 'PATH=/bin:/usr/bin:/usr/sbin')
+ }.not_to raise_error
+ end
+
+ it "does not accept environment variables that do not contain '='" do
+ expect {
+ described_class.new(name: 'foo', environment: 'INVALID')
+ }.to raise_error(Puppet::Error, %r{Invalid environment setting "INVALID"})
+ end
+
+ it "accepts empty environment variables that do not contain '='" do
+ expect {
+ described_class.new(name: 'foo', environment: 'MAILTO=')
+ }.not_to raise_error
+ end
+
+ it "accepts 'absent'" do
+ expect {
+ described_class.new(name: 'foo', environment: 'absent')
+ }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'when autorequiring resources' do
+ let(:user_bob) { Puppet::Type.type(:user).new(name: 'bob', ensure: :present) }
+ let(:user_alice) { Puppet::Type.type(:user).new(name: 'alice', ensure: :present) }
+ let(:catalog) { Puppet::Resource::Catalog.new }
+
+ before :each do
+ catalog.add_resource user_bob, user_alice
+ end
+
+ it 'autorequires the user' do
+ resource = described_class.new(name: 'dummy', command: '/usr/bin/uptime', user: 'alice')
+ catalog.add_resource resource
+ req = resource.autorequire
+ expect(req.size).to eq(1)
+ expect(req[0].target).to eq(resource)
+ expect(req[0].source).to eq(user_alice)
+ end
+ end
+
+ it 'does not require a command when removing an entry' do
+ entry = described_class.new(name: 'test_entry', ensure: :absent)
+ expect(entry.value(:command)).to eq(nil)
+ end
+
+ it 'defaults to user => root if Etc.getpwuid(Process.uid) returns nil (#12357)' do
+ Etc.expects(:getpwuid).returns(nil)
+ entry = described_class.new(name: 'test_entry', ensure: :present)
+ expect(entry.value(:user)).to eql 'root'
+ end
+end