├── .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 | [](https://github.com/puppetlabs/puppet_litmus/blob/main/CODEOWNERS)
4 | 
5 | [](https://badge.fury.io/rb/puppet_litmus)
6 |
7 |
8 |

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
--------------------------------------------------------------------------------