├── .github ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── mend.yml │ ├── nightly.yml │ ├── release.yml │ └── release_prep.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── CHANGELOG.md ├── CODEOWNERS ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── docs ├── custom_matrix.json └── md │ ├── content │ ├── about.md │ ├── litmus-concepts.md │ └── usage │ │ ├── _index.md │ │ ├── commands │ │ ├── _index.md │ │ ├── command-reference.md │ │ └── litmus-core-commands.md │ │ ├── converting-modules-to-use-Litmus.md │ │ ├── litmus-helper-functions.md │ │ ├── testing │ │ ├── _index.md │ │ ├── litmus-test-examples.md │ │ └── running-acceptance-tests.md │ │ └── tools-included-in-Litmus.md │ ├── go.mod │ └── md.go ├── exe ├── matrix.json ├── matrix_from_metadata_v2 └── matrix_from_metadata_v3 ├── lib ├── puppet_litmus.rb └── puppet_litmus │ ├── inventory_manipulation.rb │ ├── puppet_helpers.rb │ ├── rake_helper.rb │ ├── rake_tasks.rb │ ├── spec_helper_acceptance.rb │ ├── util.rb │ └── version.rb ├── puppet_litmus.gemspec ├── resources └── litmus-dark-RGB.png └── spec ├── data ├── doot.tar.gz ├── inventory.yaml └── jim.yaml ├── exe ├── fake_metadata.json ├── matrix_from_metadata_v2_spec.rb └── matrix_from_metadata_v3_spec.rb ├── lib └── puppet_litmus │ ├── inventory_manipulation_spec.rb │ ├── puppet_helpers_spec.rb │ ├── puppet_litmus_version_spec.rb │ ├── rake_helper_spec.rb │ ├── rake_tasks_spec.rb │ └── util_spec.rb ├── spec_helper.rb └── support ├── inventory.rb └── inventorytesting.yaml /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | Provide a detailed description of all the changes present in this pull request. 3 | 4 | ## Additional Context 5 | Add any additional context about the problem here. 6 | - [ ] Root cause and the steps to reproduce. (If applicable) 7 | - [ ] Thought process behind the implementation. 8 | 9 | ## Related Issues (if any) 10 | Mention any related issues or pull requests. 11 | 12 | ## Checklist 13 | - [ ] 🟢 Spec tests. 14 | - [ ] 🟢 Acceptance tests. 15 | - [ ] Manually verified. 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "ci" 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "main" 10 | workflow_dispatch: 11 | 12 | env: 13 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 14 | 15 | jobs: 16 | 17 | spec: 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | ruby_version: 22 | - '3.2' 23 | name: "spec (ruby ${{ matrix.ruby_version }})" 24 | uses: "puppetlabs/cat-github-actions/.github/workflows/gem_ci.yml@main" 25 | secrets: "inherit" 26 | with: 27 | rake_task: "spec:coverage" 28 | ruby_version: ${{ matrix.ruby_version }} 29 | -------------------------------------------------------------------------------- /.github/workflows/mend.yml: -------------------------------------------------------------------------------- 1 | name: "mend" 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "main" 7 | schedule: 8 | - cron: "0 0 * * *" 9 | workflow_dispatch: 10 | 11 | jobs: 12 | 13 | mend: 14 | uses: "puppetlabs/cat-github-actions/.github/workflows/tooling_mend_ruby.yml@main" 15 | secrets: "inherit" 16 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: "nightly" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | spec: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | ruby_version: 14 | - '3.2' 15 | name: "spec (ruby ${{ matrix.ruby_version }})" 16 | uses: "puppetlabs/cat-github-actions/.github/workflows/gem_ci.yml@main" 17 | secrets: "inherit" 18 | with: 19 | ruby_version: ${{ matrix.ruby_version }} 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | target: 7 | description: "The target for the release. This can be a commit sha or a branch." 8 | required: false 9 | default: "main" 10 | 11 | jobs: 12 | release: 13 | uses: "puppetlabs/cat-github-actions/.github/workflows/gem_release.yml@main" 14 | with: 15 | target: "${{ github.event.inputs.target }}" 16 | secrets: "inherit" 17 | -------------------------------------------------------------------------------- /.github/workflows/release_prep.yml: -------------------------------------------------------------------------------- 1 | name: "Release Prep" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | target: 7 | description: "The target for the release. This can be a commit sha or a branch." 8 | required: false 9 | default: "main" 10 | version: 11 | description: "Version of gem to be released." 12 | required: true 13 | 14 | jobs: 15 | release_prep: 16 | uses: "puppetlabs/cat-github-actions/.github/workflows/gem_release_prep.yml@main" 17 | with: 18 | target: "${{ github.event.inputs.target }}" 19 | version: "${{ github.event.inputs.version }}" 20 | secrets: "inherit" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *~ 3 | /.bundle/ 4 | /.yardoc/ 5 | /Gemfile.lock 6 | /coverage 7 | /doc/ 8 | /vendor/ 9 | /Gemfile.local 10 | /.idea/ 11 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: 4 | - rubocop-performance 5 | - rubocop-rspec 6 | 7 | AllCops: 8 | Exclude: 9 | - Gemfile 10 | - Rakefile 11 | - spec/fixtures/**/* 12 | - vendor/bundle/**/* 13 | NewCops: enable 14 | SuggestExtensions: false 15 | TargetRubyVersion: '3.1' 16 | 17 | # Disabled 18 | Style/ClassAndModuleChildren: 19 | Enabled: false 20 | Layout/LineLength: 21 | Max: 200 22 | Gemspec/RequireMFA: 23 | Enabled: false 24 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2024-02-13 06:54:13 UTC using RuboCop version 1.50.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 16 10 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 11 | Metrics/AbcSize: 12 | Max: 105 13 | 14 | # Offense count: 10 15 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode. 16 | # AllowedMethods: refine 17 | Metrics/BlockLength: 18 | Max: 348 19 | 20 | # Offense count: 8 21 | # Configuration parameters: AllowedMethods, AllowedPatterns. 22 | Metrics/CyclomaticComplexity: 23 | Max: 33 24 | 25 | # Offense count: 18 26 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 27 | Metrics/MethodLength: 28 | Max: 79 29 | 30 | # Offense count: 3 31 | # Configuration parameters: CountComments, CountAsOne. 32 | Metrics/ModuleLength: 33 | Max: 255 34 | 35 | # Offense count: 8 36 | # Configuration parameters: AllowedMethods, AllowedPatterns. 37 | Metrics/PerceivedComplexity: 38 | Max: 38 39 | 40 | # Offense count: 1 41 | # Configuration parameters: ForbiddenDelimiters. 42 | # ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$)) 43 | Naming/HeredocDelimiterNaming: 44 | Exclude: 45 | - 'puppet_litmus.gemspec' 46 | 47 | # Offense count: 1 48 | # Configuration parameters: MinSize. 49 | Performance/CollectionLiteralInLoop: 50 | Exclude: 51 | - 'lib/puppet_litmus/rake_helper.rb' 52 | 53 | # Offense count: 22 54 | RSpec/AnyInstance: 55 | Exclude: 56 | - 'spec/lib/puppet_litmus/puppet_helpers_spec.rb' 57 | - 'spec/lib/puppet_litmus/rake_helper_spec.rb' 58 | - 'spec/lib/puppet_litmus/rake_tasks_spec.rb' 59 | 60 | # Offense count: 2 61 | # Configuration parameters: IgnoredMetadata. 62 | RSpec/DescribeClass: 63 | Exclude: 64 | - '**/spec/features/**/*' 65 | - '**/spec/requests/**/*' 66 | - '**/spec/routing/**/*' 67 | - '**/spec/system/**/*' 68 | - '**/spec/views/**/*' 69 | - 'spec/exe/*.rb' 70 | - 'spec/lib/puppet_litmus/rake_tasks_spec.rb' 71 | 72 | # Offense count: 22 73 | # Configuration parameters: CountAsOne. 74 | RSpec/ExampleLength: 75 | Max: 22 76 | 77 | # Offense count: 106 78 | # Configuration parameters: . 79 | # SupportedStyles: have_received, receive 80 | RSpec/MessageSpies: 81 | EnforcedStyle: receive 82 | 83 | # Offense count: 37 84 | RSpec/MultipleExpectations: 85 | Max: 8 86 | 87 | # Offense count: 12 88 | # Configuration parameters: AllowSubject. 89 | RSpec/MultipleMemoizedHelpers: 90 | Max: 9 91 | 92 | # Offense count: 5 93 | # Configuration parameters: AllowedPatterns. 94 | # AllowedPatterns: ^expect_, ^assert_ 95 | RSpec/NoExpectationExample: 96 | Exclude: 97 | - 'spec/lib/puppet_litmus/rake_helper_spec.rb' 98 | 99 | # Offense count: 93 100 | RSpec/StubbedMock: 101 | Exclude: 102 | - 'spec/lib/puppet_litmus/puppet_helpers_spec.rb' 103 | - 'spec/lib/puppet_litmus/rake_helper_spec.rb' 104 | - 'spec/lib/puppet_litmus/rake_tasks_spec.rb' 105 | 106 | # Offense count: 7 107 | Style/OpenStructUse: 108 | Exclude: 109 | - 'exe/matrix_from_metadata_v3' 110 | - 'lib/puppet_litmus/puppet_helpers.rb' 111 | - 'spec/spec_helper.rb' 112 | 113 | # Offense count: 4 114 | # This cop supports safe autocorrection (--autocorrect). 115 | Style/StderrPuts: 116 | Exclude: 117 | - 'exe/matrix_from_metadata_v3' 118 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Setting ownership to the tooling team 2 | * @puppetlabs/devx 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem 'rake' 7 | gem 'rspec', '~> 3.1' 8 | gem 'rspec-collection_matchers', '~> 1.0' 9 | gem 'rspec-its', '~> 1.0' 10 | 11 | gem 'rubocop', '~> 1.64.0' 12 | gem 'rubocop-rspec', '~> 3.0' 13 | gem 'rubocop-performance', '~> 1.16' 14 | 15 | gem 'simplecov' 16 | gem 'simplecov-console' 17 | end 18 | 19 | group :development do 20 | gem 'pry' 21 | gem 'yard' 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Litmus 2 | 3 | [![Code Owners](https://img.shields.io/badge/owners-DevX--team-blue)](https://github.com/puppetlabs/puppet_litmus/blob/main/CODEOWNERS) 4 | ![ci](https://github.com/puppetlabs/puppet_litmus/actions/workflows/ci.yml/badge.svg) 5 | [![Gem Version](https://badge.fury.io/rb/puppet_litmus.svg)](https://badge.fury.io/rb/puppet_litmus) 6 | 7 |
8 | litmus logo 12 |
13 | 14 | ## Overview 15 | 16 | Litmus is a command line tool that allows you to run acceptance tests against Puppet modules. 17 | 18 | Litmus allows you to: 19 | 20 | - Provision targets to test against 21 | - Install a Puppet agent 22 | - Install a module 23 | - Run tests 24 | - Tear down the infrastructure 25 | 26 | Litmus also facilitates parallel test runs and running tests in isolation. Each step is standalone, allowing other operations between test runs, such as debugging or configuration updates on the test targets. 27 | 28 | Install Litmus as a gem by running `gem install puppet_litmus`. 29 | 30 | - Note if you choose to override the `litmus_inventory.yaml` location, please ensure that the directory structure you define exists. 31 | 32 | ## Agent installs 33 | 34 | ### Install a specific puppet agent version 35 | 36 | To install a specific version of the puppet agent, you can export the `PUPPET_VERSION` env var, like below: 37 | ``` 38 | export PUPPET_VERSION=8.8.1 39 | ``` 40 | 41 | When set, the `litmus:install_agent` rake task will install the specified version. The default is `latest`. 42 | 43 | ## Installing puppetcore agents 44 | 45 | To install a puppetcore puppet agent through the `litmus:install_agent` rake task, you need to export your Forge API key as an env var, like below: 46 | ``` 47 | export PUPPET_FORGE_TOKEN='' 48 | ``` 49 | 50 | ## matrix_from_metadata_v3 51 | 52 | matrix_from_metadata_v3 tool generates a github action matrix from the supported operating systems listed in the module's metadata.json. 53 | 54 | How to use it: 55 | in the project module root directory run `bundle exec matrix_from_metadata_v3` 56 | 57 | ### Optional arguments 58 | 59 | | argument | value | default | description | 60 | |---------------------|-------|-------------------|-------------| 61 | | --matrix | FILE | built-in | File containing possible collections and provisioners | 62 | | --metadata | FILE | metadata.json | File containing module metadata json | 63 | | --debug | | | Enable debug messages | 64 | | --quiet | | | Disable notice messages | 65 | | --output | TYPE | auto | Type of output to generate; auto, github or stdout | 66 | | --runner | NAME | ubuntu-latest | Default Github action runner | 67 | | --puppet-include | MAJOR | | Select puppet major version | 68 | | --puppet-exclude | MAJOR | | Filter puppet major version | 69 | | --platform-include | REGEX | | Select platform | 70 | | --platform-exclude | REGEX | | Filter platform | 71 | | --arch-include | REGEX | | Select architecture | 72 | | --arch-exclude | REGEX | | Filter architecture | 73 | | --provision-prefer | NAME | docker | Prefer provisioner | 74 | | --provision-include | NAME | all | Select provisioner | 75 | | --provision-exclude | NAME | provision_service | Filter provisioner | 76 | 77 | > Refer to the [built-in matrix.json](https://github.com/puppetlabs/puppet_litmus/blob/main/exe/matrix.json) for a list of supported collection, provisioners, and platforms. 78 | 79 | ### Examples 80 | 81 | * Only specific platforms 82 | ```sh 83 | matrix_from_metadata_v3 --platform-include redhat --platform-include 'ubuntu-(20|22).04' 84 | ``` 85 | * Exclude platforms 86 | ```sh 87 | matrix_from_metadata_v3 --platform-exclude redhat-7 --platform-exclude ubuntu-18.04 88 | ``` 89 | * Exclude architecture 90 | ```sh 91 | matrix_from_metadata_v3 --arch-exclude x86_64 92 | ``` 93 | 94 | ## Documentation 95 | 96 | For documentation, see our [Litmus Docs Site](https://puppetlabs.github.io/content-and-tooling-team/docs/litmus/). 97 | 98 | ## License 99 | 100 | This codebase is licensed under Apache 2.0. However, the open source dependencies included in this codebase might be subject to other software licenses such as AGPL, GPL2.0, and MIT. 101 | 102 | ## Other Resources 103 | 104 | - [Is it Worth the Time?](https://xkcd.com/1205/) 105 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rubocop/rake_task' 3 | require 'puppet_litmus/version' 4 | require 'rspec/core/rake_task' 5 | require 'yard' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | task :default => :spec 9 | 10 | namespace :spec do 11 | desc 'Run RSpec code examples with coverage collection' 12 | task :coverage do 13 | ENV['COVERAGE'] = 'yes' 14 | Rake::Task['spec'].execute 15 | end 16 | end 17 | 18 | YARD::Rake::YardocTask.new do |t| 19 | end 20 | -------------------------------------------------------------------------------- /docs/custom_matrix.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label":"AlmaLinux-8", 4 | "provider":"provision_service", 5 | "image":"almalinux-cloud/almalinux-8" 6 | }, 7 | { 8 | "label":"RedHat-9", 9 | "provider":"provision_service", 10 | "image":"rhel-9" 11 | }, 12 | { 13 | "label":"centos-7", 14 | "provider":"docker", 15 | "image":"litmusimage/centos-7" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /docs/md/content/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About Litmus 3 | layout: page 4 | description: An overview of Litmus. 5 | weight: 1 6 | --- 7 | 8 | Litmus is a command line tool that allows you to run acceptance tests against Puppet modules for a variety of OSes and deployment scenarios. 9 | 10 | Litmus is installed as (experimental) part of the [Puppet Development Kit](https://puppet.com/try-puppet/puppet-development-kit/). 11 | 12 | * Start with [running acceptance tests with Litmus](/content-and-tooling-team/docs/litmus/usage/testing/running-acceptance-tests/). This example that walks you through running an acceptance test on a module that already has Litmus tests. 13 | * Follow the [Converting modules to use Litmus](/content-and-tooling-team/docs/litmus/usage/converting-modules-to-use-litmus/) to enable Litmus on your module. 14 | * [Litmus core commands](/content-and-tooling-team/docs/litmus/usage/commands/litmus-core-commands/) has a list of what else you can do with it. 15 | * [Litmus Concepts](/content-and-tooling-team/docs/litmus/litmus-concepts/) explains the concepts, parts and their connection inside litmus. 16 | 17 | Other pages on this site: 18 | 19 | * [Tools included in Litmus](/content-and-tooling-team/docs/litmus/usage/tools-included-in-litmus/). An overview of the tools Litmus uses. 20 | * [Test examples](/content-and-tooling-team/docs/litmus/usage/testing/litmus-test-examples/). Common examples you can use in your tests. 21 | * [Helper functions](/content-and-tooling-team/docs/litmus/usage/litmus-helper-functions/). A list of the helper functions that you can use in your tests. 22 | * [Command reference](/content-and-tooling-team/docs/litmus/usage/commands/command-reference/). Including useful Docker commands. 23 | -------------------------------------------------------------------------------- /docs/md/content/litmus-concepts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Concepts 4 | description: Expanded information on how Litmus works. 5 | --- 6 | 7 | 8 | The main engineering goal of Litmus is to re-use as much existing content and external code as is feasible. 9 | Smaller re-usable components and leveraging functionality of other projects allows for easier adaptation and allows Litmus to ride along on the success of others, like bolt. 10 | It also means that it is easier to replace the parts that did not turn out to fulfill the needs of the users. 11 | 12 | ## Components 13 | 14 | The following list is the current set of components and implementation choices. Some of those choices are pure expediency to get something working. Others clear strategic decisions to build a better-together story. 15 | 16 | * UI: litmus rake tasks 17 | * Communication Layer: bolt 18 | * Configuration: 19 | * `provision.yaml` 20 | * CI job setup 21 | * `spec/fixtures/litmus_inventory.yaml` 22 | * test dependencies: .fixtures.yml 23 | * Test Infrastructure: 24 | * puppetlabs-provision module 25 | * hypervisors: docker, lxd, vagrant, vmpooler, abs 26 | * external provisioners: e.g. terraform 27 | * test systems: 28 | * litmusimage 29 | * upstream images 30 | * custom images 31 | * utility code 32 | * puppetlabs-facts module 33 | * puppetlabs-puppet_agent module 34 | * Testing System 35 | * runner: RSpec 36 | * test case definition: RSpec 37 | * test setup: 38 | * a manifest string embedded in the test case 39 | * ruby code to orchestrate a change 40 | * test assertion 41 | * serverspec 42 | * hand-coded checks 43 | * Packaging and delivery: 44 | * Litmus as gem 45 | * Litmus as PDK component 46 | * utility modules as git repos 47 | * bolt as gem 48 | 49 | The following sections go over the various components, their reasoning and discuss alternative options. 50 | 51 | ## UI 52 | 53 | The current UI/UX of Litmus is implemented as a set of rake tasks in [puppet_litmus:lib/puppet_litmus/rake_tasks.rb](https://github.com/puppetlabs/puppet_litmus/blob/master/lib/puppet_litmus/rake_tasks.rb). 54 | 55 | **Reasoning:** 56 | Rake is a ubiquitous choice of interacting with a set of connected tasks in the ruby world. 57 | The limited option parsing capabilities push the design towards simple interactions and storing important information in configuration files. 58 | It is very easy to get started with a project based on rake tasks, as there is a wealth of examples and tutorials on how to build rake tasks. 59 | 60 | **Alternatives:** 61 | As the Litmus workflow and configuration matures we might want to consider a dedicated CLI with more ergonomic option parsing possibilities. 62 | This could be implemented as part of the pdk (`pdk test acceptance`) or a standalone CLI tool shipped as part of the PDK. 63 | 64 | The VSCode plugin could provide additional UI to make running and interacting with acceptance tests directly from the IDE possible. 65 | 66 | ## Communication Layer 67 | 68 | The communications layer for litmus needs to be able to talk to all the various systems that users might want to use in testing. 69 | 70 | **Reasoning:** 71 | Bolt supports SSH, WinRM natively and allows extension to other remote protocols using Puppet's extension points. 72 | With Bolt being integrated into our entire product line, re-using it also for content testing is an easy choice. 73 | Content generated for Bolt can be re-used within tests, and vice versa. 74 | This allows users to re-use skills acquired and strenghtens Puppet's better-together story. 75 | 76 | **Alternatives:** 77 | Bolt as communication layer touches most other components and - while having a well-defined interface - would not be easy to replace. 78 | There is currently no reason to consider alternatives to Bolt. 79 | 80 | ## Configuration 81 | 82 | Configuration for Litmus currently is spread over several files. 83 | The `provision.yaml` contains various lists of platforms to target for tests. 84 | The CI job setup (usually in `.travis.yml`, `appveyor.yml`, `Github Actions` or similiar) contains the overall sequencing of steps for testing. 85 | This setup is usually the same everywhere and is encoded in pdk-templates. 86 | There are slight variations to choose the puppet version and platform combinations from `provision.yaml`. 87 | 88 | Last but not least, transient state and roles of provisioned systems is stored in Bolt's `inventory.yaml`. 89 | The inventory is usually managed by Litmus as an implementation detail. 90 | In advanced scenarios users can create, add, edit, or replace the inventory in pursuit of their use cases. 91 | 92 | **Reasoning:** 93 | The current set of configurations is the absolute minimum to get Litmus working. 94 | Most of the files are inherited from existing practices and tools. 95 | 96 | **Alternatives:** 97 | While the current set of configuration files works for now, it's main purpose is to carry Litmus over this phase and highlight the requirements for the next iteration. 98 | * Replace scattered configuration with a [Boltdir](https://puppet.com/docs/bolt/latest/bolt_project_directories.html) that can contain all the module-specific info required 99 | * Replace .fixtures.yml with `Boltdir/Puppetfile` for unit and acceptance testing 100 | * Investigate/implement pdk/litmus specific `Boltdir` location/name to avoid colliding with production use of a Boltdir 101 | * Get test-hiera data/config from `Boltdir/data` by default 102 | 103 | * The current setup of (re-)writing the inventory is problematic as it deprives advanced users of safe ways to enhance the inventory to suit their needs 104 | * Example: custom SSH key/options 105 | * Bolt has now plugins to receive inventory information from outside sources - would this be a good way to keep dynamic litmus data out of inventory.yaml? 106 | 107 | * Find ways to pass arguments to provisioners 108 | * Hypervisor Credentials and arguments 109 | * bolt options (e.g custom SSH key/options) 110 | * which parts of this are module/repo specific? which parts need to be per-user? 111 | 112 | 113 | ## Test Infrastructure 114 | 115 | For full system-level testing of acceptance criteria, tests require access to systems to test. 116 | Depending on the use-case and resources available, this test infrastructure can be accessed in a variety of ways. 117 | There are a few necessary conditions for all those test systems: 118 | * accessible through bolt - this allows tests to act on such a system 119 | * initially clean - without a well-defined initial state, tests become complex, unreliable, or both 120 | * dedicated - running tests on shared systems is inviting troubles, either by tests stepping on each other or the tests interfering with user activities or the other way around 121 | * representative of production systems - running the tests should provide insight about expected behaviour in the real world 122 | 123 | ### What can be used as Test Infrastructure? 124 | 125 | Throw-away VMs are the easiest way to fulfil those conditions. 126 | Provisioned from upstream images or organisations' golden templates, 127 | they provide a complete operating system, accurate representation of production and full isolation. 128 | The downside of virtual machines are the high resource usage and provisioning times in the order of minutes. 129 | You can use local VMs on your development workstation, or private and public cloud providers. 130 | 131 | To reduce resource usage and provisioning times docker and containers come in handy. 132 | They deploy in seconds and achieve high packing densities with low overhead. 133 | Best practices for container images justifiably frowns on SSH access or complete operating system services, 134 | thus common public images are usually not representative of production on full VMs. 135 | To avoid this limitation, [puppetlabs/litmusimages](https://github.com/puppetlabs/litmusimage) provides a set of pre-fabbed [docker images](https://hub.docker.com/u/litmusimage) that allow SSH access and have an init process running. 136 | 137 | In some cases, using bare metal servers or already running systems is unavoidable. 138 | 139 | ### How to provision Test Infrastructure 140 | 141 | There are as many ways to aqcuire access to test systems as there are kinds of test systems. 142 | By default, Litmus calls out to a provisioner task to provision or tear down systems on demand. 143 | In the `puppetlabs-provision` module, we ship a number of commonly needed provisioners for docker (with SSH), docker_exp (using bolt's docker transport), vagrant (for local VMs) and the private cloud APIs [VMpooler](https://github.com/puppetlabs/vmpooler) and the puppet-private [ABS](https://github.com/puppetlabs/always-be-scheduling). 144 | 145 | Since Litmus 0.18 the rake tasks also allow calling arbitrary tasks outside the `provision` module to provision test systems. [Daniel's terraform demo](https://youtu.be/8BMo9DcZ4-Q) shows one application of this. 146 | 147 | The provision task will allocate/provision/start VMs or containers of the requested platforms and add them to the `spec/fixtures/litmus_inventory.yaml` file. 148 | 149 | Through the use of the inventory file Litmus can also consume arbitrary other systems by users supplying their own data, independently of Litmus' provision capabilities. 150 | 151 | ### Alternatives 152 | 153 | There are a number of opportunities to improve today's provision capabilities. 154 | 155 | * image customisation: 156 | To make images usable with litmus, some customisation and workarounds need to be applied. 157 | Be that installing SSH (litmusimage), removing tty handling (`fix_missing_tty_error_message`), configuring root access (`docker` provisioner), configure sudo access (`vagrant` provisioner) or installing pre-reqs to install the agent (`wget`/`curl`/etc). 158 | These workarounds are distributed across different components at the moment. 159 | Collecting all of them into the image baking process (litmusimage) would 160 | * make the provisioning code path easier and faster 161 | * allow users to discover litmus' requirements for custom images by inspecting the litmusimage build process 162 | * reduce "magic" happenings when using custom images 163 | 164 | * move inventory manipulation from provisioning tasks to litmus: 165 | Currently provisioning tasks are required to update the `spec/fixtures/litmus_inventory.yaml` file with new target information and remove targets from the `spec/fixtures/litmus_inventory.yaml` file on tear down. 166 | This causes a number of problems: 167 | * code duplication 168 | * prohibits running provisioners in parallel 169 | * unnecessarily pushes some operations (see `vars` handling) into provision tasks 170 | * requires provision tasks to run on the caller's host 171 | Instead of writing directly to `spec/fixtures/litmus_inventory.yaml` file the provision tasks could return bolt transport configuration as part of the task result data. 172 | Litmus could then process that data as required in the work flow. 173 | Provisioners could now run in parallel and Litmus can coalesce the data at the end into a `spec/fixtures/litmus_inventory.yaml` file. 174 | This approach requires only minimal code in the task (return data as JSON). 175 | 176 | * allow more granular data than the platform name: 177 | Some provisioners could benefit from additional parameters passed in. 178 | For example choosing a specific AWS zone, VPC, GCP project or tags to host the test systems. 179 | This change also interacts with changes to how configuration data is stored. See 'Configuration' section above. 180 | 181 | ## Utility Code 182 | 183 | Litmus makes use of the [`puppetlabs-facts`](https://github.com/puppetlabs/puppetlabs-facts) and [`puppetlabs-puppet_agent`](https://github.com/puppetlabs/puppetlabs-puppet_agent) modules for some key operations. 184 | 185 | **Reasoning:** 186 | These modules are maintained and used in other parts of the ecosystem already. 187 | Reusing them provides for a consistent behaviour across different products and additional value gained from the existing development and maintenance effort. 188 | 189 | **Alternatives:** 190 | None required at the moment. 191 | 192 | **Alternatives:** 193 | None required at the moment. 194 | 195 | ## Testing System 196 | 197 | This section covers all the things concerned with defining and executing the test cases. 198 | 199 | ### Test Runner 200 | 201 | RSpec is a mature and widely used unit-testing framework in the Ruby Ecosystem. 202 | We are maintaining mature integrations for Puppet catalog-level unit testing and there is a rich ecosystem of adjacent tooling by the puppet community. 203 | 204 | **Rationale:** 205 | Using an existing product keeps us from re-inventing the wheel. 206 | RSpec has a host of already built in features and an ecosystem of plugins as well as tutorials and documentation. 207 | We've been able to adapt RSpec to our needs by using existing extension points. 208 | RSpec allows for fine-grained and dynamic test-selection, and provides excellent feedback to the user. 209 | 210 | **Alternatives:** 211 | None required at the moment. 212 | 213 | ### Test Case Definition 214 | 215 | While RSpec's fine-grained test setup capabilities are very necessary and useful for unit testing, the same capabilities have reduced applicability when looking at acceptance testing. 216 | These system-level tests usually have a lot more setup requirements and fewer, but more specialised tests. 217 | 218 | **Rationale:** 219 | RSpec test case definition language was chosen by default as part of the RSpec test runner in the early stages of the evolution of Puppet test practices. 220 | 221 | **Alternatives:** 222 | For a while [cucumber-puppet](https://github.com/petems/cucumber-puppet) was developed by some as an alternative but never gained as much traction or support in the Puppet community. 223 | 224 | ### Test Setup 225 | 226 | The test setup describes the necessary steps to reach the state to test. 227 | In litmus this is currently implemented by a mix of manifest strings embedded in ruby and litmus' ruby DSL calls. 228 | 229 | **Rationale:** 230 | This style of test setup is inherited from beaker-rspec and "The RSpec Way". 231 | 232 | **Alternatives:** 233 | Creating RSpec extensions for acceptance testing - like rspec-puppet does for unit testing - to represent common patterns would allow for crisper test setup definition. 234 | The additional abstraction could allow for more clarity and efficiency in setting up tests. 235 | 236 | ### Test Assertion 237 | 238 | After a test scenario is set up, assertions are executed to understand whether or not the original expectations are met. 239 | This can be as simple as checking a file's content to as complex as interacting with an API to check its behaviour. 240 | 241 | In acceptance tests the first way to check a system's target state is idempotency. 242 | This is implemented as first-class operation `idempotent_apply` in the Litmus DSL. 243 | 244 | To ascertain that a service is not only working, but also correctly configured more in-depth checks need to be implemented in ruby. 245 | [serverspec](https://serverspec.org/) is preconfigured inside Litmus to allow for checking a number of common resources. 246 | 247 | Other checks can be implemented in plain ruby or RSpec as required. 248 | 249 | #### Rationale 250 | 251 | Tests derive value from being easier to understand than the code under test. Through this they provide a confidence that merely reading the business logic (or, in Litmus' case the manifest) does not support. 252 | 253 | If puppet runs the same manifest a second time without errors or changes, this already implies that the desired system state has been reached. 254 | In many cases - for example when managing services - this is a more in-depth check that a test could do on its own. 255 | A service starting up and staying running implies that its configuration was valid and consistent. 256 | This is a check that would be very hard, nay prohibitively expensive, to implement in a test. 257 | Idempotency checking makes this (almost) free. 258 | 259 | Serverspec was created for acceptance testing infrastructure. 260 | It integrates nicely with RSpec. 261 | Serverspec provides [resources and checks](https://serverspec.org/resource_types.html) that are currently not expressible in puppet code, like "port should be listening" or partial matching on file contents. 262 | 263 | #### Alternatives 264 | 265 | * [Chef's InSpec](https://www.inspec.io/) was forked from serverspec and developed by Chef. 266 | * [Selenium](https://www.selenium.dev/) automates browsers and arguably could be used to check the health of a service deployed for testing. 267 | * Any kind of monitoring or application-level testing framework that is already used for a specific service could be used to create insight into the health of said service. 268 | 269 | ## Packaging and Delivery 270 | 271 | * Litmus is currently released as a gem first. 272 | 273 | **Rationale:** 274 | In this phase of development this allows for rapid iteration. 275 | Deploying gems is well-understood in the development community. 276 | 277 | * Litmus is also shipped as part of the PDK. 278 | 279 | **Rationale:** 280 | As an experimental part of the PDK Litmus becomes available offline to those who otherwise would need to cross air-gaps. 281 | At the same time, this also ensures that all the required dependencies are available in a PDK installation. 282 | Specifically this also would notify us if Litmus adds a dependency that is not compatible with the PDK runtime. 283 | 284 | * The Utility Modules are currently by default deployed into a test run directly from github. 285 | 286 | **Rationale:** 287 | Expediency and speed. 288 | 289 | **Alternatives:** 290 | To avoid poisoning test environments with litmus' implementation details and better support offline work, 291 | the utility modules should be packaged up as part of the litmus gem and sourced from a private modulepath when needed. 292 | Grouping all content in that way enhances confidence that it is a fully tested package. 293 | This would also require more integrated testing and more frequent litmus releases to make changes available. 294 | 295 | * bolt is currently consumed as a gem 296 | 297 | **Rationale:** 298 | Litmus is tightly bound to specific internal APIs and its dependencies. 299 | Depending on a gem provides flexibility in consuming bolt updates as Litmus is ready for it. 300 | Using the gem avoids any environmental dependencies and support issues arising from version mismatches. 301 | 302 | **Alternatives:** 303 | Require bolt to be installed from its package. 304 | -------------------------------------------------------------------------------- /docs/md/content/usage/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Usage" 3 | description: "Learn how to use litmus." 4 | weight: 100 5 | skipTerminal: true 6 | --- -------------------------------------------------------------------------------- /docs/md/content/usage/commands/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Commands" 3 | description: "Learn the Litmus commands." 4 | weight: 100 5 | skipTerminal: true 6 | --- -------------------------------------------------------------------------------- /docs/md/content/usage/commands/command-reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Command reference 4 | description: List of useful Litmus commands. 5 | --- 6 | 7 | ### Debug 8 | 9 | Litmus has the ability to display more information when it is running, this can help you diagnose some issues. 10 | 11 | ```bash 12 | export DEBUG=true 13 | ``` 14 | 15 | ### Useful Docker commands 16 | 17 | To list all docker images, including stopped ones, run: 18 | 19 | ```bash 20 | docker ps -a 21 | ``` 22 | 23 | You will get output similar to: 24 | 25 | ```bash 26 | docker container ls -a 27 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 28 | e7bc7e5b3d9b litmusimage/oraclelinux9 "/bin/sh -c /usr/sbi…" About a minute ago Up About a minute 0.0.0.0:2225->22/tcp litmusimage_oraclelinux9_-2225 29 | ae94def06077 litmusimage/oraclelinux8 "/bin/sh -c /sbin/in…" 3 minutes ago Up 3 minutes 0.0.0.0:2224->22/tcp litmusimage_oraclelinux8_-2224 30 | 80b22735494e litmusimage/stream9 "/bin/sh -c /sbin/in…" 5 minutes ago Up 5 minutes 0.0.0.0:2223->22/tcp litmusimage_centosstream9_-2223 31 | b7923a25f95b ubuntu:22.04 "/bin/bash" 6 weeks ago Exited (255) 4 weeks ago 0.0.0.0:2222->22/tcp ubuntu_22.04-2222 32 | ``` 33 | 34 | To stop and remove an image, run: 35 | 36 | ```bash 37 | docker rm -f ubuntu_22.04-2222 38 | ``` 39 | 40 | To connect via ssh to the Docker image, run: 41 | 42 | ```bash 43 | ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@localhost -p 2222 44 | ``` 45 | 46 | Note that you don't need to add to the known hosts file or check keys. 47 | 48 | To attach to the docker image and detach, run: 49 | 50 | ```bash 51 | docker attach litmusimage_centosstream9_-2223 52 | to deattach then 53 | ``` 54 | 55 | Note that you cannot attach to a Docker image that is running systemd/upstart, for example, the `litmus_image` images. 56 | -------------------------------------------------------------------------------- /docs/md/content/usage/commands/litmus-core-commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Core commands 4 | description: Learn Litmus core commands. 5 | --- 6 | 7 | Using the Litmus commands, you can provision test platforms such as containers/images, install a Puppet agent, install a module and run tests. 8 | 9 | Litmus has five commands: 10 | 11 | 1. [Provision: 'rake litmus:provision'](#provision) 12 | 2. [Install the agent: 'rake litmus:install_agent](#agent) 13 | 3. [Install the module: 'rake litmus:install_module'](#module) 14 | 4. [Run the tests: 'rake litmus:acceptance:parallel'](#test) 15 | 5. [Remove the provisioned machines: 'rake litmus:tear_down'](#teardown) 16 | 17 | These commands allow you to create a test environment and run tests against your systems. Note that not all of these steps are needed for every deployment. 18 | 19 | Three common test setups are: 20 | 21 | * Run against localhost 22 | * Run against an existing machine that has Puppet installed 23 | * Provision a fresh system and install Puppet 24 | 25 | Once you have your environment, Litmus is designed to speed up the following workflow: 26 | 27 | ``` 28 | edit code -> install module -> run test 29 | ``` 30 | 31 | At any point you can re-run tests, or provision new systems and add them to your test environment. 32 | 33 | 34 | 35 | ## Provisioning 36 | 37 | Using the Litmus [provision](https://github.com/puppetlabs/provision) command, you can spin up Docker containers, vagrant boxes or VMs in private clouds, such as vmpooler. 38 | 39 | For example: 40 | 41 | ```bash 42 | pdk bundle exec rake 'litmus:provision[vmpooler, redhat-9-x86_64]' 43 | pdk bundle exec rake 'litmus:provision[lxd, images:debian/11]' 44 | pdk bundle exec rake 'litmus:provision[docker, litmusimage/ubuntu:22.04]' 45 | pdk bundle exec rake 'litmus:provision[vagrant, gusztavvargadr/windows-server]' 46 | ``` 47 | 48 | > Note: Provisioning is extensible — if your chosen provisioner isn't available, you can add your own provisioner task to your test set up through a separate module in `.fixtures.yml`. 49 | 50 | The provision command creates a Bolt `spec/fixtures/litmus_inventory.yml` file for Litmus to use. You can manually add machines to this file. 51 | 52 | For example: 53 | 54 | ```yaml 55 | --- 56 | version: 2 57 | groups: 58 | - name: docker_nodes 59 | targets: [] 60 | - name: lxd_nodes 61 | targets: [] 62 | - name: ssh_nodes 63 | targets: 64 | - uri: localhost:2222 65 | config: 66 | transport: ssh 67 | ssh: 68 | user: root 69 | password: root 70 | port: 2222 71 | host-key-check: false 72 | facts: 73 | provisioner: docker 74 | container_name: centos_stream9-2222 75 | platform: centos:stream9 76 | - name: winrm_nodes 77 | targets: [] 78 | ``` 79 | 80 | For more examples of inventory files, see the [Bolt documentation](https://puppet.com/docs/bolt/latest/inventory_file_v2.html). 81 | 82 | Note that you can test some modules against localhost — the machine you are running your test from. Note that this is only recommended if you are familiar with the code base, as tests may have unexpected side effects on your local machine. To run a test against localhost, see [Run the tests: 'rake litmus:parallel'](#test) 83 | 84 | ### Testing services 85 | 86 | For testing services that require a service manager (like systemd), the default Docker images might not be enough. In this case, there is a collection of Docker images, with a service manager enabled, based on our [litmus image repository](https://github.com/puppetlabs/litmusimage). For available images, see the [docker hub](https://hub.docker.com/u/litmusimage). 87 | 88 | Alternatively, you can use a dedicated VM that uses another provisioner, for example vmpooler, vagrant or lxd. 89 | 90 | ### Provisioning via YAML 91 | 92 | In addition to directly provisioning one or more machines using `litmus:provision`, you can also define one or more sets of nodes in a `provision.yaml` file and use that to provision targets. 93 | 94 | An example of a `provision.yaml` defining a single node: 95 | 96 | ```yaml 97 | --- 98 | list_name: 99 | provisioner: vagrant 100 | images: ['centos/stream9', 'generic/ubuntu2204', 'gusztavvargadr/windows-server'] 101 | params: 102 | param_a: someone 103 | param_b: something 104 | ``` 105 | 106 | Take note of the following: 107 | 108 | * The `list_name` is arbitrary and can be any string you want. 109 | * The `provisioner` specifies which provision task to use. 110 | * The `images` must specify an array of one or more images to provision. 111 | * Any keys inside of `params` will be turned into process-scope environment variables as the key, upcased. In the example above, `param_a` would become an environment variable called `PARAM_A` with a value of `someone`. 112 | 113 | An example of a `provision.yaml` defining multiple nodes: 114 | 115 | ```yaml 116 | --- 117 | --- 118 | default: 119 | provisioner: docker 120 | images: ['litmusimage/centos:stream9'] 121 | vagrant: 122 | provisioner: vagrant 123 | images: ['centos/stream9', 'generic/ubuntu2204', 'gusztavvargadr/windows-server'] 124 | lxd: 125 | provisioner: lxd 126 | images: ['images:ubuntu/22.04', 'images:centos/7'] 127 | params: 128 | vm: true 129 | docker_deb: 130 | provisioner: docker 131 | images: ['litmusimage/debian:10', 'litmusimage/debian:11', 'litmusimage/debian:12'] 132 | docker_ub: 133 | provisioner: docker 134 | images: ['litmusimage/ubuntu:18.04', 'litmusimage/ubuntu:20.04', 'litmusimage/ubuntu:22.04'] 135 | docker_el6: 136 | provisioner: docker 137 | images: ['litmusimage/centos:6', 'litmusimage/oraclelinux:6', 'litmusimage/scientificlinux:6'] 138 | docker_el7: 139 | provisioner: docker 140 | images: ['litmusimage/centos:7', 'litmusimage/oraclelinux:7', 'litmusimage/scientificlinux:7'] 141 | release_checks: 142 | provisioner: vmpooler 143 | images: ['redhat-5-x86_64', 'redhat-6-x86_64', 'redhat-7-x86_64', 'redhat-8-x86_64', 'centos-5-x86_64', 'centos-6-x86_64', 'centos-7-x86_64', 'centos-8-x86_64', 'oracle-5-x86_64', 'oracle-6-x86_64', 'oracle-7-x86_64', 'scientific-6-x86_64', 'scientific-7-x86_64', 'debian-8-x86_64', 'debian-9-x86_64', 'debian-10-x86_64', 'sles-11-x86_64', 'sles-12-x86_64', 'sles-15-x86_64', 'ubuntu-1404-x86_64', 'ubuntu-1604-x86_64', 'ubuntu-1804-x86_64', 'win-2008r2-x86_64', 'win-2012r2-x86_64', 'win-2016-core-x86_64', 'win-2019-core-x86_64', 'win-10-pro-x86_64'] 144 | ``` 145 | 146 | You can then provision a list of targets from that file: 147 | 148 | ```bash 149 | # This will spin up all the nodes defined in the `release_checks` key via VMPooler 150 | pdk bundle exec rake 'litmus:provision_list[release_checks]' 151 | # This will spin up the three nodes listed in the `vagrant` key via Vagrant. 152 | # Note that it will also turn the listed key-value pairs in `params` into 153 | # the environment variables and enable the task to leverage them. 154 | pdk bundle exec rake 'litmus:provision_list[vagrant]' 155 | ``` 156 | 157 | 158 | 159 | ## Installing a Puppet agent 160 | 161 | Install an agent on the provisioned targets using the [Puppet Agent module](https://github.com/puppetlabs/puppetlabs-puppet_agent). The tasks in this module allow you to install different versions of the Puppet agent, on different OSes. 162 | 163 | Use the following command to install an agent on a single target or on all the targets in the `spec/fixtures/litmus_inventory.yaml` file. Note that agents are installed in parallel when running against multiple targets. 164 | 165 | Install an agent on a target using the following commands: 166 | 167 | ```bash 168 | # Install the latest Puppet agent on a specific target 169 | pdk bundle exec rake 'litmus:install_agent[gn55owqktvej9fp.delivery.puppetlabs.net]' 170 | 171 | # Install the latest Puppet agent on all targets 172 | pdk bundle exec rake "litmus:install_agent" 173 | 174 | # Install Puppet 8 on all targets 175 | pdk bundle exec rake 'litmus:install_agent[puppet8-nightly]' 176 | 177 | ``` 178 | 179 | 180 | 181 | ## Installing a module 182 | 183 | Using PDK and Bolt, the `rake litmus:install_module` command builds and installs a module on the target. 184 | 185 | For example: 186 | 187 | ```bash 188 | pdk bundle exec rake "litmus:install_module" 189 | ``` 190 | 191 | If you need multiple modules on the target system (e.g. fixtures pulled down through `pdk bundle exec rake spec_prep`, or a previous unit test run): 192 | 193 | ```bash 194 | pdk bundle exec rake "litmus:install_modules_from_directory[spec/fixtures/modules]" 195 | ``` 196 | 197 | 198 | 199 | ## Running tests 200 | 201 | There are several options for running tests. Litmus primarily uses [serverspec](https://serverspec.org/), though you can use other testing tools. 202 | 203 | When running tests with Litmus, you can: 204 | 205 | * Run all tests against a single target. 206 | * Run all tests against all targets in parallel. 207 | * Run a single test against a single target. 208 | 209 | An example running all tests against a single target: 210 | 211 | ```bash 212 | # On Linux/MacOS: 213 | TARGET_HOST=lk8g530gzpjxogh.delivery.puppetlabs.net pdk bundle exec rspec ./spec/acceptance 214 | TARGET_HOST=localhost:2223 pdk bundle exec rspec ./spec/acceptance 215 | ``` 216 | 217 | ```powershell 218 | # On Windows: 219 | $ENV:TARGET_HOST = 'lk8g530gzpjxogh.delivery.puppetlabs.net' 220 | pdk bundle exec rspec ./spec/acceptance 221 | ``` 222 | 223 | An example running a specific test against a single target: 224 | 225 | ```bash 226 | # On Linux/MacOS: 227 | TARGET_HOST=lk8g530gzpjxogh.delivery.puppetlabs.net pdk bundle exec rspec ./spec/acceptance/test_spec.rb:21 228 | TARGET_HOST=localhost:2223 pdk bundle exec rspec ./spec/acceptance/test_spec.rb:21 229 | ``` 230 | 231 | ```powershell 232 | # On Windows: 233 | $ENV:TARGET_HOST = 'lk8g530gzpjxogh.delivery.puppetlabs.net' 234 | pdk bundle exec rspec ./spec/acceptance/test_spec.rb:21 235 | ``` 236 | 237 | An example running all tests against all targets, as specified in the `spec/fixtures/litmus_inventory.yaml` file: 238 | 239 | ```bash 240 | pdk bundle exec rake litmus:acceptance:parallel 241 | ``` 242 | 243 | An example running all tests against localhost. Note that this is only recommended if you are familiar with the code base, as tests may have unexpected side effects on your local machine. 244 | 245 | ```bash 246 | pdk bundle exec rake litmus:acceptance:localhost 247 | ``` 248 | 249 | For more test examples, see [run_tests task](https://github.com/puppetlabs/provision/wiki#run_tests) or [run tests plan](https://github.com/puppetlabs/provision/wiki#tests_against_agents) 250 | 251 | 252 | 253 | ## Removing provisioned systems 254 | 255 | Use the commands below to clean up provisioned systems after running tests. Specify whether to to remove an individual target or all the targets in the `spec/fixtures/litmus_inventory.yaml` file. 256 | 257 | ```bash 258 | # Tear down a specific target vm 259 | pdk bundle exec rake "litmus:tear_down[c985f9svvvu95nv.delivery.puppetlabs.net]" 260 | 261 | # Tear down a specific target running locally 262 | pdk bundle exec rake "litmus:tear_down[localhost:2222]" 263 | 264 | # Tear down all targets in `spec/fixtures/litmus_inventory.yaml` file 265 | pdk bundle exec rake "litmus:tear_down" 266 | ``` 267 | -------------------------------------------------------------------------------- /docs/md/content/usage/converting-modules-to-use-Litmus.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Quick Start Guide 4 | description: Learn how to use Litmus in your Puppet modules. 5 | weight: 1 6 | --- 7 | 8 | The following example walks you through enabling Litmus testing in a module. 9 | 10 | The process involves editing or adding code to the following files: 11 | 12 | 1. The `Gemfile` 13 | 2. The `Rakefile` 14 | 3. The`.fixtures.yml` file 15 | 4. The `spec_helper_acceptance.rb` file 16 | 5. The `spec_helper_acceptance_local.rb` file 17 | 18 | ## Before you begin 19 | 20 | This guide assumes your module is compatible with [Puppet Development Kit (PDK)](https://puppet.com/docs/pdk/1.x/pdk.html), 21 | meaning it was either created with `pdk new module` or has been converted to use PDK using the `pdk convert` command. 22 | To verify that your module is compatible with PDK, look in the modules `metadata.json` file and see whether there is an entry that states the PDK version. 23 | It will look something like `"pdk-version": "1.18.0"`. 24 | The PDK ships litmus as an experimental component. 25 | 26 | To enable it, follow the steps below. 27 | 28 | ## 1. Add required development dependencies 29 | 30 | Inside the root directory of your module, add the following entries to the `.fixtures.yml`: 31 | 32 | ```yaml 33 | --- 34 | fixtures: 35 | repositories: 36 | facts: 'https://github.com/puppetlabs/puppetlabs-facts.git' 37 | puppet_agent: 'https://github.com/puppetlabs/puppetlabs-puppet_agent.git' 38 | provision: 'https://github.com/puppetlabs/provision.git' 39 | ``` 40 | 41 | ## 2. Create the `spec/spec_helper_acceptance.rb` file 42 | 43 | Inside the `spec` folder of the module, create a `spec_helper_acceptance.rb` file with the following contents: 44 | 45 | ```ruby 46 | # frozen_string_literal: true 47 | 48 | require 'puppet_litmus' 49 | PuppetLitmus.configure! 50 | 51 | require 'spec_helper_acceptance_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_acceptance_local.rb')) 52 | ``` 53 | 54 | This file will later become managed by the PDK. For local changes, see the next step. 55 | 56 | ## 3. Create the `spec/spec_helper_acceptance_local.rb` file 57 | 58 | ***Optional:*** For module-specific methods to be used during acceptance testing, create a `spec/spec_helper_acceptance_local.rb` file. This will be loaded at the start of each test run. If you need to use any of the Litmus methods in this file, include Litmus as a singleton class: 59 | 60 | ```ruby 61 | # frozen_string_literal: true 62 | require 'singleton' 63 | 64 | class Helper 65 | include Singleton 66 | include PuppetLitmus 67 | end 68 | 69 | def some_helper_method 70 | Helper.instance.bolt_run_script('path/to/file') 71 | end 72 | ``` 73 | 74 | ## 4. Add tests to `spec/acceptance` 75 | 76 | You can find [litmus test examples](/content-and-tooling-team/docs/litmus/usage/testing/litmus-test-examples/) on their own page. 77 | -------------------------------------------------------------------------------- /docs/md/content/usage/litmus-helper-functions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Helper functions 3 | layout: page 4 | description: Learn all about Litmus functions. 5 | --- 6 | 7 | Inside of the Litmus gem, there are three distinct sets of functions: 8 | 9 | * Rake tasks for the CLI that allows you to use the Litmus commands (provision, install an agent, install a module and run tests.). Run `pdk bundle exec rake -T` to get a list of available rake tasks. 10 | * Helper functions for serverspec / test. These apply manifests or run shell commands. For more information, see [Puppet Helpers](https://www.rubydoc.info/gems/puppet_litmus/PuppetLitmus/PuppetHelpers) 11 | * Helper Functions for Bolt inventory file manipulation. For more information, see [Inventory Manipulation](https://www.rubydoc.info/gems/puppet_litmus/PuppetLitmus/InventoryManipulation). 12 | -------------------------------------------------------------------------------- /docs/md/content/usage/testing/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Testing" 3 | description: "Learn how to test with Litmus." 4 | weight: 100 5 | skipTerminal: true 6 | --- -------------------------------------------------------------------------------- /docs/md/content/usage/testing/litmus-test-examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Example tests 4 | description: A list of example Litmus tests. 5 | --- 6 | 7 | These are some common examples you can use in your tests. Take note of the differences between beaker-rspec style testing and Litmus. 8 | 9 | ## Testing Puppet code 10 | 11 | The following example tests that your Puppet code works. Take note of the repeatable pattern. 12 | 13 | ```ruby 14 | require 'spec_helper_acceptance' 15 | 16 | describe 'a feature', if: ['debian', 'redhat', 'ubuntu'].include?(os[:family]) do 17 | let(:pp) do 18 | <<-MANIFEST 19 | include feature::some_class 20 | MANIFEST 21 | end 22 | 23 | it 'applies idempotently' do 24 | idempotent_apply(pp) 25 | end 26 | 27 | describe file("/etc/feature.conf") do 28 | it { is_expected.to be_file } 29 | its(:content) { is_expected.to match %r{key = default value} } 30 | end 31 | 32 | describe port(777) do 33 | it { is_expected.to be_listening } 34 | end 35 | end 36 | ``` 37 | 38 | ## Testing manifest code for idempotency 39 | 40 | The `idempotent_apply` helper function runs the given manifest twice and will test that the first run doesn't have errors and the second run doesn't have changes. For many regular modules that already will give good confidence that it is working: 41 | 42 | ```ruby 43 | pp = 'class { "mysql::server": }' 44 | idempotent_apply(pp) 45 | ``` 46 | 47 | ## Running shell commands 48 | 49 | To run a shell command and test it's output: 50 | 51 | ```ruby 52 | expect(run_shell('/usr/local/sbin/mysqlbackup.sh').stderr).to eq('') 53 | ``` 54 | 55 | ### Serverspec Idioms 56 | 57 | An example of a serverspec declaration: 58 | 59 | ```ruby 60 | describe command('/usr/local/sbin/mysqlbackup.sh') do 61 | its(:stderr) { should eq '' } 62 | end 63 | ``` 64 | 65 | ## Checking facts 66 | 67 | With Litmus, you can use the serverspec functions — these are cached so are quick to call. For example: 68 | 69 | ```ruby 70 | os[:family] 71 | ``` 72 | 73 | or 74 | 75 | ```ruby 76 | host_inventory['facter']['os']['release'] 77 | ``` 78 | 79 | For more information, see the [serverspec docs](https://serverspec.org/host_inventory.html). 80 | 81 | ## Debugging tests 82 | 83 | There is a known issue when running certain commands from within a pry session. To debug tests, use the following pry-byebug gem: 84 | 85 | ```ruby 86 | gem 'pry-byebug', '> 3.4.3' 87 | ``` 88 | 89 | ## Setting up Travis and Appveyor 90 | 91 | To see this running on CI, enable the `use_litmus` flags for Travis CI and/or Appveyor. 92 | -------------------------------------------------------------------------------- /docs/md/content/usage/testing/running-acceptance-tests.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Acceptance tests 4 | description: Acceptance testing with Litmus. 5 | --- 6 | 7 | The following example walks you through running an acceptance test on the [MoTD](https://github.com/puppetlabs/puppetlabs-motd) module. 8 | 9 | The process involves these steps: 10 | 11 | 1. Clone the MoTD module from GitHub. 12 | 1. Provision a CentOS Docker image. 13 | 1. Install Puppet agent on the CentOS image. 14 | 1. Install the MoTD module on the CentOS image. 15 | 1. Run the MoTD acceptance tests. 16 | 1. Remove the Docker image. 17 | 18 | ## Before you begin 19 | 20 | Ensure you have installed the following: 21 | 22 | * [Docker](https://runnable.com/docker/getting-started/). 23 | * To check whether you already have Docker, run `docker --version` from the command line. 24 | * To check Docker is working, run `docker run litmusimage/centos:stream9 ls` in your terminal. You should see a list of folders in the CentOS image. 25 | * [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 26 | * To check where you already have git, run `git --version` in your terminal. 27 | * [Puppet Development Kit (PDK)](https://puppet.com/docs/pdk/3.x/pdk_install.html). 28 | * To check whether you already have PDK, run `pdk --version` from the command line. Note that you need version `1.17.0` or later. If not, then following the [installation instructions](https://puppet.com/docs/pdk/3.x/pdk_install.html). 29 | 30 | 31 | ## 1. Clone the MoTD module from GitHub. 32 | 33 | From the command line, clone the Litmus branch of MoTD module: 34 | 35 | ```bash 36 | git clone https://github.com/puppetlabs/puppetlabs-motd.git 37 | ``` 38 | 39 | You now have a local copy of the module on your machine. In this example, you can work off the master branch. 40 | 41 | Change directory to the MoTD module 42 | 43 | ```bash 44 | cd puppetlabs-motd 45 | ``` 46 | 47 | ## 2. Install the necessary gems. 48 | 49 | The MoTD module relies on a number of gems. To install these on your machine, run the following command: 50 | 51 | ```bash 52 | pdk bundle install 53 | ``` 54 | 55 | ## 3. Provision a CentOS Docker image 56 | 57 | Provision a CentOS stream 9 image in a Docker container to be the target you will test against 58 | 59 | To provision the CentOS stream 9 target (or any OS of your choice), run the following command: 60 | 61 | ```bash 62 | pdk bundle exec rake 'litmus:provision[docker, litmusimage/centos:stream9]' 63 | ``` 64 | 65 | > Note: Provisioning is extensible. If your preferred provisioner is missing, let us know by raising an issue on the [provision repo](https://github.com/puppetlabs/provision/issues) or submitting a [PR](https://github.com/puppetlabs/provision/pulls). 66 | 67 | The last lines of the output should look like: 68 | 69 | ```bash 70 | Provisioning centos:stream9 using docker provisioner.[✔] 71 | localhost:2222, centos:stream9 72 | ``` 73 | 74 | To check that it worked, run `docker ps` and you should see output similar to: 75 | 76 | ```bash 77 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 78 | 7b12b616cf65 centos:stream9 "/bin/bash" 4 minutes ago Up 4 minutes 0.0.0.0:2222->22/tcp centos_stream9-2222 79 | ``` 80 | 81 | Note that the provisioned targets will be in the `spec/fixtures/litmus_inventory.yaml` file. Litmus creates this file in your working directory. If you run `cat spec/fixtures/litmus_inventory.yaml`, you should see the targets you just created. For example: 82 | 83 | ```yaml 84 | # litmus_inventory.yaml 85 | --- 86 | version: 2 87 | groups: 88 | - name: docker_nodes 89 | targets: [] 90 | - name: ssh_nodes 91 | targets: 92 | - uri: localhost:2222 93 | config: 94 | transport: ssh 95 | ssh: 96 | user: root 97 | password: root 98 | port: 2222 99 | host-key-check: false 100 | facts: 101 | provisioner: docker 102 | container_name: centos_stream9-2222 103 | platform: centos:stream9 104 | - name: winrm_nodes 105 | targets: [] 106 | ``` 107 | 108 | ## 4. Install Puppet agent on your target 109 | 110 | To install the latest version of Puppet agent on the CentOS Docker image, run the following command: 111 | 112 | ```bash 113 | pdk bundle exec rake litmus:install_agent 114 | ``` 115 | 116 | Use Bolt to verify that you have installed the agent on the target. Run the following command: 117 | 118 | ```bash 119 | pdk bundle exec bolt command run 'puppet --version' --targets localhost:2222 --inventoryfile spec/fixtures/litmus_inventory.yaml 120 | ``` 121 | 122 | Note that `localhost:2222` is the name of the node in the spec/fixtures/litmus_inventory.yaml file. You should see output with the version of the Puppet agent that was installed: 123 | 124 | ```bash 125 | bolt command run 'puppet --version' --targets localhost:2222 --inventoryfile spec/fixtures/litmus_inventory.yaml 126 | ``` 127 | 128 | Running the command will produce output similar to this: 129 | 130 | ```bash 131 | Started on localhost:2222... 132 | Finished on localhost:2222: 133 | STDOUT: 134 | 6.13.0 135 | Successful on 1 target: localhost:2222 136 | Ran on 1 target in 1.72 sec 137 | ``` 138 | 139 | If you want to install a specific version of puppet you can use the following command: 140 | 141 | ```bash 142 | pdk bundle exec rake 'litmus:install_agent[puppet8-nightly] 143 | ``` 144 | 145 | Examples of other versions you can pass in are: 'puppet', 'puppet-nightly', 'puppet7', 'puppet7-nightly'. 146 | 147 | ## 5. Install the MoTD module on the CentOS image 148 | 149 | To install the MoTD module on the CentOS image, run the following command from inside your working directory: 150 | 151 | ```bash 152 | pdk bundle exec rake litmus:install_module 153 | ``` 154 | 155 | *Note: If you are interactively modifying code and testing, this step must be run after your changes are made and before you run your tests.* 156 | 157 | You will see output similar to: 158 | 159 | ```bash 160 | ➜ puppetlabs-motd git:(main) pdk bundle exec rake litmus:install_module 161 | pdk (INFO): Using Ruby 3.2.0 162 | pdk (INFO): Using Puppet 8.1.0 163 | Building '/Users/paula/workspace/puppetlabs-mysql' into '/Users/paula/workspace/puppetlabs-motd/pkg' 164 | Built '/Users/paula/workspace/puppetlabs-motd/pkg/puppetlabs-motd-11.0.3.tar.gz' 165 | Installed '/Users/paula/workspace/puppetlabs-motd/pkg/puppetlabs-motd-11.0.3.tar.gz' on 166 | ``` 167 | 168 | Use Bolt to verify that you have installed the MoTD module. Run the following command: 169 | 170 | ```bash 171 | pdk bundle exec bolt command run 'puppet module list' --targets localhost:2222 -i spec/fixtures/litmus_inventory.yaml 172 | ``` 173 | 174 | The output should look similar to: 175 | 176 | ```bash 177 | Started on localhost... 178 | Finished on localhost: 179 | STDOUT: 180 | /etc/puppetlabs/code/environments/production/modules 181 | ├── puppetlabs-motd (v2.1.2) 182 | ├── puppetlabs-registry (v2.1.0) 183 | ├── puppetlabs-stdlib (v5.2.0) 184 | └── puppetlabs-translate (v1.2.0) 185 | /etc/puppetlabs/code/modules (no modules installed) 186 | /opt/puppetlabs/puppet/modules (no modules installed) 187 | Successful on 1 node: localhost:2222 188 | Ran on 1 node in 1.11 seconds 189 | Started on localhost:2222... 190 | Finished on localhost:2222: 191 | STDOUT: 192 | /etc/puppetlabs/code/environments/production/modules 193 | ├── puppetlabs-motd (v4.1.0) 194 | ├── puppetlabs-registry (v3.1.0) 195 | ├── puppetlabs-stdlib (v6.2.0) 196 | └── puppetlabs-translate (v2.1.0) 197 | /etc/puppetlabs/code/modules (no modules installed) 198 | /opt/puppetlabs/puppet/modules (no modules installed) 199 | Successful on 1 target: localhost:2222 200 | Ran on 1 target in 1.77 sec 201 | ``` 202 | 203 | Note that you have also installed the MoTD module's dependent modules. 204 | 205 | ## 6. Run the MoTD acceptance tests 206 | 207 | To run acceptance tests with Litmus, run the following command from your working directory: 208 | 209 | ```bash 210 | pdk bundle exec rake litmus:acceptance:parallel 211 | ``` 212 | 213 | This command executes the acceptance tests in the [acceptance folder](https://github.com/puppetlabs/puppetlabs-motd/tree/main/spec/acceptance) of the module. If the tests have run successfully, you will see output similar to (Note it will look like it has stalled but is actually running tests in the background, please be patient and the output will appear when the tests are complete: 214 | 215 | ```bash 216 | + [✔] Running against 1 targets. 217 | |__ [✔] localhost:2222, centos:stream9 218 | ================ 219 | localhost:2222, centos:stream9 220 | ...... 221 | 222 | Finished in 42.95 seconds (files took 10.15 seconds to load) 223 | 6 examples, 0 failures 224 | 225 | pid 1476 exit 0 226 | Successful on 1 nodes: ["localhost:2222, centos:stream9"] 227 | ``` 228 | 229 | ## 7. Remove the Docker image 230 | 231 | Now that you have completed your tests, you can remove the Docker image with the Litmus tear down command: 232 | 233 | ```bash 234 | pdk bundle exec rake litmus:tear_down 235 | ``` 236 | 237 | You should see JSON output, similar to: 238 | 239 | ```bash 240 | localhost:2222: success 241 | ``` 242 | 243 | To verify that the target has been removed, run `docker ps` from the command line. You should see that it's no longer running. 244 | 245 | ## Next steps 246 | 247 | The MoTD shows you how to use Litmus to acceptance test an existing module. As you scale up your acceptance testing, you will need to write your own acceptance tests. Try out the following: 248 | 249 | * Provision more than one system, for example, `pdk bundle exec rake 'litmus:provision[docker, litmusimage/debian:12]'`. Note that you will need to re-run the `install_agent` and `install_module` command if you want to run tests. 250 | * Look at the inventory file and take note of the ssh connection information 251 | * ssh into the CentOS box when you know the password, for example, `ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@localhost -p 2222`, or use Bolt as shown in the example. 252 | * ssh into the CentOS box without a password, run `docker ps`, take note of the Container Name and then run `docker exec -it litmusimage_centos_stream9-2222 '/bin/bash'` in this example litmusimage_centos_stream9-2222 is the Container Name. 253 | 254 | > Note: We have moved all our PR testing to public pipelines to make contributing to Puppet supported modules a better experience. Check out our [Github Action templates](https://github.com/puppetlabs/cat-github-actions/tree/main/.github/workflows). All of our testing is now ran in the one place. 255 | -------------------------------------------------------------------------------- /docs/md/content/usage/tools-included-in-Litmus.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Tools 4 | description: Learn the tools Litmus uses. 5 | --- 6 | 7 | Litmus wraps functionality from other tools, providing a rake interface for you to develop modules. 8 | 9 | * [Bolt](https://github.com/puppetlabs/bolt) is an open source orchestration tool that automates the manual work it takes to maintain your infrastructure. Litmus is built on top of bolt, so it natively handles SSH, WinRM and Docker. The inventory file specifies the protocol to use for each target, when using litmus this can be found in `spec/fixtures/litmus_inventory.yaml`, along with connection specific information. Litmus uses Bolt to execute module tasks. 10 | * [Serverspec](https://serverspec.org/) lets you check your servers are configured correctly. 11 | * Puppet Development Kit (PDK) provides a complete module structure, templates for classes, defined types, and tasks, and a testing infrastructure. 12 | * [Litmus Image](https://github.com/puppetlabs/litmus_image) is a group of Docker build files. They are specifically designed to set up systemd/upstart on various nix images. This is a prerequisite for testing services with Puppet in Docker images.`litmus_image` generates an inventory file, that contains connection information for each system instance. This is used by subsequent commands or by rspec. 13 | 14 | These tools are built into the Litmus commands: 15 | 16 | #### Provision 17 | 18 | To provision systems we created a [module](https://github.com/puppetlabs/provision) that will provision containers / images / hardware in ABS (internal to Puppet) and Docker instances. Provision is extensible, so other provisioners can be added - please raise an [issue](https://github.com/puppetlabs/provision/issues) on the Provision repository, or create your own and submit a [PR](https://github.com/puppetlabs/provision/pulls)! 19 | 20 | rake task -> litmus -> bolt -> provision -> docker 21 | -> lxd 22 | -> vagrant 23 | -> abs (internal) 24 | -> vmpooler (internal) 25 | 26 | #### Installing an agents 27 | 28 | rake task -> bolt -> puppet_agent module 29 | 30 | #### Installing modules 31 | 32 | PDK builds the module tar file and is copied to the target using Bolt. On the target machine, run `puppet module install`, specifying the tar file. This installs the dependencies listed in the metadata.json of the built module. 33 | 34 | rake task -> pdk -> bolt 35 | 36 | #### Running tests 37 | 38 | rake task -> serverspec -> rspec 39 | 40 | #### Tearing down targets 41 | 42 | rake task -> bolt provision -> docker 43 | -> lxd 44 | -> abs (internal) 45 | -> vmpooler 46 | -------------------------------------------------------------------------------- /docs/md/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/puppetlabs/puppet_litmus/docs/md 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /docs/md/md.go: -------------------------------------------------------------------------------- 1 | package md 2 | 3 | import "embed" 4 | 5 | //go:embed content/*** 6 | var DocsFS embed.FS 7 | 8 | func GetDocsFS() embed.FS { 9 | return DocsFS 10 | } 11 | -------------------------------------------------------------------------------- /exe/matrix.json: -------------------------------------------------------------------------------- 1 | { 2 | "collections": [ 3 | { 4 | "puppet": 8.0, 5 | "ruby": 3.2 6 | } 7 | ], 8 | "provisioners": { 9 | "provision_service": { 10 | "AlmaLinux": { 11 | "8": { "x86_64": "almalinux-cloud/almalinux-8" }, 12 | "9": { "x86_64": "almalinux-cloud/almalinux-9" } 13 | }, 14 | "CentOS": { 15 | "7": { "x86_64": "centos-7" }, 16 | "8": { "x86_64": "centos-stream-8" }, 17 | "9": { "x86_64": "centos-stream-9" } 18 | }, 19 | "Rocky": { 20 | "8": { "x86_64": "rocky-linux-cloud/rocky-linux-8" }, 21 | "9": { "x86_64": "rocky-linux-cloud/rocky-linux-9" } 22 | }, 23 | "Debian": { 24 | "10": { "x86_64": "debian-10" }, 25 | "11": { "x86_64": "debian-11" }, 26 | "12": { "x86_64": "debian-12", "arm": "debian-12-arm64" } 27 | }, 28 | "RedHat": { 29 | "7": { "x86_64": "rhel-7" }, 30 | "8": { "x86_64": "rhel-8" }, 31 | "9": { "x86_64": "rhel-9", "arm": "rhel-9-arm64" } 32 | }, 33 | "SLES" : { 34 | "12": { "x86_64": "sles-12" }, 35 | "15": { "x86_64": "sles-15" } 36 | }, 37 | "Ubuntu": { 38 | "20.04": { "x86_64": "ubuntu-2004-lts" }, 39 | "22.04": { "x86_64": "ubuntu-2204-lts", "arm": "ubuntu-2204-lts-arm64" }, 40 | "24.04": { "x86_64": "ubuntu-2404-lts", "arm": "ubuntu-2404-lts-arm64" } 41 | }, 42 | "Windows": { 43 | "2016": { "x86_64": "windows-2016" }, 44 | "2019": { "x86_64": "windows-2019" }, 45 | "2022": { "x86_64": "windows-2022" } 46 | } 47 | }, 48 | "docker": { 49 | "AmazonLinux": { 50 | "2": { "x86_64": "litmusimage/amazonlinux:2" }, 51 | "2023": { "x86_64": "litmusimage/amazonlinux:2023" } 52 | }, 53 | "CentOS": { 54 | "7": { "x86_64": "litmusimage/centos:7" }, 55 | "8": { "x86_64": "litmusimage/centos:stream8" }, 56 | "9": { "x86_64": "litmusimage/centos:stream9" } 57 | }, 58 | "Rocky": { 59 | "8": { "x86_64": "litmusimage/rockylinux:8" }, 60 | "9": { "x86_64": "litmusimage/rockylinux:9" } 61 | }, 62 | "AlmaLinux": { 63 | "8": { "x86_64": "litmusimage/almalinux:8" }, 64 | "9": { "x86_64": "litmusimage/almalinux:9" } 65 | }, 66 | "Debian": { 67 | "10": { "x86_64": "litmusimage/debian:10" }, 68 | "11": { "x86_64": "litmusimage/debian:11" }, 69 | "12": { "x86_64": "litmusimage/debian:12" } 70 | }, 71 | "OracleLinux": { 72 | "7": { "x86_64": "litmusimage/oraclelinux:7" }, 73 | "8": { "x86_64": "litmusimage/oraclelinux:8" }, 74 | "9": { "x86_64": "litmusimage/oraclelinux:9" } 75 | }, 76 | "Scientific": { 77 | "7": { "x86_64": "litmusimage/scientificlinux:7" } 78 | }, 79 | "Ubuntu": { 80 | "18.04": { "x86_64": "litmusimage/ubuntu:18.04" }, 81 | "20.04": { "x86_64": "litmusimage/ubuntu:20.04" }, 82 | "22.04": { "x86_64": "litmusimage/ubuntu:22.04" }, 83 | "24.04": { "x86_64": "litmusimage/ubuntu:24.04" } 84 | } 85 | } 86 | }, 87 | "github_runner": { 88 | "docker": { 89 | "^(AmazonLinux-2|(CentOS|OracleLinux|Scientific)-7|Ubuntu-18|Debian-10)": "ubuntu-22.04" 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /exe/matrix_from_metadata_v2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # this script creates a build matrix for github actions from the claimed supported platforms and puppet versions in metadata.json 5 | 6 | require 'json' 7 | 8 | # Sets an output variable in GitHub Actions. If the GITHUB_OUTPUT environment 9 | # variable is not set, this will fail with an exit code of 1 and 10 | # send an ::error:: message to the GitHub Actions log. 11 | # @param name [String] The name of the output variable 12 | # @param value [String] The value of the output variable 13 | 14 | def set_output(name, value) 15 | # Get the output path 16 | output = ENV.fetch('GITHUB_OUTPUT') 17 | 18 | # Write the output variable to GITHUB_OUTPUT 19 | File.open(output, 'a') do |f| 20 | f.puts "#{name}=#{value}" 21 | end 22 | rescue KeyError 23 | puts '::error::GITHUB_OUTPUT environment variable not set.' 24 | exit 1 25 | end 26 | 27 | IMAGE_TABLE = { 28 | 'RedHat-7' => 'rhel-7', 29 | 'RedHat-8' => 'rhel-8', 30 | 'RedHat-9' => 'rhel-9', 31 | 'SLES-12' => 'sles-12', 32 | 'SLES-15' => 'sles-15', 33 | 'Windows-2016' => 'windows-2016', 34 | 'Windows-2019' => 'windows-2019', 35 | 'Windows-2022' => 'windows-2022' 36 | }.freeze 37 | 38 | ARM_IMAGE_TABLE = { 39 | 'Debian-12-arm' => 'debian-12-arm64', 40 | 'RedHat-9-arm' => 'rhel-9-arm64', 41 | 'Ubuntu-22.04-arm' => 'ubuntu-2204-lts-arm64', 42 | 'Ubuntu-24.04-arm' => 'ubuntu-2404-lts-arm64' 43 | }.freeze 44 | 45 | DOCKER_PLATFORMS = { 46 | 'AmazonLinux-2' => 'litmusimage/amazonlinux:2', 47 | 'AmazonLinux-2023' => 'litmusimage/amazonlinux:2023', 48 | 'CentOS-7' => 'litmusimage/centos:7', 49 | 'CentOS-8' => 'litmusimage/centos:stream8', # Support officaly moved to Stream8, metadata is being left as is 50 | 'CentOS-9' => 'litmusimage/centos:stream9', 51 | 'Rocky-8' => 'litmusimage/rockylinux:8', 52 | 'Rocky-9' => 'litmusimage/rockylinux:9', 53 | 'AlmaLinux-8' => 'litmusimage/almalinux:8', 54 | 'AlmaLinux-9' => 'litmusimage/almalinux:9', 55 | 'Debian-10' => 'litmusimage/debian:10', 56 | 'Debian-11' => 'litmusimage/debian:11', 57 | 'Debian-12' => 'litmusimage/debian:12', 58 | 'OracleLinux-7' => 'litmusimage/oraclelinux:7', 59 | 'OracleLinux-8' => 'litmusimage/oraclelinux:8', 60 | 'OracleLinux-9' => 'litmusimage/oraclelinux:9', 61 | 'Scientific-7' => 'litmusimage/scientificlinux:7', 62 | 'Ubuntu-18.04' => 'litmusimage/ubuntu:18.04', 63 | 'Ubuntu-20.04' => 'litmusimage/ubuntu:20.04', 64 | 'Ubuntu-22.04' => 'litmusimage/ubuntu:22.04', 65 | 'Ubuntu-24.04' => 'litmusimage/ubuntu:24.04' 66 | }.freeze 67 | 68 | # This table uses the latest version in each collection for accurate 69 | # comparison when evaluating puppet requirements from the metadata 70 | COLLECTION_TABLE = [ 71 | { 72 | puppet_maj_version: 7, 73 | ruby_version: 2.7 74 | }, 75 | { 76 | puppet_maj_version: 8, 77 | ruby_version: 3.2 78 | } 79 | ].freeze 80 | 81 | matrix = { 82 | platforms: [], 83 | collection: [] 84 | } 85 | 86 | spec_matrix = { 87 | include: [] 88 | } 89 | 90 | if ARGV.include?('--exclude-platforms') 91 | exclude_platforms_occurencies = ARGV.count { |arg| arg == '--exclude-platforms' } 92 | raise '--exclude-platforms argument should be present just one time in the command' unless exclude_platforms_occurencies <= 1 93 | 94 | exclude_platforms_list = ARGV[ARGV.find_index('--exclude-platforms') + 1] 95 | raise 'you need to provide a list of platforms in JSON format' if exclude_platforms_list.nil? 96 | 97 | begin 98 | exclude_list = JSON.parse(exclude_platforms_list).map(&:downcase) 99 | rescue JSON::ParserError 100 | raise 'the exclude platforms list must valid JSON' 101 | end 102 | else 103 | exclude_list = [] 104 | end 105 | 106 | # Force the use of the provision_service provisioner, if the --provision-service argument is present 107 | if ARGV.include?('--provision-service') 108 | provision_service_occurrences = ARGV.count { |arg| arg == '--provision-service' } 109 | raise 'the --provision-service argument should be present just one time in the command' unless provision_service_occurrences <= 1 110 | 111 | # NOTE: that the below are the only available images for the provision service 112 | updated_platforms = { 113 | 'AlmaLinux-8' => 'almalinux-cloud/almalinux-8', 114 | 'AlmaLinux-9' => 'almalinux-cloud/almalinux-9', 115 | 'CentOS-7' => 'centos-7', 116 | 'CentOS-8' => 'centos-stream-8', 117 | 'CentOS-9' => 'centos-stream-9', 118 | 'Rocky-8' => 'rocky-linux-cloud/rocky-linux-8', 119 | 'Rocky-9' => 'rocky-linux-cloud/rocky-linux-9', 120 | 'Debian-10' => 'debian-10', 121 | 'Debian-11' => 'debian-11', 122 | 'Debian-12' => 'debian-12', 123 | 'Ubuntu-20.04' => 'ubuntu-2004-lts', 124 | 'Ubuntu-22.04' => 'ubuntu-2204-lts', 125 | 'Ubuntu-24.04' => 'ubuntu-2404-lts' 126 | } 127 | updated_list = IMAGE_TABLE.dup.clone 128 | updated_list.merge!(updated_platforms) 129 | 130 | IMAGE_TABLE = updated_list.freeze 131 | DOCKER_PLATFORMS = {}.freeze 132 | end 133 | 134 | # disable provision service if repository owner is not puppetlabs 135 | unless ['puppetlabs', nil].include?(ENV.fetch('GITHUB_REPOSITORY_OWNER', nil)) 136 | IMAGE_TABLE = {}.freeze 137 | ARM_IMAGE_TABLE = {}.freeze 138 | end 139 | 140 | metadata_path = ENV['TEST_MATRIX_FROM_METADATA'] || 'metadata.json' 141 | metadata = JSON.parse(File.read(metadata_path)) 142 | 143 | # Allow the user to pass a file containing a custom matrix 144 | if ARGV.include?('--custom-matrix') 145 | custom_matrix_occurrences = ARGV.count { |arg| arg == '--custom-matrix' } 146 | raise '--custom-matrix argument should be present just one time in the command' unless custom_matrix_occurrences <= 1 147 | 148 | file_path = ARGV[ARGV.find_index('--custom-matrix') + 1] 149 | raise 'no file path specified' if file_path.nil? 150 | 151 | begin 152 | custom_matrix = JSON.parse(File.read(file_path)) 153 | rescue StandardError => e 154 | case e 155 | when JSON::ParserError 156 | raise 'the matrix must be an array of valid JSON objects' 157 | when Errno::ENOENT 158 | raise "File not found: #{e.message}" 159 | else 160 | raise "An unknown exception occurred: #{e.message}" 161 | end 162 | end 163 | custom_matrix.each do |platform| 164 | matrix[:platforms] << platform 165 | end 166 | else 167 | # Set platforms based on declared operating system support 168 | metadata['operatingsystem_support'].sort_by { |a| a['operatingsystem'] }.each do |sup| 169 | os = sup['operatingsystem'] 170 | sup['operatingsystemrelease'].sort_by(&:to_i).each do |ver| 171 | image_key = "#{os}-#{ver}" 172 | # Add ARM images if they exist and are not excluded 173 | if ARM_IMAGE_TABLE.key?("#{image_key}-arm") && !exclude_list.include?("#{image_key.downcase}-arm") 174 | matrix[:platforms] << { 175 | label: "#{image_key}-arm", 176 | provider: 'provision_service', 177 | image: ARM_IMAGE_TABLE["#{image_key}-arm"] 178 | } 179 | end 180 | if IMAGE_TABLE.key?(image_key) && !exclude_list.include?(image_key.downcase) 181 | matrix[:platforms] << { 182 | label: image_key, 183 | provider: 'provision_service', 184 | image: IMAGE_TABLE[image_key] 185 | } 186 | elsif DOCKER_PLATFORMS.key?(image_key) && !exclude_list.include?(image_key.downcase) 187 | matrix[:platforms] << { 188 | label: image_key, 189 | provider: 'docker', 190 | image: DOCKER_PLATFORMS[image_key] 191 | } 192 | else 193 | puts "::warning::#{image_key} was excluded from testing" if exclude_list.include?(image_key.downcase) 194 | puts "::warning::Cannot find image for #{image_key}" unless exclude_list.include?(image_key.downcase) 195 | end 196 | end 197 | end 198 | end 199 | 200 | # Set collections based on puppet version requirements 201 | if metadata.key?('requirements') && metadata['requirements'].length.positive? 202 | metadata['requirements'].each do |req| 203 | next unless req.key?('name') && req.key?('version_requirement') && req['name'] == 'puppet' 204 | 205 | ver_regexp = /^([>=<]{1,2})\s*([\d.]+)\s+([>=<]{1,2})\s*([\d.]+)$/ 206 | match = ver_regexp.match(req['version_requirement']) 207 | if match.nil? 208 | puts "::warning::Didn't recognize version_requirement '#{req['version_requirement']}'" 209 | break 210 | end 211 | 212 | cmp_one, ver_one, cmp_two, ver_two = match.captures 213 | reqs = ["#{cmp_one} #{ver_one}", "#{cmp_two} #{ver_two}"] 214 | 215 | COLLECTION_TABLE.each do |collection| 216 | # Test against the "largest" puppet version in a collection, e.g. `7.9999` to allow puppet requirements with a non-zero lower bound on minor/patch versions. 217 | # This assumes that such a boundary will always allow the latest actually existing puppet version of a release stream, trading off simplicity vs accuracy here. 218 | next unless Gem::Requirement.create(reqs).satisfied_by?(Gem::Version.new("#{collection[:puppet_maj_version]}.9999")) 219 | 220 | matrix[:collection] << "puppetcore#{collection[:puppet_maj_version]}" 221 | 222 | include_version = { 223 | 8 => "~> #{collection[:puppet_maj_version]}.0", 224 | 7 => "~> #{collection[:puppet_maj_version]}.24", 225 | 6 => "~> #{collection[:puppet_maj_version]}.0" 226 | } 227 | spec_matrix[:include] << { puppet_version: include_version[collection[:puppet_maj_version]], ruby_version: collection[:ruby_version] } 228 | end 229 | end 230 | end 231 | 232 | puts '::warning::matrix_from_metadata_v2 is now deprecated and will be removed in puppet_litmus v3, please migrate to matrix_from_metadata_v3.' 233 | # Set to defaults (all collections) if no matches are found 234 | matrix[:collection] = COLLECTION_TABLE.map { |collection| "puppet#{collection[:puppet_maj_version]}-nightly" } if matrix[:collection].empty? 235 | # Just to make sure there aren't any duplicates 236 | matrix[:platforms] = matrix[:platforms].uniq.sort_by { |a| a[:label] } unless ARGV.include?('--custom-matrix') 237 | matrix[:collection] = matrix[:collection].uniq.sort 238 | 239 | set_output('matrix', JSON.generate(matrix)) 240 | set_output('spec_matrix', JSON.generate(spec_matrix)) 241 | 242 | acceptance_test_cell_count = matrix[:platforms].length * matrix[:collection].length 243 | spec_test_cell_count = spec_matrix[:include].length 244 | 245 | puts "Created matrix with #{acceptance_test_cell_count + spec_test_cell_count} cells:" 246 | puts " - Acceptance Test Cells: #{acceptance_test_cell_count}" 247 | puts " - Spec Test Cells: #{spec_test_cell_count}" 248 | -------------------------------------------------------------------------------- /exe/matrix_from_metadata_v3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'English' 5 | require 'json' 6 | require 'optparse' 7 | require 'ostruct' 8 | 9 | # wrap up running in a Github Action 10 | module Action 11 | class << self 12 | attr_reader :type 13 | 14 | def init(to = 'auto') 15 | @notice = true 16 | @type = if to.eql? 'auto' 17 | ENV['GITHUB_ACTIONS'] ? 'github' : 'stdout' 18 | else 19 | to 20 | end 21 | $stderr = $stdout if @type == 'github' 22 | end 23 | 24 | def config(**args) 25 | error("invalid Action.config: #{args}") unless args.is_a?(Hash) 26 | args.each do |arg| 27 | instance_variable_set(:"@#{arg[0]}", arg[1]) 28 | end 29 | end 30 | 31 | def debug(msg = nil) 32 | return @debug if msg.nil? 33 | 34 | output(msg, '::debug::') if @debug 35 | end 36 | 37 | def notice(msg = nil) 38 | return @notice if msg.nil? 39 | 40 | output(msg, '::notice::') if @notice 41 | end 42 | 43 | def error(msg) 44 | output(msg, '::error::') 45 | exit 1 46 | end 47 | 48 | def warning(msg) 49 | output(msg, '::warning::') 50 | end 51 | 52 | def group(name, data, **kwargs) 53 | output(name, '::group::') 54 | output(data, **kwargs) 55 | output('', '::endgroup::') if @type == 'github' 56 | 57 | self 58 | end 59 | 60 | def set_output(key, value) 61 | @output ||= @type == 'github' ? ENV.fetch('GITHUB_OUTPUT', nil) : '/dev/stdout' 62 | 63 | if @output.nil? 64 | Action.warning('GITHUB_OUTPUT environment is not set, sending output to stdout') 65 | @output = '/dev/stdout' 66 | end 67 | 68 | File.open(@output, 'a') { |f| f.puts "#{key}=#{JSON.generate(value)}" } 69 | 70 | self 71 | end 72 | 73 | private 74 | 75 | def output(msg, prefix = nil, pretty: false) 76 | $stderr.print prefix if @type == 'github' 77 | $stderr.puts pretty ? JSON.pretty_generate(msg) : msg.to_s 78 | 79 | self 80 | end 81 | end 82 | end 83 | 84 | options = OpenStruct.new( 85 | puppet_exclude: [], 86 | puppet_include: [], 87 | platform_exclude: [], 88 | platform_include: [], 89 | arch_include: [], 90 | arch_exclude: [], 91 | provision_prefer: [], 92 | provision_include: [], 93 | provision_exclude: [] 94 | ) 95 | 96 | default_options = { 97 | 'provision-prefer': 'docker', 98 | runner: 'ubuntu-latest', 99 | output: 'auto', 100 | matrix: File.join(File.dirname(__FILE__), 'matrix.json'), 101 | metadata: 'metadata.json' 102 | } 103 | 104 | begin 105 | Action.init 106 | 107 | # default disable provision_service if puppetlabs is not the owner 108 | default_options[:'provision-exclude'] = 'provision_service' \ 109 | if ARGV.reject! { |x| x == '--puppetlabs' }.nil? && !['puppetlabs'].include?(ENV.fetch('GITHUB_REPOSITORY_OWNER', nil)) 110 | 111 | # apply default_options if not overridden on the command line 112 | default_options.each do |arg, value| 113 | ARGV.unshift("--#{arg}", value) unless ARGV.grep(/\A--#{arg}(=.*)?\z/).any? 114 | end 115 | 116 | OptionParser.accept(JSON) do |v| 117 | begin 118 | x = JSON.parse(File.read(v)) if v 119 | raise "nothing parsed from file #{v}" if x.empty? 120 | 121 | x 122 | rescue JSON::ParserError 123 | raise "error parsing file #{v}" 124 | end 125 | rescue RuntimeError, Errno::ENOENT 126 | raise OptionParser::InvalidArgument, $ERROR_INFO unless ARGV.grep(/^-(h|help)$/).any? 127 | end 128 | 129 | OutputType = ->(value) {} 130 | OptionParser.accept(OutputType) do |v| 131 | raise OptionParser::InvalidArgument, v \ 132 | unless %w[auto github stdout].include?(v) 133 | 134 | Action.init(v) 135 | end 136 | 137 | OptionParser.accept(Regexp) { |v| Regexp.new(v, Regexp::IGNORECASE) } 138 | 139 | OptionParser.new do |opt| 140 | opt.separator "Generate Github Actions Matrices from Puppet metadata.json\n\nOptions:" 141 | opt.on('--matrix FILE', JSON, 'File containing possible collections and provisioners (default: built-in)') { |o| options.matrix = o } 142 | opt.on('--metadata FILE', JSON, "File containing module metadata json (default: #{default_options[:metadata]})\n\n") { |o| options.metadata = o } 143 | 144 | opt.on('--debug', TrueClass, 'Enable debug messages') { |o| options.debug = o } 145 | opt.on('--quiet', TrueClass, 'Disable notice messages') { |o| options.quiet = o } 146 | opt.on('--output TYPE', OutputType, "Type of output to generate; auto, github or stdout (default: #{default_options[:output]})\n\n") { |o| options.output = o } 147 | 148 | opt.on('--runner NAME', String, "Default Github action runner (default: #{default_options[:runner]})") { |o| options.runner = o } 149 | 150 | opt.on('--pe-include', TrueClass, 'Include Puppet Enterprise LTS') { |o| options.pe_include = o } 151 | 152 | opt.on('--puppet-include MAJOR', Integer, 'Select puppet major version') { |o| options.puppet_include << o } 153 | opt.on('--puppet-exclude MAJOR', Integer, 'Filter puppet major version') { |o| options.puppet_exclude << o } 154 | 155 | opt.on('--platform-include REGEX', Regexp, 'Select platform') { |o| options.platform_include << o } 156 | opt.on('--platform-exclude REGEX', Regexp, 'Filter platform') { |o| options.platform_exclude << o } 157 | 158 | opt.on('--arch-include REGEX', Regexp, 'Select architecture') { |o| options.arch_include << o } 159 | opt.on('--arch-exclude REGEX', Regexp, 'Filter architecture') { |o| options.arch_exclude << o } 160 | 161 | opt.on('--provision-prefer NAME', String, "Prefer provisioner (default: #{default_options[:'provision-prefer']})") { |o| options.provision_prefer.push(*o.split(',')) } 162 | opt.on('--provision-include NAME', String, 'Select provisioner (default: all)') { |o| options.provision_include.push(*o.split(',')) } 163 | opt.on('--provision-exclude NAME', String, "Filter provisioner (default: #{default_options[:'provision-exclude'] || 'none'})") { |o| options.provision_exclude.push(*o.split(',')) } 164 | end.parse! 165 | 166 | Action.config(debug: true) if options[:debug] 167 | Action.config(notice: false) if options[:quiet] && !options[:debug] 168 | 169 | # validate provisioners 170 | options[:provision_include].select! do |p| 171 | options[:matrix]['provisioners'].key?(p) or raise OptionParser::InvalidArgument, "--provision-include '#{p}' not found in provisioners" 172 | end 173 | 174 | # filter provisioners 175 | unless options[:provision_include].empty? 176 | options[:matrix]['provisioners'].delete_if do |k, _| 177 | unless options[:provision_include].include?(k.to_s) 178 | Action.debug("provision-include filtered #{k}") 179 | true 180 | end 181 | end 182 | end 183 | options[:matrix]['provisioners'].delete_if do |k, _| 184 | if options[:provision_exclude].include?(k.to_s) 185 | Action.debug("provision-exclude filtered #{k}") 186 | true 187 | end 188 | end 189 | 190 | # sort provisioners 191 | options[:matrix]['provisioners'] = options[:matrix]['provisioners'].sort_by { |key, _| options[:provision_prefer].index(key.to_s) || options[:provision_prefer].length }.to_h \ 192 | unless options[:provision_prefer].empty? 193 | 194 | # union regexp option values 195 | %w[platform arch].each do |c| 196 | [:"#{c}_exclude", :"#{c}_include"].each do |k| 197 | options[k] = if options[k].empty? 198 | nil 199 | else 200 | Regexp.new(format('\A(?:%s)\z', Regexp.union(options[k])), Regexp::IGNORECASE) 201 | end 202 | end 203 | end 204 | 205 | raise OptionParser::ParseError, 'no provisioners left after filters applied' if options[:matrix]['provisioners'].empty? 206 | rescue OptionParser::ParseError => e 207 | Action.error(e) 208 | end 209 | 210 | matrix = { platforms: [], collection: [] } 211 | spec_matrix = { include: [] } 212 | 213 | # collection matrix 214 | version_re = /([>=<]{1,2})\s*([\d.]+)/ 215 | options[:metadata]['requirements']&.each do |req| 216 | next unless req['name'] == 'puppet' && req['version_requirement'] 217 | 218 | puppet_version_reqs = req['version_requirement'].scan(version_re).map(&:join) 219 | if puppet_version_reqs.empty? 220 | Action.warning("Didn't recognize version_requirement '#{req['version_requirement']}'") 221 | break 222 | end 223 | 224 | gem_req = Gem::Requirement.create(puppet_version_reqs) 225 | 226 | # Add PE LTS to the collection matrix 227 | if options[:pe_include] 228 | require 'puppet_forge' 229 | 230 | PuppetForge.user_agent = 'Puppet/Litmus' 231 | 232 | forge_conn = PuppetForge::Connection.make_connection('https://forge.puppet.com') 233 | pe_tracks = forge_conn.get('/private/versions/pe') 234 | lts_tracklist = pe_tracks.body.select { |ver| ver[:lts] == true } 235 | 236 | lts_tracklist.each do |track| 237 | if gem_req.satisfied_by?(Gem::Version.new(track[:versions][0][:puppet].to_s)) 238 | matrix[:collection] << "#{track[:latest]}-puppet_enterprise" 239 | else 240 | Action.debug("PE #{track[:latest]} (puppet v#{track[:versions][0][:puppet]}) outside requirements #{puppet_version_reqs}") 241 | end 242 | end 243 | end 244 | 245 | options[:matrix]['collections'].each do |collection| 246 | next unless options[:puppet_include].each do |major| 247 | break if major != collection['puppet'].to_i 248 | 249 | Action.debug("puppet-include matched collection #{collection.inspect}") 250 | end 251 | 252 | next unless options[:puppet_exclude].each do |major| 253 | if major.eql? collection['puppet'].to_i 254 | Action.debug("puppet-exclude matched collection #{collection.inspect}") 255 | break 256 | end 257 | end 258 | 259 | # Test against the "largest" puppet version in a collection, e.g. `7.9999` to allow puppet requirements with a non-zero lower bound on minor/patch versions. 260 | # This assumes that such a boundary will always allow the latest actually existing puppet version of a release stream, trading off simplicity vs accuracy here. 261 | next unless gem_req.satisfied_by?(Gem::Version.new("#{collection['puppet'].to_i}.9999")) 262 | 263 | matrix[:collection] << "puppetcore#{collection['puppet'].to_i}" 264 | 265 | spec_matrix[:include] << { 266 | puppet_version: "~> #{collection['puppet']}", 267 | ruby_version: collection['ruby'] 268 | } 269 | end 270 | end 271 | 272 | # Set platforms based on declared operating system support 273 | options[:metadata]['operatingsystem_support'].each do |os_sup| 274 | os_sup['operatingsystemrelease'].sort_by(&:to_i).each do |os_ver| 275 | os_ver_platforms = [] 276 | platform_key = [os_sup['operatingsystem'], os_ver] 277 | 278 | # filter platforms 279 | if options[:platform_include] && platform_key[0].match?(options[:platform_include]) == false && platform_key.join('-').match?(options[:platform_include]) == false 280 | Action.notice("platform-include filtered #{platform_key.join('-')}") 281 | next 282 | end 283 | 284 | if options[:platform_exclude] && (platform_key[0].match?(options[:platform_exclude]) || platform_key.join('-').match?(options[:platform_exclude])) 285 | Action.notice("platform-exclude filtered #{platform_key.join('-')}") 286 | next 287 | end 288 | 289 | options[:matrix]['provisioners'].each do |provisioner, platforms| 290 | images = platforms.dig(*platform_key) 291 | next if images.nil? 292 | 293 | # filter arch 294 | images.delete_if do |arch, _| 295 | next if options[:arch_include]&.match?(arch.downcase) == true 296 | next unless options[:arch_exclude]&.match?(arch.downcase) 297 | 298 | Action.notice("arch filtered #{platform_key.join('-')}-#{arch} from #{provisioner}") 299 | end 300 | next if images.empty? 301 | 302 | images.each do |arch, image| 303 | label = (arch.eql?('x86_64') ? platform_key : platform_key + [arch]).join('-') 304 | next if os_ver_platforms.any? { |h| h[:label] == label } 305 | 306 | runner = options[:matrix]['github_runner'][provisioner]&.reduce(options[:runner]) do |memo, (reg, run)| 307 | label.match?(/#{reg}/i) ? run : memo 308 | end 309 | 310 | os_ver_platforms << { 311 | label:, 312 | provider: provisioner, 313 | arch:, 314 | image:, 315 | runner: runner.nil? ? options[:runner] : runner 316 | } 317 | end 318 | end 319 | 320 | if os_ver_platforms.empty? 321 | Action.warning("#{platform_key.join('-')} no provisioner found") 322 | else 323 | matrix[:platforms].push(*os_ver_platforms) 324 | end 325 | end 326 | end 327 | 328 | Action.group('matrix', matrix, pretty: true).group('spec_matrix', spec_matrix, pretty: true) if Action.type == 'github' && Action.notice 329 | 330 | Action.error('no supported puppet versions') if matrix[:collection].empty? 331 | 332 | if Action.type == 'stdout' 333 | $stdout.puts JSON.generate({ matrix:, spec_matrix: }) 334 | else 335 | Action.set_output('matrix', matrix).set_output('spec_matrix', spec_matrix) 336 | end 337 | -------------------------------------------------------------------------------- /lib/puppet_litmus.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Helper methods for testing puppet content 4 | module PuppetLitmus; end 5 | 6 | require 'bolt_spec/run' 7 | require 'puppet_litmus/inventory_manipulation' 8 | require 'puppet_litmus/puppet_helpers' 9 | require 'puppet_litmus/rake_helper' 10 | require 'puppet_litmus/spec_helper_acceptance' 11 | 12 | # Helper methods for testing puppet content 13 | module PuppetLitmus 14 | include BoltSpec::Run 15 | include PuppetLitmus::InventoryManipulation 16 | include PuppetLitmus::PuppetHelpers 17 | include PuppetLitmus::RakeHelper 18 | end 19 | -------------------------------------------------------------------------------- /lib/puppet_litmus/inventory_manipulation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module PuppetLitmus; end # rubocop:disable Style/Documentation 4 | 5 | # helper functions for manipulating and reading a bolt inventory file 6 | module PuppetLitmus::InventoryManipulation 7 | # Creates an inventory hash from the inventory.yaml. 8 | # 9 | # @param inventory_full_path [String] path to the litmus_inventory.yaml file 10 | # @return [Hash] hash of the litmus_inventory.yaml file. 11 | def inventory_hash_from_inventory_file(inventory_full_path = nil) 12 | require 'yaml' 13 | inventory_full_path = "#{Dir.pwd}/spec/fixtures/litmus_inventory.yaml" if inventory_full_path.nil? 14 | raise "There is no inventory file at '#{inventory_full_path}'." unless File.exist?(inventory_full_path) 15 | 16 | YAML.load_file(inventory_full_path) 17 | end 18 | 19 | # Provide a default hash for executing against localhost 20 | # 21 | # @return [Hash] inventory.yaml hash containing only an entry for localhost 22 | def localhost_inventory_hash 23 | { 24 | 'groups' => [ 25 | { 26 | 'name' => 'local', 27 | 'targets' => [ 28 | { 29 | 'uri' => 'litmus_localhost', 30 | 'config' => { 'transport' => 'local' }, 31 | 'feature' => 'puppet-agent' 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | end 38 | 39 | # Finds targets to perform operations on from an inventory hash. 40 | # 41 | # @param inventory_hash [Hash] hash of the inventory.yaml file 42 | # @param targets [Array] 43 | # @return [Array] array of targets. 44 | def find_targets(inventory_hash, targets) 45 | if targets.nil? 46 | inventory_hash.to_s.scan(/uri"=>"(\S*)"/).flatten 47 | else 48 | [targets] 49 | end 50 | end 51 | 52 | # Recursively find and iterate over the groups in an inventory. If no block is passed 53 | # to the function then only the name of the group is returned. If a block is passed 54 | # then the block is executed against each group and the value of the block is returned. 55 | # 56 | # @param inventory_hash [Hash] Inventory hash from inventory.yaml 57 | # @param block [Block] Block to execute against each node 58 | def groups_in_inventory(inventory_hash, &block) 59 | inventory_hash['groups'].flat_map do |group| 60 | output_collector = [] 61 | output_collector << if block 62 | yield group 63 | else 64 | group['name'].downcase 65 | end 66 | output_collector << groups_in_inventory({ 'groups' => group['groups'] }, &block) if group.key? 'groups' 67 | output_collector.flatten.compact 68 | end 69 | end 70 | 71 | # Iterate over all targets in an inventory. If no block is given to the function 72 | # it will return the name of every target in the inventory. If a block is passed 73 | # it will execute the block on each target and return the value of the block. 74 | # 75 | # @param inventory_hash [Hash] Inventory hash from inventory.yaml 76 | # @param block [Block] Block to execute against each node 77 | def targets_in_inventory(inventory_hash) 78 | groups_in_inventory(inventory_hash) do |group| 79 | if group.key? 'targets' 80 | group['targets'].map do |target| 81 | if block_given? 82 | (yield target) 83 | else 84 | target['uri'].downcase 85 | end 86 | end 87 | end 88 | end 89 | end 90 | 91 | # Find all targets in an inventory that have a role. The roles for a target are 92 | # specified in the vars hash for a target. This function is tolerant to the roles 93 | # hash being called either 'role' or 'roles' and it is tolerant to the roles being 94 | # either a single key value or an array of roles. 95 | # 96 | # @param role [String] The name of a role to search for 97 | # @param inventory [Hash] Inventory hash from inventory.yaml 98 | def nodes_with_role(role, inventory) 99 | output_collector = [] 100 | targets_in_inventory(inventory) do |target| 101 | vars = target['vars'] 102 | roles = [vars['role'] || vars['roles']].flatten 103 | roles = roles.map(&:downcase) 104 | output_collector << target['uri'] if roles.include? role.downcase 105 | end 106 | output_collector unless output_collector.empty? 107 | end 108 | 109 | # Searches through the inventory hash to either validate that a group being targeted exists, 110 | # validate that a specific target being targeted exists, or resolves role names to a 111 | # list of nodes to target. Targets and roles can be specified as strings or as symbols, and 112 | # the functions are tolerant to incorrect capitalization. 113 | # 114 | # @param target [String] || [Array[String]] A list of targets 115 | # @param inventory [Hash] inventory hash from inventory.yaml 116 | def search_for_target(target, inventory) 117 | result_collector = [] 118 | groups = groups_in_inventory(inventory) 119 | Array(target).map do |name| 120 | result_collector << name if groups.include? name.to_s.downcase 121 | result_collector << name if targets_in_inventory(inventory).include? name.to_s.downcase 122 | result_collector << nodes_with_role(name.to_s, inventory) 123 | end 124 | 125 | result_collector = result_collector.flatten.compact 126 | raise 'targets not found in inventory' if result_collector.empty? 127 | 128 | result_collector 129 | end 130 | 131 | # Determines if a node_name exists in a group in the inventory_hash. 132 | # 133 | # @param inventory_hash [Hash] hash of the inventory.yaml file 134 | # @param node_name [String] node to locate in the group 135 | # @param group_name [String] group of nodes to limit the search for the node_name in 136 | # @return [Boolean] true if node_name exists in group_name. 137 | def target_in_group(inventory_hash, node_name, group_name) 138 | exists = false 139 | inventory_hash['groups'].each do |group| 140 | next unless group['name'] == group_name 141 | 142 | group['targets'].each do |node| 143 | exists = true if node['uri'] == node_name 144 | end 145 | end 146 | exists 147 | end 148 | 149 | # Determines if a node_name exists in the inventory_hash. 150 | # 151 | # @param inventory_hash [Hash] hash of the inventory.yaml file 152 | # @param node_name [String] node to locate in the group 153 | # @return [Boolean] true if node_name exists in the inventory_hash. 154 | def target_in_inventory?(inventory_hash, node_name) 155 | find_targets(inventory_hash, nil).include?(node_name) 156 | end 157 | 158 | # Finds a config hash in the inventory hash by searching for a node name. 159 | # 160 | # @param inventory_hash [Hash] hash of the inventory.yaml file 161 | # @param node_name [String] node to locate in the group 162 | # @return [Hash] config for node of name node_name 163 | def config_from_node(inventory_hash, node_name) 164 | config = targets_in_inventory(inventory_hash) do |target| 165 | next unless target['uri'].casecmp(node_name).zero? 166 | 167 | return target['config'] unless target['config'].nil? 168 | end 169 | 170 | config.empty? ? nil : config[0] 171 | end 172 | 173 | # Finds a facts hash in the inventory hash by searching for a node name. 174 | # 175 | # @param inventory_hash [Hash] hash of the inventory.yaml file 176 | # @param node_name [String] node to locate in the group 177 | # @return [Hash] facts for node of name node_name 178 | def facts_from_node(inventory_hash, node_name) 179 | facts = targets_in_inventory(inventory_hash) do |target| 180 | next unless target['uri'].casecmp(node_name).zero? 181 | 182 | target['facts'] unless target['facts'].nil? 183 | end 184 | 185 | facts.empty? ? nil : facts[0] 186 | end 187 | 188 | # Finds a var hash in the inventory hash by searching for a node name. 189 | # 190 | # @param inventory_hash [Hash] hash of the inventory.yaml file 191 | # @param node_name [String] node to locate in the group 192 | # @return [Hash] vars for node of name node_name 193 | def vars_from_node(inventory_hash, node_name) 194 | vars = targets_in_inventory(inventory_hash) do |target| 195 | next unless target['uri'].casecmp(node_name).zero? 196 | 197 | target['vars'] unless target['vars'].nil? 198 | end 199 | vars.empty? ? {} : vars[0] 200 | end 201 | 202 | # Adds a node to a group specified, if group_name exists in inventory hash. 203 | # 204 | # @param inventory_hash [Hash] hash of the inventory.yaml file 205 | # @param node [Hash] node to add to the group 206 | # group_name [String] group of nodes to limit the search for the node_name in 207 | # @return [Hash] inventory_hash with node added to group if group_name exists in inventory hash. 208 | def add_node_to_group(inventory_hash, node, group_name) 209 | # check if group exists 210 | if inventory_hash['groups'].any? { |g| g['name'] == group_name } 211 | inventory_hash['groups'].each do |group| 212 | group['targets'].push node if group['name'] == group_name 213 | end 214 | else 215 | # add new group 216 | group = { 'name' => group_name, 'targets' => [node] } 217 | inventory_hash['groups'].push group 218 | end 219 | inventory_hash 220 | end 221 | 222 | # Removes named node from a group inside an inventory_hash. 223 | # 224 | # @param inventory_hash [Hash] hash of the inventory.yaml file 225 | # @param node_name [String] node to locate in the group 226 | # @return [Hash] inventory_hash with node of node_name removed. 227 | def remove_node(inventory_hash, node_name) 228 | inventory_hash['groups'].each do |group| 229 | group['targets'].delete_if { |i| i['uri'] == node_name } 230 | end 231 | inventory_hash 232 | end 233 | 234 | # Adds a feature to the group specified/ 235 | # 236 | # @param inventory_hash [Hash] hash of the inventory.yaml file 237 | # @param feature_name [String] feature to locate in the group 238 | # group_name [String] group of nodes to limit the search for the group_name in 239 | # @return inventory.yaml file with feature added to group. 240 | # @return [Hash] inventory_hash with feature added to group if group_name exists in inventory hash. 241 | def add_feature_to_group(inventory_hash, feature_name, group_name) 242 | i = 0 243 | inventory_hash['groups'].each do |group| 244 | if group['name'] == group_name 245 | group = group.merge('features' => []) if group['features'].nil? == true 246 | group['features'].push feature_name unless group['features'].include?(feature_name) 247 | inventory_hash['groups'][i] = group 248 | end 249 | i += 1 250 | end 251 | inventory_hash 252 | end 253 | 254 | # Removes a feature from the group specified/ 255 | # 256 | # @param inventory_hash [Hash] hash of the inventory.yaml file 257 | # @param feature_name [String] feature to locate in the group 258 | # group_name [String] group of nodes to limit the search for the group_name in 259 | # @return inventory.yaml file with feature removed from the group. 260 | # @return [Hash] inventory_hash with feature added to group if group_name exists in inventory hash. 261 | def remove_feature_from_group(inventory_hash, feature_name, group_name) 262 | inventory_hash['groups'].each do |group| 263 | group['features'].delete(feature_name) if group['name'] == group_name && group['features'].nil? != true 264 | end 265 | inventory_hash 266 | end 267 | 268 | # Adds a feature to the node specified/ 269 | # 270 | # @param inventory_hash [Hash] hash of the inventory.yaml file 271 | # @param feature_name [String] feature to locate in the node 272 | # node_name [String] node of nodes to limit the search for the node_name in 273 | # @return inventory.yaml file with feature added to node. 274 | # @return [Hash] inventory_hash with feature added to node if node_name exists in inventory hash. 275 | def add_feature_to_node(inventory_hash, feature_name, node_name) 276 | group_index = 0 277 | inventory_hash['groups'].each do |group| 278 | node_index = 0 279 | group['targets'].each do |node| 280 | if node['uri'] == node_name 281 | node = node.merge('features' => []) if node['features'].nil? == true 282 | node['features'].push feature_name unless node['features'].include?(feature_name) 283 | inventory_hash['groups'][group_index]['targets'][node_index] = node 284 | end 285 | node_index += 1 286 | end 287 | group_index += 1 288 | end 289 | inventory_hash 290 | end 291 | 292 | # Removes a feature from the node specified/ 293 | # 294 | # @param inventory_hash [Hash] hash of the inventory.yaml file 295 | # @param feature_name [String] feature to locate in the node 296 | # @param node_name [String] node of nodes to limit the search for the node_name in 297 | # @return inventory.yaml file with feature removed from the node. 298 | # @return [Hash] inventory_hash with feature added to node if node_name exists in inventory hash. 299 | def remove_feature_from_node(inventory_hash, feature_name, node_name) 300 | inventory_hash['groups'].each do |group| 301 | group['targets'].each do |node| 302 | node['features'].delete(feature_name) if node['uri'] == node_name && node['features'].nil? != true 303 | end 304 | end 305 | inventory_hash 306 | end 307 | 308 | # Write inventory_hash to inventory_yaml file/ 309 | # 310 | # @param inventory_full_path [String] path to the inventory.yaml file 311 | # @param inventory_hash [Hash] hash of the inventory.yaml file 312 | # @return inventory.yaml file with feature added to group. 313 | def write_to_inventory_file(inventory_hash, inventory_full_path) 314 | File.open(inventory_full_path, 'wb+') { |f| f.write(inventory_hash.to_yaml) } 315 | end 316 | 317 | # Add the `litmus.platform` with platform information for the target 318 | # 319 | # @param inventory_hash [Hash] hash of the inventory.yaml file 320 | # @param node_name [String] node of nodes to limit the search for the node_name in 321 | def add_platform_field(inventory_hash, node_name) 322 | facts_from_node(inventory_hash, node_name) 323 | rescue StandardError => e 324 | warn e 325 | {} 326 | end 327 | end 328 | -------------------------------------------------------------------------------- /lib/puppet_litmus/rake_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bolt_spec/run' 4 | require 'puppet_litmus/version' 5 | 6 | # helper methods for the litmus rake tasks 7 | module PuppetLitmus::RakeHelper # rubocop:disable Metrics/ModuleLength 8 | # DEFAULT_CONFIG_DATA should be frozen for our safety, but it needs to work around https://github.com/puppetlabs/bolt/pull/1696 9 | DEFAULT_CONFIG_DATA = { 'modulepath' => File.join(Dir.pwd, 'spec', 'fixtures', 'modules') } # .freeze # rubocop:disable Style/MutableConstant 10 | SUPPORTED_PROVISIONERS = %w[abs docker docker_exp lxd provision_service vagrant vmpooler].freeze 11 | 12 | # Gets a string representing the operating system and version. 13 | # 14 | # @param metadata [Hash] metadata to parse for operating system info 15 | # @return [String] the operating system string with version info for use in provisioning. 16 | def get_metadata_operating_systems(metadata) 17 | return unless metadata.is_a?(Hash) 18 | return unless metadata['operatingsystem_support'].is_a?(Array) 19 | 20 | metadata['operatingsystem_support'].each do |os_info| 21 | next unless os_info['operatingsystem'] && os_info['operatingsystemrelease'] 22 | 23 | os_name = case os_info['operatingsystem'] 24 | when 'Amazon', 'Archlinux', 'AIX', 'OSX' 25 | next 26 | when 'OracleLinux' 27 | 'oracle' 28 | when 'Windows' 29 | 'win' 30 | else 31 | os_info['operatingsystem'].downcase 32 | end 33 | 34 | os_info['operatingsystemrelease'].each do |release| 35 | version = case os_name 36 | when 'ubuntu', 'osx' 37 | release.sub('.', '') 38 | when 'sles' 39 | release.gsub(%r{ SP[14]}, '') 40 | when 'win' 41 | release = release.delete('.') if release.include? '8.1' 42 | release.sub('Server', '').sub('10', '10-pro') 43 | else 44 | release 45 | end 46 | 47 | yield "#{os_name}-#{version.downcase}-x86_64".delete(' ') 48 | end 49 | end 50 | end 51 | 52 | # Executes a command on the test runner. 53 | # 54 | # @param command [String] command to execute. 55 | # @return [Object] the standard out stream. 56 | def run_local_command(command) 57 | require 'open3' 58 | stdout, stderr, status = Open3.capture3(command) 59 | error_message = "Attempted to run\ncommand:'#{command}'\nstdout:#{stdout}\nstderr:#{stderr}" 60 | 61 | raise error_message unless status.to_i.zero? 62 | 63 | stdout 64 | end 65 | 66 | def provision(provisioner, platform, inventory_vars) 67 | include ::BoltSpec::Run 68 | raise "the provision module was not found in #{DEFAULT_CONFIG_DATA['modulepath']}, please amend the .fixtures.yml file" unless 69 | File.directory?(File.join(DEFAULT_CONFIG_DATA['modulepath'], 'provision')) 70 | 71 | params = { 'action' => 'provision', 'platform' => platform, 'inventory' => File.join(Dir.pwd, 'spec', 'fixtures', 'litmus_inventory.yaml') } 72 | params['vars'] = inventory_vars unless inventory_vars.nil? 73 | 74 | task_name = provisioner_task(provisioner) 75 | bolt_result = run_task(task_name, 'localhost', params, config: DEFAULT_CONFIG_DATA, inventory: nil) 76 | raise_bolt_errors(bolt_result, "provisioning of #{platform} failed.") 77 | 78 | bolt_result 79 | end 80 | 81 | def provision_list(provision_hash, key) 82 | provisioner = provision_hash[key]['provisioner'] 83 | inventory_vars = provision_hash[key]['vars'] 84 | # Splat the params into environment variables to pass to the provision task but only in this runspace 85 | provision_hash[key]['params']&.each { |k, value| ENV[k.upcase] = value.to_s } 86 | provision_hash[key]['images'].map do |image| 87 | provision(provisioner, image, inventory_vars) 88 | end 89 | end 90 | 91 | def tear_down_nodes(targets, inventory_hash) 92 | include ::BoltSpec::Run 93 | config_data = { 'modulepath' => File.join(Dir.pwd, 'spec', 'fixtures', 'modules') } 94 | raise "the provision module was not found in #{config_data['modulepath']}, please amend the .fixtures.yml file" unless File.directory?(File.join(config_data['modulepath'], 'provision')) 95 | 96 | results = {} 97 | targets.each do |node_name| 98 | # next if local host or provisioner fact empty/not set (GH-421) 99 | next if node_name == 'litmus_localhost' || facts_from_node(inventory_hash, node_name)['provisioner'].nil? 100 | 101 | result = tear_down(node_name, inventory_hash) 102 | # Some provisioners tear_down targets that were created as a batch job. 103 | # These provisioners should return the list of additional targets 104 | # removed so that we do not attempt to process them. 105 | if result != [] && result[0]['value'].key?('removed') 106 | removed_targets = result[0]['value']['removed'] 107 | result[0]['value'].delete('removed') 108 | removed_targets.each do |removed_target| 109 | targets.delete(removed_target) 110 | results[removed_target] = result 111 | end 112 | end 113 | 114 | results[node_name] = result unless result == [] 115 | end 116 | results 117 | end 118 | 119 | def tear_down(node_name, inventory_hash) 120 | # how do we know what provisioner to use 121 | add_platform_field(inventory_hash, node_name) 122 | 123 | params = { 'action' => 'tear_down', 'node_name' => node_name, 'inventory' => File.join(Dir.pwd, 'spec', 'fixtures', 'litmus_inventory.yaml') } 124 | node_facts = facts_from_node(inventory_hash, node_name) 125 | bolt_result = run_task(provisioner_task(node_facts['provisioner']), 'localhost', params, config: DEFAULT_CONFIG_DATA, inventory: nil) 126 | raise_bolt_errors(bolt_result, "tear_down of #{node_name} failed.") 127 | bolt_result 128 | end 129 | 130 | def install_agent(collection, targets, inventory_hash) 131 | include ::BoltSpec::Run 132 | puppet_version = ENV.fetch('PUPPET_VERSION', nil) 133 | forge_token = ENV.fetch('PUPPET_FORGE_TOKEN', nil) 134 | params = {} 135 | params['password'] = forge_token if forge_token 136 | params['collection'] = collection if collection 137 | params['version'] = puppet_version if puppet_version 138 | 139 | raise "puppet_agent was not found in #{DEFAULT_CONFIG_DATA['modulepath']}, please amend the .fixtures.yml file" \ 140 | unless File.directory?(File.join(DEFAULT_CONFIG_DATA['modulepath'], 'puppet_agent')) 141 | 142 | raise 'puppetcore agent installs require a valid PUPPET_FORGE_TOKEN set in the env.' \ 143 | if collection =~ /\Apuppetcore.*/ && !forge_token 144 | 145 | # using boltspec, when the runner is called it changes the inventory_hash dropping the version field. The clone works around this 146 | bolt_result = run_task('puppet_agent::install', targets, params, config: DEFAULT_CONFIG_DATA, inventory: inventory_hash.clone) 147 | targets.each do |target| 148 | params = { 149 | 'path' => '/opt/puppetlabs/bin' 150 | } 151 | node_facts = facts_from_node(inventory_hash, target) 152 | next unless node_facts['provisioner'] == 'vagrant' 153 | 154 | puts "Adding puppet agent binary to the secure_path on target #{target}." 155 | result = run_task('provision::fix_secure_path', target, params, config: DEFAULT_CONFIG_DATA, inventory: inventory_hash.clone) 156 | raise_bolt_errors(result, "Failed to add the Puppet agent binary to the secure_path on target #{target}.") 157 | end 158 | raise_bolt_errors(bolt_result, 'Installation of agent failed.') 159 | bolt_result 160 | end 161 | 162 | def configure_path(inventory_hash) 163 | results = [] 164 | # fix the path on ssh_nodes 165 | unless inventory_hash['groups'].none? { |group| group['name'] == 'ssh_nodes' && !group['targets'].empty? } 166 | results << run_command('echo PATH="$PATH:/opt/puppetlabs/puppet/bin" > /etc/environment', 167 | 'ssh_nodes', config: nil, inventory: inventory_hash) 168 | end 169 | unless inventory_hash['groups'].none? { |group| group['name'] == 'winrm_nodes' && !group['targets'].empty? } 170 | results << run_command('[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\Program Files\Puppet Labs\Puppet\bin;C:\Program Files (x86)\Puppet Labs\Puppet\bin", "Machine")', 171 | 'winrm_nodes', config: nil, inventory: inventory_hash) 172 | end 173 | results 174 | end 175 | 176 | # Build the module in `module_dir` and put the resulting compressed tarball into `target_dir`. 177 | # 178 | # @param opts Hash of options to build the module 179 | # @param module_dir [String] The path of the module to build. If missing defaults to Dir.pwd 180 | # @param target_dir [String] The path the module will be built into. The default is /pkg 181 | # @return [String] The path to the built module 182 | def build_module(module_dir = nil, target_dir = nil) 183 | require 'puppet/modulebuilder' 184 | 185 | module_dir ||= Dir.pwd 186 | target_dir ||= File.join(source_dir, 'pkg') 187 | 188 | puts "Building '#{module_dir}' into '#{target_dir}'" 189 | builder = Puppet::Modulebuilder::Builder.new(module_dir, target_dir, nil) 190 | 191 | # Force the metadata to be read. Raises if metadata could not be found 192 | _metadata = builder.metadata 193 | 194 | builder.build 195 | end 196 | 197 | # Builds all the modules in a specified directory 198 | # 199 | # @param source_dir [String] the directory to get the modules from 200 | # @param target_dir [String] temporary location to store tarballs before uploading. This directory will be cleaned before use. The default is /pkg 201 | # @return [Array] an array of module tars' filenames 202 | def build_modules_in_dir(source_dir, target_dir = nil) 203 | target_dir ||= File.join(Dir.pwd, 'pkg') 204 | # remove old build dir if exists, before we build afresh 205 | FileUtils.rm_rf(target_dir) if File.directory?(target_dir) 206 | 207 | module_tars = Dir.entries(source_dir).map do |entry| 208 | next if ['.', '..'].include? entry 209 | 210 | module_dir = File.join(source_dir, entry) 211 | next unless File.directory? module_dir 212 | 213 | build_module(module_dir, target_dir) 214 | end 215 | module_tars.compact 216 | end 217 | 218 | # @deprecated Use `build_modules_in_dir` instead 219 | def build_modules_in_folder(source_folder) 220 | build_modules_in_dir(source_folder) 221 | end 222 | 223 | # Install a specific module tarball to the specified target. 224 | # This method installs dependencies using a forge repository. 225 | # 226 | # @param inventory_hash [Hash] the pre-loaded inventory 227 | # @param target_node_name [String] the name of the target where the module should be installed 228 | # @param module_tar [String] the filename of the module tarball to upload 229 | # @param module_repository [String] the URL for the forge to use for downloading modules. Defaults to the public Forge API. 230 | # @param ignore_dependencies [Boolean] flag used to ignore module dependencies defaults to false. 231 | # @return a bolt result 232 | def install_module(inventory_hash, target_node_name, module_tar, module_repository = nil, ignore_dependencies = false) # rubocop:disable Style/OptionalBooleanParameter 233 | # make sure the module to install is not installed 234 | # otherwise `puppet module install` might silently skip it 235 | module_name = File.basename(module_tar, '.tar.gz').split('-', 3)[0..1].join('-') 236 | uninstall_module(inventory_hash.clone, target_node_name, module_name, force: true) 237 | 238 | include ::BoltSpec::Run 239 | 240 | target_nodes = find_targets(inventory_hash, target_node_name) 241 | bolt_result = upload_file(module_tar, File.basename(module_tar), target_nodes, options: {}, config: nil, inventory: inventory_hash.clone) 242 | raise_bolt_errors(bolt_result, 'Failed to upload module.') 243 | 244 | module_repository_opts = "--module_repository '#{module_repository}'" unless module_repository.nil? 245 | install_module_command = "puppet module install #{module_repository_opts} #{File.basename(module_tar)}" 246 | install_module_command += ' --ignore-dependencies --force' if ignore_dependencies.to_s.casecmp('true').zero? 247 | 248 | bolt_result = run_command(install_module_command, target_nodes, config: nil, inventory: inventory_hash.clone) 249 | raise_bolt_errors(bolt_result, "Installation of package #{File.basename(module_tar)} failed.") 250 | bolt_result 251 | end 252 | 253 | def metadata_module_name 254 | require 'json' 255 | raise 'Could not find metadata.json' unless File.exist?(File.join(Dir.pwd, 'metadata.json')) 256 | 257 | metadata = JSON.parse(File.read(File.join(Dir.pwd, 'metadata.json'))) 258 | raise 'Could not read module name from metadata.json' if metadata['name'].nil? 259 | 260 | metadata['name'] 261 | end 262 | 263 | # Uninstall a module from a specified target 264 | # @param inventory_hash [Hash] the pre-loaded inventory 265 | # @param target_node_name [String] the name of the target where the module should be uninstalled 266 | # @param module_to_remove [String] the name of the module to remove. Defaults to the module under test. 267 | # @param opts [Hash] additional options to pass on to `puppet module uninstall` 268 | def uninstall_module(inventory_hash, target_node_name, module_to_remove = nil, **opts) 269 | include ::BoltSpec::Run 270 | module_name = module_to_remove || metadata_module_name 271 | target_nodes = find_targets(inventory_hash, target_node_name) 272 | install_module_command = "puppet module uninstall #{module_name}" 273 | install_module_command += ' --force' if opts[:force] 274 | bolt_result = run_command(install_module_command, target_nodes, config: nil, inventory: inventory_hash) 275 | # `puppet module uninstall --force` fails if the module is not installed. Ignore errors when force is set 276 | raise_bolt_errors(bolt_result, "uninstalling #{module_name} failed.") unless opts[:force] 277 | bolt_result 278 | end 279 | 280 | def check_connectivity?(inventory_hash, target_node_name) 281 | # if we're only checking connectivity for a single node 282 | add_platform_field(inventory_hash, target_node_name) if target_node_name 283 | 284 | include ::BoltSpec::Run 285 | target_nodes = find_targets(inventory_hash, target_node_name) 286 | puts "Checking connectivity for #{target_nodes.inspect}" 287 | 288 | results = run_command('cd .', target_nodes, config: nil, inventory: inventory_hash) 289 | failed = [] 290 | results.reject { |r| r['status'] == 'success' }.each do |result| 291 | puts "Failure connecting to #{result['target']}:\n#{result.inspect}" 292 | failed.push(result['target']) 293 | end 294 | raise "Connectivity has failed on: #{failed}" unless failed.empty? 295 | 296 | puts 'Connectivity check PASSED.' 297 | true 298 | end 299 | 300 | def provisioner_task(provisioner) 301 | if SUPPORTED_PROVISIONERS.include?(provisioner) 302 | "provision::#{provisioner}" 303 | else 304 | warn "WARNING: Unsupported provisioner '#{provisioner}', try #{SUPPORTED_PROVISIONERS.join('/')}" 305 | provisioner.to_s 306 | end 307 | end 308 | 309 | # Parse out errors messages in result set returned by Bolt command. 310 | # 311 | # @param result_set [Array] result set returned by Bolt command. 312 | # @return [Hash] Errors grouped by target. 313 | def check_bolt_errors(result_set) 314 | errors = {} 315 | # iterate through each error 316 | result_set.each do |target_result| 317 | status = target_result['status'] 318 | # jump to the next one when there is not fail 319 | next if status != 'failure' 320 | 321 | target = target_result['target'] 322 | # get some info from error 323 | errors[target] = target_result['value'] 324 | end 325 | errors 326 | end 327 | 328 | # Parse out errors messages in result set returned by Bolt command. If there are errors, raise them. 329 | # 330 | # @param result_set [Array] result set returned by Bolt command. 331 | # @param error_msg [String] error message to raise when errors are detected. The actual errors will be appended. 332 | def raise_bolt_errors(result_set, error_msg) 333 | errors = check_bolt_errors(result_set) 334 | 335 | unless errors.empty? 336 | formatted_results = errors.map { |k, v| " #{k}: #{v.inspect}" }.join("\n") 337 | raise "#{error_msg}\nResults:\n#{formatted_results}}" 338 | end 339 | 340 | nil 341 | end 342 | 343 | def start_spinner(message) 344 | if (ENV['CI'] || '').casecmp('true').zero? || Gem.win_platform? 345 | puts message 346 | spinner = Thread.new do 347 | # CI systems are strange beasts, we only output a '.' every wee while to keep the terminal alive. 348 | loop do 349 | printf '.' 350 | sleep(10) 351 | end 352 | end 353 | else 354 | require 'tty-spinner' 355 | spinner = TTY::Spinner.new("[:spinner] #{message}") 356 | spinner.auto_spin 357 | end 358 | spinner 359 | end 360 | 361 | def stop_spinner(spinner) 362 | if (ENV['CI'] || '').casecmp('true').zero? || Gem.win_platform? 363 | Thread.kill(spinner) 364 | else 365 | spinner.success 366 | end 367 | end 368 | 369 | require 'retryable' 370 | 371 | Retryable.configure do |config| 372 | config.sleep = ->(n) { (1.5**n) + Random.rand(0.5) } 373 | # config.log_method = ->(retries, exception) do 374 | # Logger.new($stdout).debug("[Attempt ##{retries}] Retrying because [#{exception.class} - #{exception.message}]: #{exception.backtrace.first(5).join(' | ')}") 375 | # end 376 | end 377 | 378 | class LitmusTimeoutError < StandardError; end 379 | 380 | def with_retries(options: { tries: Float::INFINITY }, max_wait_minutes: 15) 381 | stop = Time.now + (max_wait_minutes * 60) 382 | Retryable.retryable(options.merge(not: [LitmusTimeoutError])) do 383 | raise LitmusTimeoutError if Time.now > stop 384 | 385 | yield 386 | end 387 | end 388 | end 389 | -------------------------------------------------------------------------------- /lib/puppet_litmus/spec_helper_acceptance.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Helper methods for testing puppet content 4 | module PuppetLitmus 5 | extend PuppetLitmus::InventoryManipulation 6 | 7 | def self.configure! 8 | require 'serverspec' 9 | 10 | RSpec.configure do |config| 11 | config.include PuppetLitmus 12 | config.extend PuppetLitmus 13 | end 14 | 15 | if ENV['TARGET_HOST'].nil? || ENV['TARGET_HOST'] == 'localhost' 16 | puts 'Running tests against this machine !' 17 | if Gem.win_platform? 18 | set :backend, :cmd 19 | else 20 | set :backend, :exec 21 | end 22 | else 23 | # load inventory 24 | inventory_hash = inventory_hash_from_inventory_file 25 | node_config = config_from_node(inventory_hash, ENV.fetch('TARGET_HOST', nil)) 26 | 27 | if target_in_group(inventory_hash, ENV.fetch('TARGET_HOST', nil), 'docker_nodes') 28 | host = ENV.fetch('TARGET_HOST', nil) 29 | set :backend, :dockercli 30 | set :docker_container, host 31 | elsif target_in_group(inventory_hash, ENV.fetch('TARGET_HOST', nil), 'lxd_nodes') 32 | host = ENV.fetch('TARGET_HOST', nil) 33 | set :backend, :lxd 34 | set :login_shell, true 35 | set :lxd_remote, node_config.dig('lxd', 'remote') unless node_config.dig('lxd', 'remote').nil? 36 | set :lxd_instance, host 37 | elsif target_in_group(inventory_hash, ENV.fetch('TARGET_HOST', nil), 'ssh_nodes') 38 | set :backend, :ssh 39 | options = Net::SSH::Config.for(host) 40 | options[:user] = node_config.dig('ssh', 'user') unless node_config.dig('ssh', 'user').nil? 41 | options[:port] = node_config.dig('ssh', 'port') unless node_config.dig('ssh', 'port').nil? 42 | options[:keys] = node_config.dig('ssh', 'private-key') unless node_config.dig('ssh', 'private-key').nil? 43 | options[:password] = node_config.dig('ssh', 'password') unless node_config.dig('ssh', 'password').nil? 44 | run_as = node_config.dig('ssh', 'run-as') unless node_config.dig('ssh', 'run-as').nil? 45 | # Support both net-ssh 4 and 5. 46 | # rubocop:disable Metrics/BlockNesting 47 | options[:verify_host_key] = if node_config.dig('ssh', 'host-key-check').nil? 48 | # Fall back to SSH behavior. This variable will only be set in net-ssh 5.3+. 49 | if @strict_host_key_checking.nil? || @strict_host_key_checking 50 | Net::SSH::Verifiers::Always.new 51 | else 52 | # SSH's behavior with StrictHostKeyChecking=no: adds new keys to known_hosts. 53 | # If known_hosts points to /dev/null, then equivalent to :never where it 54 | # accepts any key beacuse they're all new. 55 | Net::SSH::Verifiers::AcceptNewOrLocalTunnel.new 56 | end 57 | elsif node_config.dig('ssh', 'host-key-check') 58 | if defined?(Net::SSH::Verifiers::Always) 59 | Net::SSH::Verifiers::Always.new 60 | else 61 | Net::SSH::Verifiers::Secure.new 62 | end 63 | elsif defined?(Net::SSH::Verifiers::Never) 64 | Net::SSH::Verifiers::Never.new 65 | else 66 | Net::SSH::Verifiers::Null.new 67 | end 68 | # rubocop:enable Metrics/BlockNesting 69 | host = if ENV['TARGET_HOST'].include?(':') 70 | ENV['TARGET_HOST'].split(':').first 71 | else 72 | ENV['TARGET_HOST'] 73 | end 74 | set :host, options[:host_name] || host 75 | set :ssh_options, options 76 | set :request_pty, false 77 | set :sudo_password, options[:password] if run_as 78 | puts "Running tests as #{run_as}" if run_as 79 | elsif target_in_group(inventory_hash, ENV.fetch('TARGET_HOST', nil), 'winrm_nodes') 80 | require 'winrm' 81 | 82 | set :backend, :winrm 83 | set :os, family: 'windows' 84 | user = node_config.dig('winrm', 'user') unless node_config.dig('winrm', 'user').nil? 85 | pass = node_config.dig('winrm', 'password') unless node_config.dig('winrm', 'password').nil? 86 | endpoint = URI("http://#{ENV.fetch('TARGET_HOST', nil)}") 87 | endpoint.port = 5985 if endpoint.port == 80 88 | endpoint.path = '/wsman' 89 | 90 | opts = { 91 | user:, 92 | password: pass, 93 | endpoint:, 94 | operation_timeout: 300 95 | } 96 | 97 | winrm = WinRM::Connection.new opts 98 | Specinfra.configuration.winrm = winrm 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/puppet_litmus/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Helper methods for testing puppet content 4 | module PuppetLitmus::Util 5 | # Ensure that a passed command is base 64 encoded and passed to PowerShell; this obviates the need to 6 | # carefully interpolate strings for passing to ruby which will then be passed to PowerShell/CMD which will 7 | # then be executed. This also ensures that a single PowerShell command may be specified for Windows targets 8 | # leveraging PowerShell as bolt run_shell will use PowerShell against a remote target but CMD against a 9 | # localhost target. 10 | # 11 | # @param :command [String] A PowerShell script block to be converted to base64 12 | # @return [String] An invocation for PowerShell with the encoded command which may be passed to run_shell 13 | def self.interpolate_powershell(command) 14 | encoded_command = Base64.strict_encode64(command.encode('UTF-16LE')) 15 | "powershell.exe -NoProfile -EncodedCommand #{encoded_command}" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/puppet_litmus/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # version of this gem 4 | module PuppetLitmus 5 | VERSION = '2.0.0' 6 | end 7 | -------------------------------------------------------------------------------- /puppet_litmus.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'puppet_litmus/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'puppet_litmus' 9 | spec.version = PuppetLitmus::VERSION 10 | spec.homepage = 'https://github.com/puppetlabs/puppet_litmus' 11 | spec.license = 'Apache-2.0' 12 | spec.authors = ['Puppet, Inc.'] 13 | spec.email = ['info@puppet.com'] 14 | spec.files = Dir[ 15 | 'README.md', 16 | 'LICENSE', 17 | 'exe/**/*', 18 | 'lib/**/*', 19 | 'spec/**/*', 20 | ] 21 | spec.bindir = 'exe' 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.description = <<-EOF 24 | Providing a simple command line tool for puppet content creators, to enable simple and complex test deployments. 25 | EOF 26 | spec.summary = 'Providing a simple command line tool for puppet content creators, to enable simple and complex test deployments.' 27 | spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0') 28 | spec.add_runtime_dependency 'bolt', '~> 4.0' 29 | spec.add_runtime_dependency 'docker-api', '>= 1.34', '< 3.0.0' 30 | spec.add_runtime_dependency 'parallel' 31 | spec.add_runtime_dependency 'puppet-modulebuilder', '>= 0.3.0' 32 | spec.add_runtime_dependency 'retryable', '~> 3.0' 33 | spec.add_runtime_dependency 'rspec' 34 | spec.add_runtime_dependency 'tty-spinner', ['>= 0.5.0', '< 1.0.0'] 35 | end 36 | -------------------------------------------------------------------------------- /resources/litmus-dark-RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/puppet_litmus/ea7a622b4ac601316682a5e8ea0686769ae77742/resources/litmus-dark-RGB.png -------------------------------------------------------------------------------- /spec/data/doot.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puppetlabs/puppet_litmus/ea7a622b4ac601316682a5e8ea0686769ae77742/spec/data/doot.tar.gz -------------------------------------------------------------------------------- /spec/data/inventory.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | groups: 3 | - name: ssh_nodes 4 | targets: 5 | - uri: test.delivery.puppetlabs.net 6 | config: 7 | transport: ssh 8 | ssh: 9 | user: root 10 | password: Qu@lity! 11 | host-key-check: false 12 | facts: 13 | provisioner: vmpooler 14 | platform: centos-5-x86_64 15 | - name: winrm_nodes 16 | targets: [] 17 | -------------------------------------------------------------------------------- /spec/data/jim.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | groups: 3 | - name: ssh_nodes 4 | targets: 5 | - uri: test.delivery.puppetlabs.net 6 | config: 7 | transport: ssh 8 | ssh: 9 | user: root 10 | password: Qu@lity! 11 | host-key-check: false 12 | facts: 13 | provisioner: vmpooler 14 | platform: centos-5-x86_64 15 | - name: winrm_nodes 16 | nodes: [] 17 | -------------------------------------------------------------------------------- /spec/exe/fake_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-fake_module", 3 | "version": "3.1.0", 4 | "author": "puppetlabs", 5 | "summary": "fake_module is a test", 6 | "license": "Apache-2.0", 7 | "source": "it has no source", 8 | "project_page": "it has no project page", 9 | "issues_url": "https://tickets.puppetlabs.com/browse/MODULES", 10 | "dependencies": [ 11 | { 12 | "name": "puppetlabs/stdlib", 13 | "version_requirement": ">= 4.0.0 < 9.0.0" 14 | } 15 | ], 16 | "operatingsystem_support": [ 17 | { 18 | "operatingsystem": "AmazonLinux", 19 | "operatingsystemrelease": [ 20 | "2", 21 | "2023" 22 | ] 23 | }, 24 | { 25 | "operatingsystem": "CentOS", 26 | "operatingsystemrelease": [ 27 | "6" 28 | ] 29 | }, 30 | { 31 | "operatingsystem": "RedHat", 32 | "operatingsystemrelease": [ 33 | "8", 34 | "9" 35 | ] 36 | }, 37 | { 38 | "operatingsystem": "Ubuntu", 39 | "operatingsystemrelease": [ 40 | "14.04", 41 | "18.04", 42 | "22.04" 43 | ] 44 | } 45 | ], 46 | "requirements": [ 47 | { 48 | "name": "puppet", 49 | "version_requirement": ">= 7.0.0 < 9.0.0" 50 | } 51 | ], 52 | "template-url": "https://github.com/puppetlabs/pdk-templates.git#main", 53 | "template-ref": "tags/2.7.4", 54 | "pdk-version": "2.7.4" 55 | } 56 | -------------------------------------------------------------------------------- /spec/exe/matrix_from_metadata_v2_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'matrix_from_metadata_v2' do 6 | context 'without arguments' do 7 | let(:github_output) { Tempfile.new('github_output') } 8 | let(:github_output_content) { github_output.read } 9 | let(:result) { run_matrix_from_metadata_v2 } 10 | 11 | before do 12 | ENV['GITHUB_OUTPUT'] = github_output.path 13 | end 14 | 15 | it 'run successfully' do 16 | expect(result.status_code).to eq 0 17 | end 18 | 19 | it 'generates the matrix' do # rubocop:disable RSpec/ExampleLength 20 | expect(result.stdout).to include('::warning::Cannot find image for CentOS-6') 21 | expect(result.stdout).to include('::warning::Cannot find image for Ubuntu-14.04') 22 | expect(github_output_content).to include( 23 | [ 24 | 'matrix={', 25 | '"platforms":[', 26 | '{"label":"AmazonLinux-2","provider":"docker","image":"litmusimage/amazonlinux:2"},', 27 | '{"label":"AmazonLinux-2023","provider":"docker","image":"litmusimage/amazonlinux:2023"},', 28 | '{"label":"RedHat-8","provider":"provision_service","image":"rhel-8"},', 29 | '{"label":"RedHat-9","provider":"provision_service","image":"rhel-9"},', 30 | '{"label":"RedHat-9-arm","provider":"provision_service","image":"rhel-9-arm64"},', 31 | '{"label":"Ubuntu-18.04","provider":"docker","image":"litmusimage/ubuntu:18.04"},', 32 | '{"label":"Ubuntu-22.04","provider":"docker","image":"litmusimage/ubuntu:22.04"},', 33 | '{"label":"Ubuntu-22.04-arm","provider":"provision_service","image":"ubuntu-2204-lts-arm64"}', 34 | '],', 35 | '"collection":[', 36 | '"puppetcore7","puppetcore8"', 37 | ']', 38 | '}' 39 | ].join 40 | ) 41 | expect(github_output_content).to include( 42 | 'spec_matrix={"include":[{"puppet_version":"~> 7.24","ruby_version":2.7},{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 43 | ) 44 | expect(result.stdout).to include("Created matrix with 18 cells:\n - Acceptance Test Cells: 16\n - Spec Test Cells: 2") 45 | end 46 | end 47 | 48 | context 'with --exclude-platforms ["ubuntu-18.04"]' do 49 | let(:github_output) { Tempfile.new('github_output') } 50 | let(:github_output_content) { github_output.read } 51 | let(:result) { run_matrix_from_metadata_v2({ '--exclude-platforms' => ['ubuntu-18.04'] }) } 52 | 53 | before do 54 | ENV['GITHUB_OUTPUT'] = github_output.path 55 | end 56 | 57 | it 'run successfully' do 58 | expect(result.status_code).to eq 0 59 | end 60 | 61 | it 'generates the matrix without excluded platforms' do # rubocop:disable RSpec/ExampleLength 62 | expect(result.stdout).to include('::warning::Cannot find image for CentOS-6') 63 | expect(result.stdout).to include('::warning::Cannot find image for Ubuntu-14.04') 64 | expect(result.stdout).to include('::warning::Ubuntu-18.04 was excluded from testing') 65 | expect(github_output_content).to include( 66 | [ 67 | 'matrix={', 68 | '"platforms":[', 69 | '{"label":"AmazonLinux-2","provider":"docker","image":"litmusimage/amazonlinux:2"},', 70 | '{"label":"AmazonLinux-2023","provider":"docker","image":"litmusimage/amazonlinux:2023"},', 71 | '{"label":"RedHat-8","provider":"provision_service","image":"rhel-8"},', 72 | '{"label":"RedHat-9","provider":"provision_service","image":"rhel-9"},', 73 | '{"label":"RedHat-9-arm","provider":"provision_service","image":"rhel-9-arm64"},', 74 | '{"label":"Ubuntu-22.04","provider":"docker","image":"litmusimage/ubuntu:22.04"},', 75 | '{"label":"Ubuntu-22.04-arm","provider":"provision_service","image":"ubuntu-2204-lts-arm64"}', 76 | '],', 77 | '"collection":[', 78 | '"puppetcore7","puppetcore8"', 79 | ']', 80 | '}' 81 | ].join 82 | ) 83 | expect(github_output_content).to include( 84 | 'spec_matrix={"include":[{"puppet_version":"~> 7.24","ruby_version":2.7},{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 85 | ) 86 | expect(result.stdout).to include("Created matrix with 16 cells:\n - Acceptance Test Cells: 14\n - Spec Test Cells: 2") 87 | end 88 | end 89 | 90 | context 'with --exclude-platforms \'["ubuntu-18.04","redhat-8"]\'' do 91 | let(:github_output) { Tempfile.new('github_output') } 92 | let(:github_output_content) { github_output.read } 93 | let(:result) { run_matrix_from_metadata_v2({ '--exclude-platforms' => ['amazonlinux-2', 'amazonlinux-2023', 'ubuntu-18.04', 'ubuntu-22.04', 'redhat-8', 'redhat-9'] }) } 94 | 95 | before do 96 | ENV['GITHUB_OUTPUT'] = github_output.path 97 | end 98 | 99 | it 'run successfully' do 100 | expect(result.status_code).to eq 0 101 | end 102 | 103 | it 'generates the matrix without excluded platforms' do 104 | expect(result.stdout).to include('::warning::Cannot find image for Ubuntu-14.04') 105 | expect(result.stdout).to include('::warning::Cannot find image for CentOS-6') 106 | expect(result.stdout).to include('::warning::Ubuntu-18.04 was excluded from testing') 107 | expect(result.stdout).to include('::warning::Ubuntu-22.04 was excluded from testing') 108 | expect(result.stdout).to include('::warning::RedHat-8 was excluded from testing') 109 | expect(github_output_content).to include( 110 | [ 111 | 'matrix={', 112 | '"platforms":[', 113 | '{"label":"RedHat-9-arm","provider":"provision_service","image":"rhel-9-arm64"},', 114 | '{"label":"Ubuntu-22.04-arm","provider":"provision_service","image":"ubuntu-2204-lts-arm64"}', 115 | '],', 116 | '"collection":[', 117 | '"puppetcore7","puppetcore8"', 118 | ']', 119 | '}' 120 | ].join 121 | ) 122 | expect(github_output_content).to include( 123 | 'spec_matrix={"include":[{"puppet_version":"~> 7.24","ruby_version":2.7},{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 124 | ) 125 | expect(result.stdout).to include("Created matrix with 6 cells:\n - Acceptance Test Cells: 4\n - Spec Test Cells: 2") 126 | end 127 | end 128 | 129 | context 'without arguments and GITHUB_REPOSITORY_OWNER is not puppetlabs' do 130 | let(:github_output) { Tempfile.new('github_output') } 131 | let(:github_output_content) { github_output.read } 132 | let(:result) { run_matrix_from_metadata_v2 } 133 | 134 | before do 135 | ENV['GITHUB_OUTPUT'] = github_output.path 136 | ENV['GITHUB_REPOSITORY_OWNER'] = 'aforkuser' 137 | end 138 | 139 | it 'run successfully' do 140 | expect(result.status_code).to eq 0 141 | end 142 | 143 | it 'generates the matrix' do 144 | expect(result.stdout).to include('::warning::Cannot find image for CentOS-6') 145 | expect(result.stdout).to include('::warning::Cannot find image for Ubuntu-14.04') 146 | expect(github_output_content).to include( 147 | [ 148 | 'matrix={', 149 | '"platforms":[', 150 | '{"label":"AmazonLinux-2","provider":"docker","image":"litmusimage/amazonlinux:2"},', 151 | '{"label":"AmazonLinux-2023","provider":"docker","image":"litmusimage/amazonlinux:2023"},', 152 | '{"label":"Ubuntu-18.04","provider":"docker","image":"litmusimage/ubuntu:18.04"},', 153 | '{"label":"Ubuntu-22.04","provider":"docker","image":"litmusimage/ubuntu:22.04"}', 154 | '],', 155 | '"collection":[', 156 | '"puppetcore7","puppetcore8"', 157 | ']', 158 | '}' 159 | ].join 160 | ) 161 | expect(github_output_content).to include( 162 | 'spec_matrix={"include":[{"puppet_version":"~> 7.24","ruby_version":2.7},{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 163 | ) 164 | expect(result.stdout).to include("Created matrix with 10 cells:\n - Acceptance Test Cells: 8\n - Spec Test Cells: 2") 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/exe/matrix_from_metadata_v3_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe 'matrix_from_metadata_v3' do 6 | let(:github_output) { Tempfile.new('github_output') } 7 | let(:github_output_content) { github_output.read } 8 | let(:github_repository_owner) { nil } 9 | 10 | before do 11 | ENV['GITHUB_ACTIONS'] = '1' 12 | ENV['GITHUB_OUTPUT'] = github_output.path 13 | ENV['GITHUB_REPOSITORY_OWNER'] = github_repository_owner 14 | end 15 | 16 | context 'without arguments' do 17 | let(:result) { run_matrix_from_metadata_v3 } 18 | 19 | it 'run successfully' do 20 | expect(result.status_code).to eq 0 21 | end 22 | 23 | it 'generates the matrix' do 24 | matrix = [ 25 | 'matrix={', 26 | '"platforms":[', 27 | '{"label":"AmazonLinux-2","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2","runner":"ubuntu-22.04"},', 28 | '{"label":"AmazonLinux-2023","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2023","runner":"ubuntu-22.04"},', 29 | '{"label":"Ubuntu-18.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:18.04","runner":"ubuntu-22.04"},', 30 | '{"label":"Ubuntu-22.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:22.04","runner":"ubuntu-latest"}', 31 | '],', 32 | '"collection":[', 33 | '"puppetcore8"', 34 | ']', 35 | '}' 36 | ].join 37 | expect(result.stdout).to include( 38 | '::warning::CentOS-6 no provisioner found', 39 | '::warning::Ubuntu-14.04 no provisioner found', 40 | '::group::matrix', 41 | '::group::spec_matrix' 42 | ) 43 | expect(github_output_content).to include(matrix) 44 | expect(github_output_content).to include('spec_matrix={"include":[{"puppet_version":"~> 8.0","ruby_version":3.2}]}') 45 | end 46 | end 47 | 48 | context 'with puppetlabs GITHUB_REPOSITORY_OWNER' do 49 | let(:result) { run_matrix_from_metadata_v3 } 50 | let(:github_repository_owner) { 'puppetlabs' } 51 | 52 | let(:matrix) do 53 | [ 54 | 'matrix={', 55 | '"platforms":[', 56 | '{"label":"AmazonLinux-2","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2","runner":"ubuntu-22.04"},', 57 | '{"label":"AmazonLinux-2023","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2023","runner":"ubuntu-22.04"},', 58 | '{"label":"RedHat-8","provider":"provision_service","arch":"x86_64","image":"rhel-8","runner":"ubuntu-latest"},', 59 | '{"label":"RedHat-9","provider":"provision_service","arch":"x86_64","image":"rhel-9","runner":"ubuntu-latest"},', 60 | '{"label":"RedHat-9-arm","provider":"provision_service","arch":"arm","image":"rhel-9-arm64","runner":"ubuntu-latest"},', 61 | '{"label":"Ubuntu-18.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:18.04","runner":"ubuntu-22.04"},', 62 | '{"label":"Ubuntu-22.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:22.04","runner":"ubuntu-latest"},', 63 | '{"label":"Ubuntu-22.04-arm","provider":"provision_service","arch":"arm","image":"ubuntu-2204-lts-arm64","runner":"ubuntu-latest"}', 64 | '],', 65 | '"collection":[', 66 | '"puppetcore8"', 67 | ']', 68 | '}' 69 | ].join 70 | end 71 | 72 | it 'run successfully' do 73 | expect(result.status_code).to eq 0 74 | end 75 | 76 | it 'generates the matrix' do 77 | expect(result.stdout).to include( 78 | '::warning::CentOS-6 no provisioner found', 79 | '::warning::Ubuntu-14.04 no provisioner found', 80 | '::group::matrix', 81 | '::group::spec_matrix' 82 | ) 83 | expect(github_output_content).to include(matrix) 84 | expect(github_output_content).to include( 85 | 'spec_matrix={"include":[{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 86 | ) 87 | end 88 | end 89 | 90 | context 'with argument --puppetlabs' do 91 | let(:result) { run_matrix_from_metadata_v3(['--puppetlabs']) } 92 | let(:matrix) do 93 | [ 94 | 'matrix={', 95 | '"platforms":[', 96 | '{"label":"AmazonLinux-2","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2","runner":"ubuntu-22.04"},', 97 | '{"label":"AmazonLinux-2023","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2023","runner":"ubuntu-22.04"},', 98 | '{"label":"RedHat-8","provider":"provision_service","arch":"x86_64","image":"rhel-8","runner":"ubuntu-latest"},', 99 | '{"label":"RedHat-9","provider":"provision_service","arch":"x86_64","image":"rhel-9","runner":"ubuntu-latest"},', 100 | '{"label":"RedHat-9-arm","provider":"provision_service","arch":"arm","image":"rhel-9-arm64","runner":"ubuntu-latest"},', 101 | '{"label":"Ubuntu-18.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:18.04","runner":"ubuntu-22.04"},', 102 | '{"label":"Ubuntu-22.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:22.04","runner":"ubuntu-latest"},', 103 | '{"label":"Ubuntu-22.04-arm","provider":"provision_service","arch":"arm","image":"ubuntu-2204-lts-arm64","runner":"ubuntu-latest"}', 104 | '],', 105 | '"collection":[', 106 | '"puppetcore8"', 107 | ']', 108 | '}' 109 | ].join 110 | end 111 | 112 | it 'run successfully' do 113 | expect(result.status_code).to eq 0 114 | end 115 | 116 | it 'generates the matrix' do 117 | expect(result.stdout).to include( 118 | '::warning::CentOS-6 no provisioner found', 119 | '::warning::Ubuntu-14.04 no provisioner found', 120 | '::group::matrix', 121 | '::group::spec_matrix' 122 | ) 123 | expect(github_output_content).to include(matrix) 124 | expect(github_output_content).to include( 125 | 'spec_matrix={"include":[{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 126 | ) 127 | end 128 | end 129 | 130 | context 'with --exclude-platforms "ubuntu-18.04"' do 131 | let(:result) { run_matrix_from_metadata_v3(['--puppetlabs', '--platform-exclude', 'ubuntu-18.04']) } 132 | let(:matrix) do 133 | [ 134 | 'matrix={', 135 | '"platforms":[', 136 | '{"label":"AmazonLinux-2","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2","runner":"ubuntu-22.04"},', 137 | '{"label":"AmazonLinux-2023","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2023","runner":"ubuntu-22.04"},', 138 | '{"label":"RedHat-8","provider":"provision_service","arch":"x86_64","image":"rhel-8","runner":"ubuntu-latest"},', 139 | '{"label":"RedHat-9","provider":"provision_service","arch":"x86_64","image":"rhel-9","runner":"ubuntu-latest"},', 140 | '{"label":"RedHat-9-arm","provider":"provision_service","arch":"arm","image":"rhel-9-arm64","runner":"ubuntu-latest"},', 141 | '{"label":"Ubuntu-22.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:22.04","runner":"ubuntu-latest"},', 142 | '{"label":"Ubuntu-22.04-arm","provider":"provision_service","arch":"arm","image":"ubuntu-2204-lts-arm64","runner":"ubuntu-latest"}', 143 | '],', 144 | '"collection":[', 145 | '"puppetcore8"', 146 | ']', 147 | '}' 148 | ].join 149 | end 150 | 151 | it 'run successfully' do 152 | expect(result.status_code).to eq 0 153 | end 154 | 155 | it 'generates the matrix without excluded platforms' do 156 | expect(result.stdout).to include( 157 | '::warning::CentOS-6 no provisioner found', 158 | '::warning::Ubuntu-14.04 no provisioner found', 159 | '::notice::platform-exclude filtered Ubuntu-18.04', 160 | '::group::matrix', 161 | '::group::spec_matrix' 162 | ) 163 | expect(github_output_content).to include(matrix) 164 | expect(github_output_content).to include( 165 | 'spec_matrix={"include":[{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 166 | ) 167 | end 168 | end 169 | 170 | context 'with --platform-exclude "ubuntu-(18.04|22.04)" --platform-exclude "redhat-[89]"' do 171 | let(:result) { run_matrix_from_metadata_v3(['--puppetlabs', '--platform-exclude', '(amazonlinux|ubuntu)-(2|18.04|22.04|2023)', '--platform-exclude', 'redhat-[89]']) } 172 | let(:matrix) do 173 | [ 174 | 'matrix={', 175 | '"platforms":[', 176 | '],', 177 | '"collection":[', 178 | '"puppetcore8"', 179 | ']', 180 | '}' 181 | ].join 182 | end 183 | 184 | it 'run successfully' do 185 | expect(result.status_code).to eq 0 186 | end 187 | 188 | it 'generates the matrix without excluded platforms' do 189 | expect(result.stdout).to include( 190 | '::warning::CentOS-6 no provisioner found', 191 | '::warning::Ubuntu-14.04 no provisioner found', 192 | '::notice::platform-exclude filtered RedHat-8', 193 | '::notice::platform-exclude filtered RedHat-9', 194 | '::notice::platform-exclude filtered Ubuntu-18.04', 195 | '::notice::platform-exclude filtered Ubuntu-22.04', 196 | '::group::matrix', 197 | '::group::spec_matrix' 198 | ) 199 | expect(github_output_content).to include(matrix) 200 | expect(github_output_content).to include( 201 | 'spec_matrix={"include":[{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 202 | ) 203 | end 204 | end 205 | 206 | context 'with --pe-include' do 207 | let(:result) { run_matrix_from_metadata_v3(['--puppetlabs', '--pe-include']) } 208 | let(:matrix) do 209 | [ 210 | 'matrix={', 211 | '"platforms":[', 212 | '{"label":"AmazonLinux-2","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2","runner":"ubuntu-22.04"},', 213 | '{"label":"AmazonLinux-2023","provider":"docker","arch":"x86_64","image":"litmusimage/amazonlinux:2023","runner":"ubuntu-20.04"},', 214 | '{"label":"Ubuntu-18.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:18.04","runner":"ubuntu-22.04"},', 215 | '{"label":"Ubuntu-22.04","provider":"docker","arch":"x86_64","image":"litmusimage/ubuntu:22.04","runner":"ubuntu-latest"}', 216 | '],', 217 | '"collection":[', 218 | '"puppetcore8"', 219 | ']', 220 | '}' 221 | ].join 222 | end 223 | 224 | it 'run successfully' do 225 | expect(result.status_code).to eq 0 226 | end 227 | 228 | it 'generates the matrix with PE LTS versions' do 229 | expect(result.stdout).to include( 230 | '::warning::CentOS-6 no provisioner found', 231 | '::warning::Ubuntu-14.04 no provisioner found', 232 | '::group::matrix', 233 | '::group::spec_matrix' 234 | ) 235 | expect(github_output_content).to include( 236 | '"collection":["2023.8.3-puppet_enterprise","2021.7.9-puppet_enterprise","puppetcore8"' 237 | ) 238 | expect(github_output_content).to include( 239 | 'spec_matrix={"include":[{"puppet_version":"~> 8.0","ruby_version":3.2}]}' 240 | ) 241 | end 242 | end 243 | end 244 | -------------------------------------------------------------------------------- /spec/lib/puppet_litmus/inventory_manipulation_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require 'support/inventory' 5 | 6 | RSpec.describe PuppetLitmus::InventoryManipulation do 7 | let(:inventory_full_path) { 'spec/data/inventory.yaml' } 8 | 9 | context 'with config_from_node' do 10 | it 'no matching node, returns nil' do 11 | expect(config_from_node(config_hash, 'not.here')).to be_nil 12 | end 13 | 14 | it 'no config section, returns nil' do 15 | expect(config_from_node(no_config_hash, 'test.delivery.puppetlabs.net')).to be_nil 16 | end 17 | 18 | it 'config exists, and returns' do 19 | expect(config_from_node(config_hash, 'test.delivery.puppetlabs.net')).to eq('transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false }) 20 | end 21 | 22 | it 'facts exists, and returns' do 23 | expect(facts_from_node(config_hash, 'test.delivery.puppetlabs.net')).to eq('provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64') 24 | end 25 | 26 | it 'vars exists, and returns' do 27 | expect(vars_from_node(config_hash, 'test.delivery.puppetlabs.net')).to eq('role' => 'agent') 28 | end 29 | 30 | it 'no feature exists for the group, and returns hash with feature added' do 31 | expect(add_feature_to_group(no_feature_hash, 'puppet-agent', 'ssh_nodes')).to eq('groups' => [{ 'features' => ['puppet-agent'], 'name' => 'ssh_nodes', 'targets' => [{ 'config' => { 'ssh' => { 'host-key-check' => false, 'password' => 'Qu@lity!', 'user' => 'root' }, 'transport' => 'ssh' }, 'facts' => { 'platform' => 'centos-5-x86_64', 'provisioner' => 'vmpooler' }, 'uri' => 'test.delivery.puppetlabs.net' }] }, { 'name' => 'winrm_nodes', 'targets' => [] }]) # rubocop:disable Layout/LineLength: Line is too long 32 | end 33 | 34 | it 'feature exists for the group, and returns hash with feature removed' do 35 | expect(remove_feature_from_group(feature_hash_group, 'puppet-agent', 'ssh_nodes')).to eq('groups' => [{ 'features' => [], 'name' => 'ssh_nodes', 'targets' => [{ 'config' => { 'ssh' => { 'host-key-check' => false, 'password' => 'Qu@lity!', 'user' => 'root' }, 'transport' => 'ssh' }, 'facts' => { 'platform' => 'centos-5-x86_64', 'provisioner' => 'vmpooler' }, 'uri' => 'test.delivery.puppetlabs.net' }] }, { 'name' => 'winrm_nodes', 'targets' => [] }]) # rubocop:disable Layout/LineLength: Line is too long 36 | end 37 | 38 | it 'write from inventory_hash to inventory_yaml file feature_hash_group' do 39 | expect { write_to_inventory_file(feature_hash_group, inventory_full_path) }.not_to raise_error 40 | end 41 | 42 | it 'empty feature exists for the group, and returns hash with feature added' do 43 | expect(add_feature_to_group(empty_feature_hash_group, 'puppet-agent', 'ssh_nodes')).to eq('groups' => [{ 'features' => ['puppet-agent'], 'name' => 'ssh_nodes', 'targets' => [{ 'config' => { 'ssh' => { 'host-key-check' => false, 'password' => 'Qu@lity!', 'user' => 'root' }, 'transport' => 'ssh' }, 'facts' => { 'platform' => 'centos-5-x86_64', 'provisioner' => 'vmpooler' }, 'uri' => 'test.delivery.puppetlabs.net' }] }, { 'name' => 'winrm_nodes', 'targets' => [] }]) # rubocop:disable Layout/LineLength: Line is too long 44 | end 45 | 46 | it 'no feature exists for the node, and returns hash with feature added' do 47 | expect(add_feature_to_node(no_feature_hash, 'puppet-agent', 'test.delivery.puppetlabs.net')).to eq('groups' => [{ 'name' => 'ssh_nodes', 'targets' => [{ 'config' => { 'ssh' => { 'host-key-check' => false, 'password' => 'Qu@lity!', 'user' => 'root' }, 'transport' => 'ssh' }, 'facts' => { 'platform' => 'centos-5-x86_64', 'provisioner' => 'vmpooler' }, 'uri' => 'test.delivery.puppetlabs.net', 'features' => ['puppet-agent'] }] }, { 'name' => 'winrm_nodes', 'targets' => [] }]) # rubocop:disable Layout/LineLength: Line is too long 48 | end 49 | 50 | it 'feature exists for the node, and returns hash with feature removed' do 51 | expect(remove_feature_from_node(feature_hash_node, 'puppet-agent', 'test.delivery.puppetlabs.net')).to eq('groups' => [{ 'name' => 'ssh_nodes', 'targets' => [{ 'config' => { 'ssh' => { 'host-key-check' => false, 'password' => 'Qu@lity!', 'user' => 'root' }, 'transport' => 'ssh' }, 'facts' => { 'platform' => 'centos-5-x86_64', 'provisioner' => 'vmpooler' }, 'uri' => 'test.delivery.puppetlabs.net', 'features' => [] }] }, { 'name' => 'winrm_nodes', 'targets' => [] }]) # rubocop:disable Layout/LineLength: Line is too long 52 | end 53 | 54 | it 'write from inventory_hash to inventory_yaml file feature_hash_node' do 55 | expect { write_to_inventory_file(feature_hash_node, inventory_full_path) }.not_to raise_error 56 | end 57 | 58 | it 'empty feature exists for the node, and returns hash with feature added' do 59 | expect(add_feature_to_node(empty_feature_hash_node, 'puppet-agent', 'test.delivery.puppetlabs.net')).to eq('groups' => [{ 'name' => 'ssh_nodes', 'targets' => [{ 'config' => { 'ssh' => { 'host-key-check' => false, 'password' => 'Qu@lity!', 'user' => 'root' }, 'transport' => 'ssh' }, 'facts' => { 'platform' => 'centos-5-x86_64', 'provisioner' => 'vmpooler' }, 'uri' => 'test.delivery.puppetlabs.net', 'features' => ['puppet-agent'] }] }, { 'name' => 'winrm_nodes', 'targets' => [] }]) # rubocop:disable Layout/LineLength: Line is too long 60 | end 61 | 62 | it 'write from inventory_hash to inventory_yaml file no feature_hash' do 63 | expect(File).to exist(inventory_full_path) 64 | expect { write_to_inventory_file(no_feature_hash, inventory_full_path) }.not_to raise_error 65 | end 66 | 67 | it 'group does not exist in inventory, and returns hash with group added' do 68 | expect(add_node_to_group(no_docker_hash, foo_node, 'docker_nodes')).to eq('groups' => 69 | [{ 'name' => 'ssh_nodes', 'targets' => [] }, { 'name' => 'winrm_nodes', 'targets' => [] }, { 'name' => 'docker_nodes', 'targets' => [foo_node] }]) 70 | end 71 | 72 | it 'group exists in inventory, and returns hash with node added' do 73 | expect(add_node_to_group(no_docker_hash, foo_node, 'ssh_nodes')).to eq('groups' => 74 | [{ 'name' => 'ssh_nodes', 'targets' => [foo_node] }, { 'name' => 'winrm_nodes', 'targets' => [] }]) 75 | end 76 | end 77 | 78 | context 'with target searching' do 79 | it 'gets correct groups names from an inventory' do 80 | expect(groups_in_inventory(complex_inventory)).to eql(%w[ssh_nodes frontend winrm_nodes]) 81 | end 82 | 83 | it 'applies a code block to groups' do 84 | counts = groups_in_inventory(complex_inventory) do |group| 85 | group['targets'].count if group.key? 'targets' 86 | end 87 | expect(counts.sum).to be 4 88 | end 89 | 90 | it 'gets names of targets' do 91 | target_list = ['test.delivery.puppetlabs.net', 'test2.delivery.puppetlabs.net', 'test3.delivery.puppetlabs.net', 'test4.delivery.puppetlabs.net'] 92 | expect(targets_in_inventory(complex_inventory)).to eql target_list 93 | end 94 | 95 | it 'applies a code block to targets' do 96 | target_list = targets_in_inventory(complex_inventory) do |target| 97 | next unless target['config']['transport'] == 'winrm' 98 | 99 | target['uri'] 100 | end 101 | 102 | expect(target_list).to eql ['test4.delivery.puppetlabs.net'] 103 | end 104 | 105 | it 'returns agent nodes' do 106 | node_list = nodes_with_role('agent', complex_inventory) 107 | expected_node_list = ['test.delivery.puppetlabs.net', 'test3.delivery.puppetlabs.net', 'test4.delivery.puppetlabs.net'] 108 | expect(node_list).to eql expected_node_list 109 | end 110 | 111 | it 'returns agent nodes with different capitolization' do 112 | node_list = nodes_with_role('Agent', complex_inventory) 113 | expected_node_list = ['test.delivery.puppetlabs.net', 'test3.delivery.puppetlabs.net', 'test4.delivery.puppetlabs.net'] 114 | expect(node_list).to eql expected_node_list 115 | end 116 | 117 | it 'searches for a group' do 118 | expect(search_for_target('winrm_nodes', complex_inventory)).to eql ['winrm_nodes'] 119 | end 120 | 121 | it 'seaches for an array of groups' do 122 | expect(search_for_target(%w[winrm_nodes ssh_nodes], complex_inventory)).to eql %w[winrm_nodes ssh_nodes] 123 | end 124 | 125 | it 'searches for a specific target' do 126 | expect(search_for_target('test.delivery.puppetlabs.net', complex_inventory)).to eql ['test.delivery.puppetlabs.net'] 127 | end 128 | 129 | it 'searches for an array of roles' do 130 | expect(search_for_target(%w[iis nginx], complex_inventory)).to eql ['test4.delivery.puppetlabs.net', 'test3.delivery.puppetlabs.net'] 131 | end 132 | 133 | it 'searches for roles as symbols' do 134 | expect(search_for_target(%i[iis nginx], complex_inventory)).to eql ['test4.delivery.puppetlabs.net', 'test3.delivery.puppetlabs.net'] 135 | end 136 | 137 | it 'raises an error if target not found' do 138 | expect { search_for_target(:blah, complex_inventory) }.to raise_error 'targets not found in inventory' 139 | end 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /spec/lib/puppet_litmus/puppet_litmus_version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.describe PuppetLitmus do 6 | it 'has a version number' do 7 | expect(described_class::VERSION).not_to be_nil 8 | expect(described_class::VERSION).to be_a(String) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/lib/puppet_litmus/rake_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | RSpec.shared_examples 'supported provisioner' do |args| 6 | let(:provisioner) { args[:provisioner] } 7 | let(:platform) { args[:platform] } 8 | let(:inventory_vars) { args[:inventory_vars] } 9 | let(:provision_hash) { args[:provision_hash] } 10 | let(:results) { args[:results] } 11 | let(:params) { args[:params] } 12 | 13 | it 'calls function' do 14 | allow(File).to receive(:directory?).and_call_original 15 | allow(File).to receive(:directory?) 16 | .with(File.join(described_class::DEFAULT_CONFIG_DATA['modulepath'], 'provision')) 17 | .and_return(true) 18 | allow_any_instance_of(BoltSpec::Run).to receive(:run_task) 19 | .with("provision::#{provisioner}", 'localhost', params, config: described_class::DEFAULT_CONFIG_DATA, inventory: nil) 20 | .and_return(results) 21 | result = provision(provisioner, platform, inventory_vars) 22 | expect(result).to eq(results) 23 | end 24 | end 25 | 26 | RSpec.describe PuppetLitmus::RakeHelper do 27 | inventory_file = File.join(Dir.pwd, 'spec', 'fixtures', 'litmus_inventory.yaml') 28 | let(:inventory_file) { inventory_file } 29 | 30 | context 'with provision_list' do 31 | let(:provision_hash) { { 'default' => { 'provisioner' => 'docker', 'images' => ['waffleimage/centos7'] } } } 32 | let(:results) { [] } 33 | 34 | it 'calls function' do 35 | expect(self).to receive(:provision).with('docker', 'waffleimage/centos7', nil).and_return(results) 36 | provision_list(provision_hash, 'default') 37 | end 38 | end 39 | 40 | context 'with provision' do 41 | examples = [ 42 | { 43 | provisioner: 'docker', 44 | platform: 'waffleimage/centos7', 45 | inventory_vars: nil, 46 | provision_hash: { 'default' => { 'provisioner' => 'docker', 'images' => ['waffleimage/centos7'] } }, 47 | results: [], 48 | params: { 'action' => 'provision', 'platform' => 'waffleimage/centos7', 'inventory' => inventory_file } 49 | }, 50 | { 51 | provisioner: 'vagrant', 52 | platform: 'centos7', 53 | inventory_vars: nil, 54 | provision_hash: { 'default' => { 'provisioner' => 'vagrant', 'images' => ['centos7'] } }, 55 | results: [], 56 | params: { 'action' => 'provision', 'platform' => 'centos7', 'inventory' => inventory_file } 57 | }, 58 | { 59 | provisioner: 'lxd', 60 | platform: 'images:centos/7', 61 | inventory_vars: nil, 62 | provision_hash: { 'default' => { 'provisioner' => 'lxd', 'images' => ['images:centos/7'] } }, 63 | results: [], 64 | params: { 'action' => 'provision', 'platform' => 'images:centos/7', 'inventory' => inventory_file } 65 | } 66 | ].freeze 67 | 68 | examples.each do |e| 69 | describe e[:provisioner] do 70 | it_behaves_like 'supported provisioner', e 71 | end 72 | end 73 | end 74 | 75 | context 'with tear_down' do 76 | let(:inventory_hash) do 77 | { 'groups' => 78 | [{ 'name' => 'ssh_nodes', 'targets' => 79 | [{ 'uri' => 'some.host', 'facts' => { 'provisioner' => 'docker', 'container_name' => 'foo', 'platform' => 'some.host' } }] }] } 80 | end 81 | let(:targets) { ['some.host'] } 82 | let(:params) { { 'action' => 'tear_down', 'node_name' => 'some.host', 'inventory' => inventory_file } } 83 | 84 | it 'calls function' do 85 | allow(File).to receive(:directory?).with(File.join(described_class::DEFAULT_CONFIG_DATA['modulepath'], 'provision')).and_return(true) 86 | allow_any_instance_of(BoltSpec::Run).to receive(:run_task).with('provision::docker', 'localhost', params, config: described_class::DEFAULT_CONFIG_DATA, inventory: nil).and_return([]) 87 | tear_down_nodes(targets, inventory_hash) 88 | end 89 | end 90 | 91 | context 'with bulk tear_down' do 92 | let(:inventory_hash) do 93 | { 'groups' => 94 | [{ 'name' => 'ssh_nodes', 'targets' => 95 | [ 96 | { 'uri' => 'one.host', 'facts' => { 'provisioner' => 'abs', 'platform' => 'ubuntu-1604-x86_64', 'job_id' => 'iac-task-pid-21648' } }, 97 | { 'uri' => 'two.host', 'facts' => { 'provisioner' => 'abs', 'platform' => 'ubuntu-1804-x86_64', 'job_id' => 'iac-task-pid-21648' } }, 98 | { 'uri' => 'three.host', 'facts' => { 'provisioner' => 'abs', 'platform' => 'ubuntu-2004-x86_64', 'job_id' => 'iac-task-pid-21648' } }, 99 | { 'uri' => 'four.host', 'facts' => { 'provisioner' => 'abs', 'platform' => 'ubuntu-2004-x86_64', 'job_id' => 'iac-task-pid-21649' } } 100 | ] }] } 101 | end 102 | let(:targets) { ['one.host'] } 103 | let(:params) { { 'action' => 'tear_down', 'node_name' => 'one.host', 'inventory' => inventory_file } } 104 | 105 | it 'calls function' do 106 | allow(File).to receive(:directory?).with(File.join(described_class::DEFAULT_CONFIG_DATA['modulepath'], 'provision')).and_return(true) 107 | allow_any_instance_of(BoltSpec::Run).to receive(:run_task).with('provision::abs', 'localhost', params, config: described_class::DEFAULT_CONFIG_DATA, inventory: nil).and_return( 108 | [{ 'target' => 'localhost', 109 | 'action' => 'task', 110 | 'object' => 'provision::abs', 111 | 'status' => 'success', 112 | 'value' => 113 | { 'status' => 'ok', 114 | 'removed' => 115 | ['one.host', 116 | 'two.host', 117 | 'three.host'] } }] 118 | ) 119 | results = tear_down_nodes(targets, inventory_hash) 120 | expect(results.keys).to eq(['one.host', 'two.host', 'three.host']) 121 | results.each_value do |value| 122 | expect(value[0]['value']).to eq({ 'status' => 'ok' }) 123 | end 124 | end 125 | end 126 | 127 | context 'with install_agent' do 128 | let(:inventory_hash) do 129 | { 'groups' => 130 | [{ 'name' => 'ssh_nodes', 'targets' => 131 | [{ 'uri' => 'some.host', 'facts' => { 'provisioner' => 'docker', 'container_name' => 'foo', 'platform' => 'some.host' } }] }] } 132 | end 133 | let(:targets) { ['some.host'] } 134 | let(:token) { 'some_token' } 135 | let(:params) { { 'collection' => 'puppet6', 'password' => token } } 136 | 137 | it 'calls function' do 138 | allow(ENV).to receive(:fetch).with('PUPPET_VERSION', nil).and_return(nil) 139 | allow(ENV).to receive(:fetch).with('PUPPET_FORGE_TOKEN', nil).and_return(token) 140 | allow(File).to receive(:directory?).with(File.join(described_class::DEFAULT_CONFIG_DATA['modulepath'], 'puppet_agent')).and_return(true) 141 | allow_any_instance_of(BoltSpec::Run).to receive(:run_task).with('puppet_agent::install', targets, params, config: described_class::DEFAULT_CONFIG_DATA, inventory: inventory_hash).and_return([]) 142 | install_agent('puppet6', targets, inventory_hash) 143 | end 144 | 145 | it 'adds puppet version' do 146 | params = { 'collection' => 'puppet7', 'version' => '7.35.0' } 147 | allow(ENV).to receive(:fetch).with('PUPPET_VERSION', nil).and_return('7.35.0') 148 | allow(ENV).to receive(:fetch).with('PUPPET_FORGE_TOKEN', nil).and_return(nil) 149 | allow(File).to receive(:directory?).with(File.join(described_class::DEFAULT_CONFIG_DATA['modulepath'], 'puppet_agent')).and_return(true) 150 | allow_any_instance_of(BoltSpec::Run).to receive(:run_task).with('puppet_agent::install', targets, params, config: described_class::DEFAULT_CONFIG_DATA, inventory: inventory_hash).and_return([]) 151 | install_agent('puppet7', targets, inventory_hash) 152 | end 153 | 154 | it 'fails for puppetcore if no token supplied' do 155 | params = { 'collection' => 'puppetcore7' } 156 | allow(ENV).to receive(:fetch).with('PUPPET_VERSION', nil).and_return(nil) 157 | allow(ENV).to receive(:fetch).with('PUPPET_FORGE_TOKEN', nil).and_return(nil) 158 | allow(File).to receive(:directory?).with(File.join(described_class::DEFAULT_CONFIG_DATA['modulepath'], 'puppet_agent')).and_return(true) 159 | allow_any_instance_of(BoltSpec::Run).to receive(:run_task).with('puppet_agent::install', targets, params, config: described_class::DEFAULT_CONFIG_DATA, inventory: inventory_hash).and_return([]) 160 | expect { install_agent('puppetcore7', targets, inventory_hash) }.to raise_error(RuntimeError, /puppetcore agent installs require a valid PUPPET_FORGE_TOKEN set in the env\./) 161 | end 162 | end 163 | 164 | context 'with install_module' do 165 | let(:inventory_hash) do 166 | { 'version' => 2, 167 | 'groups' => 168 | [{ 'name' => 'ssh_nodes', 'targets' => 169 | [{ 'uri' => 'some.host', 'facts' => { 'provisioner' => 'docker', 'container_name' => 'foo', 'platform' => 'some.host' } }] }] } 170 | end 171 | let(:module_tar) { 'foo.tar.gz' } 172 | let(:targets) { ['some.host'] } 173 | let(:uninstall_module_command) { 'puppet module uninstall foo --force' } 174 | let(:install_module_command) { "puppet module install --module_repository 'https://forgeapi.example.com' #{module_tar}" } 175 | 176 | it 'calls function' do 177 | allow_any_instance_of(BoltSpec::Run).to receive(:upload_file).with(module_tar, module_tar, targets, options: {}, config: nil, inventory: inventory_hash).and_return([]) 178 | allow(File).to receive(:exist?).with(File.join(Dir.pwd, 'metadata.json')).and_return(true) 179 | allow(File).to receive(:read).with(File.join(Dir.pwd, 'metadata.json')).and_return(JSON.dump({ name: 'foo' })) 180 | allow(Open3).to receive(:capture3).with("bundle exec bolt file upload \"#{module_tar}\" #{File.basename(module_tar)} --targets all --inventoryfile inventory.yaml") 181 | .and_return(['success', '', 0]) 182 | allow_any_instance_of(BoltSpec::Run).to receive(:run_command).with(uninstall_module_command, targets, config: nil, inventory: inventory_hash).and_return([]) 183 | allow_any_instance_of(BoltSpec::Run).to receive(:run_command).with(install_module_command, targets, config: nil, inventory: inventory_hash).and_return([]) 184 | install_module(inventory_hash, nil, module_tar, 'https://forgeapi.example.com') 185 | end 186 | end 187 | 188 | context 'with check_connectivity' do 189 | let(:inventory_hash) do 190 | { 'groups' => 191 | [{ 'name' => 'ssh_nodes', 'targets' => 192 | [{ 'uri' => 'some.host', 'facts' => { 'provisioner' => 'docker', 'container_name' => 'foo', 'platform' => 'some.host' } }] }] } 193 | end 194 | let(:targets) { ['some.host'] } 195 | let(:command) { 'cd .' } 196 | 197 | it 'node available' do 198 | allow(Open3).to receive(:capture3).with('cd .').and_return(['success', '', 0]) 199 | allow_any_instance_of(BoltSpec::Run).to receive(:run_command).with(command, targets, config: nil, inventory: inventory_hash).and_return([{ 'target' => 'some.host', 'status' => 'success' }]) 200 | check_connectivity?(inventory_hash, 'some.host') 201 | end 202 | 203 | it 'node unavailable' do 204 | allow_any_instance_of(BoltSpec::Run).to receive(:run_command).with(command, targets, config: nil, inventory: inventory_hash).and_return([{ 'target' => 'some.host', 'status' => 'failure' }]) 205 | expect { check_connectivity?(inventory_hash, 'some.host') }.to raise_error(RuntimeError, /Connectivity has failed on:/) 206 | end 207 | end 208 | 209 | context 'with uninstall module' do 210 | let(:inventory_hash) do 211 | { 'groups' => 212 | [{ 'name' => 'ssh_nodes', 'targets' => 213 | [{ 'uri' => 'some.host', 'facts' => { 'provisioner' => 'docker', 'container_name' => 'foo', 'platform' => 'some.host' } }] }] } 214 | end 215 | let(:targets) { ['some.host'] } 216 | let(:uninstall_module_command) { 'puppet module uninstall foo-bar' } 217 | 218 | it 'uninstalls module' do 219 | allow_any_instance_of(BoltSpec::Run).to receive(:run_command).with(uninstall_module_command, targets, config: nil, inventory: inventory_hash).and_return([]) 220 | expect(self).to receive(:metadata_module_name).and_return('foo-bar') 221 | uninstall_module(inventory_hash, nil) 222 | end 223 | 224 | it 'and custom name' do 225 | allow_any_instance_of(BoltSpec::Run).to receive(:run_command).with(uninstall_module_command, targets, config: nil, inventory: inventory_hash).and_return([]) 226 | uninstall_module(inventory_hash, nil, 'foo-bar') 227 | end 228 | end 229 | 230 | context 'with module name' do 231 | let(:metadata) { '{ "name" : "foo-bar" }' } 232 | 233 | it 'reads module name' do 234 | allow(File).to receive(:exist?).with(File.join(Dir.pwd, 'metadata.json')).and_return(true) 235 | allow(File).to receive(:read).with(File.join(Dir.pwd, 'metadata.json')).and_return(metadata) 236 | name = metadata_module_name 237 | expect(name).to eq('foo-bar') 238 | end 239 | end 240 | 241 | context 'with provisioner_task' do 242 | described_class::SUPPORTED_PROVISIONERS.each do |supported_provisioner| 243 | it "returns supported provisioner task name for #{supported_provisioner}" do 244 | expect(provisioner_task(supported_provisioner)).to eq("provision::#{supported_provisioner}") 245 | end 246 | end 247 | 248 | it 'returns an unsupported provisioner name' do 249 | expect(provisioner_task('my_org::custom_provisioner')).to eql('my_org::custom_provisioner') 250 | end 251 | end 252 | end 253 | -------------------------------------------------------------------------------- /spec/lib/puppet_litmus/rake_tasks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe 'litmus rake tasks' do 6 | before(:all) do # rubocop:disable RSpec/BeforeAfterAll 7 | load File.expand_path('../../../lib/puppet_litmus/rake_tasks.rb', __dir__) 8 | # the spec_prep task is stubbed, rather than load from another gem. 9 | Rake::Task.define_task(:spec_prep) 10 | end 11 | 12 | context 'with litmus:metadata task' do 13 | it 'happy path' do 14 | metadata = { 'name' => 'puppetlabs-postgresql', 15 | 'version' => '6.0.0', 16 | 'operatingsystem_support' => 17 | [{ 'operatingsystem' => 'RedHat', 'operatingsystemrelease' => ['5'] }, 18 | { 'operatingsystem' => 'Ubuntu', 'operatingsystemrelease' => ['14.04', '18.04'] }], 19 | 'template-ref' => 'heads/main-0-g7827fc2' } 20 | expect(File).to receive(:read).with(any_args).once 21 | expect(JSON).to receive(:parse).with(any_args).and_return(metadata) 22 | expect($stdout).to receive(:puts).with('redhat-5-x86_64') 23 | expect($stdout).to receive(:puts).with('ubuntu-1404-x86_64') 24 | expect($stdout).to receive(:puts).with('ubuntu-1804-x86_64') 25 | allow_any_instance_of(Object).to receive(:exit) 26 | Rake::Task['litmus:metadata'].invoke 27 | end 28 | end 29 | 30 | context 'with litmus:install_module' do 31 | let(:args) { { target_node_name: nil, module_repository: nil } } 32 | let(:inventory_hash) { { 'groups' => [{ 'name' => 'ssh_nodes', 'nodes' => [{ 'uri' => 'some.host' }, { 'uri' => 'some.otherhost' }] }] } } 33 | let(:target_nodes) { ['some.host', 'some.otherhost'] } 34 | let(:dummy_tar) { 'spec/data/doot.tar.gz' } 35 | 36 | before do 37 | Rake::Task['litmus:install_module'].reenable 38 | allow_any_instance_of(PuppetLitmus::InventoryManipulation).to receive(:inventory_hash_from_inventory_file).and_return(inventory_hash) 39 | allow_any_instance_of(PuppetLitmus::InventoryManipulation).to receive(:find_targets).with(inventory_hash, args[:target_node_name]).and_return(target_nodes) 40 | end 41 | 42 | it 'installs module' do 43 | expect_any_instance_of(Object).to receive(:build_module).and_return(dummy_tar) 44 | expect($stdout).to receive(:puts).with("Built '#{dummy_tar}'") 45 | 46 | expect_any_instance_of(Object).to receive(:install_module).with(inventory_hash, target_nodes, dummy_tar, args[:module_repository]) 47 | expect($stdout).to receive(:puts).with("Installed '#{dummy_tar}' on #{target_nodes.join(', ')}") 48 | allow_any_instance_of(Object).to receive(:exit) 49 | Rake::Task['litmus:install_module'].invoke(*args.values) 50 | end 51 | 52 | context 'with unknown target' do 53 | let(:args) { { target_node_name: 'un.known', module_repository: nil } } 54 | let(:target_nodes) { [] } 55 | 56 | it 'exits with No targets found' do 57 | expect do 58 | expect($stdout).to receive(:puts).with('No targets found') 59 | Rake::Task['litmus:install_module'].invoke(*args.values) 60 | end.to raise_error(SystemExit) { |error| 61 | expect(error.status).to eq(0) 62 | } 63 | end 64 | end 65 | 66 | context 'when build_module returns nil' do 67 | let(:dummy_tar) { nil } 68 | 69 | it 'raises error if build fails' do 70 | expect_any_instance_of(Object).to receive(:build_module).and_return(dummy_tar) 71 | expect($stdout).to receive(:puts).with("Built '#{dummy_tar}'") 72 | 73 | expect { Rake::Task['litmus:install_module'].invoke(*args.values) } 74 | .to raise_error(RuntimeError, "Unable to find package in 'pkg/*.tar.gz'") 75 | end 76 | end 77 | end 78 | 79 | context 'with litmus:install_modules_from_directory' do 80 | let(:inventory_hash) { { 'groups' => [{ 'name' => 'ssh_nodes', 'nodes' => [{ 'uri' => 'some.host' }] }] } } 81 | let(:target_dir) { File.join(Dir.pwd, 'spec/fixtures/modules') } 82 | let(:dummy_tar) { 'spec/data/doot.tar.gz' } 83 | 84 | it 'happy path' do 85 | allow(File).to receive(:exist?).with(File.join(Dir.pwd, 'metadata.json')).and_return(true) 86 | allow(File).to receive(:read).with(File.join(Dir.pwd, 'metadata.json')).and_return(JSON.dump({ name: 'foo' })) 87 | 88 | stub_const('ENV', ENV.to_hash.merge('TARGET_HOST' => 'some.host')) 89 | expect_any_instance_of(PuppetLitmus::InventoryManipulation).to receive(:inventory_hash_from_inventory_file).and_return(inventory_hash) 90 | expect(File).to receive(:directory?).with(target_dir).and_return(true) 91 | expect_any_instance_of(Object).to receive(:build_modules_in_dir).with(target_dir).and_return([dummy_tar]) 92 | expect($stdout).to receive(:puts).with(start_with('Building all modules in')) 93 | expect_any_instance_of(Object).to receive(:upload_file).once.and_return([]) 94 | expect($stdout).to receive(:puts).with(start_with('Installing \'spec/data/doot.tar.gz\'')) 95 | expect_any_instance_of(Object).to receive(:run_command).twice.and_return([]) 96 | expect($stdout).to receive(:puts).with(start_with('Installed \'spec/data/doot.tar.gz\'')) 97 | allow_any_instance_of(Object).to receive(:exit) 98 | Rake::Task['litmus:install_modules_from_directory'].invoke('./spec/fixtures/modules') 99 | end 100 | end 101 | 102 | context 'with litmus:provision_install task' do 103 | it 'happy path' do 104 | expect(Rake::Task['spec_prep']).to receive(:invoke).and_return('') 105 | expect(Rake::Task['litmus:provision_list']).to receive(:invoke).with('default') 106 | expect(Rake::Task['litmus:install_agent']).to receive(:invoke).with('puppet6') 107 | expect(Rake::Task['litmus:install_module']).to receive(:invoke) 108 | allow_any_instance_of(Object).to receive(:exit) 109 | Rake::Task['litmus:provision_install'].invoke('default', 'puppet6') 110 | end 111 | end 112 | 113 | context 'with litmus:provision task' do 114 | let(:expected_output) do 115 | <<~OUTPUT 116 | Successfully provisioned centos:7 using docker 117 | localhost:2222, centos:7 118 | OUTPUT 119 | end 120 | 121 | it 'happy path' do 122 | results = [{ 'target' => 'localhost', 123 | 'action' => 'task', 124 | 'object' => 'provision::docker', 125 | 'status' => 'success', 126 | 'value' => { 'status' => 'ok', 'node_name' => 'localhost:2222' } }] 127 | 128 | allow(File).to receive(:directory?).with(any_args).and_return(true) 129 | allow_any_instance_of(BoltSpec::Run).to receive(:run_task).with(any_args).and_return(results) 130 | allow_any_instance_of(PuppetLitmus::InventoryManipulation).to receive(:inventory_hash_from_inventory_file).with(any_args).and_return({}) 131 | allow_any_instance_of(PuppetLitmus::RakeHelper).to receive(:check_connectivity?).with(any_args).and_return(true) 132 | allow_any_instance_of(Object).to receive(:exit) 133 | expect { Rake::Task['litmus:provision'].invoke('docker', 'centos:7') }.to output(/#{expected_output}/).to_stdout 134 | end 135 | end 136 | 137 | context 'with litmus:provision_list task' do 138 | let(:provision_file) { './provision.yaml' } 139 | let(:provision_hash) { { 'default' => { 'provisioner' => 'docker', 'images' => ['waffleimage/centos7'] } } } 140 | 141 | it 'no key in provision file' do 142 | allow(File).to receive(:file?).with(any_args).and_return(true) 143 | expect(YAML).to receive(:load_file).with(provision_file).and_return(provision_hash) 144 | allow_any_instance_of(Object).to receive(:exit) 145 | expect { Rake::Task['litmus:provision_list'].invoke('deet') }.to raise_error(/deet/) 146 | end 147 | end 148 | 149 | context 'with litmus:check_connectivity task' do 150 | let(:inventory_hash) { { 'groups' => [{ 'name' => 'ssh_nodes', 'nodes' => [{ 'name' => 'some.host' }] }] } } 151 | 152 | it 'happy path' do 153 | stub_const('ENV', ENV.to_hash.merge('TARGET_HOST' => 'some.host')) 154 | expect_any_instance_of(PuppetLitmus::InventoryManipulation).to receive(:inventory_hash_from_inventory_file).and_return(inventory_hash) 155 | expect_any_instance_of(PuppetLitmus::RakeHelper).to receive(:check_connectivity?).with(inventory_hash, nil).and_return(true) 156 | allow_any_instance_of(Object).to receive(:exit) 157 | Rake::Task['litmus:check_connectivity'].invoke 158 | end 159 | end 160 | 161 | context 'with litmus:acceptance:localhost task' do 162 | it 'calls spec_prep' do 163 | expect(Rake::Task['spec_prep']).to receive(:invoke).and_return('') 164 | expect_any_instance_of(RSpec::Core::RakeTask).to receive(:run_task) 165 | allow_any_instance_of(Object).to receive(:exit) 166 | Rake::Task['litmus:acceptance:localhost'].invoke 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /spec/lib/puppet_litmus/util_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | load File.expand_path('../../../lib/puppet_litmus/util.rb', __dir__) 5 | 6 | RSpec.describe PuppetLitmus::Util do 7 | context 'when using interpolate_powershell' do 8 | let(:command) { 'foo' } 9 | let(:encoded) { Base64.strict_encode64(command.encode('UTF-16LE')) } 10 | 11 | it 'interpolates the command' do 12 | expect(described_class.interpolate_powershell(command)).to eql("powershell.exe -NoProfile -EncodedCommand #{encoded}") 13 | expect(described_class.interpolate_powershell(command)).not_to match(/#{command}/) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rspec' 4 | require 'open3' 5 | require 'ostruct' 6 | 7 | if ENV['COVERAGE'] == 'yes' 8 | begin 9 | require 'simplecov' 10 | require 'simplecov-console' 11 | SimpleCov.formatters = [ 12 | SimpleCov::Formatter::HTMLFormatter, 13 | SimpleCov::Formatter::Console 14 | ] 15 | 16 | SimpleCov.start do 17 | track_files 'lib/**/*.rb' 18 | 19 | add_filter '/spec' 20 | add_filter 'lib/puppet_litmus/version.rb' 21 | # do not track vendored files 22 | add_filter '/vendor' 23 | add_filter '/.vendor' 24 | end 25 | rescue LoadError 26 | raise 'Add the simplecov & simplecov-console gems to Gemfile to enable this task' 27 | end 28 | end 29 | 30 | def run_matrix_from_metadata_v2(options = {}) 31 | command = 'bundle exec ./exe/matrix_from_metadata_v2' 32 | command += " --exclude-platforms '#{options['--exclude-platforms']}'" unless options['--exclude-platforms'].nil? 33 | result = Open3.capture3({ 'TEST_MATRIX_FROM_METADATA' => 'spec/exe/fake_metadata.json' }, command) 34 | OpenStruct.new( 35 | stdout: result[0], 36 | stderr: result[1], 37 | status_code: result[2] 38 | ) 39 | end 40 | 41 | def run_matrix_from_metadata_v3(options = []) 42 | command = %w[bundle exec ./exe/matrix_from_metadata_v3] 43 | unless options.include? '--metadata' 44 | options << '--metadata' 45 | options << File.join(File.dirname(__FILE__), 'exe', 'fake_metadata.json') 46 | end 47 | command += options 48 | result = Open3.capture3(*command) 49 | OpenStruct.new( 50 | stdout: result[0], 51 | stderr: result[1], 52 | status_code: result[2] 53 | ) 54 | end 55 | 56 | # This is basically how `configure!` sets up RSpec in tests. 57 | require 'puppet_litmus' 58 | RSpec.configure do |config| 59 | config.include PuppetLitmus 60 | config.extend PuppetLitmus 61 | end 62 | -------------------------------------------------------------------------------- /spec/support/inventory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def no_config_hash 4 | { 'groups' => 5 | [ 6 | { 'name' => 'ssh_nodes', 7 | 'targets' => 8 | [{ 'uri' => 'test.delivery.puppetlabs.net', 9 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' } }] }, 10 | { 'name' => 'winrm_nodes', 'targets' => [] } 11 | ] } 12 | end 13 | 14 | def no_docker_hash 15 | { 'groups' => 16 | [{ 'name' => 'ssh_nodes', 'targets' => [] }, 17 | { 'name' => 'winrm_nodes', 'targets' => [] }] } 18 | end 19 | 20 | def config_hash 21 | { 'groups' => 22 | [{ 'name' => 'ssh_nodes', 23 | 'targets' => 24 | [{ 'uri' => 'test.delivery.puppetlabs.net', 25 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 26 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' }, 27 | 'vars' => { 'role' => 'agent' } }] }, 28 | { 'name' => 'winrm_nodes', 'targets' => [] }] } 29 | end 30 | 31 | def no_feature_hash 32 | { 'groups' => 33 | [{ 'name' => 'ssh_nodes', 34 | 'targets' => 35 | [{ 'uri' => 'test.delivery.puppetlabs.net', 36 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 37 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' } }] }, 38 | { 'name' => 'winrm_nodes', 'targets' => [] }] } 39 | end 40 | 41 | def feature_hash_group 42 | { 'groups' => 43 | [{ 'name' => 'ssh_nodes', 44 | 'targets' => 45 | [{ 'uri' => 'test.delivery.puppetlabs.net', 46 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 47 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' } }], 48 | 'features' => ['puppet-agent'] }, 49 | { 'name' => 'winrm_nodes', 'targets' => [] }] } 50 | end 51 | 52 | def empty_feature_hash_group 53 | { 'groups' => 54 | [{ 'name' => 'ssh_nodes', 55 | 'targets' => 56 | [{ 'uri' => 'test.delivery.puppetlabs.net', 57 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 58 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' } }], 59 | 'features' => [] }, 60 | { 'name' => 'winrm_nodes', 'targets' => [] }] } 61 | end 62 | 63 | def feature_hash_node 64 | { 'groups' => 65 | [{ 'name' => 'ssh_nodes', 66 | 'targets' => 67 | [{ 'uri' => 'test.delivery.puppetlabs.net', 68 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 69 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' }, 70 | 'features' => ['puppet-agent'] }] }, 71 | { 'name' => 'winrm_nodes', 'targets' => [] }] } 72 | end 73 | 74 | def empty_feature_hash_node 75 | { 'groups' => 76 | [{ 'name' => 'ssh_nodes', 77 | 'targets' => 78 | [{ 'uri' => 'test.delivery.puppetlabs.net', 79 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 80 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' }, 81 | 'features' => [] }] }, 82 | { 'name' => 'winrm_nodes', 'targets' => [] }] } 83 | end 84 | 85 | def foo_node 86 | { 'uri' => 'foo', 87 | 'facts' => { 'provisioner' => 'bar', 'platform' => 'ubuntu' } } 88 | end 89 | 90 | def complex_inventory 91 | { 'groups' => 92 | [ 93 | { 94 | 'name' => 'ssh_nodes', 95 | 'groups' => [ 96 | { 'name' => 'frontend', 97 | 'targets' => [ 98 | { 99 | 'uri' => 'test.delivery.puppetlabs.net', 100 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 101 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' }, 102 | 'vars' => { 'role' => 'agent' } 103 | }, 104 | { 105 | 'uri' => 'test2.delivery.puppetlabs.net', 106 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 107 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' }, 108 | 'vars' => { 'role' => 'server' } 109 | }, 110 | { 111 | 'uri' => 'test3.delivery.puppetlabs.net', 112 | 'config' => { 'transport' => 'ssh', 'ssh' => { 'user' => 'root', 'password' => 'Qu@lity!', 'host-key-check' => false } }, 113 | 'vars' => { 'roles' => %w[agent nginx webserver] } 114 | } 115 | ] } 116 | ] 117 | }, 118 | { 119 | 'name' => 'winrm_nodes', 120 | 'targets' => [ 121 | { 122 | 'uri' => 'test4.delivery.puppetlabs.net', 123 | 'config' => { 'transport' => 'winrm', 'winrm' => { 'user' => 'admin', 'password' => 'Qu@lity!' } }, 124 | 'facts' => { 'provisioner' => 'vmpooler', 'platform' => 'centos-5-x86_64' }, 125 | 'vars' => { 'roles' => %w[agent iis webserver] } 126 | } 127 | ] 128 | } 129 | ] } 130 | end 131 | -------------------------------------------------------------------------------- /spec/support/inventorytesting.yaml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: ssh_nodes 3 | groups: 4 | - name: webservers 5 | targets: 6 | - 192.168.100.179 7 | - 192.168.100.180 8 | - 192.168.100.181 9 | - name: memcached 10 | targets: 11 | - 192.168.101.50 12 | - 192.168.101.60 13 | config: 14 | ssh: 15 | user: root 16 | config: 17 | transport: ssh 18 | ssh: 19 | user: centos 20 | private-key: ~/.ssh/id_rsa 21 | host-key-check: false 22 | - name: win_nodes 23 | groups: 24 | - name: domaincontrollers 25 | targets: 26 | - 192.168.110.10 27 | - 192.168.110.20 28 | - name: testservers 29 | targets: 30 | - 172.16.219.20 31 | - 172.16.219.30 32 | config: 33 | winrm: 34 | realm: MYDOMAIN 35 | ssl: false 36 | config: 37 | transport: winrm 38 | winrm: 39 | user: DOMAIN\opsaccount 40 | password: S3cretP@ssword 41 | ssl: true --------------------------------------------------------------------------------