Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
============================
OKD Collection Release Notes
============================
.. contents:: Topics
v2.2.0
======
Minor Changes
-------------
- add action groups to runtime.yml (https://github.com/openshift/community.okd/issues/41).
Bugfixes
--------
- fix ocp auth failing against cluster api url with trailing slash (https://github.com/openshift/community.okd/issues/139)
New Modules
-----------
- openshift_adm_migrate_template_instances - Update TemplateInstances to point to the latest group-version-kinds
- openshift_adm_prune_auth - Removes references to the specified roles, clusterroles, users, and groups
- openshift_adm_prune_deployments - Remove old completed and failed deployment configs
- openshift_adm_prune_images - Remove unreferenced images
- openshift_import_image - Import the latest image information from a tag in a container image registry.
- openshift_registry_info - Display information about the integrated registry.
v2.1.0
======
Minor Changes
-------------
- add support for turbo mode (https://github.com/openshift/community.okd/pull/102).
- openshift_route - Add support for Route annotations (https://github.com/ansible-collections/community.okd/pull/99).
Bugfixes
--------
- fix broken links in Automation Hub for redhat.openshift (https://github.com/openshift/community.okd/issues/100).
v2.0.1
======
Minor Changes
-------------
- increase kubernetes.core dependency version (https://github.com/openshift/community.okd/pull/97).
v2.0.0
======
Major Changes
-------------
- update to use kubernetes.core 2.0 (https://github.com/openshift/community.okd/pull/93).
Minor Changes
-------------
- Added documentation for the ``community.okd`` collection.
- openshift - inventory plugin supports FQCN ``redhat.openshift``.
Breaking Changes / Porting Guide
--------------------------------
- drop python 2 support (https://github.com/openshift/community.okd/pull/93).
Bugfixes
--------
- fixes test suite to use correct versions of python and dependencies (https://github.com/ansible-collections/community.okd/pull/89).
- openshift_process - fix module execution when template does not include a message (https://github.com/ansible-collections/community.okd/pull/87).
v1.1.2
======
Bugfixes
--------
- include requirements.txt in downstream build process (https://github.com/ansible-collections/community.okd/pull/81).
v1.1.1
======
Bugfixes
--------
- add missing requirements.txt file needed for execution environments (https://github.com/ansible-collections/community.okd/pull/78).
- openshift_route - default to ``no_log=False`` for the ``key`` parameter in TLS configuration to fix sanity failures (https://github.com/ansible-collections/community.okd/pull/77).
- restrict molecule version to <3.3.0 to address breaking change (https://github.com/ansible-collections/community.okd/pull/77).
- update CI to work with ansible 2.11 (https://github.com/ansible-collections/community.okd/pull/80).
v1.1.0
======
Minor Changes
-------------
- increase the kubernetes.core dependency version number (https://github.com/ansible-collections/community.okd/pull/71).
v1.0.2
======
Minor Changes
-------------
- restrict the version of kubernetes.core dependency (https://github.com/ansible-collections/community.okd/pull/66).
v1.0.1
======
Bugfixes
--------
- Generate downstream redhat.openshift documentation (https://github.com/ansible-collections/community.okd/pull/59).
v1.0.0
======
Minor Changes
-------------
- Released version 1 to Automation Hub as redhat.openshift (https://github.com/ansible-collections/community.okd/issues/51).
v0.3.0
======
Major Changes
-------------
- Add openshift_process module for template rendering and optional application of rendered resources (https://github.com/ansible-collections/community.okd/pull/44).
- Add openshift_route module for creating routes from services (https://github.com/ansible-collections/community.okd/pull/40).
New Modules
-----------
- openshift_process - Process an OpenShift template.openshift.io/v1 Template
- openshift_route - Expose a Service as an OpenShift Route.
v0.2.0
======
Major Changes
-------------
- openshift_auth - new module (migrated from k8s_auth in community.kubernetes) (https://github.com/ansible-collections/community.okd/pull/33).
Minor Changes
-------------
- Add a contribution guide (https://github.com/ansible-collections/community.okd/pull/37).
- Use the API Group APIVersion for the `Route` object (https://github.com/ansible-collections/community.okd/pull/27).
New Modules
-----------
- openshift_auth - Authenticate to OpenShift clusters which require an explicit login step
v0.1.0
======
Major Changes
-------------
- Add custom k8s module, integrate better Molecule tests (https://github.com/ansible-collections/community.okd/pull/7).
- Add downstream build scripts to build redhat.openshift (https://github.com/ansible-collections/community.okd/pull/20).
- Add openshift connection plugin, update inventory plugin to use it (https://github.com/ansible-collections/community.okd/pull/18).
- Initial content migration from community.kubernetes (https://github.com/ansible-collections/community.okd/pull/3).
Minor Changes
-------------
- Add incluster Makefile target for CI (https://github.com/ansible-collections/community.okd/pull/13).
- Add tests for inventory plugin (https://github.com/ansible-collections/community.okd/pull/16).
- CI Documentation for working with Prow (https://github.com/ansible-collections/community.okd/pull/15).
- Docker container can run as an arbitrary user (https://github.com/ansible-collections/community.okd/pull/12).
- Dockerfile now is properly set up to run tests in a rootless container (https://github.com/ansible-collections/community.okd/pull/11).
- Integrate stale bot for issue queue maintenance (https://github.com/ansible-collections/community.okd/pull/14).

View File

@@ -0,0 +1,65 @@
# Contributing
## Getting Started
General information about setting up your Python environment, testing modules,
Ansible coding styles, and more can be found in the [Ansible Community Guide](
https://docs.ansible.com/ansible/latest/community/index.html).
## Kubernetes Collections
### community.okd
This collection contains modules and plugins contributed and maintained by the Ansible Kubernetes
community.
New modules and plugins developed by the community should be proposed to `community.okd`.
## Submitting Issues
All software has bugs, and the `community.okd` collection is no exception. When you find a bug,
you can help tremendously by [telling us about it](https://github.com/ansible-collections/community.okd/issues/new/choose).
If you should discover that the bug you're trying to file already exists in an issue,
you can help by verifying the behavior of the reported bug with a comment in that
issue, or by reporting any additional information.
## Pull Requests
All modules MUST have integration tests for new features.
Bug fixes for modules that currently have integration tests SHOULD have tests added.
New modules should be submitted to the [community.okd](https://github.com/ansible-collections/community.okd) collection and MUST have integration tests.
Expected test criteria:
* Resource creation under check mode
* Resource creation
* Resource creation again (idempotency) under check mode
* Resource creation again (idempotency)
* Resource modification under check mode
* Resource modification
* Resource modification again (idempotency) under check mode
* Resource modification again (idempotency)
* Resource deletion under check mode
* Resource deletion
* Resource deletion (of a non-existent resource) under check mode
* Resource deletion (of a non-existent resource)
Where modules have multiple parameters we recommend running through the 4-step modification cycle for each parameter the module accepts, as well as a modification cycle where as most, if not all, parameters are modified at the same time.
For general information on running the integration tests see the
[Integration Tests page of the Module Development Guide](https://docs.ansible.com/ansible/devel/dev_guide/testing_integration.html#testing-integration),
especially the section on configuration for cloud tests. For questions about writing tests the Ansible Kubernetes community can be found on Libera.Chat IRC as detailed below.
### Code of Conduct
The `community.okd` collection follows the Ansible project's
[Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html).
Please read and familiarize yourself with this document.
### IRC
Our IRC channels may require you to register your nickname. If you receive an error when you connect, see
[Libera.Chat's Nickname Registration guide](https://libera.chat/guides/registration) for instructions.
The `#ansible-kubernetes` channel on [irc.libera.chat](https://libera.chat/) is the main and official place to discuss use and development of the `community.okd` collection.
For more information about Ansible's Kubernetes and OpenShift integration, browse the resources in the [Kubernetes Working Group](https://github.com/ansible/community/wiki/Kubernetes) Community wiki page.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -0,0 +1,41 @@
{
"collection_info": {
"namespace": "community",
"name": "okd",
"version": "2.2.0",
"authors": [
"geerlingguy (https://www.jeffgeerling.com/)",
"fabianvf (https://github.com/fabianvf)",
"willthames (https://github.com/willthames)",
"Akasurde (https://github.com/akasurde)"
],
"readme": "README.md",
"tags": [
"kubernetes",
"k8s",
"cloud",
"infrastructure",
"openshift",
"okd",
"cluster"
],
"description": "OKD Collection for Ansible.",
"license": [],
"license_file": "LICENSE",
"dependencies": {
"kubernetes.core": ">=2.1.0,<2.4.0"
},
"repository": "https://github.com/openshift/community.okd",
"documentation": "",
"homepage": "",
"issues": "https://github.com/openshift/community.okd/issues"
},
"file_manifest_file": {
"name": "FILES.json",
"ftype": "file",
"chksum_type": "sha256",
"chksum_sha256": "484db15262b2d1e55c27d5fd1a137bc01b08b5756bc72add9246978954e48e17",
"format": 1
},
"format": 1
}

View File

@@ -0,0 +1,55 @@
.PHONY: molecule
# Also needs to be updated in galaxy.yml
VERSION = 2.2.0
SANITY_TEST_ARGS ?= --docker --color
UNITS_TEST_ARGS ?= --docker --color
PYTHON_VERSION ?= `python3 -c 'import platform; print("{0}.{1}".format(platform.python_version_tuple()[0], platform.python_version_tuple()[1]))'`
clean:
rm -f community-okd-$(VERSION).tar.gz
rm -f redhat-openshift-$(VERSION).tar.gz
rm -rf ansible_collections
build: clean
ansible-galaxy collection build
install: build
ansible-galaxy collection install -p ansible_collections community-okd-$(VERSION).tar.gz
sanity: install
cd ansible_collections/community/okd && ansible-test sanity -v --python $(PYTHON_VERSION) $(SANITY_TEST_ARGS)
units: install
cd ansible_collections/community/okd && ansible-test units -v --python $(PYTHON_VERSION) $(UNITS_TEST_ARGS)
molecule: install
molecule test
test-integration: upstream-test-integration downstream-test-integration
test-sanity: upstream-test-sanity downstream-test-sanity
test-units: upstream-test-units downstream-test-units
test-integration-incluster:
./ci/incluster_integration.sh
upstream-test-sanity: sanity
upstream-test-units: units
upstream-test-integration: molecule
downstream-test-sanity:
./ci/downstream.sh -s
downstream-test-units:
./ci/downstream.sh -u
downstream-test-integration:
./ci/downstream.sh -i
downstream-build:
./ci/downstream.sh -b

View File

@@ -0,0 +1,6 @@
# This file just uses aliases defined in OWNERS_ALIASES.
approvers:
- community.okd-approvers
reviewers:
- community.okd-reviewers

View File

@@ -0,0 +1,19 @@
aliases:
community.okd-approvers:
- gravesm
- akasurde
- fabianvf
- tima
- goneri
- jillr
- alinabuzachis
- abikouo
community.okd-reviewers:
- gravesm
- akasurde
- fabianvf
- tima
- goneri
- jillr
- alinabuzachis
- abikouo

View File

@@ -0,0 +1,177 @@
# OKD Collection for Ansible
<!--- STARTREMOVE --->
[![CI](https://github.com/ansible-collections/community.okd/workflows/CI/badge.svg?event=push)](https://github.com/ansible-collections/community.okd/actions) [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.okd)](https://codecov.io/gh/ansible-collections/community.okd)
This repo hosts the `community.okd` Ansible Collection.
The collection includes a variety of Ansible content to help automate the management of applications in OKD clusters, as well as the provisioning and maintenance of clusters themselves.
<!--start requires_ansible-->
## Ansible version compatibility
This collection has been tested against following Ansible versions: **>=2.9.17**.
For collections that support Ansible 2.9, please ensure you update your `network_os` to use the
fully qualified collection name (for example, `cisco.ios.ios`).
Plugins and modules within a collection may be tested with only specific Ansible versions.
A collection may contain metadata that identifies these versions.
PEP440 is the schema used to describe the versions of Ansible.
<!--end requires_ansible-->
## Included content
Click on the name of a plugin or module to view that content's documentation:
<!--start collection content-->
### Connection plugins
Name | Description
--- | ---
[community.okd.oc](https://github.com/openshift/community.okd/blob/main/docs/community.okd.oc_connection.rst)|Execute tasks in pods running on OpenShift.
### Inventory plugins
Name | Description
--- | ---
[community.okd.openshift](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_inventory.rst)|OpenShift inventory source
### Modules
Name | Description
--- | ---
[community.okd.k8s](https://github.com/openshift/community.okd/blob/main/docs/community.okd.k8s_module.rst)|Manage OpenShift objects
[community.okd.openshift_adm_groups_sync](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_adm_groups_sync_module.rst)|Sync OpenShift Groups with records from an external provider.
[community.okd.openshift_adm_migrate_template_instances](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_adm_migrate_template_instances_module.rst)|Update TemplateInstances to point to the latest group-version-kinds
[community.okd.openshift_adm_prune_auth](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_adm_prune_auth_module.rst)|Removes references to the specified roles, clusterroles, users, and groups
[community.okd.openshift_adm_prune_deployments](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_adm_prune_deployments_module.rst)|Remove old completed and failed deployment configs
[community.okd.openshift_adm_prune_images](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_adm_prune_images_module.rst)|Remove unreferenced images
[community.okd.openshift_auth](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_auth_module.rst)|Authenticate to OpenShift clusters which require an explicit login step
[community.okd.openshift_import_image](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_import_image_module.rst)|Import the latest image information from a tag in a container image registry.
[community.okd.openshift_process](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_process_module.rst)|Process an OpenShift template.openshift.io/v1 Template
[community.okd.openshift_registry_info](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_registry_info_module.rst)|Display information about the integrated registry.
[community.okd.openshift_route](https://github.com/openshift/community.okd/blob/main/docs/community.okd.openshift_route_module.rst)|Expose a Service as an OpenShift Route.
<!--end collection content-->
<!--- ENDREMOVE --->
## Installation and Usage
### Installing the Collection from Ansible Galaxy
Before using the OKD collection, you need to install it with the Ansible Galaxy CLI:
ansible-galaxy collection install community.okd
You can also include it in a `requirements.yml` file and install it via `ansible-galaxy collection install -r requirements.yml`, using the format:
```yaml
---
collections:
- name: community.okd
version: 2.2.0
```
### Installing the Kubernetes Python Library
Content in this collection requires the [Kubernetes Python client](https://pypi.org/project/kubernetes/) to interact with Kubernetes' APIs. You can install it with:
pip3 install kubernetes
### Using modules from the OKD Collection in your playbooks
It's preferable to use content in this collection using their Fully Qualified Collection Namespace (FQCN), for example `community.okd.openshift`:
```yaml
---
plugin: community.okd.openshift
connections:
- namespaces:
- testing
```
For documentation on how to use individual plugins included in this collection, please see the links in the 'Included content' section earlier in this README.
## Ansible Turbo mode Tech Preview
The ``community.okd`` collection supports Ansible Turbo mode as a tech preview via the ``cloud.common`` collection. By default, this feature is disabled. To enable Turbo mode, set the environment variable `ENABLE_TURBO_MODE=1` on the managed node. For example:
```yaml
---
- hosts: remote
environment:
ENABLE_TURBO_MODE: 1
tasks:
...
```
Please read more about Ansible Turbo mode - [here](https://github.com/ansible-collections/community.okd/blob/main/docs/ansible_turbo_mode.rst).
<!--- STARTREMOVE --->
## Testing and Development
If you want to develop new content for this collection or improve what's already here, the easiest way to work on the collection is to clone it into one of the configured [`COLLECTIONS_PATHS`](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths), and work on it there.
See [Contributing to community.okd](CONTRIBUTING.md).
The `tests` directory contains configuration for running sanity tests using [`ansible-test`](https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html).
You can run the `ansible-test` sanity tests with the command:
make test-sanity
The `molecule` directory contains configuration for running integration tests using [`molecule`](https://molecule.readthedocs.io/).
You can run the `molecule` integration tests with the command:
make test-integration
These commands will create a directory called `ansible_collections` which should not be committed or added to the `.gitignore` (Tracking issue: https://github.com/ansible/ansible/issues/68499)
### Prow
This repository uses the OpenShift [Prow](https://github.com/kubernetes/test-infra/blob/master/prow/README.md) instance for testing against live OpenShift clusters.
The configuration for the CI jobs that this repository runs can be found in the [`openshift/release repository`](https://github.com/openshift/release/blob/master/ci-operator/config/openshift/community.okd/openshift-community.okd-main.yaml).
The [Prow CI integration test job](https://github.com/openshift/release/blob/master/ci-operator/config/openshift/community.okd/openshift-community.okd-main.yaml#L40-L43)
runs the command:
make test-integration-incluster
which will create a job that runs the normal `make integration` target. In order to mimic the Prow CI job, you must
first build the test image using the Dockerfile in [`ci/Dockerfile`](ci/Dockerfile). Then, push the image
somewhere that it will be accessible to the cluster, and run
IMAGE_FORMAT=<your image> make test-integration-incluser
where the `IMAGE_FORMAT` environment variable is the full reference to your container (ie, `IMAGE_FORMAT=quay.io/example/molecule-test-runner`)
## Publishing New Versions
Releases are automatically built and pushed to Ansible Galaxy for any new tag. Before tagging a release, make sure to do the following:
1. Update the version in the following places:
a. The `version` in `galaxy.yml`
b. This README's `requirements.yml` example
c. The `DOWNSTREAM_VERSION` in `ci/downstream.sh`
d. The `VERSION` in `Makefile`
e. The version in `requirements.yml`
1. Update the CHANGELOG:
1. Make sure you have [`antsibull-changelog`](https://pypi.org/project/antsibull-changelog/) installed.
1. Make sure there are fragments for all known changes in `changelogs/fragments`.
1. Run `antsibull-changelog release`.
1. Commit the changes and create a PR with the changes. Wait for tests to pass, then merge it once they have.
1. Tag the version in Git and push to GitHub.
After the version is published, verify it exists on the [OKD Collection Galaxy page](https://galaxy.ansible.com/community/okd).
<!--- ENDREMOVE --->
## More Information
For more information about Ansible's Kubernetes and OpenShift integrations, join the `#ansible-kubernetes` channel on [libera.chat](https://libera.chat/) IRC, and browse the resources in the [Kubernetes Working Group](https://github.com/ansible/community/wiki/Kubernetes) Community wiki page.
## License
GNU General Public License v3.0 or later
See LICENCE to see the full text.

View File

@@ -0,0 +1,83 @@
objects:
role: {}
plugins:
become: {}
cache: {}
callback: {}
cliconf: {}
connection:
oc:
description: Execute tasks in pods running on OpenShift.
name: oc
version_added: null
httpapi: {}
inventory:
openshift:
description: OpenShift inventory source
name: openshift
version_added: null
lookup: {}
module:
k8s:
description: Manage OpenShift objects
name: k8s
namespace: ''
version_added: null
openshift_adm_groups_sync:
description: Sync OpenShift Groups with records from an external provider.
name: openshift_adm_groups_sync
namespace: ''
version_added: 2.1.0
openshift_adm_migrate_template_instances:
description: Update TemplateInstances to point to the latest group-version-kinds
name: openshift_adm_migrate_template_instances
namespace: ''
version_added: 2.2.0
openshift_adm_prune_auth:
description: Removes references to the specified roles, clusterroles, users,
and groups
name: openshift_adm_prune_auth
namespace: ''
version_added: 2.2.0
openshift_adm_prune_deployments:
description: Remove old completed and failed deployment configs
name: openshift_adm_prune_deployments
namespace: ''
version_added: 2.2.0
openshift_adm_prune_images:
description: Remove unreferenced images
name: openshift_adm_prune_images
namespace: ''
version_added: 2.2.0
openshift_auth:
description: Authenticate to OpenShift clusters which require an explicit login
step
name: openshift_auth
namespace: ''
version_added: 0.2.0
openshift_import_image:
description: Import the latest image information from a tag in a container image
registry.
name: openshift_import_image
namespace: ''
version_added: 2.2.0
openshift_process:
description: Process an OpenShift template.openshift.io/v1 Template
name: openshift_process
namespace: ''
version_added: 0.3.0
openshift_registry_info:
description: Display information about the integrated registry.
name: openshift_registry_info
namespace: ''
version_added: 2.2.0
openshift_route:
description: Expose a Service as an OpenShift Route.
name: openshift_route
namespace: ''
version_added: 0.3.0
netconf: {}
shell: {}
strategy: {}
vars: {}
version: 2.2.0

View File

@@ -0,0 +1,182 @@
ancestor: null
releases:
0.1.0:
changes:
major_changes:
- Add custom k8s module, integrate better Molecule tests (https://github.com/ansible-collections/community.okd/pull/7).
- Add downstream build scripts to build redhat.openshift (https://github.com/ansible-collections/community.okd/pull/20).
- Add openshift connection plugin, update inventory plugin to use it (https://github.com/ansible-collections/community.okd/pull/18).
- Initial content migration from community.kubernetes (https://github.com/ansible-collections/community.okd/pull/3).
minor_changes:
- Add incluster Makefile target for CI (https://github.com/ansible-collections/community.okd/pull/13).
- Add tests for inventory plugin (https://github.com/ansible-collections/community.okd/pull/16).
- CI Documentation for working with Prow (https://github.com/ansible-collections/community.okd/pull/15).
- Docker container can run as an arbitrary user (https://github.com/ansible-collections/community.okd/pull/12).
- Dockerfile now is properly set up to run tests in a rootless container (https://github.com/ansible-collections/community.okd/pull/11).
- Integrate stale bot for issue queue maintenance (https://github.com/ansible-collections/community.okd/pull/14).
fragments:
- 1-initial-content.yml
- 11-dockerfile-tests.yml
- 12-dockerfile-tests.yml
- 13-makefile-tests.yml
- 15-ci-documentation.yml
- 16-inventory-plugin-tests.yml
- 18-openshift-connection-plugin.yml
- 20-downstream-build-scripts.yml
- 7-molecule-tests.yml
- 8-stale-bot.yml
release_date: '2020-09-04'
0.2.0:
changes:
major_changes:
- openshift_auth - new module (migrated from k8s_auth in community.kubernetes)
(https://github.com/ansible-collections/community.okd/pull/33).
minor_changes:
- Add a contribution guide (https://github.com/ansible-collections/community.okd/pull/37).
- Use the API Group APIVersion for the `Route` object (https://github.com/ansible-collections/community.okd/pull/27).
fragments:
- 27-route-api-group.yml
- 33-add-k8s_auth.yml
- 36-contribution-guide.yml
modules:
- description: Authenticate to OpenShift clusters which require an explicit login
step
name: openshift_auth
namespace: ''
release_date: '2020-09-24'
0.3.0:
changes:
major_changes:
- Add openshift_process module for template rendering and optional application
of rendered resources (https://github.com/ansible-collections/community.okd/pull/44).
- Add openshift_route module for creating routes from services (https://github.com/ansible-collections/community.okd/pull/40).
fragments:
- 40-openshift_route.yml
- 44-openshift_process.yml
modules:
- description: Process an OpenShift template.openshift.io/v1 Template
name: openshift_process
namespace: ''
- description: Expose a Service as an OpenShift Route.
name: openshift_route
namespace: ''
release_date: '2020-10-12'
1.0.0:
changes:
minor_changes:
- Released version 1 to Automation Hub as redhat.openshift (https://github.com/ansible-collections/community.okd/issues/51).
fragments:
- 51-redhat-openshift-ah-release.yml
release_date: '2020-11-12'
1.0.1:
changes:
bugfixes:
- Generate downstream redhat.openshift documentation (https://github.com/ansible-collections/community.okd/pull/59).
fragments:
- 59-downstream-docs.yml
release_date: '2020-11-17'
1.0.2:
changes:
minor_changes:
- restrict the version of kubernetes.core dependency (https://github.com/ansible-collections/community.okd/pull/66).
fragments:
- 66-restrict-kubernetes-core-version.yaml
release_date: '2021-02-19'
1.1.0:
changes:
minor_changes:
- increase the kubernetes.core dependency version number (https://github.com/ansible-collections/community.okd/pull/71).
fragments:
- 71-bump-kubernetes-core-version.yaml
release_date: '2021-02-23'
1.1.1:
changes:
bugfixes:
- add missing requirements.txt file needed for execution environments (https://github.com/ansible-collections/community.okd/pull/78).
- openshift_route - default to ``no_log=False`` for the ``key`` parameter in
TLS configuration to fix sanity failures (https://github.com/ansible-collections/community.okd/pull/77).
- restrict molecule version to <3.3.0 to address breaking change (https://github.com/ansible-collections/community.okd/pull/77).
- update CI to work with ansible 2.11 (https://github.com/ansible-collections/community.okd/pull/80).
fragments:
- 77-fix-ci-failure.yaml
- 78-add-requirements-file.yaml
- 80-update-ci.yaml
release_date: '2021-04-06'
1.1.2:
changes:
bugfixes:
- include requirements.txt in downstream build process (https://github.com/ansible-collections/community.okd/pull/81).
fragments:
- 81-include-requirements.yaml
release_date: '2021-04-08'
2.0.0:
changes:
breaking_changes:
- drop python 2 support (https://github.com/openshift/community.okd/pull/93).
bugfixes:
- fixes test suite to use correct versions of python and dependencies (https://github.com/ansible-collections/community.okd/pull/89).
- openshift_process - fix module execution when template does not include a
message (https://github.com/ansible-collections/community.okd/pull/87).
major_changes:
- update to use kubernetes.core 2.0 (https://github.com/openshift/community.okd/pull/93).
minor_changes:
- Added documentation for the ``community.okd`` collection.
- openshift - inventory plugin supports FQCN ``redhat.openshift``.
fragments:
- 87-openshift_process-fix-template-without-message.yaml
- 89-clean-up-ci.yaml
- 93-update-to-k8s-2.yaml
- add_docs.yml
- fqcn_inventory.yml
release_date: '2021-06-22'
2.0.1:
changes:
minor_changes:
- increase kubernetes.core dependency version (https://github.com/openshift/community.okd/pull/97).
fragments:
- 97-bump-k8s-version.yaml
release_date: '2021-06-24'
2.1.0:
changes:
bugfixes:
- fix broken links in Automation Hub for redhat.openshift (https://github.com/openshift/community.okd/issues/100).
minor_changes:
- add support for turbo mode (https://github.com/openshift/community.okd/pull/102).
- openshift_route - Add support for Route annotations (https://github.com/ansible-collections/community.okd/pull/99).
fragments:
- 0-copy_ignore_txt.yml
- 100-fix-broken-links.yml
- 102-support-turbo-mode.yaml
- 99-openshift_route-add-support-for-annotations.yml
release_date: '2021-10-20'
2.2.0:
changes:
bugfixes:
- fix ocp auth failing against cluster api url with trailing slash (https://github.com/openshift/community.okd/issues/139)
minor_changes:
- add action groups to runtime.yml (https://github.com/openshift/community.okd/issues/41).
fragments:
- 152-add-action-groups.yml
- auth-against-api-with-trailing-slash.yaml
modules:
- description: Update TemplateInstances to point to the latest group-version-kinds
name: openshift_adm_migrate_template_instances
namespace: ''
- description: Removes references to the specified roles, clusterroles, users,
and groups
name: openshift_adm_prune_auth
namespace: ''
- description: Remove old completed and failed deployment configs
name: openshift_adm_prune_deployments
namespace: ''
- description: Remove unreferenced images
name: openshift_adm_prune_images
namespace: ''
- description: Import the latest image information from a tag in a container image
registry.
name: openshift_import_image
namespace: ''
- description: Display information about the integrated registry.
name: openshift_registry_info
namespace: ''
release_date: '2022-05-05'

View File

@@ -0,0 +1,30 @@
---
changelog_filename_template: ../CHANGELOG.rst
changelog_filename_version_depth: 0
changes_file: changelog.yaml
changes_format: combined
keep_fragments: false
mention_ancestor: true
new_plugins_after_name: removed_features
notesdir: fragments
prelude_section_name: release_summary
prelude_section_title: Release Summary
sections:
- - major_changes
- Major Changes
- - minor_changes
- Minor Changes
- - breaking_changes
- Breaking Changes / Porting Guide
- - deprecated_features
- Deprecated Features
- - removed_features
- Removed Features (previously deprecated)
- - security_fixes
- Security Fixes
- - bugfixes
- Bugfixes
- - known_issues
- Known Issues
title: OKD Collection
trivial_section_name: trivial

View File

@@ -0,0 +1,43 @@
FROM registry.access.redhat.com/ubi8/ubi
ENV OPERATOR=/usr/local/bin/ansible-operator \
USER_UID=1001 \
USER_NAME=ansible-operator\
HOME=/opt/ansible \
ANSIBLE_LOCAL_TMP=/opt/ansible/tmp \
DOWNSTREAM_BUILD_PYTHON=python3.9
RUN yum install -y \
glibc-langpack-en \
git \
make \
python39 \
python39-devel \
python39-pip \
python39-setuptools \
gcc \
openldap-devel \
&& pip3 install --no-cache-dir --upgrade setuptools pip \
&& pip3 install --no-cache-dir \
kubernetes \
ansible==2.9.* \
"molecule<3.3.0" \
&& yum clean all \
&& rm -rf $HOME/.cache \
&& curl -L https://github.com/openshift/okd/releases/download/4.5.0-0.okd-2020-08-12-020541/openshift-client-linux-4.5.0-0.okd-2020-08-12-020541.tar.gz | tar -xz -C /usr/local/bin
# TODO: Is there a better way to install this client in ubi8?
COPY . /opt/ansible
WORKDIR /opt/ansible
RUN echo "${USER_NAME}:x:${USER_UID}:0:${USER_NAME} user:${HOME}:/sbin/nologin" >> /etc/passwd \
&& mkdir -p "${HOME}/.ansible/tmp" \
&& chown -R "${USER_UID}:0" "${HOME}" \
&& chmod -R ug+rwX "${HOME}" \
&& mkdir /go \
&& chown -R "${USER_UID}:0" /go \
&& chmod -R ug+rwX /go
USER ${USER_UID}

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import ast
from pathlib import PosixPath
import yaml
import argparse
import os
def read_docstring(filename):
"""
Search for assignment of the DOCUMENTATION and EXAMPLES variables in the given file.
Parse DOCUMENTATION from YAML and return the YAML doc or None together with EXAMPLES, as plain text.
"""
data = {
'doc': None,
'plainexamples': None,
'returndocs': None,
'metadata': None, # NOTE: not used anymore, kept for compat
'seealso': None,
}
string_to_vars = {
'DOCUMENTATION': 'doc',
'EXAMPLES': 'plainexamples',
'RETURN': 'returndocs',
'ANSIBLE_METADATA': 'metadata', # NOTE: now unused, but kept for backwards compat
}
try:
with open(filename, 'rb') as b_module_data:
M = ast.parse(b_module_data.read())
for child in M.body:
if isinstance(child, ast.Assign):
for t in child.targets:
try:
theid = t.id
except AttributeError:
# skip errors can happen when trying to use the normal code
# sys.stderr.write("Failed to assign id for %s on %s, skipping\n" % (t, filename))
continue
if theid in string_to_vars:
varkey = string_to_vars[theid]
if isinstance(child.value, ast.Dict):
data[varkey] = ast.literal_eval(child.value)
else:
if theid != 'EXAMPLES':
# string should be yaml if already not a dict
data[varkey] = child.value.s
# sys.stderr.write('assigned: %s\n' % varkey)
except Exception:
# sys.stderr.write("unable to parse %s" % filename)
return
return yaml.safe_load(data["doc"]) if data["doc"] is not None else None
def is_extending_collection(result, col_fqcn):
if result:
for x in result.get("extends_documentation_fragment", []):
if x.startswith(col_fqcn):
return True
return False
def main():
parser = argparse.ArgumentParser(
description="list modules with inherited doc fragments from kubernetes.core that need rendering to deal with Galaxy/AH lack of functionality."
)
parser.add_argument(
"-c", "--collection-path", type=str, default=os.getcwd(), help="path to the collection"
)
args = parser.parse_args()
path = PosixPath(args.collection_path) / PosixPath("plugins/modules")
output = []
for d in path.iterdir():
if d.is_file():
result = read_docstring(str(d))
if is_extending_collection(result, "kubernetes.core."):
output.append(d.stem.replace(".py", ""))
print("\n".join(output))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,298 @@
#!/bin/bash -eu
# Script to dual-home the upstream and downstream Collection in a single repo
#
# This script will build or test a downstream collection, removing any
# upstream components that will not ship in the downstream release
#
# NOTES:
# - All functions are prefixed with f_ so it's obvious where they come
# from when in use throughout the script
DOWNSTREAM_VERSION="2.2.0"
KEEP_DOWNSTREAM_TMPDIR="${KEEP_DOWNSTREAM_TMPDIR:-''}"
INSTALL_DOWNSTREAM_COLLECTION_PATH="${INSTALL_DOWNSTREAM_COLLECTION_PATH:-}"
_build_dir=""
f_log_info()
{
printf "%s:LOG:INFO: %s\n" "${0}" "${1}"
}
f_show_help()
{
printf "Usage: downstream.sh [OPTION]\n"
printf "\t-s\t\tCreate a temporary downstream release and perform sanity tests.\n"
printf "\t-u\t\tCreate a temporary downstream release and perform units tests.\n"
printf "\t-i\t\tCreate a temporary downstream release and perform integration tests.\n"
printf "\t-m\t\tCreate a temporary downstream release and perform molecule tests.\n"
printf "\t-b\t\tCreate a downstream release and stage for release.\n"
printf "\t-r\t\tCreate a downstream release and publish release.\n"
}
f_text_sub()
{
# Switch FQCN and dependent components
OKD_sed_files="${_build_dir}/README.md ${_build_dir}/CHANGELOG.rst ${_build_dir}/changelogs/config.yaml ${_build_dir}/ci/downstream.sh ${_build_dir}/galaxy.yml"
# shellcheck disable=SC2068
for okd_file in ${OKD_sed_files[@]}; do sed -i.bak "s/OKD/OpenShift/g" "${okd_file}"; done
sed -i.bak "s/============================/==================================/" "${_build_dir}/CHANGELOG.rst"
sed -i.bak "s/Ansible Galaxy/Automation Hub/" "${_build_dir}/README.md"
sed -i.bak "s/community-okd/redhat-openshift/" "${_build_dir}/Makefile"
sed -i.bak "s/community\/okd/redhat\/openshift/" "${_build_dir}/Makefile"
sed -i.bak "s/^VERSION\:/VERSION: ${DOWNSTREAM_VERSION}/" "${_build_dir}/Makefile"
sed -i.bak "s/name\:.*$/name: openshift/" "${_build_dir}/galaxy.yml"
sed -i.bak "s/namespace\:.*$/namespace: redhat/" "${_build_dir}/galaxy.yml"
sed -i.bak "s/Kubernetes/OpenShift/g" "${_build_dir}/galaxy.yml"
sed -i.bak "s/^version\:.*$/version: ${DOWNSTREAM_VERSION}/" "${_build_dir}/galaxy.yml"
sed -i.bak "/STARTREMOVE/,/ENDREMOVE/d" "${_build_dir}/README.md"
sed -i.bak "s/[[:space:]]okd:$/ openshift:/" ${_build_dir}/meta/runtime.yml
find "${_build_dir}" -type f ! -name galaxy.yml -exec sed -i.bak "s/community\.okd/redhat\.openshift/g" {} \;
find "${_build_dir}" -type f -name "*.bak" -delete
}
f_prep()
{
f_log_info "${FUNCNAME[0]}"
# Array of excluded files from downstream build (relative path)
_file_exclude=(
)
# Files to copy downstream (relative repo root dir path)
_file_manifest=(
CHANGELOG.rst
galaxy.yml
LICENSE
README.md
Makefile
setup.cfg
.yamllint
requirements.txt
requirements.yml
test-requirements.txt
)
# Directories to recursively copy downstream (relative repo root dir path)
_dir_manifest=(
changelogs
ci
meta
molecule
plugins
tests
)
# Temp build dir
_tmp_dir=$(mktemp -d)
_start_dir="${PWD}"
_build_dir="${_tmp_dir}/ansible_collections/redhat/openshift"
mkdir -p "${_build_dir}"
}
f_cleanup()
{
f_log_info "${FUNCNAME[0]}"
if [[ -n "${_build_dir}" ]]; then
if [[ -n ${KEEP_DOWNSTREAM_TMPDIR} ]]; then
if [[ -d ${_build_dir} ]]; then
rm -fr "${_build_dir}"
fi
fi
else
exit 0
fi
}
# Exit and handle cleanup processes if needed
f_exit()
{
f_cleanup
exit "$0"
}
f_create_collection_dir_structure()
{
f_log_info "${FUNCNAME[0]}"
# Create the Collection
for f_name in "${_file_manifest[@]}";
do
cp "./${f_name}" "${_build_dir}/${f_name}"
done
for d_name in "${_dir_manifest[@]}";
do
cp -r "./${d_name}" "${_build_dir}/${d_name}"
done
if [ -n "${_file_exclude:-}" ]; then
for exclude_file in "${_file_exclude[@]}";
do
if [[ -f "${_build_dir}/${exclude_file}" ]]; then
rm -f "${_build_dir}/${exclude_file}"
fi
done
fi
}
f_handle_doc_fragments_workaround()
{
f_log_info "${FUNCNAME[0]}"
local install_collections_dir="${_build_dir}/collections/"
local temp_fragments_json="${_tmp_dir}/fragments.json"
local temp_start="${_tmp_dir}/startfile.txt"
local temp_end="${_tmp_dir}/endfile.txt"
local rendered_fragments="./rendereddocfragments.txt"
# FIXME: Check Python interpreter from environment variable to work with prow
PYTHON=${DOWNSTREAM_BUILD_PYTHON:-/usr/bin/python3.6}
f_log_info "Using Python interpreter: ${PYTHON}"
# Modules with inherited doc fragments from kubernetes.core that need
# rendering to deal with Galaxy/AH lack of functionality.
# shellcheck disable=SC2207
_doc_fragment_modules=($("${PYTHON}" "${_start_dir}/ci/doc_fragment_modules.py" -c "${_start_dir}"))
# Build the collection, export docs, render them, stitch it all back together
pushd "${_build_dir}" || return
ansible-galaxy collection build
ansible-galaxy collection install -p "${install_collections_dir}" ./*.tar.gz
rm ./*.tar.gz
for doc_fragment_mod in "${_doc_fragment_modules[@]}"
do
local module_py="plugins/modules/${doc_fragment_mod}.py"
f_log_info "Processing doc fragments for ${module_py}"
# We need following variable for ansible-doc only
# shellcheck disable=SC2097,SC2098
ANSIBLE_COLLECTIONS_PATH="${install_collections_dir}" \
ANSIBLE_COLLECTIONS_PATHS="${ANSIBLE_COLLECTIONS_PATH}:${install_collections_dir}" \
ansible-doc -j "redhat.openshift.${doc_fragment_mod}" > "${temp_fragments_json}"
"${PYTHON}" "${_start_dir}/ci/downstream_fragments.py" "redhat.openshift.${doc_fragment_mod}" "${temp_fragments_json}"
sed -n '/STARTREMOVE/q;p' "${module_py}" > "${temp_start}"
sed '1,/ENDREMOVE/d' "${module_py}" > "${temp_end}"
cat "${temp_start}" "${rendered_fragments}" "${temp_end}" > "${module_py}"
done
rm -f "${rendered_fragments}"
rm -fr "${install_collections_dir}"
popd
}
f_copy_collection_to_working_dir()
{
f_log_info "${FUNCNAME[0]}"
# Copy the Collection build result into original working dir
f_log_info "copying built collection *.tar.gz into ./"
cp "${_build_dir}"/*.tar.gz ./
# Install downstream collection into provided path
if [[ -n ${INSTALL_DOWNSTREAM_COLLECTION_PATH} ]]; then
f_log_info "Install built collection *.tar.gz into ${INSTALL_DOWNSTREAM_COLLECTION_PATH}"
ansible-galaxy collection install -p "${INSTALL_DOWNSTREAM_COLLECTION_PATH}" "${_build_dir}"/*.tar.gz
fi
rm -f "${_build_dir}"/*.tar.gz
}
f_common_steps()
{
f_log_info "${FUNCNAME[0]}"
f_prep
f_create_collection_dir_structure
f_text_sub
f_handle_doc_fragments_workaround
}
# Run the test sanity scanerio
f_test_sanity_option()
{
f_log_info "${FUNCNAME[0]}"
f_common_steps
pushd "${_build_dir}" || return
if command -v docker &> /dev/null
then
make sanity
else
SANITY_TEST_ARGS="--venv --color" make sanity
fi
f_log_info "SANITY TEST PWD: ${PWD}"
make sanity
popd || return
f_cleanup
}
# Run the test integration
f_test_integration_option()
{
f_log_info "${FUNCNAME[0]}"
f_common_steps
pushd "${_build_dir}" || return
f_log_info "INTEGRATION TEST WD: ${PWD}"
make molecule
popd || return
f_cleanup
}
# Run the test units
f_test_units_option()
{
f_log_info "${FUNCNAME[0]}"
f_common_steps
pushd "${_build_dir}" || return
if command -v docker &> /dev/null
then
make units
else
UNITS_TEST_ARGS="--venv --color" make units
fi
f_log_info "UNITS TEST PWD: ${PWD}"
make units
popd || return
f_cleanup
}
# Run the build scanerio
f_build_option()
{
f_log_info "${FUNCNAME[0]}"
f_common_steps
pushd "${_build_dir}" || return
f_log_info "BUILD WD: ${PWD}"
make build
popd || return
f_copy_collection_to_working_dir
f_cleanup
}
# If no options are passed, display usage and exit
if [[ "${#}" -eq "0" ]]; then
f_show_help
f_exit 0
fi
# Handle options
while getopts ":siurb" option
do
case $option in
s)
f_test_sanity_option
;;
i)
f_test_integration_option
;;
u)
f_test_units_option
;;
r)
f_release_option
;;
b)
f_build_option
;;
*)
printf "ERROR: Unimplemented option chosen.\n"
f_show_help
f_exit 1
;; # Default.
esac
done
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import sys
import yaml
with open("./rendereddocfragments.txt", 'w') as df_fd:
with open(sys.argv[2], 'r') as fd:
json_docs = json.load(fd)
json_docs[sys.argv[1]]['doc'].pop('collection', '')
json_docs[sys.argv[1]]['doc'].pop('filename', '')
json_docs[sys.argv[1]]['doc'].pop('has_action', '')
df_fd.write('DOCUMENTATION = """\n')
df_fd.write(yaml.dump(json_docs[sys.argv[1]]['doc'], default_flow_style=False))
df_fd.write('"""\n\n')
df_fd.write('EXAMPLES = """')
df_fd.write(json_docs[sys.argv[1]]['examples'])
df_fd.write('"""\n\n')
df_fd.write('RETURN = r"""')
data = json_docs[sys.argv[1]]['return']
if isinstance(data, dict):
df_fd.write(yaml.dump(data, default_flow_style=False))
else:
df_fd.write(data)
df_fd.write('"""\n\n')

View File

@@ -0,0 +1,92 @@
#!/bin/bash -eu
set -x
NAMESPACE=${NAMESPACE:-default}
# IMAGE_FORMAT is in the form $registry/$org/$image:$$component, ie
# quay.io/openshift/release:$component
# To test with your own image, build and push the test image
# (using the Dockerfile in ci/Dockerfile)
# and set the IMAGE_FORMAT environment variable so that it properly
# resolves to your image. For example, quay.io/mynamespace/$component
# would resolve to quay.io/mynamespace/molecule-test-runner
# shellcheck disable=SC2034
component='molecule-test-runner'
if [[ -n "${MOLECULE_IMAGE}" ]]; then
IMAGE="${MOLECULE_IMAGE}"
else
IMAGE="${IMAGE_FORMAT}"
fi
PULL_POLICY=${PULL_POLICY:-IfNotPresent}
if ! oc get namespace "$NAMESPACE"
then
oc create namespace "$NAMESPACE"
fi
oc project "$NAMESPACE"
oc adm policy add-cluster-role-to-user cluster-admin -z default
oc adm policy who-can create projectrequests
echo "Deleting test job if it exists"
oc delete job molecule-integration-test --wait --ignore-not-found
echo "Creating molecule test job"
cat << EOF | oc create -f -
---
apiVersion: batch/v1
kind: Job
metadata:
name: molecule-integration-test
spec:
template:
spec:
containers:
- name: test-runner
image: ${IMAGE}
imagePullPolicy: ${PULL_POLICY}
command:
- make
- test-integration
restartPolicy: Never
backoffLimit: 2
completions: 1
parallelism: 1
EOF
function check_success {
oc wait --for=condition=complete job/molecule-integration-test --timeout 5s -n "$NAMESPACE" \
&& oc logs job/molecule-integration-test \
&& echo "Molecule integration tests ran successfully" \
&& return 0
return 1
}
function check_failure {
oc wait --for=condition=failed job/molecule-integration-test --timeout 5s -n "$NAMESPACE" \
&& oc logs job/molecule-integration-test \
&& echo "Molecule integration tests failed, see logs for more information..." \
&& return 0
return 1
}
runtime="30 minute"
endtime=$(date -ud "$runtime" +%s)
echo "Waiting for test job to complete"
while [[ $(date -u +%s) -le $endtime ]]
do
if check_success
then
exit 0
elif check_failure
then
exit 1
fi
sleep 10
done
oc logs job/molecule-integration-test
exit 1

View File

@@ -0,0 +1,8 @@
---
coverage:
precision: 2
round: down
range: "70...100"
status:
project:
default: false

View File

@@ -0,0 +1,24 @@
---
requires_ansible: '>=2.9.17'
action_groups:
okd:
- k8s
- openshift_adm_groups_sync
- openshift_adm_migrate_template_instances
- openshift_adm_prune_auth
- openshift_adm_prune_deployments
- openshift_adm_prune_images
- openshift_auth
- openshift_import_image
- openshift_process
- openshift_registry_info
- openshift_route
plugin_routing:
modules:
k8s_auth:
redirect: community.okd.openshift_auth
action:
k8s:
redirect: kubernetes.core.k8s_info
openshift_adm_groups_sync:
redirect: kubernetes.core.k8s_info

View File

@@ -0,0 +1,19 @@
Wait tests
----------
wait tests require at least one node, and don't work on the normal k8s
openshift-origin container as provided by ansible-test --docker -v k8s
minikube, Kubernetes from Docker or any other Kubernetes service will
suffice.
If kubectl is already using the right config file and context, you can
just do
```
cd tests/integration/targets/okd
./runme.sh -vv
```
otherwise set one or both of `K8S_AUTH_KUBECONFIG` and `K8S_AUTH_CONTEXT`
and use the same command

View File

@@ -0,0 +1,99 @@
---
- name: Converge
hosts: localhost
connection: local
gather_facts: no
vars:
ansible_python_interpreter: '{{ virtualenv_interpreter }}'
vars_files:
- vars/main.yml
tasks:
# OpenShift Resources
- name: Create a project
community.okd.k8s:
name: testing
kind: Project
api_version: project.openshift.io/v1
apply: no
register: output
- name: show output
debug:
var: output
- name: Create deployment config
community.okd.k8s:
state: present
name: hello-world
namespace: testing
definition: '{{ okd_dc_template }}'
wait: yes
wait_condition:
type: Available
status: True
vars:
k8s_pod_name: hello-world
k8s_pod_image: python
k8s_pod_command:
- python
- '-m'
- http.server
k8s_pod_env:
- name: TEST
value: test
okd_dc_triggers:
- type: ConfigChange
register: output
- name: Show output
debug:
var: output
- vars:
image: docker.io/python
image_name: python
image_tag: latest
k8s_pod_image: python
k8s_pod_command:
- python
- '-m'
- http.server
namespace: idempotence-testing
block:
- name: Create a namespace
community.okd.k8s:
name: '{{ namespace }}'
kind: Namespace
api_version: v1
- name: Create imagestream
community.okd.k8s:
namespace: '{{ namespace }}'
definition: '{{ okd_imagestream_template }}'
- name: Create DeploymentConfig to reference ImageStream
community.okd.k8s:
name: '{{ k8s_pod_name }}'
namespace: '{{ namespace }}'
definition: '{{ okd_dc_template }}'
vars:
k8s_pod_name: is-idempotent-dc
- name: Create Deployment to reference ImageStream
community.okd.k8s:
name: '{{ k8s_pod_name }}'
namespace: '{{ namespace }}'
definition: '{{ k8s_deployment_template | combine(metadata) }}'
vars:
k8s_pod_annotations:
"alpha.image.policy.openshift.io/resolve-names": "*"
k8s_pod_name: is-idempotent-deployment
annotation:
- from:
kind: ImageStreamTag
name: "{{ image_name }}:{{ image_tag}}}"
fieldPath: 'spec.template.spec.containers[?(@.name=="{{ k8s_pod_name }}")].image}'
metadata:
metadata:
annotations:
image.openshift.io/triggers: '{{ annotation | to_json }}'

View File

@@ -0,0 +1,6 @@
---
- name: Destroy
hosts: localhost
connection: local
gather_facts: no
tasks: []

View File

@@ -0,0 +1,21 @@
---
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: acme-crt
spec:
secretName: acme-crt-secret
dnsNames:
- foo.example.com
- bar.example.com
acme:
config:
- ingressClass: nginx
domains:
- foo.example.com
- bar.example.com
issuerRef:
name: letsencrypt-prod
# We can reference ClusterIssuers by changing the kind here.
# The default value is Issuer (i.e. a locally namespaced Issuer)
kind: Issuer

View File

@@ -0,0 +1,9 @@
#
NAME=example
# Multiline values shouldn't break things
export CONTENT=This is a long message\
that may take one or more lines to parse\
but should still work without issue
# This shouldn't throw an error
UNUSED=

View File

@@ -0,0 +1,22 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: kuard
name: kuard
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: kuard
unwanted: value
template:
metadata:
labels:
app: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard

View File

@@ -0,0 +1,21 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: kuard
name: kuard
namespace: default
spec:
replicas: hello
selector:
matchLabels:
app: kuard
template:
metadata:
labels:
app: kuard
spec:
containers:
- image: gcr.io/kuar-demo/kuard-amd64:1
name: kuard

View File

@@ -0,0 +1,12 @@
# Want to make sure comments don't break it
export NAME=test123
NAMESPACE=openshift
# Blank lines should be fine too
# Equals in comments shouldn't break things=True
MEMORY_LIMIT=1Gi

View File

@@ -0,0 +1,23 @@
---
kind: Template
apiVersion: template.openshift.io/v1
metadata:
name: pod-template
objects:
- apiVersion: v1
kind: Pod
metadata:
name: "Pod-${{ NAME }}"
spec:
containers:
- args:
- /bin/sh
- -c
- while true; do echo $(date); sleep 15; done
image: python:3.7-alpine
imagePullPolicy: Always
name: python
parameters:
- name: NAME
description: trailing name of the pod
required: true

View File

@@ -0,0 +1,15 @@
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: certificates.certmanager.k8s.io
spec:
group: certmanager.k8s.io
version: v1alpha1
scope: Namespaced
names:
kind: Certificate
plural: certificates
shortNames:
- cert
- certs

View File

@@ -0,0 +1,34 @@
---
apiVersion: template.openshift.io/v1
kind: Template
labels:
template: simple-example-test
message: |-
The following configmaps have been created in your project: ${NAME}.
metadata:
annotations:
description: A super basic template for testing
openshift.io/display-name: Super basic template
openshift.io/provider-display-name: Red Hat, Inc.
tags: quickstart,examples
name: simple-example
objects:
- apiVersion: v1
kind: ConfigMap
metadata:
annotations:
description: Big example
name: ${NAME}
data:
content: "${CONTENT}"
parameters:
- description: The name assigned to the ConfigMap
displayName: Name
name: NAME
required: true
value: example
- description: The value for the content key of the configmap
displayName: Content
name: CONTENT
required: true
value: ''

View File

@@ -0,0 +1,49 @@
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: delegated
platforms:
- name: cluster
groups:
- k8s
provisioner:
name: ansible
log: true
options:
vvv: True
config_options:
inventory:
enable_plugins: community.okd.openshift
lint: |
set -e
ansible-lint
inventory:
hosts:
plugin: community.okd.openshift
host_vars:
localhost:
virtualenv: ${MOLECULE_EPHEMERAL_DIRECTORY}/virtualenv
virtualenv_command: '{{ ansible_playbook_python }} -m virtualenv'
virtualenv_interpreter: '{{ virtualenv }}/bin/python'
playbook_namespace: molecule-tests
env:
ANSIBLE_FORCE_COLOR: 'true'
ANSIBLE_COLLECTIONS_PATHS: ${OVERRIDE_COLLECTION_PATH:-$MOLECULE_PROJECT_DIRECTORY}
verifier:
name: ansible
lint: |
set -e
ansible-lint
scenario:
name: default
test_sequence:
- dependency
- lint
- syntax
- prepare
- converge
- idempotence
- verify

View File

@@ -0,0 +1,61 @@
---
- name: Prepare
hosts: localhost
connection: local
gather_facts: no
tasks:
- pip:
name: virtualenv
- pip:
name:
- kubernetes>=12.0.0
- coverage
- python-ldap
virtualenv: "{{ virtualenv }}"
virtualenv_command: "{{ virtualenv_command }}"
virtualenv_site_packages: no
- name: 'Configure htpasswd secret (username: test, password: testing123)'
community.okd.k8s:
definition:
apiVersion: v1
kind: Secret
metadata:
name: htpass-secret
namespace: openshift-config
stringData:
htpasswd: "test:$2y$05$zgjczyp96jCIp//CGmnWiefhd7G3l54IdsZoV4IwA1UWtd04L0lE2"
- name: Configure htpasswd identity provider
community.okd.k8s:
definition:
apiVersion: config.openshift.io/v1
kind: OAuth
metadata:
name: cluster
spec:
identityProviders:
- name: htpasswd_provider
mappingMethod: claim
type: HTPasswd
htpasswd:
fileData:
name: htpass-secret
- name: Create ClusterRoleBinding for test user
community.okd.k8s:
definition:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: test-cluster-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-reader
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: test

View File

@@ -0,0 +1,4 @@
---
ldap_admin_user: "admin"
ldap_admin_password: "testing123!"
ldap_root: "dc=ansible,dc=redhat"

View File

@@ -0,0 +1,186 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
module: openshift_ldap_entry
short_description: add/remove entry to LDAP Server.
author:
- Aubin Bikouo (@abikouo)
description:
- This module perform basic operations on the LDAP Server (add/remove entries).
- Similar to `community.general.ldap_entry` this has been created to avoid dependency with this collection for the test.
- This module is not supported outside of testing this collection.
options:
attributes:
description:
- If I(state=present), attributes necessary to create an entry. Existing
entries are never modified. To assert specific attribute values on an
existing entry, use M(community.general.ldap_attrs) module instead.
type: dict
objectClass:
description:
- If I(state=present), value or list of values to use when creating
the entry. It can either be a string or an actual list of
strings.
type: list
elements: str
state:
description:
- The target state of the entry.
choices: [present, absent]
default: present
type: str
bind_dn:
description:
- A DN to bind with. If this is omitted, we'll try a SASL bind with the EXTERNAL mechanism as default.
- If this is blank, we'll use an anonymous bind.
type: str
required: true
bind_pw:
description:
- The password to use with I(bind_dn).
type: str
dn:
required: true
description:
- The DN of the entry to add or remove.
type: str
server_uri:
description:
- A URI to the LDAP server.
- The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location.
type: str
default: ldapi:///
requirements:
- python-ldap
'''
EXAMPLES = r'''
'''
RETURN = r'''
# Default return values
'''
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_bytes
LDAP_IMP_ERR = None
try:
import ldap
import ldap.modlist
HAS_LDAP = True
except ImportError:
LDAP_IMP_ERR = traceback.format_exc()
HAS_LDAP = False
def argument_spec():
args = {}
args['attributes'] = dict(default={}, type='dict')
args['objectClass'] = dict(type='list', elements='str')
args['state'] = dict(default='present', choices=['present', 'absent'])
args['bind_dn'] = dict(required=True)
args['bind_pw'] = dict(default='', no_log=True)
args['dn'] = dict(required=True)
args['server_uri'] = dict(default='ldapi:///')
return args
class LdapEntry(AnsibleModule):
def __init__(self):
AnsibleModule.__init__(
self,
argument_spec=argument_spec(),
required_if=[('state', 'present', ['objectClass'])],
)
if not HAS_LDAP:
self.fail_json(msg=missing_required_lib('python-ldap'), exception=LDAP_IMP_ERR)
self.__connection = None
# Add the objectClass into the list of attributes
self.params['attributes']['objectClass'] = (self.params['objectClass'])
# Load attributes
if self.params['state'] == 'present':
self.attrs = {}
for name, value in self.params['attributes'].items():
if isinstance(value, list):
self.attrs[name] = list(map(to_bytes, value))
else:
self.attrs[name] = [to_bytes(value)]
@property
def connection(self):
if not self.__connection:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
self.__connection = ldap.initialize(self.params['server_uri'])
try:
self.__connection.simple_bind_s(self.params['bind_dn'], self.params['bind_pw'])
except ldap.LDAPError as e:
self.fail_json(msg="Cannot bind to the server due to: %s" % e)
return self.__connection
def add(self):
""" If self.dn does not exist, returns a callable that will add it. """
changed = False
msg = "LDAP Entry '%s' already exist." % self.params["dn"]
if not self._is_entry_present():
modlist = ldap.modlist.addModlist(self.attrs)
self.connection.add_s(self.params['dn'], modlist)
changed = True
msg = "LDAP Entry '%s' successfully created." % self.params["dn"]
self.exit_json(changed=changed, msg=msg)
def delete(self):
""" If self.dn exists, returns a callable that will delete it. """
changed = False
msg = "LDAP Entry '%s' does not exist." % self.params["dn"]
if self._is_entry_present():
self.connection.delete_s(self.params['dn'])
changed = True
msg = "LDAP Entry '%s' successfully deleted." % self.params["dn"]
self.exit_json(changed=changed, msg=msg)
def _is_entry_present(self):
try:
self.connection.search_s(self.params['dn'], ldap.SCOPE_BASE)
except ldap.NO_SUCH_OBJECT:
is_present = False
else:
is_present = True
return is_present
def execute(self):
try:
if self.params['state'] == 'present':
self.add()
else:
self.delete()
except Exception as e:
self.fail_json(msg="Entry action failed.", details=to_native(e), exception=traceback.format_exc())
def main():
module = LdapEntry()
module.execute()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,109 @@
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
module: openshift_ldap_entry_info
short_description: Validate entry from LDAP server.
author:
- Aubin Bikouo (@abikouo)
description:
- This module connect to a ldap server and search for entry.
- This module is not supported outside of testing this collection.
options:
bind_dn:
description:
- A DN to bind with. If this is omitted, we'll try a SASL bind with the EXTERNAL mechanism as default.
- If this is blank, we'll use an anonymous bind.
type: str
required: true
bind_pw:
description:
- The password to use with I(bind_dn).
type: str
required: True
dn:
description:
- The DN of the entry to test.
type: str
required: True
server_uri:
description:
- A URI to the LDAP server.
- The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location.
type: str
default: ldapi:///
required: True
requirements:
- python-ldap
'''
EXAMPLES = r'''
'''
RETURN = r'''
# Default return values
'''
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
LDAP_IMP_ERR = None
try:
import ldap
import ldap.modlist
HAS_LDAP = True
except ImportError:
LDAP_IMP_ERR = traceback.format_exc()
HAS_LDAP = False
def argument_spec():
args = {}
args['bind_dn'] = dict(required=True)
args['bind_pw'] = dict(required=True, no_log=True)
args['dn'] = dict(required=True)
args['server_uri'] = dict(required=True)
return args
def execute():
module = AnsibleModule(
argument_spec=argument_spec(),
supports_check_mode=True
)
if not HAS_LDAP:
module.fail_json(msg=missing_required_lib("python-ldap"), exception=LDAP_IMP_ERR)
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
connection = ldap.initialize(module.params['server_uri'])
try:
connection.simple_bind_s(module.params['bind_dn'], module.params['bind_pw'])
except ldap.LDAPError as e:
module.fail_json(msg="Cannot bind to the server due to: %s" % e)
try:
connection.search_s(module.params['dn'], ldap.SCOPE_BASE)
module.exit_json(changed=False, found=True)
except ldap.NO_SUCH_OBJECT:
module.exit_json(changed=False, found=False)
def main():
execute()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,4 @@
---
collections:
- community.okd
- kubernetes.core

View File

@@ -0,0 +1,235 @@
- block:
- name: Get LDAP definition
set_fact:
ldap_entries: "{{ lookup('template', 'ad/definition.j2') | from_yaml }}"
- name: Delete openshift groups if existing
community.okd.k8s:
state: absent
kind: Group
version: "user.openshift.io/v1"
name: "{{ item }}"
with_items:
- admins
- developers
- name: Delete existing LDAP Entries
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
state: absent
with_items: "{{ ldap_entries.users + ldap_entries.units | reverse | list }}"
- name: Create LDAP Entries
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
attributes: "{{ item.attr }}"
objectClass: "{{ item.class }}"
with_items: "{{ ldap_entries.units + ldap_entries.users }}"
- name: Load test configurations
set_fact:
sync_config: "{{ lookup('template', 'ad/sync-config.j2') | from_yaml }}"
- name: Synchronize Groups
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
check_mode: yes
register: result
- name: Validate Group going to be created
assert:
that:
- result is changed
- admins_group
- devs_group
- '"jane.smith@ansible.org" in {{ admins_group.users }}'
- '"jim.adams@ansible.org" in {{ admins_group.users }}'
- '"jordanbulls@ansible.org" in {{ devs_group.users }}'
- admins_group.users | length == 2
- devs_group.users | length == 1
vars:
admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'admins') | first }}"
devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'developers') | first }}"
- name: Synchronize Groups (Remove check_mode)
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
register: result
- name: Validate Group going to be created
assert:
that:
- result is changed
- name: Read admins group
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: admins
register: result
- name: Validate group was created
assert:
that:
- result.resources | length == 1
- '"jane.smith@ansible.org" in {{ result.resources.0.users }}'
- '"jim.adams@ansible.org" in {{ result.resources.0.users }}'
- name: Read developers group
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: developers
register: result
- name: Validate group was created
assert:
that:
- result.resources | length == 1
- '"jordanbulls@ansible.org" in {{ result.resources.0.users }}'
- name: Define user dn to delete
set_fact:
user_to_delete: "cn=Jane,ou=engineers,ou=activeD,{{ ldap_root }}"
- name: Delete 1 admin user
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ user_to_delete }}"
state: absent
- name: Synchronize Openshift groups using allow_groups
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
allow_groups:
- developers
type: openshift
register: openshift_sync
- name: Validate that only developers group was sync
assert:
that:
- openshift_sync is changed
- openshift_sync.groups | length == 1
- openshift_sync.groups.0.metadata.name == "developers"
- name: Read admins group
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: admins
register: result
- name: Validate admins group content has not changed
assert:
that:
- result.resources | length == 1
- '"jane.smith@ansible.org" in {{ result.resources.0.users }}'
- '"jim.adams@ansible.org" in {{ result.resources.0.users }}'
- name: Synchronize Openshift groups using deny_groups
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
deny_groups:
- developers
type: openshift
register: openshift_sync
- name: Validate that only admins group was sync
assert:
that:
- openshift_sync is changed
- openshift_sync.groups | length == 1
- openshift_sync.groups.0.metadata.name == "admins"
- name: Read admins group
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: admins
register: result
- name: Validate admins group contains only 1 user now
assert:
that:
- result.resources | length == 1
- result.resources.0.users == ["jim.adams@ansible.org"]
- name: Set users to delete (delete all developers users)
set_fact:
user_to_delete: "cn=Jordan,ou=engineers,ou=activeD,{{ ldap_root }}"
- name: Delete 1 admin user
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ user_to_delete }}"
state: absent
- name: Prune groups
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
state: absent
register: result
- name: Validate result is changed (only developers group be deleted)
assert:
that:
- result is changed
- result.groups | length == 1
- name: Get developers group info
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: developers
register: result
- name: assert group was deleted
assert:
that:
- result.resources | length == 0
- name: Get admins group info
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: admins
register: result
- name: assert group was not deleted
assert:
that:
- result.resources | length == 1
- name: Prune groups once again (idempotency)
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
state: absent
register: result
- name: Assert nothing was changed
assert:
that:
- result is not changed
always:
- name: Delete openshift groups if existing
community.okd.k8s:
state: absent
kind: Group
version: "user.openshift.io/v1"
name: "{{ item }}"
with_items:
- admins
- developers

View File

@@ -0,0 +1,174 @@
- block:
- name: Get LDAP definition
set_fact:
ldap_entries: "{{ lookup('template', 'augmented-ad/definition.j2') | from_yaml }}"
- name: Delete openshift groups if existing
community.okd.k8s:
state: absent
kind: Group
version: "user.openshift.io/v1"
name: "{{ item }}"
with_items:
- banking
- insurance
- name: Delete existing LDAP entries
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
state: absent
with_items: "{{ ldap_entries.users + ldap_entries.groups + ldap_entries.units | reverse | list }}"
- name: Create LDAP Entries
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
attributes: "{{ item.attr }}"
objectClass: "{{ item.class }}"
with_items: "{{ ldap_entries.units + ldap_entries.groups + ldap_entries.users }}"
- name: Load test configurations
set_fact:
sync_config: "{{ lookup('template', 'augmented-ad/sync-config.j2') | from_yaml }}"
- name: Synchronize Groups
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
check_mode: yes
register: result
- name: Validate that 'banking' and 'insurance' groups were created
assert:
that:
- result is changed
- banking_group
- insurance_group
- '"james-allan@ansible.org" in {{ banking_group.users }}'
- '"gordon-kane@ansible.org" in {{ banking_group.users }}'
- '"alice-courtney@ansible.org" in {{ insurance_group.users }}'
- banking_group.users | length == 2
- insurance_group.users | length == 1
vars:
banking_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'banking') | first }}"
insurance_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'insurance') | first }}"
- name: Synchronize Groups (Remove check_mode)
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
register: result
- name: Validate Group going to be created
assert:
that:
- result is changed
- name: Define facts for group to create
set_fact:
ldap_groups:
- name: banking
users:
- "james-allan@ansible.org"
- "gordon-kane@ansible.org"
- name: insurance
users:
- "alice-courtney@ansible.org"
- name: Read 'banking' openshift group
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: banking
register: result
- name: Validate group info
assert:
that:
- result.resources | length == 1
- '"james-allan@ansible.org" in {{ result.resources.0.users }}'
- '"gordon-kane@ansible.org" in {{ result.resources.0.users }}'
- name: Read 'insurance' openshift group
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: insurance
register: result
- name: Validate group info
assert:
that:
- result.resources | length == 1
- 'result.resources.0.users == ["alice-courtney@ansible.org"]'
- name: Delete employee from 'insurance' group
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "cn=Alice,ou=employee,ou=augmentedAD,{{ ldap_root }}"
state: absent
- name: Prune groups
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
state: absent
register: result
- name: Validate result is changed (only insurance group be deleted)
assert:
that:
- result is changed
- result.groups | length == 1
- name: Get 'insurance' openshift group info
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: insurance
register: result
- name: assert group was deleted
assert:
that:
- result.resources | length == 0
- name: Get 'banking' openshift group info
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: banking
register: result
- name: assert group was not deleted
assert:
that:
- result.resources | length == 1
- name: Prune groups once again (idempotency)
community.okd.openshift_adm_groups_sync:
config: "{{ sync_config }}"
state: absent
register: result
- name: Assert no change was made
assert:
that:
- result is not changed
always:
- name: Delete openshift groups if existing
community.okd.k8s:
state: absent
kind: Group
version: "user.openshift.io/v1"
name: "{{ item }}"
with_items:
- banking
- insurance

View File

@@ -0,0 +1,61 @@
---
- name: Get cluster information
kubernetes.core.k8s_cluster_info:
register: info
- name: Create LDAP Pod
community.okd.k8s:
namespace: "default"
wait: yes
definition:
kind: Pod
apiVersion: v1
metadata:
name: ldap-pod
labels:
app: ldap
spec:
containers:
- name: ldap
image: bitnami/openldap
env:
- name: LDAP_ADMIN_USERNAME
value: "{{ ldap_admin_user }}"
- name: LDAP_ADMIN_PASSWORD
value: "{{ ldap_admin_password }}"
- name: LDAP_USERS
value: "ansible"
- name: LDAP_PASSWORDS
value: "ansible123"
- name: LDAP_ROOT
value: "{{ ldap_root }}"
ports:
- containerPort: 1389
register: pod_info
- name: Set Pod Internal IP
set_fact:
podIp: "{{ pod_info.result.status.podIP }}"
- name: Set LDAP Common facts
set_fact:
ldap_server_uri: "ldap://{{ podIp }}:1389"
ldap_bind_dn: "cn={{ ldap_admin_user }},{{ ldap_root }}"
ldap_bind_pw: "{{ ldap_admin_password }}"
- name: Display LDAP Server URI
debug:
var: ldap_server_uri
- name: Test existing user from LDAP server
openshift_ldap_entry_info:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
dn: "ou=users,{{ ldap_root }}"
server_uri: "{{ ldap_server_uri }}"
# ignore_errors: true
# register: ping_ldap
- include_tasks: "tasks/rfc2307.yml"
- include_tasks: "tasks/activeDirectory.yml"
- include_tasks: "tasks/augmentedActiveDirectory.yml"

View File

@@ -0,0 +1,468 @@
- block:
- name: Get LDAP definition
set_fact:
ldap_resources: "{{ lookup('template', 'rfc2307/definition.j2') | from_yaml }}"
- name: Delete openshift groups if existing
community.okd.k8s:
state: absent
kind: Group
version: "user.openshift.io/v1"
name: "{{ item }}"
with_items:
- admins
- engineers
- developers
- name: Delete existing LDAP entries
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
state: absent
with_items: "{{ ldap_resources.users + ldap_resources.groups + ldap_resources.units | reverse | list }}"
- name: Create LDAP units
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
attributes: "{{ item.attr }}"
objectClass: "{{ item.class }}"
with_items: "{{ ldap_resources.units }}"
- name: Create LDAP Groups
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
attributes: "{{ item.attr }}"
objectClass: "{{ item.class }}"
with_items: "{{ ldap_resources.groups }}"
- name: Create LDAP users
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item.dn }}"
attributes: "{{ item.attr }}"
objectClass: "{{ item.class }}"
with_items: "{{ ldap_resources.users }}"
- name: Load test configurations
set_fact:
configs: "{{ lookup('template', 'rfc2307/sync-config.j2') | from_yaml }}"
- name: Synchronize Groups
community.okd.openshift_adm_groups_sync:
config: "{{ configs.simple }}"
check_mode: yes
register: result
- name: Validate Group going to be created
assert:
that:
- result is changed
- admins_group
- devs_group
- '"jane.smith@ansible.org" in {{ admins_group.users }}'
- '"jim.adams@ansible.org" in {{ devs_group.users }}'
- '"jordanbulls@ansible.org" in {{ devs_group.users }}'
- admins_group.users | length == 1
- devs_group.users | length == 2
vars:
admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'admins') | first }}"
devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'developers') | first }}"
- name: Synchronize Groups - User defined mapping
community.okd.openshift_adm_groups_sync:
config: "{{ configs.user_defined }}"
check_mode: yes
register: result
- name: Validate Group going to be created
assert:
that:
- result is changed
- admins_group
- devs_group
- '"jane.smith@ansible.org" in {{ admins_group.users }}'
- '"jim.adams@ansible.org" in {{ devs_group.users }}'
- '"jordanbulls@ansible.org" in {{ devs_group.users }}'
- admins_group.users | length == 1
- devs_group.users | length == 2
vars:
admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'ansible-admins') | first }}"
devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'ansible-devs') | first }}"
- name: Synchronize Groups - Using dn for every query
community.okd.openshift_adm_groups_sync:
config: "{{ configs.dn_everywhere }}"
check_mode: yes
register: result
- name: Validate Group going to be created
assert:
that:
- result is changed
- admins_group
- devs_group
- '"cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }}" in {{ admins_group.users }}'
- '"cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }}" in {{ devs_group.users }}'
- '"cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }}" in {{ devs_group.users }}'
- admins_group.users | length == 1
- devs_group.users | length == 2
vars:
admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'cn=admins,ou=groups,ou=rfc2307,' + ldap_root ) | first }}"
devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'cn=developers,ou=groups,ou=rfc2307,' + ldap_root ) | first }}"
- name: Synchronize Groups - Partially user defined mapping
community.okd.openshift_adm_groups_sync:
config: "{{ configs.partially_user_defined }}"
check_mode: yes
register: result
- name: Validate Group going to be created
assert:
that:
- result is changed
- admins_group
- devs_group
- '"jane.smith@ansible.org" in {{ admins_group.users }}'
- '"jim.adams@ansible.org" in {{ devs_group.users }}'
- '"jordanbulls@ansible.org" in {{ devs_group.users }}'
- admins_group.users | length == 1
- devs_group.users | length == 2
vars:
admins_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'ansible-admins') | first }}"
devs_group: "{{ result.groups | selectattr('metadata.name', 'equalto', 'developers') | first }}"
- name: Delete Group 'engineers' if created before
community.okd.k8s:
state: absent
kind: Group
version: "user.openshift.io/v1"
name: 'engineers'
wait: yes
ignore_errors: yes
- name: Synchronize Groups - Partially user defined mapping
community.okd.openshift_adm_groups_sync:
config: "{{ configs.out_scope }}"
check_mode: yes
register: result
ignore_errors: yes
- name: Assert group sync failed due to non-existent member
assert:
that:
- result is failed
- result.msg.startswith("Entry not found for base='cn=Matthew,ou=people,ou=outrfc2307,{{ ldap_root }}'")
- name: Define sync configuration with tolerateMemberNotFoundErrors
set_fact:
config_out_of_scope_tolerate_not_found: "{{ configs.out_scope | combine({'rfc2307': merge_rfc2307 })}}"
vars:
merge_rfc2307: "{{ configs.out_scope.rfc2307 | combine({'tolerateMemberNotFoundErrors': 'true'}) }}"
- name: Synchronize Groups - Partially user defined mapping (tolerateMemberNotFoundErrors=true)
community.okd.openshift_adm_groups_sync:
config: "{{ config_out_of_scope_tolerate_not_found }}"
check_mode: yes
register: result
- name: Assert group sync did not fail (tolerateMemberNotFoundErrors=true)
assert:
that:
- result is changed
- result.groups | length == 1
- result.groups.0.metadata.name == 'engineers'
- result.groups.0.users == ['Abraham']
- name: Create Group 'engineers'
community.okd.k8s:
state: present
wait: yes
definition:
kind: Group
apiVersion: "user.openshift.io/v1"
metadata:
name: engineers
users: []
- name: Try to sync LDAP group with Openshift existing group not created using sync should failed
community.okd.openshift_adm_groups_sync:
config: "{{ config_out_of_scope_tolerate_not_found }}"
check_mode: yes
register: result
ignore_errors: yes
- name: Validate group sync failed
assert:
that:
- result is failed
- '"openshift.io/ldap.host label did not match sync host" in result.msg'
- name: Define allow_groups and deny_groups groups
set_fact:
allow_groups:
- "cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}"
deny_groups:
- "cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}"
- name: Synchronize Groups using allow_groups
community.okd.openshift_adm_groups_sync:
config: "{{ configs.simple }}"
allow_groups: "{{ allow_groups }}"
register: result
check_mode: yes
- name: Validate Group going to be created
assert:
that:
- result is changed
- result.groups | length == 1
- result.groups.0.metadata.name == "developers"
- name: Synchronize Groups using deny_groups
community.okd.openshift_adm_groups_sync:
config: "{{ configs.simple }}"
deny_groups: "{{ deny_groups }}"
register: result
check_mode: yes
- name: Validate Group going to be created
assert:
that:
- result is changed
- result.groups | length == 1
- result.groups.0.metadata.name == "developers"
- name: Synchronize groups, remove check_mode
community.okd.openshift_adm_groups_sync:
config: "{{ configs.simple }}"
register: result
- name: Validate result is changed
assert:
that:
- result is changed
- name: Read Groups
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: admins
register: result
- name: Validate group was created
assert:
that:
- result.resources | length == 1
- '"jane.smith@ansible.org" in {{ result.resources.0.users }}'
- name: Read Groups
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: developers
register: result
- name: Validate group was created
assert:
that:
- result.resources | length == 1
- '"jim.adams@ansible.org" in {{ result.resources.0.users }}'
- '"jordanbulls@ansible.org" in {{ result.resources.0.users }}'
- name: Set users to delete (no admins users anymore and only 1 developer kept)
set_fact:
users_to_delete:
- "cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }}"
- "cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }}"
- name: Delete users from LDAP servers
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item }}"
state: absent
with_items: "{{ users_to_delete }}"
- name: Define sync configuration with tolerateMemberNotFoundErrors
set_fact:
config_simple_tolerate_not_found: "{{ configs.simple | combine({'rfc2307': merge_rfc2307 })}}"
vars:
merge_rfc2307: "{{ configs.simple.rfc2307 | combine({'tolerateMemberNotFoundErrors': 'true'}) }}"
- name: Synchronize groups once again after users deletion
community.okd.openshift_adm_groups_sync:
config: "{{ config_simple_tolerate_not_found }}"
register: result
- name: Validate result is changed
assert:
that:
- result is changed
- name: Read Groups
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: admins
register: result
- name: Validate admins group does not contains users anymore
assert:
that:
- result.resources | length == 1
- result.resources.0.users == []
- name: Read Groups
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: developers
register: result
- name: Validate group was created
assert:
that:
- result.resources | length == 1
- '"jordanbulls@ansible.org" in {{ result.resources.0.users }}'
- name: Set group to delete
set_fact:
groups_to_delete:
- "cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}"
- name: Delete Group from LDAP servers
openshift_ldap_entry:
bind_dn: "{{ ldap_bind_dn }}"
bind_pw: "{{ ldap_bind_pw }}"
server_uri: "{{ ldap_server_uri }}"
dn: "{{ item }}"
state: absent
with_items: "{{ groups_to_delete }}"
- name: Prune groups
community.okd.openshift_adm_groups_sync:
config: "{{ config_simple_tolerate_not_found }}"
state: absent
register: result
check_mode: yes
- name: Validate that only developers group is candidate for Prune
assert:
that:
- result is changed
- result.groups | length == 1
- result.groups.0.metadata.name == "developers"
- name: Read Group (validate that check_mode did not performed update in the cluster)
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: developers
register: result
- name: Assert group was found
assert:
that:
- result.resources | length == 1
- name: Prune using allow_groups
community.okd.openshift_adm_groups_sync:
config: "{{ config_simple_tolerate_not_found }}"
allow_groups:
- developers
state: absent
register: result
check_mode: yes
- name: assert developers group was candidate for prune
assert:
that:
- result is changed
- result.groups | length == 1
- result.groups.0.metadata.name == "developers"
- name: Prune using deny_groups
community.okd.openshift_adm_groups_sync:
config: "{{ config_simple_tolerate_not_found }}"
deny_groups:
- developers
state: absent
register: result
check_mode: yes
- name: assert nothing found candidate for prune
assert:
that:
- result is not changed
- result.groups | length == 0
- name: Prune groups
community.okd.openshift_adm_groups_sync:
config: "{{ config_simple_tolerate_not_found }}"
state: absent
register: result
- name: Validate result is changed
assert:
that:
- result is changed
- result.groups | length == 1
- name: Get developers group info
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: developers
register: result
- name: assert group was deleted
assert:
that:
- result.resources | length == 0
- name: Get admins group info
kubernetes.core.k8s_info:
kind: Group
version: "user.openshift.io/v1"
name: admins
register: result
- name: assert group was not deleted
assert:
that:
- result.resources | length == 1
- name: Prune groups once again (idempotency)
community.okd.openshift_adm_groups_sync:
config: "{{ config_simple_tolerate_not_found }}"
state: absent
register: result
- name: Assert nothing changed
assert:
that:
- result is not changed
- result.groups | length == 0
always:
- name: Delete openshift groups if existing
community.okd.k8s:
state: absent
kind: Group
version: "user.openshift.io/v1"
name: "{{ item }}"
with_items:
- admins
- engineers
- developers

View File

@@ -0,0 +1,39 @@
units:
- dn: "ou=activeD,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: activeD
- dn: "ou=engineers,ou=activeD,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: engineers
users:
- dn: cn=Jane,ou=engineers,ou=activeD,{{ ldap_root }}
class:
- inetOrgPerson
attr:
cn: Jane
sn: Smith
displayName: Jane Smith
mail: jane.smith@ansible.org
employeeType: admins
- dn: cn=Jim,ou=engineers,ou=activeD,{{ ldap_root }}
class:
- inetOrgPerson
attr:
cn: Jim
sn: Adams
displayName: Jim Adams
mail: jim.adams@ansible.org
employeeType: admins
- dn: cn=Jordan,ou=engineers,ou=activeD,{{ ldap_root }}
class:
- inetOrgPerson
attr:
cn: Jordan
sn: Bulls
displayName: Jordan Bulls
mail: jordanbulls@ansible.org
employeeType: developers

View File

@@ -0,0 +1,12 @@
kind: LDAPSyncConfig
apiVersion: v1
url: "{{ ldap_server_uri }}"
insecure: true
activeDirectory:
usersQuery:
baseDN: "ou=engineers,ou=activeD,{{ ldap_root }}"
scope: sub
derefAliases: never
filter: (objectclass=inetOrgPerson)
userNameAttributes: [ mail ]
groupMembershipAttributes: [ employeeType ]

View File

@@ -0,0 +1,59 @@
units:
- dn: "ou=augmentedAD,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: augmentedAD
- dn: "ou=employee,ou=augmentedAD,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: employee
- dn: "ou=category,ou=augmentedAD,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: category
groups:
- dn: "cn=banking,ou=category,ou=augmentedAD,{{ ldap_root }}"
class:
- groupOfNames
attr:
cn: banking
description: Banking employees
member:
- cn=James,ou=employee,ou=augmentedAD,{{ ldap_root }}
- cn=Gordon,ou=employee,ou=augmentedAD,{{ ldap_root }}
- dn: "cn=insurance,ou=category,ou=augmentedAD,{{ ldap_root }}"
class:
- groupOfNames
attr:
cn: insurance
description: Insurance employees
member:
- cn=Alice,ou=employee,ou=augmentedAD,{{ ldap_root }}
users:
- dn: cn=James,ou=employee,ou=augmentedAD,{{ ldap_root }}
class:
- inetOrgPerson
attr:
cn: James
sn: Allan
mail: james-allan@ansible.org
businessCategory: cn=banking,ou=category,ou=augmentedAD,{{ ldap_root }}
- dn: cn=Gordon,ou=employee,ou=augmentedAD,{{ ldap_root }}
class:
- inetOrgPerson
attr:
cn: Gordon
sn: Kane
mail: gordon-kane@ansible.org
businessCategory: cn=banking,ou=category,ou=augmentedAD,{{ ldap_root }}
- dn: cn=Alice,ou=employee,ou=augmentedAD,{{ ldap_root }}
class:
- inetOrgPerson
attr:
cn: Alice
sn: Courtney
mail: alice-courtney@ansible.org
businessCategory: cn=insurance,ou=category,ou=augmentedAD,{{ ldap_root }}

View File

@@ -0,0 +1,20 @@
kind: LDAPSyncConfig
apiVersion: v1
url: "{{ ldap_server_uri }}"
insecure: true
augmentedActiveDirectory:
groupsQuery:
baseDN: "ou=category,ou=augmentedAD,{{ ldap_root }}"
scope: sub
derefAliases: never
pageSize: 0
groupUIDAttribute: dn
groupNameAttributes: [ cn ]
usersQuery:
baseDN: "ou=employee,ou=augmentedAD,{{ ldap_root }}"
scope: sub
derefAliases: never
filter: (objectclass=inetOrgPerson)
pageSize: 0
userNameAttributes: [ mail ]
groupMembershipAttributes: [ businessCategory ]

View File

@@ -0,0 +1,102 @@
units:
- dn: "ou=rfc2307,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: rfc2307
- dn: "ou=groups,ou=rfc2307,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: groups
- dn: "ou=people,ou=rfc2307,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: people
- dn: "ou=outrfc2307,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: outrfc2307
- dn: "ou=groups,ou=outrfc2307,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: groups
- dn: "ou=people,ou=outrfc2307,{{ ldap_root }}"
class:
- organizationalUnit
attr:
ou: people
groups:
- dn: "cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}"
class:
- groupOfNames
attr:
cn: admins
description: System Administrators
member:
- cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }}
- dn: "cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}"
class:
- groupOfNames
attr:
cn: developers
description: Developers
member:
- cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }}
- cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }}
- dn: "cn=engineers,ou=groups,ou=outrfc2307,{{ ldap_root }}"
class:
- groupOfNames
attr:
cn: engineers
description: Engineers
member:
- cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }}
- cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }}
- cn=Julia,ou=people,ou=outrfc2307,{{ ldap_root }}
- cn=Matthew,ou=people,ou=outrfc2307,{{ ldap_root }}
users:
- dn: cn=Jane,ou=people,ou=rfc2307,{{ ldap_root }}
class:
- person
- organizationalPerson
- inetOrgPerson
attr:
cn: Jane
sn: Smith
displayName: Jane Smith
mail: jane.smith@ansible.org
admin: yes
- dn: cn=Jim,ou=people,ou=rfc2307,{{ ldap_root }}
class:
- person
- organizationalPerson
- inetOrgPerson
attr:
cn: Jim
sn: Adams
displayName: Jim Adams
mail: jim.adams@ansible.org
- dn: cn=Jordan,ou=people,ou=rfc2307,{{ ldap_root }}
class:
- person
- organizationalPerson
- inetOrgPerson
attr:
cn: Jordan
sn: Bulls
displayName: Jordan Bulls
mail: jordanbulls@ansible.org
- dn: cn=Julia,ou=people,ou=outrfc2307,{{ ldap_root }}
class:
- person
- organizationalPerson
- inetOrgPerson
attr:
cn: Julia
sn: Abraham
displayName: Julia Abraham
mail: juliaabraham@ansible.org

View File

@@ -0,0 +1,105 @@
simple:
kind: LDAPSyncConfig
apiVersion: v1
url: "{{ ldap_server_uri }}"
insecure: true
rfc2307:
groupsQuery:
baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
filter: (objectclass=groupOfNames)
groupUIDAttribute: dn
groupNameAttributes: [ cn ]
groupMembershipAttributes: [ member ]
usersQuery:
baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
userUIDAttribute: dn
userNameAttributes: [ mail ]
user_defined:
kind: LDAPSyncConfig
apiVersion: v1
url: "{{ ldap_server_uri }}"
insecure: true
groupUIDNameMapping:
"cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}": ansible-admins
"cn=developers,ou=groups,ou=rfc2307,{{ ldap_root }}": ansible-devs
rfc2307:
groupsQuery:
baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
filter: (objectclass=groupOfNames)
groupUIDAttribute: dn
groupNameAttributes: [ cn ]
groupMembershipAttributes: [ member ]
usersQuery:
baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
userUIDAttribute: dn
userNameAttributes: [ mail ]
partially_user_defined:
kind: LDAPSyncConfig
apiVersion: v1
url: "{{ ldap_server_uri }}"
insecure: true
groupUIDNameMapping:
"cn=admins,ou=groups,ou=rfc2307,{{ ldap_root }}": ansible-admins
rfc2307:
groupsQuery:
baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
filter: (objectclass=groupOfNames)
groupUIDAttribute: dn
groupNameAttributes: [ cn ]
groupMembershipAttributes: [ member ]
usersQuery:
baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
userUIDAttribute: dn
userNameAttributes: [ mail ]
dn_everywhere:
kind: LDAPSyncConfig
apiVersion: v1
url: "{{ ldap_server_uri }}"
insecure: true
rfc2307:
groupsQuery:
baseDN: "ou=groups,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
filter: (objectclass=groupOfNames)
groupUIDAttribute: dn
groupNameAttributes: [ dn ]
groupMembershipAttributes: [ member ]
usersQuery:
baseDN: "ou=people,ou=rfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
userUIDAttribute: dn
userNameAttributes: [ dn ]
out_scope:
kind: LDAPSyncConfig
apiVersion: v1
url: "{{ ldap_server_uri }}"
insecure: true
rfc2307:
groupsQuery:
baseDN: "ou=groups,ou=outrfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
filter: (objectclass=groupOfNames)
groupUIDAttribute: dn
groupNameAttributes: [ cn ]
groupMembershipAttributes: [ member ]
usersQuery:
baseDN: "ou=people,ou=outrfc2307,{{ ldap_root }}"
scope: sub
derefAliases: never
userUIDAttribute: dn
userNameAttributes: [ sn ]

View File

@@ -0,0 +1,319 @@
- block:
- set_fact:
test_sa: "clusterrole-sa"
test_ns: "clusterrole-ns"
- name: Ensure namespace
kubernetes.core.k8s:
kind: Namespace
name: "{{ test_ns }}"
- name: Get cluster information
kubernetes.core.k8s_cluster_info:
register: cluster_info
no_log: true
- set_fact:
cluster_host: "{{ cluster_info['connection']['host'] }}"
- name: Create Service account
kubernetes.core.k8s:
definition:
apiVersion: v1
kind: ServiceAccount
metadata:
name: "{{ test_sa }}"
namespace: "{{ test_ns }}"
- name: Read Service Account
kubernetes.core.k8s_info:
kind: ServiceAccount
namespace: "{{ test_ns }}"
name: "{{ test_sa }}"
register: result
- set_fact:
secret_token: "{{ result.resources[0]['secrets'][0]['name'] }}"
- name: Get secret details
kubernetes.core.k8s_info:
kind: Secret
namespace: '{{ test_ns }}'
name: '{{ secret_token }}'
register: _secret
retries: 10
delay: 10
until:
- ("'openshift.io/token-secret.value' in _secret.resources[0]['metadata']['annotations']") or ("'token' in _secret.resources[0]['data']")
- set_fact:
api_token: "{{ _secret.resources[0]['metadata']['annotations']['openshift.io/token-secret.value'] }}"
when: "'openshift.io/token-secret.value' in _secret.resources[0]['metadata']['annotations']"
- set_fact:
api_token: "{{ _secret.resources[0]['data']['token'] | b64decode }}"
when: "'token' in _secret.resources[0]['data']"
- name: list Node should failed (forbidden user)
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Node
register: error
ignore_errors: true
- assert:
that:
- '"nodes is forbidden: User" in error.msg'
- name: list Pod for all namespace should failed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
register: error
ignore_errors: true
- assert:
that:
- '"pods is forbidden: User" in error.msg'
- name: list Pod for test namespace should failed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
namespace: "{{ test_ns }}"
register: error
ignore_errors: true
- assert:
that:
- '"pods is forbidden: User" in error.msg'
- set_fact:
test_labels:
phase: dev
cluster_roles:
- name: pod-manager
resources:
- pods
verbs:
- list
api_version_binding: "authorization.openshift.io/v1"
- name: node-manager
resources:
- nodes
verbs:
- list
api_version_binding: "rbac.authorization.k8s.io/v1"
- name: Create cluster roles
kubernetes.core.k8s:
definition:
kind: ClusterRole
apiVersion: "rbac.authorization.k8s.io/v1"
metadata:
name: "{{ item.name }}"
labels: "{{ test_labels }}"
rules:
- apiGroups: [""]
resources: "{{ item.resources }}"
verbs: "{{ item.verbs }}"
with_items: '{{ cluster_roles }}'
- name: Create Role Binding (namespaced)
kubernetes.core.k8s:
definition:
kind: RoleBinding
apiVersion: "rbac.authorization.k8s.io/v1"
metadata:
name: "{{ cluster_roles[0].name }}-binding"
namespace: "{{ test_ns }}"
labels: "{{ test_labels }}"
subjects:
- kind: ServiceAccount
name: "{{ test_sa }}"
namespace: "{{ test_ns }}"
apiGroup: ""
roleRef:
kind: ClusterRole
name: "{{ cluster_roles[0].name }}"
apiGroup: ""
- name: list Pod for all namespace should failed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
register: error
ignore_errors: true
- assert:
that:
- '"pods is forbidden: User" in error.msg'
- name: list Pod for test namespace should succeed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
namespace: "{{ test_ns }}"
no_log: true
- name: Create Cluster role Binding
kubernetes.core.k8s:
definition:
kind: ClusterRoleBinding
apiVersion: "{{ item.api_version_binding }}"
metadata:
name: "{{ item.name }}-binding"
labels: "{{ test_labels }}"
subjects:
- kind: ServiceAccount
name: "{{ test_sa }}"
namespace: "{{ test_ns }}"
apiGroup: ""
roleRef:
kind: ClusterRole
name: "{{ item.name }}"
apiGroup: ""
with_items: "{{ cluster_roles }}"
- name: list Pod for all namespace should succeed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
no_log: true
- name: list Pod for test namespace should succeed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
namespace: "{{ test_ns }}"
no_log: true
- name: list Node using ServiceAccount
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Node
namespace: "{{ test_ns }}"
no_log: true
- name: Prune clusterroles (check mode)
community.okd.openshift_adm_prune_auth:
resource: clusterroles
label_selectors:
- phase=dev
register: check
check_mode: true
- name: validate clusterrole binding candidates for prune
assert:
that:
- '"{{ item.name }}-binding" in check.cluster_role_binding'
- '"{{ test_ns }}/{{ cluster_roles[0].name }}-binding" in check.role_binding'
with_items: "{{ cluster_roles }}"
- name: Prune Cluster Role for managing Pod
community.okd.openshift_adm_prune_auth:
resource: clusterroles
name: "{{ cluster_roles[0].name }}"
- name: list Pod for all namespace should failed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
register: error
no_log: true
ignore_errors: true
- assert:
that:
- '"pods is forbidden: User" in error.msg'
- name: list Pod for test namespace should failed
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Pod
namespace: "{{ test_ns }}"
register: error
no_log: true
ignore_errors: true
- assert:
that:
- '"pods is forbidden: User" in error.msg'
- name: list Node using ServiceAccount
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Node
namespace: "{{ test_ns }}"
no_log: true
- name: Prune clusterroles (remaining)
community.okd.openshift_adm_prune_auth:
resource: clusterroles
label_selectors:
- phase=dev
- name: list Node using ServiceAccount should fail
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
kind: Node
namespace: "{{ test_ns }}"
register: error
ignore_errors: true
- assert:
that:
- '"nodes is forbidden: User" in error.msg'
always:
- name: Ensure namespace is deleted
kubernetes.core.k8s:
state: absent
kind: Namespace
name: "{{ test_ns }}"
wait: yes
ignore_errors: true
- name: Delete ClusterRoleBinding
kubernetes.core.k8s:
kind: ClusterRoleBinding
api_version: "rbac.authorization.k8s.io/v1"
name: "{{ item.name }}-binding"
state: absent
ignore_errors: true
with_items: "{{ cluster_roles }}"
when: cluster_roles is defined
- name: Delete ClusterRole
kubernetes.core.k8s:
kind: ClusterRole
api_version: "rbac.authorization.k8s.io/v1"
name: "{{ item.name }}"
state: absent
ignore_errors: true
with_items: "{{ cluster_roles }}"
when: cluster_roles is defined

View File

@@ -0,0 +1,340 @@
- block:
- set_fact:
test_ns: "prune-roles"
sa_name: "roles-sa"
pod_name: "pod-prune"
role_definition:
- name: pod-list
labels:
action: list
verbs:
- list
role_binding:
api_version: rbac.authorization.k8s.io/v1
- name: pod-create
labels:
action: create
verbs:
- create
- get
role_binding:
api_version: authorization.openshift.io/v1
- name: pod-delete
labels:
action: delete
verbs:
- delete
role_binding:
api_version: rbac.authorization.k8s.io/v1
- name: Ensure namespace
kubernetes.core.k8s:
kind: Namespace
name: '{{ test_ns }}'
- name: Get cluster information
kubernetes.core.k8s_cluster_info:
register: cluster_info
no_log: true
- set_fact:
cluster_host: "{{ cluster_info['connection']['host'] }}"
- name: Create Service account
kubernetes.core.k8s:
definition:
apiVersion: v1
kind: ServiceAccount
metadata:
name: '{{ sa_name }}'
namespace: '{{ test_ns }}'
- name: Read Service Account
kubernetes.core.k8s_info:
kind: ServiceAccount
namespace: '{{ test_ns }}'
name: '{{ sa_name }}'
register: sa_out
- set_fact:
secret_token: "{{ sa_out.resources[0]['secrets'][0]['name'] }}"
- name: Get secret details
kubernetes.core.k8s_info:
kind: Secret
namespace: '{{ test_ns }}'
name: '{{ secret_token }}'
register: r_secret
retries: 10
delay: 10
until:
- ("'openshift.io/token-secret.value' in r_secret.resources[0]['metadata']['annotations']") or ("'token' in r_secret.resources[0]['data']")
- set_fact:
api_token: "{{ r_secret.resources[0]['metadata']['annotations']['openshift.io/token-secret.value'] }}"
when: "'openshift.io/token-secret.value' in r_secret.resources[0]['metadata']['annotations']"
- set_fact:
api_token: "{{ r_secret.resources[0]['data']['token'] | b64decode }}"
when: "'token' in r_secret.resources[0]['data']"
- name: list resources using service account
kubernetes.core.k8s_info:
api_key: '{{ api_token }}'
host: '{{ cluster_host }}'
validate_certs: no
kind: Pod
namespace: '{{ test_ns }}'
register: error
ignore_errors: true
- assert:
that:
- '"pods is forbidden: User" in error.msg'
- name: Create a role to manage Pod from namespace "{{ test_ns }}"
kubernetes.core.k8s:
definition:
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
namespace: "{{ test_ns }}"
name: "{{ item.name }}"
labels: "{{ item.labels }}"
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: "{{ item.verbs }}"
with_items: "{{ role_definition }}"
- name: Create Role Binding
kubernetes.core.k8s:
definition:
kind: RoleBinding
apiVersion: "{{ item.role_binding.api_version }}"
metadata:
name: "{{ item.name }}-bind"
namespace: "{{ test_ns }}"
subjects:
- kind: ServiceAccount
name: "{{ sa_name }}"
namespace: "{{ test_ns }}"
apiGroup: ""
roleRef:
kind: Role
name: "{{ item.name }}"
namespace: "{{ test_ns }}"
apiGroup: ""
with_items: "{{ role_definition }}"
- name: Create Pod should succeed
kubernetes.core.k8s:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
namespace: "{{ test_ns }}"
definition:
kind: Pod
metadata:
name: "{{ pod_name }}"
spec:
containers:
- name: python
image: python:3.7-alpine
command:
- /bin/sh
- -c
- while true; do echo $(date); sleep 15; done
imagePullPolicy: IfNotPresent
register: result
- name: assert pod creation succeed
assert:
that:
- result is successful
- name: List Pod
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
namespace: "{{ test_ns }}"
kind: Pod
register: result
- name: assert user is still authorize to list pods
assert:
that:
- result is successful
- name: Prune auth roles (check mode)
community.okd.openshift_adm_prune_auth:
resource: roles
namespace: "{{ test_ns }}"
register: check
check_mode: true
- name: validate that list role binding are candidates for prune
assert:
that: '"{{ test_ns }}/{{ item.name }}-bind" in check.role_binding'
with_items: "{{ role_definition }}"
- name: Prune resource using label_selectors option
community.okd.openshift_adm_prune_auth:
resource: roles
namespace: "{{ test_ns }}"
label_selectors:
- action=delete
register: prune
- name: assert that role binding 'delete' was pruned
assert:
that:
- prune is changed
- '"{{ test_ns }}/{{ role_definition[2].name }}-bind" in check.role_binding'
- name: assert that user could not delete pod anymore
kubernetes.core.k8s:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
state: absent
namespace: "{{ test_ns }}"
kind: Pod
name: "{{ pod_name }}"
register: result
ignore_errors: true
- name: assert pod deletion failed due to forbidden user
assert:
that:
- '"forbidden: User" in error.msg'
- name: List Pod
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
namespace: "{{ test_ns }}"
kind: Pod
register: result
- name: assert user is still able to list pods
assert:
that:
- result is successful
- name: Create Pod should succeed
kubernetes.core.k8s:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
namespace: "{{ test_ns }}"
definition:
kind: Pod
metadata:
name: "{{ pod_name }}-1"
spec:
containers:
- name: python
image: python:3.7-alpine
command:
- /bin/sh
- -c
- while true; do echo $(date); sleep 15; done
imagePullPolicy: IfNotPresent
register: result
- name: assert user is still authorize to create pod
assert:
that:
- result is successful
- name: Prune role using name
community.okd.openshift_adm_prune_auth:
resource: roles
namespace: "{{ test_ns }}"
name: "{{ role_definition[1].name }}"
register: prune
- name: assert that role binding 'create' was pruned
assert:
that:
- prune is changed
- '"{{ test_ns }}/{{ role_definition[1].name }}-bind" in check.role_binding'
- name: Create Pod (should failed)
kubernetes.core.k8s:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
namespace: "{{ test_ns }}"
definition:
kind: Pod
metadata:
name: "{{ pod_name }}-2"
spec:
containers:
- name: python
image: python:3.7-alpine
command:
- /bin/sh
- -c
- while true; do echo $(date); sleep 15; done
imagePullPolicy: IfNotPresent
register: result
ignore_errors: true
- name: assert user is not authorize to create pod anymore
assert:
that:
- '"forbidden: User" in error.msg'
- name: List Pod
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
namespace: "{{ test_ns }}"
kind: Pod
register: result
- name: assert user is still able to list pods
assert:
that:
- result is successful
- name: Prune all role for namespace (neither name nor label_selectors are specified)
community.okd.openshift_adm_prune_auth:
resource: roles
namespace: "{{ test_ns }}"
register: prune
- name: assert that role binding 'list' was pruned
assert:
that:
- prune is changed
- '"{{ test_ns }}/{{ role_definition[0].name }}-bind" in check.role_binding'
- name: List Pod
kubernetes.core.k8s_info:
api_key: "{{ api_token }}"
host: "{{ cluster_host }}"
validate_certs: no
namespace: "{{ test_ns }}"
kind: Pod
register: result
ignore_errors: true
- name: assert user is not authorize to list pod anymore
assert:
that:
- '"forbidden: User" in error.msg'
always:
- name: Ensure namespace is deleted
kubernetes.core.k8s:
state: absent
kind: Namespace
name: "{{ test_ns }}"
ignore_errors: true

View File

@@ -0,0 +1,269 @@
- name: Prune deployments
block:
- set_fact:
dc_name: "hello"
deployment_ns: "prune-deployments"
deployment_ns_2: "prune-deployments-2"
- name: Ensure namespace
community.okd.k8s:
kind: Namespace
name: '{{ deployment_ns }}'
- name: Create deployment config
community.okd.k8s:
namespace: '{{ deployment_ns }}'
definition:
kind: DeploymentConfig
apiVersion: apps.openshift.io/v1
metadata:
name: '{{ dc_name }}'
spec:
replicas: 1
selector:
name: '{{ dc_name }}'
template:
metadata:
labels:
name: '{{ dc_name }}'
spec:
containers:
- name: hello-openshift
imagePullPolicy: IfNotPresent
image: python:3.7-alpine
command: [ "/bin/sh", "-c", "while true;do date;sleep 2s; done"]
wait: yes
- name: prune deployments (no candidate DeploymentConfig)
community.okd.openshift_adm_prune_deployments:
namespace: "{{ deployment_ns }}"
register: test_prune
- assert:
that:
- test_prune is not changed
- test_prune.replication_controllers | length == 0
- name: Update DeploymentConfig - set replicas to 0
community.okd.k8s:
namespace: "{{ deployment_ns }}"
definition:
kind: DeploymentConfig
apiVersion: "apps.openshift.io/v1"
metadata:
name: "{{ dc_name }}"
spec:
replicas: 0
selector:
name: "{{ dc_name }}"
template:
metadata:
labels:
name: "{{ dc_name }}"
spec:
containers:
- name: hello-openshift
imagePullPolicy: IfNotPresent
image: python:3.7-alpine
command: [ "/bin/sh", "-c", "while true;do date;sleep 2s; done"]
wait: yes
- name: Wait for ReplicationController candidate for pruning
kubernetes.core.k8s_info:
kind: ReplicationController
namespace: "{{ deployment_ns }}"
register: result
retries: 10
delay: 30
until:
- result.resources.0.metadata.annotations["openshift.io/deployment.phase"] in ("Failed", "Complete")
- name: Prune deployments - should delete 1 ReplicationController
community.okd.openshift_adm_prune_deployments:
namespace: "{{ deployment_ns }}"
check_mode: yes
register: test_prune
- name: Read ReplicationController
kubernetes.core.k8s_info:
kind: ReplicationController
namespace: "{{ deployment_ns }}"
register: replications
- name: Assert that Replication controller was not deleted
assert:
that:
- replications.resources | length == 1
- 'replications.resources.0.metadata.name is match("{{ dc_name }}-*")'
- name: Assure that candidate ReplicationController was found for pruning
assert:
that:
- test_prune is changed
- test_prune.replication_controllers | length == 1
- test_prune.replication_controllers.0.metadata.name == replications.resources.0.metadata.name
- test_prune.replication_controllers.0.metadata.namespace == replications.resources.0.metadata.namespace
- name: Prune deployments - keep younger than 45min (check_mode)
community.okd.openshift_adm_prune_deployments:
keep_younger_than: 45
namespace: "{{ deployment_ns }}"
check_mode: true
register: keep_younger
- name: assert no candidate was found
assert:
that:
- keep_younger is not changed
- keep_younger.replication_controllers == []
- name: Ensure second namespace is created
community.okd.k8s:
kind: Namespace
name: '{{ deployment_ns_2 }}'
- name: Create deployment config from 2nd namespace
community.okd.k8s:
namespace: '{{ deployment_ns_2 }}'
definition:
kind: DeploymentConfig
apiVersion: apps.openshift.io/v1
metadata:
name: '{{ dc_name }}2'
spec:
replicas: 1
selector:
name: '{{ dc_name }}2'
template:
metadata:
labels:
name: '{{ dc_name }}2'
spec:
containers:
- name: hello-openshift
imagePullPolicy: IfNotPresent
image: python:3.7-alpine
command: [ "/bin/sh", "-c", "while true;do date;sleep 2s; done"]
wait: yes
- name: Stop deployment config - replicas = 0
community.okd.k8s:
namespace: '{{ deployment_ns_2 }}'
definition:
kind: DeploymentConfig
apiVersion: apps.openshift.io/v1
metadata:
name: '{{ dc_name }}2'
spec:
replicas: 0
selector:
name: '{{ dc_name }}2'
template:
metadata:
labels:
name: '{{ dc_name }}2'
spec:
containers:
- name: hello-openshift
imagePullPolicy: IfNotPresent
image: python:3.7-alpine
command: [ "/bin/sh", "-c", "while true;do date;sleep 2s; done"]
wait: yes
- name: Wait for ReplicationController candidate for pruning
kubernetes.core.k8s_info:
kind: ReplicationController
namespace: "{{ deployment_ns_2 }}"
register: result
retries: 10
delay: 30
until:
- result.resources.0.metadata.annotations["openshift.io/deployment.phase"] in ("Failed", "Complete")
# Prune from one namespace should not have any effect on others namespaces
- name: Prune deployments from 2nd namespace
community.okd.openshift_adm_prune_deployments:
namespace: "{{ deployment_ns_2 }}"
check_mode: yes
register: test_prune
- name: Assure that candidate ReplicationController was found for pruning
assert:
that:
- test_prune is changed
- test_prune.replication_controllers | length == 1
- "test_prune.replication_controllers.0.metadata.namespace == deployment_ns_2"
# Prune without namespace option
- name: Prune from all namespace should update more deployments
community.okd.openshift_adm_prune_deployments:
check_mode: yes
register: no_namespace_prune
- name: Assure multiple ReplicationController were found for pruning
assert:
that:
- no_namespace_prune is changed
- no_namespace_prune.replication_controllers | length == 2
# Execute Prune from 2nd namespace
- name: Read ReplicationController before Prune operation
kubernetes.core.k8s_info:
kind: ReplicationController
namespace: "{{ deployment_ns_2 }}"
register: replications
- assert:
that:
- replications.resources | length == 1
- name: Prune DeploymentConfig from 2nd namespace
community.okd.openshift_adm_prune_deployments:
namespace: "{{ deployment_ns_2 }}"
register: _prune
- name: Assert DeploymentConfig was deleted
assert:
that:
- _prune is changed
- _prune.replication_controllers | length == 1
- _prune.replication_controllers.0.details.name == replications.resources.0.metadata.name
# Execute Prune without namespace option
- name: Read ReplicationController before Prune operation
kubernetes.core.k8s_info:
kind: ReplicationController
namespace: "{{ deployment_ns }}"
register: replications
- assert:
that:
- replications.resources | length == 1
- name: Prune from all namespace should update more deployments
community.okd.openshift_adm_prune_deployments:
register: _prune
- name: Assure multiple ReplicationController were found for pruning
assert:
that:
- _prune is changed
- _prune.replication_controllers | length > 0
always:
- name: Delete 1st namespace
community.okd.k8s:
state: absent
kind: Namespace
name: "{{ deployment_ns }}"
ignore_errors: yes
when: deployment_ns is defined
- name: Delete 2nd namespace
community.okd.k8s:
state: absent
kind: Namespace
name: "{{ deployment_ns_2 }}"
ignore_errors: yes
when: deployment_ns_2 is defined

View File

@@ -0,0 +1,56 @@
---
- block:
- name: Retrieve cluster info
kubernetes.core.k8s_cluster_info:
register: k8s_cluster
- name: set openshift host value
set_fact:
openshift_host: "{{ k8s_cluster.connection.host }}"
- name: Log in (obtain access token)
community.okd.openshift_auth:
username: test
password: testing123
host: '{{ openshift_host }}'
verify_ssl: false
register: openshift_auth_results
- name: Get the test User
kubernetes.core.k8s_info:
api_key: "{{ openshift_auth_results.openshift_auth.api_key }}"
host: '{{ openshift_host }}'
verify_ssl: false
kind: User
api_version: user.openshift.io/v1
name: test
register: user_result
- name: assert that the user was found
assert:
that: (user_result.resources | length) == 1
always:
- name: If login succeeded, try to log out (revoke access token)
when: openshift_auth_results.openshift_auth.api_key is defined
community.okd.openshift_auth:
state: absent
api_key: "{{ openshift_auth_results.openshift_auth.api_key }}"
host: '{{ openshift_host }}'
verify_ssl: false
- name: Get the test user
kubernetes.core.k8s_info:
api_key: "{{ openshift_auth_results.openshift_auth.api_key }}"
host: '{{ openshift_host }}'
verify_ssl: false
kind: User
name: test
api_version: user.openshift.io/v1
register: failed_user_result
ignore_errors: yes
# TODO(fabianvf) determine why token is not being rejected, maybe add more info to return
# - name: assert that the user was not found
# assert:
# that: (failed_user_result.resources | length) == 0

View File

@@ -0,0 +1,179 @@
- name: Openshift import image testing
block:
- set_fact:
test_ns: "import-images"
- name: Ensure namespace
community.okd.k8s:
kind: Namespace
name: '{{ test_ns }}'
- name: Import image using tag (should import latest tag only)
community.okd.openshift_import_image:
namespace: "{{ test_ns }}"
name: "ansible/awx"
check_mode: yes
register: import_tag
- name: Assert only latest was imported
assert:
that:
- import_tag is changed
- import_tag.result | length == 1
- import_tag.result.0.spec.import
- import_tag.result.0.spec.images.0.from.kind == "DockerImage"
- import_tag.result.0.spec.images.0.from.name == "ansible/awx"
- name: check image stream
kubernetes.core.k8s_info:
kind: ImageStream
namespace: "{{ test_ns }}"
name: awx
register: resource
- name: assert that image stream is not created when using check_mode=yes
assert:
that:
- resource.resources == []
- name: Import image using tag (should import latest tag only)
community.okd.openshift_import_image:
namespace: "{{ test_ns }}"
name: "ansible/awx"
register: import_tag
- name: Assert only latest was imported
assert:
that:
- import_tag is changed
- name: check image stream
kubernetes.core.k8s_info:
kind: ImageStream
namespace: "{{ test_ns }}"
name: awx
register: resource
- name: assert that image stream contains only tag latest
assert:
that:
- resource.resources | length == 1
- resource.resources.0.status.tags.0.tag == 'latest'
- name: Import once again the latest tag
community.okd.openshift_import_image:
namespace: "{{ test_ns }}"
name: "ansible/awx"
register: import_tag
- name: assert change was performed
assert:
that:
- import_tag is changed
- name: check image stream
kubernetes.core.k8s_info:
kind: ImageStream
version: image.openshift.io/v1
namespace: "{{ test_ns }}"
name: awx
register: resource
- name: assert that image stream still contains unique tag
assert:
that:
- resource.resources | length == 1
- resource.resources.0.status.tags.0.tag == 'latest'
- name: Import another tags
community.okd.openshift_import_image:
namespace: "{{ test_ns }}"
name: "ansible/awx:17.1.0"
register: import_another_tag
ignore_errors: yes
- name: assert that another tag was imported
assert:
that:
- import_another_tag is failed
- '"the tag 17.1.0 does not exist on the image stream" in import_another_tag.msg'
- name: Create simple ImageStream (without docker external container)
community.okd.k8s:
namespace: "{{ test_ns }}"
name: "local-is"
definition:
apiVersion: image.openshift.io/v1
kind: ImageStream
spec:
lookupPolicy:
local: false
tags: []
- name: Import all tag for image stream not pointing on external container image should failed
community.okd.openshift_import_image:
namespace: "{{ test_ns }}"
name: "local-is"
all: true
register: error_tag
ignore_errors: true
check_mode: yes
- name: Assert module cannot import from non-existing tag from ImageStream
assert:
that:
- error_tag is failed
- 'error_tag.msg == "image stream {{ test_ns }}/local-is does not have tags pointing to external container images"'
- name: import all tags for container image ibmcom/pause and specific tag for redhat/ubi8-micro
community.okd.openshift_import_image:
namespace: "{{ test_ns }}"
name:
- "ibmcom/pause"
- "redhat/ubi8-micro:8.5-437"
all: true
register: multiple_import
- name: Assert that import succeed
assert:
that:
- multiple_import is changed
- multiple_import.result | length == 2
- name: Read ibmcom/pause ImageStream
kubernetes.core.k8s_info:
version: image.openshift.io/v1
kind: ImageStream
namespace: "{{ test_ns }}"
name: pause
register: pause
- name: assert that ibmcom/pause has multiple tags
assert:
that:
- pause.resources | length == 1
- pause.resources.0.status.tags | length > 1
- name: Read redhat/ubi8-micro ImageStream
kubernetes.core.k8s_info:
version: image.openshift.io/v1
kind: ImageStream
namespace: "{{ test_ns }}"
name: ubi8-micro
register: resource
- name: assert that redhat/ubi8-micro has only one tag
assert:
that:
- resource.resources | length == 1
- resource.resources.0.status.tags | length == 1
- 'resource.resources.0.status.tags.0.tag == "8.5-437"'
always:
- name: Delete testing namespace
community.okd.k8s:
state: absent
kind: Namespace
name: "{{ test_ns }}"
ignore_errors: yes

View File

@@ -0,0 +1,183 @@
---
- name: Process a template in the cluster
community.okd.openshift_process:
name: nginx-example
namespace: openshift # only needed if using a template already on the server
parameters:
NAMESPACE: openshift
NAME: test123
register: result
- name: Create the rendered resources
community.okd.k8s:
namespace: process-test
definition: '{{ item }}'
wait: yes
apply: yes
loop: '{{ result.resources }}'
- name: Delete the rendered resources
community.okd.k8s:
namespace: process-test
definition: '{{ item }}'
wait: yes
state: absent
loop: '{{ result.resources }}'
- name: Process a template and create the resources in the cluster
community.okd.openshift_process:
name: nginx-example
namespace: openshift # only needed if using a template already on the server
parameters:
NAMESPACE: openshift
NAME: test123
state: present
namespace_target: process-test
register: result
- name: Process a template and update the resources in the cluster
community.okd.openshift_process:
name: nginx-example
namespace: openshift # only needed if using a template already on the server
parameters:
NAMESPACE: openshift
NAME: test123
MEMORY_LIMIT: 1Gi
state: present
namespace_target: process-test
register: result
- name: Process a template and delete the resources in the cluster
community.okd.openshift_process:
name: nginx-example
namespace: openshift # only needed if using a template already on the server
parameters:
NAMESPACE: openshift
NAME: test123
state: absent
namespace_target: process-test
register: result
- name: Process a template with parameters from an env file and create the resources
community.okd.openshift_process:
name: nginx-example
namespace: openshift
namespace_target: process-test
parameter_file: '{{ files_dir }}/nginx.env'
state: present
wait: yes
- name: Process a template with parameters from an env file and delete the resources
community.okd.openshift_process:
name: nginx-example
namespace: openshift
namespace_target: process-test
parameter_file: '{{ files_dir }}/nginx.env'
state: absent
wait: yes
- name: Process a template with duplicate values
community.okd.openshift_process:
name: nginx-example
namespace: openshift # only needed if using a template already on the server
parameters:
NAME: test123
parameter_file: '{{ files_dir }}/nginx.env'
ignore_errors: yes
register: result
- name: Assert the expected failure occurred
assert:
that:
- result.msg is defined
- result.msg == "Duplicate value for 'NAME' detected in parameter file"
- name: Process a local template
community.okd.openshift_process:
src: '{{ files_dir }}/simple-template.yaml'
parameter_file: '{{ files_dir }}/example.env'
register: rendered
- name: Process a local template and create the resources
community.okd.openshift_process:
src: '{{ files_dir }}/simple-template.yaml'
parameter_file: '{{ files_dir }}/example.env'
namespace_target: process-test
state: present
register: result
- assert:
that: result is changed
- name: Create the processed resources
community.okd.k8s:
namespace: process-test
definition: '{{ item }}'
loop: '{{ rendered.resources }}'
register: result
- assert:
that: result is not changed
- name: Process a local template and create the resources
community.okd.openshift_process:
definition: "{{ lookup('template', files_dir + '/simple-template.yaml') | from_yaml }}"
parameter_file: '{{ files_dir }}/example.env'
namespace_target: process-test
state: present
register: result
- assert:
that: result is not changed
- name: Get the created configmap
kubernetes.core.k8s_info:
api_version: v1
kind: ConfigMap
name: example
namespace: process-test
register: templated_cm
- assert:
that:
- (templated_cm.resources | length) == 1
- templated_cm.resources.0.data.content is defined
- templated_cm.resources.0.data.content == "This is a long message that may take one or more lines to parse but should still work without issue"
- name: Create the Template resource
community.okd.k8s:
src: '{{ files_dir }}/simple-template.yaml'
namespace: process-test
- name: Process the template and create the resources
community.okd.openshift_process:
name: simple-example
namespace: process-test # only needed if using a template already on the server
namespace_target: process-test
parameter_file: '{{ files_dir }}/example.env'
state: present
register: result
- assert:
that: result is not changed
# Processing template without message
- name: create template with file {{ files_dir }}/pod-template.yaml
kubernetes.core.k8s:
namespace: process-test
src: "{{ files_dir }}/pod-template.yaml"
state: present
- name: Process pod template
community.okd.openshift_process:
name: pod-template
namespace: process-test
state: rendered
parameters:
NAME: ansible
register: rendered_template
- assert:
that: rendered_template.message == ""

View File

@@ -0,0 +1,230 @@
---
- name: Read registry information
community.okd.openshift_registry_info:
check: yes
register: registry
- name: Display registry information
debug: var=registry
- block:
- set_fact:
prune_ns: "prune-images"
prune_registry: "{{ registry.public_hostname }}"
container:
name: "httpd"
from: "centos/python-38-centos7:20210629-304c7c8"
pod_name: "test-pod"
- name: Ensure namespace is created
community.okd.k8s:
kind: Namespace
name: "{{ prune_ns }}"
- name: Import image into internal registry
community.okd.openshift_import_image:
namespace: "{{ prune_ns }}"
name: "{{ container.name }}"
source: "{{ container.from }}"
- name: Create simple Pod
community.okd.k8s:
namespace: "{{ prune_ns }}"
wait: yes
definition:
apiVersion: v1
kind: Pod
metadata:
name: "{{ pod_name }}"
spec:
containers:
- name: test-container
image: "{{ prune_registry }}/{{ prune_ns }}/{{ container.name }}:latest"
command:
- /bin/sh
- -c
- while true;do date;sleep 5; done
- name: Create limit range for images size
community.okd.k8s:
namespace: "{{ prune_ns }}"
definition:
kind: "LimitRange"
metadata:
name: "image-resource-limits"
spec:
limits:
- type: openshift.io/Image
max:
storage: 1Gi
- name: Prune images from namespace
community.okd.openshift_adm_prune_images:
registry_url: "{{ prune_registry }}"
namespace: "{{ prune_ns }}"
check_mode: yes
register: prune
- name: Assert that nothing to prune as image is in used
assert:
that:
- prune is not changed
- prune is successful
- prune.deleted_images == []
- prune.updated_image_streams == []
- name: Delete Pod created before
community.okd.k8s:
state: absent
name: "{{ pod_name }}"
kind: Pod
namespace: "{{ prune_ns }}"
wait: yes
- name: Prune images from namespace
community.okd.openshift_adm_prune_images:
registry_url: "{{ prune_registry }}"
namespace: "{{ prune_ns }}"
check_mode: yes
register: prune
- name: Read ImageStream
kubernetes.core.k8s_info:
version: image.openshift.io/v1
kind: ImageStream
namespace: "{{ prune_ns }}"
name: "{{ container.name }}"
register: isinfo
- set_fact:
is_image_name: "{{ isinfo.resources.0.status.tags[0]['items'].0.image }}"
- name: Assert that corresponding Image and ImageStream were candidate for pruning
assert:
that:
- prune is changed
- prune.deleted_images | length == 1
- prune.deleted_images.0.metadata.name == is_image_name
- prune.updated_image_streams | length == 1
- prune.updated_image_streams.0.metadata.name == container.name
- prune.updated_image_streams.0.metadata.namespace == prune_ns
- prune.updated_image_streams.0.status.tags == []
- name: Prune images from namespace keeping images and referrer younger than 60minutes
community.okd.openshift_adm_prune_images:
registry_url: "{{ prune_registry }}"
namespace: "{{ prune_ns }}"
keep_younger_than: 60
check_mode: yes
register: younger
- assert:
that:
- younger is not changed
- younger is successful
- younger.deleted_images == []
- younger.updated_image_streams == []
- name: Prune images over size limit
community.okd.openshift_adm_prune_images:
registry_url: "{{ prune_registry }}"
namespace: "{{ prune_ns }}"
prune_over_size_limit: yes
check_mode: yes
register: prune_over_size
- assert:
that:
- prune_over_size is not changed
- prune_over_size is successful
- prune_over_size.deleted_images == []
- prune_over_size.updated_image_streams == []
- name: Update limit range for images size
community.okd.k8s:
namespace: "{{ prune_ns }}"
definition:
kind: "LimitRange"
metadata:
name: "image-resource-limits"
spec:
limits:
- type: openshift.io/Image
max:
storage: 1Ki
- name: Prune images over size limit (check_mode=yes)
community.okd.openshift_adm_prune_images:
registry_url: "{{ prune_registry }}"
namespace: "{{ prune_ns }}"
prune_over_size_limit: yes
check_mode: yes
register: prune
- name: Assert Images and ImageStream were candidate for prune
assert:
that:
- prune is changed
- prune.deleted_images | length == 1
- prune.deleted_images.0.metadata.name == is_image_name
- prune.updated_image_streams | length == 1
- prune.updated_image_streams.0.metadata.name == container.name
- prune.updated_image_streams.0.metadata.namespace == prune_ns
- prune.updated_image_streams.0.status.tags == []
- name: Prune images over size limit
community.okd.openshift_adm_prune_images:
registry_url: "{{ prune_registry }}"
namespace: "{{ prune_ns }}"
prune_over_size_limit: yes
register: prune
- name: Assert that Images and ImageStream were candidate for prune
assert:
that:
- prune is changed
- prune.deleted_images | length == 1
- prune.deleted_images.0.details.name == is_image_name
- prune.updated_image_streams | length == 1
- prune.updated_image_streams.0.metadata.name == container.name
- prune.updated_image_streams.0.metadata.namespace == prune_ns
- '"tags" not in prune.updated_image_streams.0.status'
- name: Validate that ImageStream was updated
kubernetes.core.k8s_info:
version: image.openshift.io/v1
kind: ImageStream
namespace: "{{ prune_ns }}"
name: "{{ container.name }}"
register: stream
- name: Assert that ImageStream was updated
assert:
that:
- stream.resources | length == 1
- '"tags" not in stream.resources.0.status'
- name: Validate that Image was deleted
kubernetes.core.k8s_info:
version: image.openshift.io/v1
kind: Image
name: "{{ is_image_name }}"
register: image
- name: Assert that image was deleted
assert:
that:
- image.resources | length == 0
always:
- name: Delete namespace
community.okd.k8s:
name: "{{ prune_ns }}"
kind: Namespace
state: absent
wait: yes
ignore_errors: true
when:
- registry.public_hostname
- registry.check.reached

View File

@@ -0,0 +1,275 @@
---
- name: Create Deployment
community.okd.k8s:
wait: yes
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-kubernetes
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: hello-kubernetes
template:
metadata:
labels:
app: hello-kubernetes
spec:
containers:
- name: hello-kubernetes
image: docker.io/openshift/hello-openshift
ports:
- containerPort: 8080
- name: Create Service
community.okd.k8s:
wait: yes
definition:
apiVersion: v1
kind: Service
metadata:
name: hello-kubernetes
namespace: default
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: hello-kubernetes
- name: Create Route with fewest possible arguments
community.okd.openshift_route:
service: hello-kubernetes
namespace: default
register: route
- name: Attempt to hit http URL
uri:
url: 'http://{{ route.result.spec.host }}'
return_content: yes
until: result is successful
retries: 20
register: result
- name: Assert the page content is as expected
assert:
that:
- not result.redirected
- result.status == 200
- result.content == 'Hello OpenShift!\n'
- name: Delete route
community.okd.openshift_route:
name: '{{ route.result.metadata.name }}'
namespace: default
state: absent
wait: yes
- name: Create Route with custom name and wait
community.okd.openshift_route:
service: hello-kubernetes
namespace: default
name: test1
wait: yes
register: route
- name: Assert that the condition is properly set
assert:
that:
- route.duration is defined
- route.result.status.ingress.0.conditions.0.type == 'Admitted'
- route.result.status.ingress.0.conditions.0.status == 'True'
- name: Attempt to hit http URL
uri:
url: 'http://{{ route.result.spec.host }}'
return_content: yes
until: result is successful
retries: 20
register: result
- name: Assert the page content is as expected
assert:
that:
- not result.redirected
- result.status == 200
- result.content == 'Hello OpenShift!\n'
- name: Delete route
community.okd.openshift_route:
name: '{{ route.result.metadata.name }}'
namespace: default
state: absent
wait: yes
- name: Create edge-terminated route that allows insecure traffic
community.okd.openshift_route:
service: hello-kubernetes
namespace: default
name: hello-kubernetes-https
tls:
insecure_policy: allow
termination: edge
register: route
- name: Attempt to hit http URL
uri:
url: 'http://{{ route.result.spec.host }}'
return_content: yes
until: result is successful
retries: 20
register: result
- name: Assert the page content is as expected
assert:
that:
- not result.redirected
- result.status == 200
- result.content == 'Hello OpenShift!\n'
- name: Attempt to hit https URL
uri:
url: 'https://{{ route.result.spec.host }}'
validate_certs: no
return_content: yes
until: result is successful
retries: 10
register: result
- name: Assert the page content is as expected
assert:
that:
- not result.redirected
- result.status == 200
- result.content == 'Hello OpenShift!\n'
- name: Alter edge-terminated route to redirect insecure traffic
community.okd.openshift_route:
service: hello-kubernetes
namespace: default
name: hello-kubernetes-https
tls:
insecure_policy: redirect
termination: edge
register: route
- name: Attempt to hit http URL
uri:
url: 'http://{{ route.result.spec.host }}'
return_content: yes
validate_certs: no
until:
- result is successful
- result.redirected
retries: 10
register: result
- name: Assert the page content is as expected
assert:
that:
- result.redirected
- result.status == 200
- result.content == 'Hello OpenShift!\n'
- name: Attempt to hit https URL
uri:
url: 'https://{{ route.result.spec.host }}'
validate_certs: no
return_content: yes
until: result is successful
retries: 20
register: result
- name: Assert the page content is as expected
assert:
that:
- not result.redirected
- result.status == 200
- result.content == 'Hello OpenShift!\n'
- name: Alter edge-terminated route with insecure traffic disabled
community.okd.openshift_route:
service: hello-kubernetes
namespace: default
name: hello-kubernetes-https
tls:
insecure_policy: disallow
termination: edge
register: route
- debug: var=route
- name: Attempt to hit https URL
uri:
url: 'https://{{ route.result.spec.host }}'
validate_certs: no
return_content: yes
until: result is successful
retries: 20
register: result
- name: Assert the page content is as expected
assert:
that:
- not result.redirected
- result.status == 200
- result.content == 'Hello OpenShift!\n'
- name: Attempt to hit http URL
uri:
url: 'http://{{ route.result.spec.host }}'
status_code: 503
until: result is successful
retries: 20
register: result
- debug: var=result
- name: Assert the page content is as expected
assert:
that:
- not result.redirected
- result.status == 503
- name: Delete route
community.okd.openshift_route:
name: '{{ route.result.metadata.name }}'
namespace: default
state: absent
wait: yes
# Route with labels and annotations
- name: Create route with labels and annotations
community.okd.openshift_route:
service: hello-kubernetes
namespace: default
name: route-label-annotation
labels:
ansible: test
annotations:
haproxy.router.openshift.io/balance: roundrobin
- name: Get route information
kubernetes.core.k8s_info:
api_version: route.openshift.io/v1
kind: Route
name: route-label-annotation
namespace: default
register: route
- assert:
that:
- route.resources[0].metadata.annotations is defined
- '"haproxy.router.openshift.io/balance" in route.resources[0].metadata.annotations'
- route.resources[0].metadata.labels is defined
- '"ansible" in route.resources[0].metadata.labels'
- name: Delete route
community.okd.openshift_route:
name: route-label-annotation
namespace: default
state: absent
wait: yes

View File

@@ -0,0 +1,123 @@
---
- block:
- name: Create a project
community.okd.k8s:
name: "{{ playbook_namespace }}"
kind: Project
api_version: project.openshift.io/v1
- name: incredibly simple ConfigMap
community.okd.k8s:
definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: hello
namespace: "{{ playbook_namespace }}"
validate:
fail_on_error: yes
register: k8s_with_validate
- name: assert that k8s_with_validate succeeds
assert:
that:
- k8s_with_validate is successful
- name: extra property does not fail without strict
community.okd.k8s:
src: "files/kuard-extra-property.yml"
namespace: "{{ playbook_namespace }}"
validate:
fail_on_error: yes
strict: no
- name: extra property fails with strict
community.okd.k8s:
src: "files/kuard-extra-property.yml"
namespace: "{{ playbook_namespace }}"
validate:
fail_on_error: yes
strict: yes
ignore_errors: yes
register: extra_property
- name: check that extra property fails with strict
assert:
that:
- extra_property is failed
- name: invalid type fails at validation stage
community.okd.k8s:
src: "files/kuard-invalid-type.yml"
namespace: "{{ playbook_namespace }}"
validate:
fail_on_error: yes
strict: no
ignore_errors: yes
register: invalid_type
- name: check that invalid type fails
assert:
that:
- invalid_type is failed
- name: invalid type fails with warnings when fail_on_error is False
community.okd.k8s:
src: "files/kuard-invalid-type.yml"
namespace: "{{ playbook_namespace }}"
validate:
fail_on_error: no
strict: no
ignore_errors: yes
register: invalid_type_no_fail
- name: check that invalid type fails
assert:
that:
- invalid_type_no_fail is failed
- name: setup custom resource definition
community.okd.k8s:
src: "files/setup-crd.yml"
- name: wait a few seconds
pause:
seconds: 5
- name: add custom resource definition
community.okd.k8s:
src: "files/crd-resource.yml"
namespace: "{{ playbook_namespace }}"
validate:
fail_on_error: yes
strict: yes
register: unknown_kind
- name: check that unknown kind warns
assert:
that:
- unknown_kind is successful
- "'warnings' in unknown_kind"
always:
- name: remove custom resource
community.okd.k8s:
definition: "{{ lookup('file', 'files/crd-resource.yml') }}"
namespace: "{{ playbook_namespace }}"
state: absent
ignore_errors: yes
- name: remove custom resource definitions
community.okd.k8s:
definition: "{{ lookup('file', 'files/setup-crd.yml') }}"
state: absent
- name: Delete namespace
community.okd.k8s:
state: absent
definition:
- kind: Project
apiVersion: project.openshift.io/v1
metadata:
name: "{{ playbook_namespace }}"
ignore_errors: yes

View File

@@ -0,0 +1,25 @@
---
# TODO: Not available in ansible-base
# - python_requirements_info:
# dependencies:
# - openshift
# - kubernetes
# - kubernetes-validate
- community.okd.k8s:
definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: hello
namespace: default
validate:
fail_on_error: yes
ignore_errors: yes
register: k8s_no_validate
- name: assert that k8s_no_validate fails gracefully
assert:
that:
- k8s_no_validate is failed
- "k8s_no_validate.msg == 'kubernetes-validate python library is required to validate resources'"

View File

@@ -0,0 +1,94 @@
---
k8s_pod_annotations: {}
k8s_pod_metadata:
labels:
app: '{{ k8s_pod_name }}'
annotations: '{{ k8s_pod_annotations }}'
k8s_pod_spec:
serviceAccount: "{{ k8s_pod_service_account }}"
containers:
- image: "{{ k8s_pod_image }}"
imagePullPolicy: Always
name: "{{ k8s_pod_name }}"
command: "{{ k8s_pod_command }}"
readinessProbe:
initialDelaySeconds: 15
exec:
command:
- /bin/true
resources: "{{ k8s_pod_resources }}"
ports: "{{ k8s_pod_ports }}"
env: "{{ k8s_pod_env }}"
k8s_pod_service_account: default
k8s_pod_resources:
limits:
cpu: "100m"
memory: "100Mi"
k8s_pod_command: []
k8s_pod_ports: []
k8s_pod_env: []
k8s_pod_template:
metadata: "{{ k8s_pod_metadata }}"
spec: "{{ k8s_pod_spec }}"
k8s_deployment_spec:
template: '{{ k8s_pod_template }}'
selector:
matchLabels:
app: '{{ k8s_pod_name }}'
replicas: 1
k8s_deployment_template:
apiVersion: apps/v1
kind: Deployment
spec: '{{ k8s_deployment_spec }}'
okd_dc_triggers:
- type: ConfigChange
- type: ImageChange
imageChangeParams:
automatic: true
containerNames:
- '{{ k8s_pod_name }}'
from:
kind: ImageStreamTag
name: '{{ image_name }}:{{ image_tag }}'
okd_dc_spec:
template: '{{ k8s_pod_template }}'
triggers: '{{ okd_dc_triggers }}'
replicas: 1
strategy:
type: Recreate
okd_dc_template:
apiVersion: v1
kind: DeploymentConfig
spec: '{{ okd_dc_spec }}'
okd_imagestream_template:
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
name: '{{ image_name }}'
spec:
lookupPolicy:
local: true
tags:
- annotations: null
from:
kind: DockerImage
name: '{{ image }}'
name: '{{ image_tag }}'
referencePolicy:
type: Source
image_tag: latest

View File

@@ -0,0 +1,88 @@
---
- name: Verify inventory and connection plugins
# This group is created by the openshift_inventory plugin
# It is automatically configured to use the `oc` connection plugin
hosts: namespace_testing_pods
gather_facts: no
vars:
file_content: |
Hello world
tasks:
- name: End play if host not running (TODO should we not add these to the inventory?)
meta: end_host
when: pod_phase != "Running"
- setup:
- debug: var=ansible_facts
- name: Assert the TEST environment variable was retrieved
assert:
that: ansible_facts.env.TEST == 'test'
- name: Copy a file into the host
copy:
content: '{{ file_content }}'
dest: /tmp/test_file
- name: Retrieve the file from the host
slurp:
src: /tmp/test_file
register: slurped_file
- name: Assert the file content matches expectations
assert:
that: (slurped_file.content|b64decode) == file_content
- name: Verify
hosts: localhost
connection: local
gather_facts: no
vars:
ansible_python_interpreter: '{{ virtualenv_interpreter }}'
tasks:
- pip:
name: kubernetes-validate==1.12.0
virtualenv: "{{ virtualenv }}"
virtualenv_command: "{{ virtualenv_command }}"
virtualenv_site_packages: no
- import_tasks: tasks/validate_installed.yml
- pip:
name: kubernetes-validate
state: absent
virtualenv: "{{ virtualenv }}"
virtualenv_command: "{{ virtualenv_command }}"
virtualenv_site_packages: no
- import_tasks: tasks/validate_not_installed.yml
- import_tasks: tasks/openshift_auth.yml
- import_tasks: tasks/openshift_adm_prune_auth_clusterroles.yml
- import_tasks: tasks/openshift_adm_prune_auth_roles.yml
- import_tasks: tasks/openshift_adm_prune_deployments.yml
- import_tasks: tasks/openshift_route.yml
- import_tasks: tasks/openshift_import_images.yml
- import_tasks: tasks/openshift_prune_images.yml
- block:
- name: Create namespace
community.okd.k8s:
api_version: v1
kind: Namespace
name: process-test
- import_tasks: tasks/openshift_process.yml
vars:
files_dir: '{{ playbook_dir }}/files'
always:
- name: Delete namespace
community.okd.k8s:
api_version: v1
kind: Namespace
name: process-test
state: absent
roles:
- role: openshift_adm_groups

View File

@@ -0,0 +1,173 @@
# Based on the docker connection plugin
#
# Connection plugin for configuring kubernetes containers with kubectl
# (c) 2017, XuXinkun <xuxinkun@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
author:
- xuxinkun
connection: oc
short_description: Execute tasks in pods running on OpenShift.
description:
- Use the oc exec command to run tasks in, or put/fetch files to, pods running on the OpenShift
container platform.
requirements:
- oc (go binary)
options:
oc_pod:
description:
- Pod name. Required when the host name does not match pod name.
default: ''
vars:
- name: ansible_oc_pod
env:
- name: K8S_AUTH_POD
oc_container:
description:
- Container name. Required when a pod contains more than one container.
default: ''
vars:
- name: ansible_oc_container
env:
- name: K8S_AUTH_CONTAINER
oc_namespace:
description:
- The namespace of the pod
default: ''
vars:
- name: ansible_oc_namespace
env:
- name: K8S_AUTH_NAMESPACE
oc_extra_args:
description:
- Extra arguments to pass to the oc command line.
default: ''
vars:
- name: ansible_oc_extra_args
env:
- name: K8S_AUTH_EXTRA_ARGS
oc_kubeconfig:
description:
- Path to a oc config file. Defaults to I(~/.kube/config)
default: ''
vars:
- name: ansible_oc_kubeconfig
- name: ansible_oc_config
env:
- name: K8S_AUTH_KUBECONFIG
oc_context:
description:
- The name of a context found in the K8s config file.
default: ''
vars:
- name: ansible_oc_context
env:
- name: K8S_AUTH_CONTEXT
oc_host:
description:
- URL for accessing the API.
default: ''
vars:
- name: ansible_oc_host
- name: ansible_oc_server
env:
- name: K8S_AUTH_HOST
- name: K8S_AUTH_SERVER
oc_token:
description:
- API authentication bearer token.
vars:
- name: ansible_oc_token
- name: ansible_oc_api_key
env:
- name: K8S_AUTH_TOKEN
- name: K8S_AUTH_API_KEY
client_cert:
description:
- Path to a certificate used to authenticate with the API.
default: ''
vars:
- name: ansible_oc_cert_file
- name: ansible_oc_client_cert
env:
- name: K8S_AUTH_CERT_FILE
aliases: [ oc_cert_file ]
client_key:
description:
- Path to a key file used to authenticate with the API.
default: ''
vars:
- name: ansible_oc_key_file
- name: ansible_oc_client_key
env:
- name: K8S_AUTH_KEY_FILE
aliases: [ oc_key_file ]
ca_cert:
description:
- Path to a CA certificate used to authenticate with the API.
default: ''
vars:
- name: ansible_oc_ssl_ca_cert
- name: ansible_oc_ca_cert
env:
- name: K8S_AUTH_SSL_CA_CERT
aliases: [ oc_ssl_ca_cert ]
validate_certs:
description:
- Whether or not to verify the API server's SSL certificate. Defaults to I(true).
default: ''
vars:
- name: ansible_oc_verify_ssl
- name: ansible_oc_validate_certs
env:
- name: K8S_AUTH_VERIFY_SSL
aliases: [ oc_verify_ssl ]
'''
from ansible_collections.kubernetes.core.plugins.connection.kubectl import Connection as KubectlConnection
CONNECTION_TRANSPORT = 'oc'
CONNECTION_OPTIONS = {
'oc_container': '-c',
'oc_namespace': '-n',
'oc_kubeconfig': '--kubeconfig',
'oc_context': '--context',
'oc_host': '--server',
'client_cert': '--client-certificate',
'client_key': '--client-key',
'ca_cert': '--certificate-authority',
'validate_certs': '--insecure-skip-tls-verify',
'oc_token': '--token'
}
class Connection(KubectlConnection):
''' Local oc based connections '''
transport = CONNECTION_TRANSPORT
connection_options = CONNECTION_OPTIONS
documentation = DOCUMENTATION

View File

@@ -0,0 +1,204 @@
# Copyright (c) 2018 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: openshift
plugin_type: inventory
author:
- Chris Houseknecht <@chouseknecht>
short_description: OpenShift inventory source
description:
- Fetch containers, services and routes for one or more clusters
- Groups by cluster name, namespace, namespace_services, namespace_pods, namespace_routes, and labels
- Uses openshift.(yml|yaml) YAML configuration file to set parameter values.
options:
plugin:
description: token that ensures this is a source file for the 'openshift' plugin.
required: True
choices: ['openshift', 'community.okd.openshift']
connections:
description:
- Optional list of cluster connection settings. If no connections are provided, the default
I(~/.kube/config) and active context will be used, and objects will be returned for all namespaces
the active user is authorized to access.
suboptions:
name:
description:
- Optional name to assign to the cluster. If not provided, a name is constructed from the server
and port.
kubeconfig:
description:
- Path to an existing Kubernetes config file. If not provided, and no other connection
options are provided, the Kubernetes client will attempt to load the default
configuration file from I(~/.kube/config). Can also be specified via K8S_AUTH_KUBECONFIG
environment variable.
context:
description:
- The name of a context found in the config file. Can also be specified via K8S_AUTH_CONTEXT environment
variable.
host:
description:
- Provide a URL for accessing the API. Can also be specified via K8S_AUTH_HOST environment variable.
api_key:
description:
- Token used to authenticate with the API. Can also be specified via K8S_AUTH_API_KEY environment
variable.
username:
description:
- Provide a username for authenticating with the API. Can also be specified via K8S_AUTH_USERNAME
environment variable.
password:
description:
- Provide a password for authenticating with the API. Can also be specified via K8S_AUTH_PASSWORD
environment variable.
client_cert:
description:
- Path to a certificate used to authenticate with the API. Can also be specified via K8S_AUTH_CERT_FILE
environment variable.
aliases: [ cert_file ]
client_key:
description:
- Path to a key file used to authenticate with the API. Can also be specified via K8S_AUTH_KEY_FILE
environment variable.
aliases: [ key_file ]
ca_cert:
description:
- Path to a CA certificate used to authenticate with the API. Can also be specified via
K8S_AUTH_SSL_CA_CERT environment variable.
aliases: [ ssl_ca_cert ]
validate_certs:
description:
- "Whether or not to verify the API server's SSL certificates. Can also be specified via
K8S_AUTH_VERIFY_SSL environment variable."
type: bool
aliases: [ verify_ssl ]
namespaces:
description:
- List of namespaces. If not specified, will fetch all containers for all namespaces user is authorized
to access.
requirements:
- "python >= 3.6"
- "kubernetes >= 12.0.0"
- "PyYAML >= 3.11"
'''
EXAMPLES = '''
# File must be named openshift.yaml or openshift.yml
# Authenticate with token, and return all pods and services for all namespaces
plugin: community.okd.openshift
connections:
- host: https://192.168.64.4:8443
api_key: xxxxxxxxxxxxxxxx
verify_ssl: false
# Use default config (~/.kube/config) file and active context, and return objects for a specific namespace
plugin: community.okd.openshift
connections:
- namespaces:
- testing
# Use a custom config file, and a specific context.
plugin: community.okd.openshift
connections:
- kubeconfig: /path/to/config
context: 'awx/192-168-64-4:8443/developer'
'''
from ansible_collections.kubernetes.core.plugins.inventory.k8s import K8sInventoryException, InventoryModule as K8sInventoryModule, format_dynamic_api_exc
from ansible_collections.kubernetes.core.plugins.module_utils.common import get_api_client
try:
from kubernetes.dynamic.exceptions import DynamicApiError
except ImportError:
pass
class InventoryModule(K8sInventoryModule):
NAME = 'community.okd.openshift'
connection_plugin = 'community.okd.oc'
transport = 'oc'
def fetch_objects(self, connections):
super(InventoryModule, self).fetch_objects(connections)
if connections:
if not isinstance(connections, list):
raise K8sInventoryException("Expecting connections to be a list.")
for connection in connections:
client = get_api_client(**connection)
name = connection.get('name', self.get_default_host_name(client.configuration.host))
if connection.get('namespaces'):
namespaces = connection['namespaces']
else:
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(client, name, namespace)
else:
client = get_api_client()
name = self.get_default_host_name(client.configuration.host)
namespaces = self.get_available_namespaces(client)
for namespace in namespaces:
self.get_routes_for_namespace(client, name, namespace)
def get_routes_for_namespace(self, client, name, namespace):
v1_route = client.resources.get(api_version='route.openshift.io/v1', kind='Route')
try:
obj = v1_route.get(namespace=namespace)
except DynamicApiError as exc:
self.display.debug(exc)
raise K8sInventoryException('Error fetching Routes list: %s' % format_dynamic_api_exc(exc))
namespace_group = 'namespace_{0}'.format(namespace)
namespace_routes_group = '{0}_routes'.format(namespace_group)
self.inventory.add_group(name)
self.inventory.add_group(namespace_group)
self.inventory.add_child(name, namespace_group)
self.inventory.add_group(namespace_routes_group)
self.inventory.add_child(namespace_group, namespace_routes_group)
for route in obj.items:
route_name = route.metadata.name
route_annotations = {} if not route.metadata.annotations else dict(route.metadata.annotations)
self.inventory.add_host(route_name)
if route.metadata.labels:
# create a group for each label_value
for key, value in route.metadata.labels:
group_name = 'label_{0}_{1}'.format(key, value)
self.inventory.add_group(group_name)
self.inventory.add_child(group_name, route_name)
route_labels = dict(route.metadata.labels)
else:
route_labels = {}
self.inventory.add_child(namespace_routes_group, route_name)
# add hostvars
self.inventory.set_variable(route_name, 'labels', route_labels)
self.inventory.set_variable(route_name, 'annotations', route_annotations)
self.inventory.set_variable(route_name, 'cluster_name', route.metadata.clusterName)
self.inventory.set_variable(route_name, 'object_type', 'route')
self.inventory.set_variable(route_name, 'self_link', route.metadata.selfLink)
self.inventory.set_variable(route_name, 'resource_version', route.metadata.resourceVersion)
self.inventory.set_variable(route_name, 'uid', route.metadata.uid)
if route.spec.host:
self.inventory.set_variable(route_name, 'host', route.spec.host)
if route.spec.path:
self.inventory.set_variable(route_name, 'path', route.spec.path)
if hasattr(route.spec.port, 'targetPort') and route.spec.port.targetPort:
self.inventory.set_variable(route_name, 'port', dict(route.spec.port))

View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
import operator
from functools import reduce
import traceback
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
from ansible.module_utils._text import to_native
try:
from kubernetes.dynamic.exceptions import DynamicApiError, NotFoundError, ForbiddenError
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
TRIGGER_ANNOTATION = 'image.openshift.io/triggers'
TRIGGER_CONTAINER = re.compile(r"(?P<path>.*)\[((?P<index>[0-9]+)|\?\(@\.name==[\"'\\]*(?P<name>[a-z0-9]([-a-z0-9]*[a-z0-9])?))")
class OKDRawModule(K8sAnsibleMixin):
def __init__(self, module, k8s_kind=None, *args, **kwargs):
self.module = module
self.client = get_api_client(module=module)
self.check_mode = self.module.check_mode
self.params = self.module.params
self.fail_json = self.module.fail_json
self.fail = self.module.fail_json
self.exit_json = self.module.exit_json
super(OKDRawModule, self).__init__(module, *args, **kwargs)
self.warnings = []
self.kind = k8s_kind or self.params.get('kind')
self.api_version = self.params.get('api_version')
self.name = self.params.get('name')
self.namespace = self.params.get('namespace')
self.check_library_version()
self.set_resource_definitions(module)
def perform_action(self, resource, definition):
state = self.params.get('state', None)
name = definition['metadata'].get('name')
namespace = definition['metadata'].get('namespace')
if state != 'absent':
if resource.kind in ['Project', 'ProjectRequest']:
try:
resource.get(name, namespace)
except (NotFoundError, ForbiddenError):
return self.create_project_request(definition)
except DynamicApiError as exc:
self.fail_json(msg='Failed to retrieve requested object: {0}'.format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
try:
existing = resource.get(name=name, namespace=namespace).to_dict()
except Exception:
existing = None
if existing:
if resource.kind == 'DeploymentConfig':
if definition.get('spec', {}).get('triggers'):
definition = self.resolve_imagestream_triggers(existing, definition)
elif existing['metadata'].get('annotations', {}).get(TRIGGER_ANNOTATION):
definition = self.resolve_imagestream_trigger_annotation(existing, definition)
return super(OKDRawModule, self).perform_action(resource, definition)
@staticmethod
def get_index(desired, objects, keys):
""" Iterates over keys, returns the first object from objects where the value of the key
matches the value in desired
"""
# pylint: disable=use-a-generator
# Use a generator instead 'all(desired.get(key, True) == item.get(key, False) for key in keys)'
for i, item in enumerate(objects):
if item and all([desired.get(key, True) == item.get(key, False) for key in keys]):
return i
def resolve_imagestream_trigger_annotation(self, existing, definition):
import yaml
def get_from_fields(d, fields):
try:
return reduce(operator.getitem, fields, d)
except Exception:
return None
def set_from_fields(d, fields, value):
get_from_fields(d, fields[:-1])[fields[-1]] = value
if TRIGGER_ANNOTATION in definition['metadata'].get('annotations', {}).keys():
triggers = yaml.safe_load(definition['metadata']['annotations'][TRIGGER_ANNOTATION] or '[]')
else:
triggers = yaml.safe_load(existing['metadata'].get('annotations', '{}').get(TRIGGER_ANNOTATION, '[]'))
if not isinstance(triggers, list):
return definition
for trigger in triggers:
if trigger.get('fieldPath'):
parsed = self.parse_trigger_fieldpath(trigger['fieldPath'])
path = parsed.get('path', '').split('.')
if path:
existing_containers = get_from_fields(existing, path)
new_containers = get_from_fields(definition, path)
if parsed.get('name'):
existing_index = self.get_index({'name': parsed['name']}, existing_containers, ['name'])
new_index = self.get_index({'name': parsed['name']}, new_containers, ['name'])
elif parsed.get('index') is not None:
existing_index = new_index = int(parsed['index'])
else:
existing_index = new_index = None
if existing_index is not None and new_index is not None:
if existing_index < len(existing_containers) and new_index < len(new_containers):
set_from_fields(definition, path + [new_index, 'image'], get_from_fields(existing, path + [existing_index, 'image']))
return definition
def resolve_imagestream_triggers(self, existing, definition):
existing_triggers = existing.get('spec', {}).get('triggers')
new_triggers = definition['spec']['triggers']
existing_containers = existing.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
new_containers = definition.get('spec', {}).get('template', {}).get('spec', {}).get('containers', [])
for i, trigger in enumerate(new_triggers):
if trigger.get('type') == 'ImageChange' and trigger.get('imageChangeParams'):
names = trigger['imageChangeParams'].get('containerNames', [])
for name in names:
old_container_index = self.get_index({'name': name}, existing_containers, ['name'])
new_container_index = self.get_index({'name': name}, new_containers, ['name'])
if old_container_index is not None and new_container_index is not None:
image = existing['spec']['template']['spec']['containers'][old_container_index]['image']
definition['spec']['template']['spec']['containers'][new_container_index]['image'] = image
existing_index = self.get_index(trigger['imageChangeParams'],
[x.get('imageChangeParams') for x in existing_triggers],
['containerNames'])
if existing_index is not None:
existing_image = existing_triggers[existing_index].get('imageChangeParams', {}).get('lastTriggeredImage')
if existing_image:
definition['spec']['triggers'][i]['imageChangeParams']['lastTriggeredImage'] = existing_image
existing_from = existing_triggers[existing_index].get('imageChangeParams', {}).get('from', {})
new_from = trigger['imageChangeParams'].get('from', {})
existing_namespace = existing_from.get('namespace')
existing_name = existing_from.get('name', False)
new_name = new_from.get('name', True)
add_namespace = existing_namespace and 'namespace' not in new_from.keys() and existing_name == new_name
if add_namespace:
definition['spec']['triggers'][i]['imageChangeParams']['from']['namespace'] = existing_from['namespace']
return definition
def parse_trigger_fieldpath(self, expression):
parsed = TRIGGER_CONTAINER.search(expression).groupdict()
if parsed.get('index'):
parsed['index'] = int(parsed['index'])
return parsed
def create_project_request(self, definition):
definition['kind'] = 'ProjectRequest'
result = {'changed': False, 'result': {}}
resource = self.find_resource('ProjectRequest', definition['apiVersion'], fail=True)
if not self.check_mode:
try:
k8s_obj = resource.create(definition)
result['result'] = k8s_obj.to_dict()
except DynamicApiError as exc:
self.fail_json(msg="Failed to create object: {0}".format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
result['changed'] = True
result['method'] = 'create'
return result

View File

@@ -0,0 +1,335 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import traceback
from ansible.module_utils._text import to_native
try:
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
try:
from kubernetes import client
from kubernetes.dynamic.exceptions import DynamicApiError, NotFoundError
except ImportError:
pass
class OpenShiftAdmPruneAuth(K8sAnsibleMixin):
def __init__(self, module):
self.module = module
self.fail_json = self.module.fail_json
self.exit_json = self.module.exit_json
if not HAS_KUBERNETES_COLLECTION:
self.module.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception),
)
super(OpenShiftAdmPruneAuth, self).__init__(self.module)
self.params = self.module.params
self.check_mode = self.module.check_mode
self.client = get_api_client(self.module)
def prune_resource_binding(self, kind, api_version, ref_kind, ref_namespace_names, propagation_policy=None):
resource = self.find_resource(kind=kind, api_version=api_version, fail=True)
candidates = []
for ref_namespace, ref_name in ref_namespace_names:
try:
result = resource.get(name=None, namespace=ref_namespace)
result = result.to_dict()
result = result.get('items') if 'items' in result else [result]
for obj in result:
namespace = obj['metadata'].get('namespace', None)
name = obj['metadata'].get('name')
if ref_kind and obj['roleRef']['kind'] != ref_kind:
# skip this binding as the roleRef.kind does not match
continue
if obj['roleRef']['name'] == ref_name:
# select this binding as the roleRef.name match
candidates.append((namespace, name))
except NotFoundError:
continue
except DynamicApiError as exc:
msg = "Failed to get {kind} resource due to: {msg}".format(kind=kind, msg=exc.body)
self.fail_json(msg=msg)
except Exception as e:
msg = "Failed to get {kind} due to: {msg}".format(kind=kind, msg=to_native(e))
self.fail_json(msg=msg)
if len(candidates) == 0 or self.check_mode:
return [y if x is None else x + "/" + y for x, y in candidates]
delete_options = client.V1DeleteOptions()
if propagation_policy:
delete_options.propagation_policy = propagation_policy
for namespace, name in candidates:
try:
result = resource.delete(name=name, namespace=namespace, body=delete_options)
except DynamicApiError as exc:
msg = "Failed to delete {kind} {namespace}/{name} due to: {msg}".format(kind=kind, namespace=namespace, name=name, msg=exc.body)
self.fail_json(msg=msg)
except Exception as e:
msg = "Failed to delete {kind} {namespace}/{name} due to: {msg}".format(kind=kind, namespace=namespace, name=name, msg=to_native(e))
self.fail_json(msg=msg)
return [y if x is None else x + "/" + y for x, y in candidates]
def update_resource_binding(self, ref_kind, ref_names, namespaced=False):
kind = 'ClusterRoleBinding'
api_version = "rbac.authorization.k8s.io/v1",
if namespaced:
kind = "RoleBinding"
resource = self.find_resource(kind=kind, api_version=api_version, fail=True)
result = resource.get(name=None, namespace=None).to_dict()
result = result.get('items') if 'items' in result else [result]
if len(result) == 0:
return [], False
def _update_user_group(binding_namespace, subjects):
users, groups = [], []
for x in subjects:
if x['kind'] == 'User':
users.append(x['name'])
elif x['kind'] == 'Group':
groups.append(x['name'])
elif x['kind'] == 'ServiceAccount':
namespace = binding_namespace
if x.get('namespace') is not None:
namespace = x.get('namespace')
if namespace is not None:
users.append("system:serviceaccount:%s:%s" % (namespace, x['name']))
return users, groups
candidates = []
changed = False
for item in result:
subjects = item.get('subjects', [])
retainedSubjects = [x for x in subjects if x['kind'] == ref_kind and x['name'] in ref_names]
if len(subjects) != len(retainedSubjects):
updated_binding = item
updated_binding['subjects'] = retainedSubjects
binding_namespace = item['metadata'].get('namespace', None)
updated_binding['userNames'], updated_binding['groupNames'] = _update_user_group(binding_namespace, retainedSubjects)
candidates.append(binding_namespace + "/" + item['metadata']['name'] if binding_namespace else item['metadata']['name'])
changed = True
if not self.check_mode:
try:
resource.apply(updated_binding, namespace=binding_namespace)
except DynamicApiError as exc:
msg = "Failed to apply object due to: {0}".format(exc.body)
self.fail_json(msg=msg)
return candidates, changed
def update_security_context(self, ref_names, key):
params = {'kind': 'SecurityContextConstraints', 'api_version': 'security.openshift.io/v1'}
sccs = self.kubernetes_facts(**params)
if not sccs['api_found']:
self.fail_json(msg=sccs['msg'])
sccs = sccs.get('resources')
candidates = []
changed = False
resource = self.find_resource(kind="SecurityContextConstraints", api_version="security.openshift.io/v1")
for item in sccs:
subjects = item.get(key, [])
retainedSubjects = [x for x in subjects if x not in ref_names]
if len(subjects) != len(retainedSubjects):
candidates.append(item['metadata']['name'])
changed = True
if not self.check_mode:
upd_sec_ctx = item
upd_sec_ctx.update({key: retainedSubjects})
try:
resource.apply(upd_sec_ctx, namespace=None)
except DynamicApiError as exc:
msg = "Failed to apply object due to: {0}".format(exc.body)
self.fail_json(msg=msg)
return candidates, changed
def auth_prune_roles(self):
params = {'kind': 'Role', 'api_version': 'rbac.authorization.k8s.io/v1', 'namespace': self.params.get('namespace')}
for attr in ('name', 'label_selectors'):
if self.params.get(attr):
params[attr] = self.params.get(attr)
result = self.kubernetes_facts(**params)
if not result['api_found']:
self.fail_json(msg=result['msg'])
roles = result.get('resources')
if len(roles) == 0:
self.exit_json(changed=False, msg="No candidate rolebinding to prune from namespace %s." % self.params.get('namespace'))
ref_roles = [(x['metadata']['namespace'], x['metadata']['name']) for x in roles]
candidates = self.prune_resource_binding(kind="RoleBinding",
api_version="rbac.authorization.k8s.io/v1",
ref_kind="Role",
ref_namespace_names=ref_roles,
propagation_policy='Foreground')
if len(candidates) == 0:
self.exit_json(changed=False, role_binding=candidates)
self.exit_json(changed=True, role_binding=candidates)
def auth_prune_clusterroles(self):
params = {'kind': 'ClusterRole', 'api_version': 'rbac.authorization.k8s.io/v1'}
for attr in ('name', 'label_selectors'):
if self.params.get(attr):
params[attr] = self.params.get(attr)
result = self.kubernetes_facts(**params)
if not result['api_found']:
self.fail_json(msg=result['msg'])
clusterroles = result.get('resources')
if len(clusterroles) == 0:
self.exit_json(changed=False, msg="No clusterroles found matching input criteria.")
ref_clusterroles = [(None, x['metadata']['name']) for x in clusterroles]
# Prune ClusterRoleBinding
candidates_cluster_binding = self.prune_resource_binding(kind="ClusterRoleBinding",
api_version="rbac.authorization.k8s.io/v1",
ref_kind=None,
ref_namespace_names=ref_clusterroles)
# Prune Role Binding
candidates_namespaced_binding = self.prune_resource_binding(kind="RoleBinding",
api_version="rbac.authorization.k8s.io/v1",
ref_kind='ClusterRole',
ref_namespace_names=ref_clusterroles)
self.exit_json(changed=True,
cluster_role_binding=candidates_cluster_binding,
role_binding=candidates_namespaced_binding)
def list_groups(self, params=None):
options = {'kind': 'Group', 'api_version': 'user.openshift.io/v1'}
if params:
for attr in ('name', 'label_selectors'):
if params.get(attr):
options[attr] = params.get(attr)
return self.kubernetes_facts(**options)
def auth_prune_users(self):
params = {'kind': 'User', 'api_version': 'user.openshift.io/v1'}
for attr in ('name', 'label_selectors'):
if self.params.get(attr):
params[attr] = self.params.get(attr)
users = self.kubernetes_facts(**params)
if len(users) == 0:
self.exit_json(changed=False, msg="No resource type 'User' found matching input criteria.")
names = [x['metadata']['name'] for x in users]
changed = False
# Remove the user role binding
rolebinding, changed_role = self.update_resource_binding(ref_kind="User",
ref_names=names,
namespaced=True)
changed = changed or changed_role
# Remove the user cluster role binding
clusterrolesbinding, changed_cr = self.update_resource_binding(ref_kind="User",
ref_names=names)
changed = changed or changed_cr
# Remove the user from security context constraints
sccs, changed_sccs = self.update_security_context(names, 'users')
changed = changed or changed_sccs
# Remove the user from groups
groups = self.list_groups()
deleted_groups = []
resource = self.find_resource(kind="Group", api_version="user.openshift.io/v1")
for grp in groups:
subjects = grp.get('users', [])
retainedSubjects = [x for x in subjects if x not in names]
if len(subjects) != len(retainedSubjects):
deleted_groups.append(grp['metadata']['name'])
changed = True
if not self.check_mode:
upd_group = grp
upd_group.update({'users': retainedSubjects})
try:
resource.apply(upd_group, namespace=None)
except DynamicApiError as exc:
msg = "Failed to apply object due to: {0}".format(exc.body)
self.fail_json(msg=msg)
# Remove the user's OAuthClientAuthorizations
oauth = self.kubernetes_facts(kind='OAuthClientAuthorization', api_version='oauth.openshift.io/v1')
deleted_auths = []
resource = self.find_resource(kind="OAuthClientAuthorization", api_version="oauth.openshift.io/v1")
for authorization in oauth:
if authorization.get('userName', None) in names:
auth_name = authorization['metadata']['name']
deleted_auths.append(auth_name)
changed = True
if not self.check_mode:
try:
resource.delete(name=auth_name, namespace=None, body=client.V1DeleteOptions())
except DynamicApiError as exc:
msg = "Failed to delete OAuthClientAuthorization {name} due to: {msg}".format(name=auth_name, msg=exc.body)
self.fail_json(msg=msg)
except Exception as e:
msg = "Failed to delete OAuthClientAuthorization {name} due to: {msg}".format(name=auth_name, msg=to_native(e))
self.fail_json(msg=msg)
self.exit_json(changed=changed,
cluster_role_binding=clusterrolesbinding,
role_binding=rolebinding,
security_context_constraints=sccs,
authorization=deleted_auths,
group=deleted_groups)
def auth_prune_groups(self):
groups = self.list_groups(params=self.params)
if len(groups) == 0:
self.exit_json(changed=False, result="No resource type 'Group' found matching input criteria.")
names = [x['metadata']['name'] for x in groups]
changed = False
# Remove the groups role binding
rolebinding, changed_role = self.update_resource_binding(ref_kind="Group",
ref_names=names,
namespaced=True)
changed = changed or changed_role
# Remove the groups cluster role binding
clusterrolesbinding, changed_cr = self.update_resource_binding(ref_kind="Group",
ref_names=names)
changed = changed or changed_cr
# Remove the groups security context constraints
sccs, changed_sccs = self.update_security_context(names, 'groups')
changed = changed or changed_sccs
self.exit_json(changed=changed,
cluster_role_binding=clusterrolesbinding,
role_binding=rolebinding,
security_context_constraints=sccs)
def execute_module(self):
auth_prune = {
'roles': self.auth_prune_roles,
'clusterroles': self.auth_prune_clusterroles,
'users': self.auth_prune_users,
'groups': self.auth_prune_groups,
}
auth_prune[self.params.get('resource')]()

View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from datetime import datetime, timezone
import traceback
from ansible.module_utils._text import to_native
try:
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
try:
from kubernetes import client
from kubernetes.dynamic.exceptions import DynamicApiError
except ImportError as e:
pass
def get_deploymentconfig_for_replicationcontroller(replica_controller):
# DeploymentConfigAnnotation is an annotation name used to correlate a deployment with the
# DeploymentConfig on which the deployment is based.
# This is set on replication controller pod template by deployer controller.
DeploymentConfigAnnotation = "openshift.io/deployment-config.name"
try:
deploymentconfig_name = replica_controller['metadata']['annotations'].get(DeploymentConfigAnnotation)
if deploymentconfig_name is None or deploymentconfig_name == "":
return None
return deploymentconfig_name
except Exception:
return None
class OpenShiftAdmPruneDeployment(K8sAnsibleMixin):
def __init__(self, module):
self.module = module
self.fail_json = self.module.fail_json
self.exit_json = self.module.exit_json
if not HAS_KUBERNETES_COLLECTION:
self.module.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception),
)
super(OpenShiftAdmPruneDeployment, self).__init__(self.module)
self.params = self.module.params
self.check_mode = self.module.check_mode
self.client = get_api_client(self.module)
def filter_replication_controller(self, replicacontrollers):
def _deployment(obj):
return get_deploymentconfig_for_replicationcontroller(obj) is not None
def _zeroReplicaSize(obj):
return obj['spec']['replicas'] == 0 and obj['status']['replicas'] == 0
def _complete_failed(obj):
DeploymentStatusAnnotation = "openshift.io/deployment.phase"
try:
# validate that replication controller status is either 'Complete' or 'Failed'
deployment_phase = obj['metadata']['annotations'].get(DeploymentStatusAnnotation)
return deployment_phase in ('Failed', 'Complete')
except Exception:
return False
def _younger(obj):
creation_timestamp = datetime.strptime(obj['metadata']['creationTimestamp'], '%Y-%m-%dT%H:%M:%SZ')
now = datetime.now(timezone.utc).replace(tzinfo=None)
age = (now - creation_timestamp).seconds / 60
return age > self.params['keep_younger_than']
def _orphan(obj):
try:
# verify if the deploymentconfig associated to the replication controller is still existing
deploymentconfig_name = get_deploymentconfig_for_replicationcontroller(obj)
params = dict(
kind="DeploymentConfig",
api_version="apps.openshift.io/v1",
name=deploymentconfig_name,
namespace=obj["metadata"]["name"],
)
exists = self.kubernetes_facts(**params)
return not (exists.get['api_found'] and len(exists['resources']) > 0)
except Exception:
return False
predicates = [_deployment, _zeroReplicaSize, _complete_failed]
if self.params['orphans']:
predicates.append(_orphan)
if self.params['keep_younger_than']:
predicates.append(_younger)
results = replicacontrollers.copy()
for pred in predicates:
results = filter(pred, results)
return list(results)
def execute(self):
# list replicationcontroller candidate for pruning
kind = 'ReplicationController'
api_version = 'v1'
resource = self.find_resource(kind=kind, api_version=api_version, fail=True)
# Get ReplicationController
params = dict(
kind=kind,
api_version="v1",
namespace=self.params.get("namespace"),
)
candidates = self.kubernetes_facts(**params)
candidates = self.filter_replication_controller(candidates["resources"])
if len(candidates) == 0:
self.exit_json(changed=False, replication_controllers=[])
changed = True
delete_options = client.V1DeleteOptions(propagation_policy='Background')
replication_controllers = []
for replica in candidates:
try:
result = replica
if not self.check_mode:
name = replica["metadata"]["name"]
namespace = replica["metadata"]["namespace"]
result = resource.delete(name=name, namespace=namespace, body=delete_options).to_dict()
replication_controllers.append(result)
except DynamicApiError as exc:
msg = "Failed to delete ReplicationController {namespace}/{name} due to: {msg}".format(namespace=namespace, name=name, msg=exc.body)
self.fail_json(msg=msg)
except Exception as e:
msg = "Failed to delete ReplicationController {namespace}/{name} due to: {msg}".format(namespace=namespace, name=name, msg=to_native(e))
self.fail_json(msg=msg)
self.exit_json(changed=changed, replication_controllers=replication_controllers)

View File

@@ -0,0 +1,513 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from datetime import datetime, timezone, timedelta
import traceback
import copy
from ansible.module_utils._text import to_native
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.six import iteritems
try:
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
from ansible_collections.community.okd.plugins.module_utils.openshift_images_common import (
OpenShiftAnalyzeImageStream,
get_image_blobs,
is_too_young_object,
is_created_after,
)
from ansible_collections.community.okd.plugins.module_utils.openshift_docker_image import (
parse_docker_image_ref,
convert_storage_to_bytes,
)
try:
from kubernetes import client
from kubernetes.client import rest
from kubernetes.dynamic.exceptions import (
DynamicApiError,
NotFoundError,
ApiException
)
except ImportError:
pass
ApiConfiguration = {
"LimitRange": "v1",
"Pod": "v1",
"ReplicationController": "v1",
"DaemonSet": "apps/v1",
"Deployment": "apps/v1",
"ReplicaSet": "apps/v1",
"StatefulSet": "apps/v1",
"Job": "batch/v1",
"CronJob": "batch/v1beta1",
"DeploymentConfig": "apps.openshift.io/v1",
"BuildConfig": "build.openshift.io/v1",
"Build": "build.openshift.io/v1",
"Image": "image.openshift.io/v1",
"ImageStream": "image.openshift.io/v1",
}
def read_object_annotation(obj, name):
return obj["metadata"]["annotations"].get(name)
def determine_host_registry(module, images, image_streams):
# filter managed images
def _f_managed_images(obj):
value = read_object_annotation(obj, "openshift.io/image.managed")
return boolean(value) if value is not None else False
managed_images = list(filter(_f_managed_images, images))
# Be sure to pick up the newest managed image which should have an up to date information
sorted_images = sorted(managed_images,
key=lambda x: x["metadata"]["creationTimestamp"],
reverse=True)
docker_image_ref = ""
if len(sorted_images) > 0:
docker_image_ref = sorted_images[0].get("dockerImageReference", "")
else:
# 2nd try to get the pull spec from any image stream
# Sorting by creation timestamp may not get us up to date info. Modification time would be much
sorted_image_streams = sorted(image_streams,
key=lambda x: x["metadata"]["creationTimestamp"],
reverse=True)
for i_stream in sorted_image_streams:
docker_image_ref = i_stream["status"].get("dockerImageRepository", "")
if len(docker_image_ref) > 0:
break
if len(docker_image_ref) == 0:
module.exit_json(changed=False, result="no managed image found")
result, error = parse_docker_image_ref(docker_image_ref, module)
return result['hostname']
class OpenShiftAdmPruneImages(K8sAnsibleMixin):
def __init__(self, module):
self.module = module
self.fail_json = self.module.fail_json
self.exit_json = self.module.exit_json
if not HAS_KUBERNETES_COLLECTION:
self.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception),
)
super(OpenShiftAdmPruneImages, self).__init__(self.module)
self.params = self.module.params
self.check_mode = self.module.check_mode
try:
self.client = get_api_client(self.module)
except DynamicApiError as e:
self.fail_json(
msg="Failed to get kubernetes client.",
reason=e.reason,
status=e.status,
)
except Exception as e:
self.fail_json(
msg="Failed to get kubernetes client.",
error=to_native(e)
)
self.max_creation_timestamp = self.get_max_creation_timestamp()
self._rest_client = None
self.registryhost = self.params.get('registry_url')
self.changed = False
def list_objects(self):
result = {}
for kind, version in iteritems(ApiConfiguration):
namespace = None
if self.params.get("namespace") and kind.lower() == "imagestream":
namespace = self.params.get("namespace")
try:
result[kind] = self.kubernetes_facts(kind=kind,
api_version=version,
namespace=namespace).get('resources')
except DynamicApiError as e:
self.fail_json(
msg="An error occurred while trying to list objects.",
reason=e.reason,
status=e.status,
)
except Exception as e:
self.fail_json(
msg="An error occurred while trying to list objects.",
error=to_native(e)
)
return result
def get_max_creation_timestamp(self):
result = None
if self.params.get("keep_younger_than"):
dt_now = datetime.now(timezone.utc).replace(tzinfo=None)
result = dt_now - timedelta(minutes=self.params.get("keep_younger_than"))
return result
@property
def rest_client(self):
if not self._rest_client:
configuration = copy.deepcopy(self.client.configuration)
validate_certs = self.params.get('registry_validate_certs')
ssl_ca_cert = self.params.get('registry_ca_cert')
if validate_certs is not None:
configuration.verify_ssl = validate_certs
if ssl_ca_cert is not None:
configuration.ssl_ca_cert = ssl_ca_cert
self._rest_client = rest.RESTClientObject(configuration)
return self._rest_client
def delete_from_registry(self, url):
try:
response = self.rest_client.DELETE(url=url, headers=self.client.configuration.api_key)
if response.status == 404:
# Unable to delete layer
return None
# non-2xx/3xx response doesn't cause an error
if response.status < 200 or response.status >= 400:
return None
if response.status != 202 and response.status != 204:
self.fail_json(
msg="Delete URL {0}: Unexpected status code in response: {1}".format(
response.status, url),
reason=response.reason
)
return None
except ApiException as e:
if e.status != 404:
self.fail_json(
msg="Failed to delete URL: %s" % url,
reason=e.reason,
status=e.status,
)
except Exception as e:
self.fail_json(msg="Delete URL {0}: {1}".format(url, type(e)))
def delete_layers_links(self, path, layers):
for layer in layers:
url = "%s/v2/%s/blobs/%s" % (self.registryhost, path, layer)
self.changed = True
if not self.check_mode:
self.delete_from_registry(url=url)
def delete_manifests(self, path, digests):
for digest in digests:
url = "%s/v2/%s/manifests/%s" % (self.registryhost, path, digest)
self.changed = True
if not self.check_mode:
self.delete_from_registry(url=url)
def delete_blobs(self, blobs):
for blob in blobs:
self.changed = True
url = "%s/admin/blobs/%s" % (self.registryhost, blob)
if not self.check_mode:
self.delete_from_registry(url=url)
def update_image_stream_status(self, definition):
kind = definition["kind"]
api_version = definition["apiVersion"]
namespace = definition["metadata"]["namespace"]
name = definition["metadata"]["name"]
self.changed = True
result = definition
if not self.check_mode:
try:
result = self.client.request(
"PUT",
"/apis/{api_version}/namespaces/{namespace}/imagestreams/{name}/status".format(
api_version=api_version,
namespace=namespace,
name=name
),
body=definition,
content_type="application/json",
).to_dict()
except DynamicApiError as exc:
msg = "Failed to patch object: kind={0} {1}/{2}".format(
kind, namespace, name
)
self.fail_json(msg=msg, status=exc.status, reason=exc.reason)
except Exception as exc:
msg = "Failed to patch object kind={0} {1}/{2} due to: {3}".format(
kind, namespace, name, exc
)
self.fail_json(msg=msg, error=to_native(exc))
return result
def delete_image(self, image):
kind = "Image"
api_version = "image.openshift.io/v1"
resource = self.find_resource(kind=kind, api_version=api_version)
name = image["metadata"]["name"]
self.changed = True
if not self.check_mode:
try:
delete_options = client.V1DeleteOptions(grace_period_seconds=0)
return resource.delete(name=name, body=delete_options).to_dict()
except NotFoundError:
pass
except DynamicApiError as exc:
self.fail_json(
msg="Failed to delete object %s/%s due to: %s" % (
kind, name, exc.body
),
reason=exc.reason,
status=exc.status
)
else:
existing = resource.get(name=name)
if existing:
existing = existing.to_dict()
return existing
def exceeds_limits(self, namespace, image):
if namespace not in self.limit_range:
return False
docker_image_metadata = image.get("dockerImageMetadata")
if not docker_image_metadata:
return False
docker_image_size = docker_image_metadata["Size"]
for limit in self.limit_range.get(namespace):
for item in limit["spec"]["limits"]:
if item["type"] != "openshift.io/Image":
continue
limit_max = item["max"]
if not limit_max:
continue
storage = limit_max["storage"]
if not storage:
continue
if convert_storage_to_bytes(storage) < docker_image_size:
# image size is larger than the permitted limit range max size
return True
return False
def prune_image_stream_tag(self, stream, tag_event_list):
manifests_to_delete, images_to_delete = [], []
filtered_items = []
tag_event_items = tag_event_list["items"] or []
prune_over_size_limit = self.params.get("prune_over_size_limit")
stream_namespace = stream["metadata"]["namespace"]
stream_name = stream["metadata"]["name"]
for idx, item in enumerate(tag_event_items):
if is_created_after(item["created"], self.max_creation_timestamp):
filtered_items.append(item)
continue
if idx == 0:
istag = "%s/%s:%s" % (stream_namespace,
stream_name,
tag_event_list["tag"])
if istag in self.used_tags:
# keeping because tag is used
filtered_items.append(item)
continue
if item["image"] not in self.image_mapping:
# There are few options why the image may not be found:
# 1. the image is deleted manually and this record is no longer valid
# 2. the imagestream was observed before the image creation, i.e.
# this record was created recently and it should be protected by keep_younger_than
continue
image = self.image_mapping[item["image"]]
# check prune over limit size
if prune_over_size_limit and not self.exceeds_limits(stream_namespace, image):
filtered_items.append(item)
continue
image_ref = "%s/%s@%s" % (stream_namespace,
stream_name,
item["image"])
if image_ref in self.used_images:
# keeping because tag is used
filtered_items.append(item)
continue
images_to_delete.append(item["image"])
if self.params.get('prune_registry'):
manifests_to_delete.append(image["metadata"]["name"])
path = stream_namespace + "/" + stream_name
image_blobs, err = get_image_blobs(image)
if not err:
self.delete_layers_links(path, image_blobs)
return filtered_items, manifests_to_delete, images_to_delete
def prune_image_streams(self, stream):
name = stream['metadata']['namespace'] + "/" + stream['metadata']['name']
if is_too_young_object(stream, self.max_creation_timestamp):
# keeping all images because of image stream too young
return None, []
facts = self.kubernetes_facts(kind="ImageStream",
api_version=ApiConfiguration.get("ImageStream"),
name=stream["metadata"]["name"],
namespace=stream["metadata"]["namespace"])
image_stream = facts.get('resources')
if len(image_stream) != 1:
# skipping because it does not exist anymore
return None, []
stream = image_stream[0]
namespace = self.params.get("namespace")
stream_to_update = not namespace or (stream["metadata"]["namespace"] == namespace)
manifests_to_delete, images_to_delete = [], []
deleted_items = False
# Update Image stream tag
if stream_to_update:
tags = stream["status"].get("tags", [])
for idx, tag_event_list in enumerate(tags):
(
filtered_tag_event,
tag_manifests_to_delete,
tag_images_to_delete
) = self.prune_image_stream_tag(stream, tag_event_list)
stream['status']['tags'][idx]['items'] = filtered_tag_event
manifests_to_delete += tag_manifests_to_delete
images_to_delete += tag_images_to_delete
deleted_items = deleted_items or (len(tag_images_to_delete) > 0)
# Deleting tags without items
tags = []
for tag in stream["status"].get("tags", []):
if tag['items'] is None or len(tag['items']) == 0:
continue
tags.append(tag)
stream['status']['tags'] = tags
result = None
# Update ImageStream
if stream_to_update:
if deleted_items:
result = self.update_image_stream_status(stream)
if self.params.get("prune_registry"):
self.delete_manifests(name, manifests_to_delete)
return result, images_to_delete
def prune_images(self, image):
if not self.params.get("all_images"):
if read_object_annotation(image, "openshift.io/image.managed") != "true":
# keeping external image because all_images is set to false
# pruning only managed images
return None
if is_too_young_object(image, self.max_creation_timestamp):
# keeping because of keep_younger_than
return None
# Deleting image from registry
if self.params.get("prune_registry"):
image_blobs, err = get_image_blobs(image)
if err:
self.fail_json(msg=err)
# add blob for image name
image_blobs.append(image["metadata"]["name"])
self.delete_blobs(image_blobs)
# Delete image from cluster
return self.delete_image(image)
def execute_module(self):
resources = self.list_objects()
if not self.check_mode and self.params.get('prune_registry'):
if not self.registryhost:
self.registryhost = determine_host_registry(self.module, resources['Image'], resources['ImageStream'])
# validate that host has a scheme
if "://" not in self.registryhost:
self.registryhost = "https://" + self.registryhost
# Analyze Image Streams
analyze_ref = OpenShiftAnalyzeImageStream(
ignore_invalid_refs=self.params.get('ignore_invalid_refs'),
max_creation_timestamp=self.max_creation_timestamp,
module=self.module
)
self.used_tags, self.used_images, error = analyze_ref.analyze_image_stream(resources)
if error:
self.fail_json(msg=error)
# Create image mapping
self.image_mapping = {}
for m in resources["Image"]:
self.image_mapping[m["metadata"]["name"]] = m
# Create limit range mapping
self.limit_range = {}
for limit in resources["LimitRange"]:
namespace = limit["metadata"]["namespace"]
if namespace not in self.limit_range:
self.limit_range[namespace] = []
self.limit_range[namespace].append(limit)
# Stage 1: delete history from image streams
updated_image_streams = []
deleted_tags_images = []
updated_is_mapping = {}
for stream in resources['ImageStream']:
result, images_to_delete = self.prune_image_streams(stream)
if result:
updated_is_mapping[result["metadata"]["namespace"] + "/" + result["metadata"]["name"]] = result
updated_image_streams.append(result)
deleted_tags_images += images_to_delete
# Create a list with images referenced on image stream
self.referenced_images = []
for item in self.kubernetes_facts(kind="ImageStream", api_version="image.openshift.io/v1")["resources"]:
name = "%s/%s" % (item["metadata"]["namespace"], item["metadata"]["name"])
if name in updated_is_mapping:
item = updated_is_mapping[name]
for tag in item["status"].get("tags", []):
self.referenced_images += [t["image"] for t in tag["items"] or []]
# Stage 2: delete images
images = []
images_to_delete = [x["metadata"]["name"] for x in resources['Image']]
if self.params.get("namespace") is not None:
# When namespace is defined, prune only images that were referenced by ImageStream
# from the corresponding namespace
images_to_delete = deleted_tags_images
for name in images_to_delete:
if name in self.referenced_images:
# The image is referenced in one or more Image stream
continue
if name not in self.image_mapping:
# The image is not existing anymore
continue
result = self.prune_images(self.image_mapping[name])
if result:
images.append(result)
result = {
"changed": self.changed,
"deleted_images": images,
"updated_image_streams": updated_image_streams,
}
self.exit_json(**result)

View File

@@ -0,0 +1,103 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
def convert_storage_to_bytes(value):
keys = {
"Ki": 1024,
"Mi": 1024 * 1024,
"Gi": 1024 * 1024 * 1024,
"Ti": 1024 * 1024 * 1024 * 1024,
"Pi": 1024 * 1024 * 1024 * 1024 * 1024,
"Ei": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
}
for k in keys:
if value.endswith(k) or value.endswith(k[0]):
idx = value.find(k[0])
return keys.get(k) * int(value[:idx])
return int(value)
def is_valid_digest(digest):
digest_algorithm_size = dict(
sha256=64, sha384=96, sha512=128,
)
m = re.match(r'[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+', digest)
if not m:
return "Docker digest does not match expected format %s" % digest
idx = digest.find(':')
# case: "sha256:" with no hex.
if idx < 0 or idx == (len(digest) - 1):
return "Invalid docker digest %s, no hex value define" % digest
algorithm = digest[:idx]
if algorithm not in digest_algorithm_size:
return "Unsupported digest algorithm value %s for digest %s" % (algorithm, digest)
hex_value = digest[idx + 1:]
if len(hex_value) != digest_algorithm_size.get(algorithm):
return "Invalid length for digest hex expected %d found %d (digest is %s)" % (
digest_algorithm_size.get(algorithm), len(hex_value), digest
)
def parse_docker_image_ref(image_ref, module=None):
"""
Docker Grammar Reference
Reference => name [ ":" tag ] [ "@" digest ]
name => [hostname '/'] component ['/' component]*
hostname => hostcomponent ['.' hostcomponent]* [':' port-number]
hostcomponent => /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
port-number => /[0-9]+/
component => alpha-numeric [separator alpha-numeric]*
alpha-numeric => /[a-z0-9]+/
separator => /[_.]|__|[-]*/
"""
idx = image_ref.find("/")
def _contains_any(src, values):
return any(x in src for x in values)
result = {
"tag": None, "digest": None
}
default_domain = "docker.io"
if idx < 0 or (not _contains_any(image_ref[:idx], ":.") and image_ref[:idx] != "localhost"):
result["hostname"], remainder = default_domain, image_ref
else:
result["hostname"], remainder = image_ref[:idx], image_ref[idx + 1:]
# Parse remainder information
idx = remainder.find("@")
if idx > 0 and len(remainder) > (idx + 1):
# docker image reference with digest
component, result["digest"] = remainder[:idx], remainder[idx + 1:]
err = is_valid_digest(result["digest"])
if err:
if module:
module.fail_json(msg=err)
return None, err
else:
idx = remainder.find(":")
if idx > 0 and len(remainder) > (idx + 1):
# docker image reference with tag
component, result["tag"] = remainder[:idx], remainder[idx + 1:]
else:
# name only
component = remainder
v = component.split("/")
namespace = None
if len(v) > 1:
namespace = v[0]
result.update({
"namespace": namespace, "name": v[-1]
})
return result, None

View File

@@ -0,0 +1,466 @@
#!/usr/bin/env python
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import traceback
from datetime import datetime
from ansible_collections.kubernetes.core.plugins.module_utils.common import (K8sAnsibleMixin, get_api_client)
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils._text import to_native
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.okd.plugins.module_utils.openshift_ldap import (
validate_ldap_sync_config,
ldap_split_host_port,
OpenshiftLDAPRFC2307,
OpenshiftLDAPActiveDirectory,
OpenshiftLDAPAugmentedActiveDirectory
)
try:
import ldap
HAS_PYTHON_LDAP = True
except ImportError as e:
HAS_PYTHON_LDAP = False
PYTHON_LDAP_ERROR = e
try:
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
try:
from kubernetes.dynamic.exceptions import DynamicApiError
except ImportError as e:
pass
LDAP_OPENSHIFT_HOST_LABEL = "openshift.io/ldap.host"
LDAP_OPENSHIFT_URL_ANNOTATION = "openshift.io/ldap.url"
LDAP_OPENSHIFT_UID_ANNOTATION = "openshift.io/ldap.uid"
LDAP_OPENSHIFT_SYNCTIME_ANNOTATION = "openshift.io/ldap.sync-time"
def connect_to_ldap(module, server_uri, bind_dn=None, bind_pw=None, insecure=True, ca_file=None):
if insecure:
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
elif ca_file:
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_file)
try:
connection = ldap.initialize(server_uri)
connection.set_option(ldap.OPT_REFERRALS, 0)
connection.simple_bind_s(bind_dn, bind_pw)
return connection
except ldap.LDAPError as e:
module.fail_json(msg="Cannot bind to the LDAP server '{0}' due to: {1}".format(server_uri, e))
def validate_group_annotation(definition, host_ip):
name = definition['metadata']['name']
# Validate LDAP URL Annotation
annotate_url = definition['metadata'].get('annotations', {}).get(LDAP_OPENSHIFT_URL_ANNOTATION)
if host_ip:
if not annotate_url:
return "group '{0}' marked as having been synced did not have an '{1}' annotation".format(name, LDAP_OPENSHIFT_URL_ANNOTATION)
elif annotate_url != host_ip:
return "group '{0}' was not synchronized from: '{1}'".format(name, host_ip)
# Validate LDAP UID Annotation
annotate_uid = definition['metadata']['annotations'].get(LDAP_OPENSHIFT_UID_ANNOTATION)
if not annotate_uid:
return "group '{0}' marked as having been synced did not have an '{1}' annotation".format(name, LDAP_OPENSHIFT_UID_ANNOTATION)
return None
class OpenshiftLDAPGroups(object):
kind = "Group"
version = "user.openshift.io/v1"
def __init__(self, module):
self.module = module
self.cache = {}
self.__group_api = None
@property
def k8s_group_api(self):
if not self.__group_api:
params = dict(
kind=self.kind,
api_version=self.version,
fail=True
)
self.__group_api = self.module.find_resource(**params)
return self.__group_api
def get_group_info(self, return_list=False, **kwargs):
params = dict(
kind=self.kind,
api_version=self.version,
)
params.update(kwargs)
result = self.module.kubernetes_facts(**params)
if len(result["resources"]) == 0:
return None
if len(result["resources"]) == 1 and not return_list:
return result["resources"][0]
else:
return result["resources"]
def list_groups(self):
allow_groups = self.module.params.get("allow_groups")
deny_groups = self.module.params.get("deny_groups")
name_mapping = self.module.config.get("groupUIDNameMapping")
if name_mapping and (allow_groups or deny_groups):
def _map_group_names(groups):
return [name_mapping.get(value, value) for value in groups]
allow_groups = _map_group_names(allow_groups)
deny_groups = _map_group_names(deny_groups)
host = self.module.host
netlocation = self.module.netlocation
groups = []
if allow_groups:
missing = []
for grp in allow_groups:
if grp in deny_groups:
continue
resource = self.get_group_info(name=grp)
if not resource:
missing.append(grp)
continue
groups.append(resource)
if missing:
self.module.fail_json(
msg="The following groups were not found: %s" % ''.join(missing)
)
else:
label_selector = "%s=%s" % (LDAP_OPENSHIFT_HOST_LABEL, host)
resources = self.get_group_info(label_selectors=[label_selector], return_list=True)
if not resources:
return None, "Unable to find Group matching label selector '%s'" % label_selector
groups = resources
if deny_groups:
groups = [item for item in groups if item["metadata"]["name"] not in deny_groups]
uids = []
for grp in groups:
err = validate_group_annotation(grp, netlocation)
if err and allow_groups:
# We raise an error for group part of the allow_group not matching LDAP sync criteria
return None, err
group_uid = grp['metadata']['annotations'].get(LDAP_OPENSHIFT_UID_ANNOTATION)
self.cache[group_uid] = grp
uids.append(group_uid)
return uids, None
def get_group_name_for_uid(self, group_uid):
if group_uid not in self.cache:
return None, "No mapping found for Group uid: %s" % group_uid
return self.cache[group_uid]["metadata"]["name"], None
def make_openshift_group(self, group_uid, group_name, usernames):
group = self.get_group_info(name=group_name)
if not group:
group = {
"apiVersion": "user.openshift.io/v1",
"kind": "Group",
"metadata": {
"name": group_name,
"labels": {
LDAP_OPENSHIFT_HOST_LABEL: self.module.host
},
"annotations": {
LDAP_OPENSHIFT_URL_ANNOTATION: self.module.netlocation,
LDAP_OPENSHIFT_UID_ANNOTATION: group_uid,
}
}
}
# Make sure we aren't taking over an OpenShift group that is already related to a different LDAP group
ldaphost_label = group["metadata"].get("labels", {}).get(LDAP_OPENSHIFT_HOST_LABEL)
if not ldaphost_label or ldaphost_label != self.module.host:
return None, "Group %s: %s label did not match sync host: wanted %s, got %s" % (
group_name, LDAP_OPENSHIFT_HOST_LABEL, self.module.host, ldaphost_label
)
ldapurl_annotation = group["metadata"].get("annotations", {}).get(LDAP_OPENSHIFT_URL_ANNOTATION)
if not ldapurl_annotation or ldapurl_annotation != self.module.netlocation:
return None, "Group %s: %s annotation did not match sync host: wanted %s, got %s" % (
group_name, LDAP_OPENSHIFT_URL_ANNOTATION, self.module.netlocation, ldapurl_annotation
)
ldapuid_annotation = group["metadata"].get("annotations", {}).get(LDAP_OPENSHIFT_UID_ANNOTATION)
if not ldapuid_annotation or ldapuid_annotation != group_uid:
return None, "Group %s: %s annotation did not match LDAP UID: wanted %s, got %s" % (
group_name, LDAP_OPENSHIFT_UID_ANNOTATION, group_uid, ldapuid_annotation
)
# Overwrite Group Users data
group["users"] = usernames
group["metadata"]["annotations"][LDAP_OPENSHIFT_SYNCTIME_ANNOTATION] = datetime.now().isoformat()
return group, None
def create_openshift_groups(self, groups: list):
diffs = []
results = []
changed = False
for definition in groups:
name = definition["metadata"]["name"]
existing = self.get_group_info(name=name)
if not self.module.check_mode:
try:
if existing:
method = 'patch'
definition = self.k8s_group_api.patch(definition).to_dict()
else:
method = 'create'
definition = self.k8s_group_api.create(definition).to_dict()
except DynamicApiError as exc:
self.module.fail_json(msg="Failed to %s Group '%s' due to: %s" % (method, name, exc.body))
except Exception as exc:
self.module.fail_json(msg="Failed to %s Group '%s' due to: %s" % (method, name, to_native(exc)))
equals = False
if existing:
equals, diff = self.module.diff_objects(existing, definition)
diffs.append(diff)
changed = changed or not equals
results.append(definition)
return results, diffs, changed
def delete_openshift_group(self, name: str):
result = dict(
kind=self.kind,
apiVersion=self.version,
metadata=dict(
name=name
)
)
if not self.module.check_mode:
try:
result = self.k8s_group_api.delete(name=name).to_dict()
except DynamicApiError as exc:
self.module.fail_json(msg="Failed to delete Group '{0}' due to: {1}".format(name, exc.body))
except Exception as exc:
self.module.fail_json(msg="Failed to delete Group '{0}' due to: {1}".format(name, to_native(exc)))
return result
class OpenshiftGroupsSync(K8sAnsibleMixin):
def __init__(self, module):
self.module = module
if not HAS_KUBERNETES_COLLECTION:
self.module.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception),
)
if not HAS_PYTHON_LDAP:
self.fail_json(
msg=missing_required_lib('python-ldap'), error=to_native(PYTHON_LDAP_ERROR)
)
super(OpenshiftGroupsSync, self).__init__(self.module)
self.params = self.module.params
self.check_mode = self.module.check_mode
self.client = get_api_client(self.module)
self.__k8s_group_api = None
self.__ldap_connection = None
self.host = None
self.port = None
self.netlocation = None
self.scheme = None
self.config = self.params.get("sync_config")
@property
def k8s_group_api(self):
if not self.__k8s_group_api:
params = dict(
kind="Group",
api_version="user.openshift.io/v1",
fail=True
)
self.__k8s_group_api = self.find_resource(**params)
return self.__k8s_group_api
@property
def hostIP(self):
return self.netlocation
@property
def connection(self):
if not self.__ldap_connection:
# Create connection object
params = dict(
module=self,
server_uri=self.config.get('url'),
bind_dn=self.config.get('bindDN'),
bind_pw=self.config.get('bindPassword'),
insecure=boolean(self.config.get('insecure')),
ca_file=self.config.get('ca')
)
self.__ldap_connection = connect_to_ldap(**params)
return self.__ldap_connection
def close_connection(self):
if self.__ldap_connection:
self.__ldap_connection.unbind_s()
self.__ldap_connection = None
def exit_json(self, **kwargs):
self.close_connection()
self.module.exit_json(**kwargs)
def fail_json(self, **kwargs):
self.close_connection()
self.module.fail_json(**kwargs)
def get_syncer(self):
syncer = None
if "rfc2307" in self.config:
syncer = OpenshiftLDAPRFC2307(self.config, self.connection)
elif "activeDirectory" in self.config:
syncer = OpenshiftLDAPActiveDirectory(self.config, self.connection)
elif "augmentedActiveDirectory" in self.config:
syncer = OpenshiftLDAPAugmentedActiveDirectory(self.config, self.connection)
else:
msg = "No schema-specific config was found, should be one of 'rfc2307', 'activeDirectory', 'augmentedActiveDirectory'"
self.fail_json(msg=msg)
return syncer
def synchronize(self):
sync_group_type = self.module.params.get("type")
groups_uids = []
ldap_openshift_group = OpenshiftLDAPGroups(module=self)
# Get Synchronize object
syncer = self.get_syncer()
# Determine what to sync : list groups
if sync_group_type == "openshift":
groups_uids, err = ldap_openshift_group.list_groups()
if err:
self.fail_json(msg="Failed to list openshift groups", errors=err)
else:
# List LDAP Group to synchronize
groups_uids = self.params.get("allow_groups")
if not groups_uids:
groups_uids, err = syncer.list_groups()
if err:
self.module.fail_json(msg=err)
deny_groups = self.params.get("deny_groups")
if deny_groups:
groups_uids = [uid for uid in groups_uids if uid not in deny_groups]
openshift_groups = []
for uid in groups_uids:
# Get membership data
member_entries, err = syncer.extract_members(uid)
if err:
self.fail_json(msg=err)
# Determine usernames for members entries
usernames = []
for entry in member_entries:
name, err = syncer.get_username_for_entry(entry)
if err:
self.exit_json(
msg="Unable to determine username for entry %s: %s" % (entry, err)
)
if isinstance(name, list):
usernames.extend(name)
else:
usernames.append(name)
# Get group name
if sync_group_type == "openshift":
group_name, err = ldap_openshift_group.get_group_name_for_uid(uid)
else:
group_name, err = syncer.get_group_name_for_uid(uid)
if err:
self.exit_json(msg=err)
# Make Openshift group
group, err = ldap_openshift_group.make_openshift_group(uid, group_name, usernames)
if err:
self.fail_json(msg=err)
openshift_groups.append(group)
# Create Openshift Groups
results, diffs, changed = ldap_openshift_group.create_openshift_groups(openshift_groups)
self.module.exit_json(changed=True, groups=results)
def prune(self):
ldap_openshift_group = OpenshiftLDAPGroups(module=self)
groups_uids, err = ldap_openshift_group.list_groups()
if err:
self.fail_json(msg="Failed to list openshift groups", errors=err)
# Get Synchronize object
syncer = self.get_syncer()
changed = False
groups = []
for uid in groups_uids:
# Check if LDAP group exist
exists, err = syncer.is_ldapgroup_exists(uid)
if err:
msg = "Error determining LDAP group existence for group %s: %s" % (uid, err)
self.module.fail_json(msg=msg)
if exists:
continue
# if the LDAP entry that was previously used to create the group doesn't exist, prune it
group_name, err = ldap_openshift_group.get_group_name_for_uid(uid)
if err:
self.module.fail_json(msg=err)
# Delete Group
result = ldap_openshift_group.delete_openshift_group(group_name)
groups.append(result)
changed = True
self.exit_json(changed=changed, groups=groups)
def execute_module(self):
# validate LDAP sync configuration
error = validate_ldap_sync_config(self.config)
if error:
self.fail_json(msg="Invalid LDAP Sync config: %s" % error)
# Split host/port
if self.config.get('url'):
result, error = ldap_split_host_port(self.config.get('url'))
if error:
self.fail_json(msg="Failed to parse url='{0}': {1}".format(self.config.get('url'), error))
self.netlocation, self.host, self.port = result["netlocation"], result["host"], result["port"]
self.scheme = result["scheme"]
if self.params.get('state') == 'present':
self.synchronize()
else:
self.prune()

View File

@@ -0,0 +1,232 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from datetime import datetime
from ansible_collections.community.okd.plugins.module_utils.openshift_docker_image import (
parse_docker_image_ref,
)
from ansible.module_utils.six import iteritems
def get_image_blobs(image):
blobs = [layer["image"] for layer in image["dockerImageLayers"]]
docker_image_metadata = image.get("dockerImageMetadata")
if not docker_image_metadata:
return blobs, "failed to read metadata for image %s" % image["metadata"]["name"]
media_type_manifest = (
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.oci.image.manifest.v1+json"
)
media_type_has_config = image['dockerImageManifestMediaType'] in media_type_manifest
docker_image_id = docker_image_metadata.get("Id")
if media_type_has_config and docker_image_id and len(docker_image_id) > 0:
blobs.append(docker_image_id)
return blobs, None
def is_created_after(creation_timestamp, max_creation_timestamp):
if not max_creation_timestamp:
return False
creationTimestamp = datetime.strptime(creation_timestamp, '%Y-%m-%dT%H:%M:%SZ')
return creationTimestamp > max_creation_timestamp
def is_too_young_object(obj, max_creation_timestamp):
return is_created_after(obj['metadata']['creationTimestamp'],
max_creation_timestamp)
class OpenShiftAnalyzeImageStream(object):
def __init__(self, ignore_invalid_refs, max_creation_timestamp, module):
self.max_creationTimestamp = max_creation_timestamp
self.used_tags = {}
self.used_images = {}
self.ignore_invalid_refs = ignore_invalid_refs
self.module = module
def analyze_reference_image(self, image, referrer):
result, error = parse_docker_image_ref(image, self.module)
if error:
return error
if len(result['hostname']) == 0 or len(result['namespace']) == 0:
# image reference does not match hostname/namespace/name pattern - skipping
return None
if not result['digest']:
# Attempt to dereference istag. Since we cannot be sure whether the reference refers to the
# integrated registry or not, we ignore the host part completely. As a consequence, we may keep
# image otherwise sentenced for a removal just because its pull spec accidentally matches one of
# our imagestreamtags.
# set the tag if empty
if result['tag'] == "":
result['tag'] = 'latest'
key = "%s/%s:%s" % (result['namespace'], result['name'], result['tag'])
if key not in self.used_tags:
self.used_tags[key] = []
self.used_tags[key].append(referrer)
else:
key = "%s/%s@%s" % (result['namespace'], result['name'], result['digest'])
if key not in self.used_images:
self.used_images[key] = []
self.used_images[key].append(referrer)
def analyze_refs_from_pod_spec(self, podSpec, referrer):
for container in podSpec.get('initContainers', []) + podSpec.get('containers', []):
image = container.get('image')
if len(image.strip()) == 0:
# Ignoring container because it has no reference to image
continue
err = self.analyze_reference_image(image, referrer)
if err:
return err
return None
def analyze_refs_from_pods(self, pods):
for pod in pods:
# A pod is only *excluded* from being added to the graph if its phase is not
# pending or running. Additionally, it has to be at least as old as the minimum
# age threshold defined by the algorithm.
too_young = is_too_young_object(pod, self.max_creationTimestamp)
if pod['status']['phase'] not in ("Running", "Pending") and too_young:
continue
referrer = {
"kind": pod["kind"],
"namespace": pod["metadata"]["namespace"],
"name": pod["metadata"]["name"],
}
err = self.analyze_refs_from_pod_spec(pod['spec'], referrer)
if err:
return err
return None
def analyze_refs_pod_creators(self, resources):
keys = (
"ReplicationController", "DeploymentConfig", "DaemonSet",
"Deployment", "ReplicaSet", "StatefulSet", "Job", "CronJob"
)
for k, objects in iteritems(resources):
if k not in keys:
continue
for obj in objects:
if k == 'CronJob':
spec = obj["spec"]["jobTemplate"]["spec"]["template"]["spec"]
else:
spec = obj["spec"]["template"]["spec"]
referrer = {
"kind": obj["kind"],
"namespace": obj["metadata"]["namespace"],
"name": obj["metadata"]["name"],
}
err = self.analyze_refs_from_pod_spec(spec, referrer)
if err:
return err
return None
def analyze_refs_from_strategy(self, build_strategy, namespace, referrer):
# Determine 'from' reference
def _determine_source_strategy():
for src in ('sourceStrategy', 'dockerStrategy', 'customStrategy'):
strategy = build_strategy.get(src)
if strategy:
return strategy.get('from')
return None
def _parse_image_stream_image_name(name):
v = name.split('@')
if len(v) != 2:
return None, None, "expected exactly one @ in the isimage name %s" % name
name = v[0]
tag = v[1]
if len(name) == 0 or len(tag) == 0:
return None, None, "image stream image name %s must have a name and ID" % name
return name, tag, None
def _parse_image_stream_tag_name(name):
if "@" in name:
return None, None, "%s is an image stream image, not an image stream tag" % name
v = name.split(":")
if len(v) != 2:
return None, None, "expected exactly one : delimiter in the istag %s" % name
name = v[0]
tag = v[1]
if len(name) == 0 or len(tag) == 0:
return None, None, "image stream tag name %s must have a name and a tag" % name
return name, tag, None
from_strategy = _determine_source_strategy()
if not from_strategy:
# Build strategy not found
return
if from_strategy.get('kind') == "DockerImage":
docker_image_ref = from_strategy.get('name').strip()
if len(docker_image_ref) > 0:
err = self.analyze_reference_image(docker_image_ref, referrer)
elif from_strategy.get('kind') == "ImageStreamImage":
name, tag, error = _parse_image_stream_image_name(from_strategy.get('name'))
if error:
if not self.ignore_invalid_refs:
return error
else:
namespace = from_strategy.get('namespace') or namespace
self.used_images.append({
'namespace': namespace,
'name': name,
'tag': tag
})
elif from_strategy.get('kind') == "ImageStreamTag":
name, tag, error = _parse_image_stream_tag_name(from_strategy.get('name'))
if error:
if not self.ignore_invalid_refs:
return error
else:
namespace = from_strategy.get('namespace') or namespace
self.used_tags.append({
'namespace': namespace,
'name': name,
'tag': tag
})
return None
def analyze_refs_from_build_strategy(self, resources):
# Json Path is always spec.strategy
keys = ("BuildConfig", "Build")
for k, objects in iteritems(resources):
if k not in keys:
continue
for obj in objects:
referrer = {
"kind": obj["kind"],
"namespace": obj["metadata"]["namespace"],
"name": obj["metadata"]["name"],
}
error = self.analyze_refs_from_strategy(obj['spec']['strategy'],
obj['metadata']['namespace'],
referrer)
if not error:
return "%s/%s/%s: %s" % (referrer["kind"], referrer["namespace"], referrer["name"], error)
def analyze_image_stream(self, resources):
# Analyze image reference from Pods
error = self.analyze_refs_from_pods(resources['Pod'])
if error:
return None, None, error
# Analyze image reference from Resources creating Pod
error = self.analyze_refs_pod_creators(resources)
if error:
return None, None, error
# Analyze image reference from Build/BuildConfig
error = self.analyze_refs_from_build_strategy(resources)
return self.used_tags, self.used_images, error

View File

@@ -0,0 +1,414 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import traceback
import copy
from ansible.module_utils._text import to_native
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.six import string_types
try:
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
try:
from kubernetes.dynamic.exceptions import DynamicApiError
except ImportError:
pass
from ansible_collections.community.okd.plugins.module_utils.openshift_docker_image import (
parse_docker_image_ref,
)
err_stream_not_found_ref = "NotFound reference"
def follow_imagestream_tag_reference(stream, tag):
multiple = False
def _imagestream_has_tag():
for ref in stream["spec"].get("tags", []):
if ref["name"] == tag:
return ref
return None
def _imagestream_split_tag(name):
parts = name.split(":")
name = parts[0]
tag = ""
if len(parts) > 1:
tag = parts[1]
if len(tag) == 0:
tag = "latest"
return name, tag, len(parts) == 2
content = []
err_cross_stream_ref = "tag %s points to an imagestreamtag from another ImageStream" % tag
while True:
if tag in content:
return tag, None, multiple, "tag %s on the image stream is a reference to same tag" % tag
content.append(tag)
tag_ref = _imagestream_has_tag()
if not tag_ref:
return None, None, multiple, err_stream_not_found_ref
if not tag_ref.get("from") or tag_ref["from"]["kind"] != "ImageStreamTag":
return tag, tag_ref, multiple, None
if tag_ref["from"]["namespace"] != "" and tag_ref["from"]["namespace"] != stream["metadata"]["namespace"]:
return tag, None, multiple, err_cross_stream_ref
# The reference needs to be followed with two format patterns:
# a) sameis:sometag and b) sometag
if ":" in tag_ref["from"]["name"]:
name, tagref, result = _imagestream_split_tag(tag_ref["from"]["name"])
if not result:
return tag, None, multiple, "tag %s points to an invalid imagestreamtag" % tag
if name != stream["metadata"]["namespace"]:
# anotheris:sometag - this should not happen.
return tag, None, multiple, err_cross_stream_ref
# sameis:sometag - follow the reference as sometag
tag = tagref
else:
tag = tag_ref["from"]["name"]
multiple = True
class OpenShiftImportImage(K8sAnsibleMixin):
def __init__(self, module):
self.module = module
self.fail_json = self.module.fail_json
self.exit_json = self.module.exit_json
if not HAS_KUBERNETES_COLLECTION:
self.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception),
)
super(OpenShiftImportImage, self).__init__(self.module)
self.params = self.module.params
self.check_mode = self.module.check_mode
self.client = get_api_client(self.module)
self._rest_client = None
self.registryhost = self.params.get('registry_url')
self.changed = False
ref_policy = self.params.get("reference_policy")
ref_policy_type = None
if ref_policy == "source":
ref_policy_type = "Source"
elif ref_policy == "local":
ref_policy_type = "Local"
self.ref_policy = {
"type": ref_policy_type
}
self.validate_certs = self.params.get("validate_registry_certs")
self.cluster_resources = {}
def create_image_stream_import(self, stream):
isi = {
"apiVersion": "image.openshift.io/v1",
"kind": "ImageStreamImport",
"metadata": {
"name": stream["metadata"]["name"],
"namespace": stream["metadata"]["namespace"],
"resourceVersion": stream["metadata"].get("resourceVersion")
},
"spec": {
"import": True
}
}
annotations = stream.get("annotations", {})
insecure = boolean(annotations.get("openshift.io/image.insecureRepository", True))
if self.validate_certs is not None:
insecure = not self.validate_certs
return isi, insecure
def create_image_stream_import_all(self, stream, source):
isi, insecure = self.create_image_stream_import(stream)
isi["spec"]["repository"] = {
"from": {
"kind": "DockerImage",
"name": source,
},
"importPolicy": {
"insecure": insecure,
"scheduled": self.params.get("scheduled")
},
"referencePolicy": self.ref_policy,
}
return isi
def create_image_stream_import_tags(self, stream, tags):
isi, streamInsecure = self.create_image_stream_import(stream)
for k in tags:
insecure = streamInsecure
scheduled = self.params.get("scheduled")
old_tag = None
for t in stream.get("spec", {}).get("tags", []):
if t["name"] == k:
old_tag = t
break
if old_tag:
insecure = insecure or old_tag["importPolicy"].get("insecure")
scheduled = scheduled or old_tag["importPolicy"].get("scheduled")
images = isi["spec"].get("images", [])
images.append({
"from": {
"kind": "DockerImage",
"name": tags.get(k),
},
"to": {
"name": k
},
"importPolicy": {
"insecure": insecure,
"scheduled": scheduled
},
"referencePolicy": self.ref_policy,
})
isi["spec"]["images"] = images
return isi
def create_image_stream(self, ref):
"""
Create new ImageStream and accompanying ImageStreamImport
"""
source = self.params.get("source")
if not source:
source = ref["source"]
stream = dict(
apiVersion="image.openshift.io/v1",
kind="ImageStream",
metadata=dict(
name=ref["name"],
namespace=self.params.get("namespace"),
),
)
if self.params.get("all") and not ref["tag"]:
spec = dict(
dockerImageRepository=source
)
isi = self.create_image_stream_import_all(stream, source)
else:
spec = dict(
tags=[
{
"from": {
"kind": "DockerImage",
"name": source
},
"referencePolicy": self.ref_policy
}
]
)
tags = {ref["tag"]: source}
isi = self.create_image_stream_import_tags(stream, tags)
stream.update(
dict(spec=spec)
)
return stream, isi
def import_all(self, istream):
stream = copy.deepcopy(istream)
# Update ImageStream appropriately
source = self.params.get("source")
docker_image_repo = stream["spec"].get("dockerImageRepository")
if not source:
if docker_image_repo:
source = docker_image_repo
else:
tags = {}
for t in stream["spec"].get("tags", []):
if t.get("from") and t["from"].get("kind") == "DockerImage":
tags[t.get("name")] = t["from"].get("name")
if tags == {}:
msg = "image stream %s/%s does not have tags pointing to external container images" % (
stream["metadata"]["namespace"], stream["metadata"]["name"]
)
self.fail_json(msg=msg)
isi = self.create_image_stream_import_tags(stream, tags)
return stream, isi
if source != docker_image_repo:
stream["spec"]["dockerImageRepository"] = source
isi = self.create_image_stream_import_all(stream, source)
return stream, isi
def import_tag(self, stream, tag):
source = self.params.get("source")
# Follow any referential tags to the destination
final_tag, existing, multiple, err = follow_imagestream_tag_reference(stream, tag)
if err:
if err == err_stream_not_found_ref:
# Create a new tag
if not source and tag == "latest":
source = stream["spec"].get("dockerImageRepository")
# if the from is still empty this means there's no such tag defined
# nor we can't create any from .spec.dockerImageRepository
if not source:
msg = "the tag %s does not exist on the image stream - choose an existing tag to import" % tag
self.fail_json(msg=msg)
existing = {
"from": {
"kind": "DockerImage",
"name": source,
}
}
else:
self.fail_json(msg=err)
else:
# Disallow re-importing anything other than DockerImage
if existing.get("from", {}) and existing["from"].get("kind") != "DockerImage":
msg = "tag {tag} points to existing {kind}/={name}, it cannot be re-imported.".format(
tag=tag, kind=existing["from"]["kind"], name=existing["from"]["name"]
)
# disallow changing an existing tag
if not existing.get("from", {}):
msg = "tag %s already exists - you cannot change the source using this module." % tag
self.fail_json(msg=msg)
if source and source != existing["from"]["name"]:
if multiple:
msg = "the tag {0} points to the tag {1} which points to {2} you cannot change the source using this module".format(
tag, final_tag, existing["from"]["name"]
)
else:
msg = "the tag %s points to %s you cannot change the source using this module." % (tag, final_tag)
self.fail_json(msg=msg)
# Set the target item to import
source = existing["from"].get("name")
if multiple:
tag = final_tag
# Clear the legacy annotation
tag_to_delete = "openshift.io/image.dockerRepositoryCheck"
if existing["annotations"] and tag_to_delete in existing["annotations"]:
del existing["annotations"][tag_to_delete]
# Reset the generation
existing["generation"] = 0
new_stream = copy.deepcopy(stream)
new_stream["spec"]["tags"] = []
for t in stream["spec"]["tags"]:
if t["name"] == tag:
new_stream["spec"]["tags"].append(existing)
else:
new_stream["spec"]["tags"].append(t)
# Create accompanying ImageStreamImport
tags = {tag: source}
isi = self.create_image_stream_import_tags(new_stream, tags)
return new_stream, isi
def create_image_import(self, ref):
kind = "ImageStream"
api_version = "image.openshift.io/v1"
# Find existing Image Stream
params = dict(
kind=kind,
api_version=api_version,
name=ref.get("name"),
namespace=self.params.get("namespace")
)
result = self.kubernetes_facts(**params)
if not result["api_found"]:
msg = 'Failed to find API for resource with apiVersion "{0}" and kind "{1}"'.format(
api_version, kind
),
self.fail_json(msg=msg)
imagestream = None
if len(result["resources"]) > 0:
imagestream = result["resources"][0]
stream, isi = None, None
if not imagestream:
stream, isi = self.create_image_stream(ref)
elif self.params.get("all") and not ref["tag"]:
# importing the entire repository
stream, isi = self.import_all(imagestream)
else:
# importing a single tag
stream, isi = self.import_tag(imagestream, ref["tag"])
return isi
def parse_image_reference(self, image_ref):
result, err = parse_docker_image_ref(image_ref, self.module)
if result.get("digest"):
self.fail_json(msg="Cannot import by ID, error with definition: %s" % image_ref)
tag = result.get("tag") or None
if not self.params.get("all") and not tag:
tag = "latest"
source = self.params.get("source")
if not source:
source = image_ref
return dict(name=result.get("name"), tag=tag, source=image_ref)
def execute_module(self):
names = []
name = self.params.get("name")
if isinstance(name, string_types):
names.append(name)
elif isinstance(name, list):
names = name
else:
self.fail_json(msg="Parameter name should be provided as list or string.")
images_refs = [self.parse_image_reference(x) for x in names]
images_imports = []
for ref in images_refs:
isi = self.create_image_import(ref)
images_imports.append(isi)
# Create image import
kind = "ImageStreamImport"
api_version = "image.openshift.io/v1"
namespace = self.params.get("namespace")
try:
resource = self.find_resource(kind=kind, api_version=api_version, fail=True)
result = []
for isi in images_imports:
if not self.check_mode:
isi = resource.create(isi, namespace=namespace).to_dict()
result.append(isi)
self.exit_json(changed=True, result=result)
except DynamicApiError as exc:
msg = "Failed to create object {kind}/{namespace}/{name} due to: {error}".format(
kind=kind, namespace=namespace, name=isi["metadata"]["name"], error=exc
)
self.fail_json(
msg=msg,
error=exc.status,
status=exc.status,
reason=exc.reason,
)
except Exception as exc:
msg = "Failed to create object {kind}/{namespace}/{name} due to: {error}".format(
kind=kind, namespace=namespace, name=isi["metadata"]["name"], error=exc
)
self.fail_json(msg=msg)

View File

@@ -0,0 +1,777 @@
#!/usr/bin/env python
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import copy
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.six import iteritems
try:
import ldap
except ImportError as e:
pass
LDAP_SEARCH_OUT_OF_SCOPE_ERROR = "trying to search by DN for an entry that exists outside of the tree specified with the BaseDN for search"
def validate_ldap_sync_config(config):
# Validate url
url = config.get('url')
if not url:
return "url should be non empty attribute."
# Make sure bindDN and bindPassword are both set, or both unset
bind_dn = config.get('bindDN', "")
bind_password = config.get('bindPassword', "")
if (len(bind_dn) == 0) != (len(bind_password) == 0):
return "bindDN and bindPassword must both be specified, or both be empty."
insecure = boolean(config.get('insecure'))
ca_file = config.get('ca')
if insecure:
if url.startswith('ldaps://'):
return "Cannot use ldaps scheme with insecure=true."
if ca_file:
return "Cannot specify a ca with insecure=true."
elif ca_file and not os.path.isfile(ca_file):
return "could not read ca file: {0}.".format(ca_file)
nameMapping = config.get('groupUIDNameMapping', {})
for k, v in iteritems(nameMapping):
if len(k) == 0 or len(v) == 0:
return "groupUIDNameMapping has empty key or value"
schemas = []
schema_list = ('rfc2307', 'activeDirectory', 'augmentedActiveDirectory')
for schema in schema_list:
if schema in config:
schemas.append(schema)
if len(schemas) == 0:
return "No schema-specific config was provided, should be one of %s" % schema_list
if len(schemas) > 1:
return "Exactly one schema-specific config is required; found (%d) %s" % (len(schemas), ','.join(schemas))
if schemas[0] == 'rfc2307':
return validate_RFC2307(config.get("rfc2307"))
elif schemas[0] == 'activeDirectory':
return validate_ActiveDirectory(config.get("activeDirectory"))
elif schemas[0] == 'augmentedActiveDirectory':
return validate_AugmentedActiveDirectory(config.get("augmentedActiveDirectory"))
def validate_ldap_query(qry, isDNOnly=False):
# validate query scope
scope = qry.get('scope')
if scope and scope not in ("", "sub", "one", "base"):
return "invalid scope %s" % scope
# validate deref aliases
derefAlias = qry.get('derefAliases')
if derefAlias and derefAlias not in ("never", "search", "base", "always"):
return "not a valid LDAP alias dereferncing behavior: %s", derefAlias
# validate timeout
timeout = qry.get('timeout')
if timeout and float(timeout) < 0:
return "timeout must be equal to or greater than zero"
# Validate DN only
qry_filter = qry.get('filter', "")
if isDNOnly:
if len(qry_filter) > 0:
return 'cannot specify a filter when using "dn" as the UID attribute'
else:
# validate filter
if len(qry_filter) == 0 or qry_filter[0] != '(':
return "filter does not start with an '('"
return None
def validate_RFC2307(config):
qry = config.get('groupsQuery')
if not qry or not isinstance(qry, dict):
return "RFC2307: groupsQuery requires a dictionary"
error = validate_ldap_query(qry)
if not error:
return error
for field in ('groupUIDAttribute', 'groupNameAttributes', 'groupMembershipAttributes',
'userUIDAttribute', 'userNameAttributes'):
value = config.get(field)
if not value:
return "RFC2307: {0} is required.".format(field)
users_qry = config.get('usersQuery')
if not users_qry or not isinstance(users_qry, dict):
return "RFC2307: usersQuery requires a dictionary"
isUserDNOnly = (config.get('userUIDAttribute').strip() == 'dn')
return validate_ldap_query(users_qry, isDNOnly=isUserDNOnly)
def validate_ActiveDirectory(config, label="ActiveDirectory"):
users_qry = config.get('usersQuery')
if not users_qry or not isinstance(users_qry, dict):
return "{0}: usersQuery requires as dictionnary".format(label)
error = validate_ldap_query(users_qry)
if not error:
return error
for field in ('userNameAttributes', 'groupMembershipAttributes'):
value = config.get(field)
if not value:
return "{0}: {1} is required.".format(field, label)
return None
def validate_AugmentedActiveDirectory(config):
error = validate_ActiveDirectory(config, label="AugmentedActiveDirectory")
if not error:
return error
for field in ('groupUIDAttribute', 'groupNameAttributes'):
value = config.get(field)
if not value:
return "AugmentedActiveDirectory: {0} is required".format(field)
groups_qry = config.get('groupsQuery')
if not groups_qry or not isinstance(groups_qry, dict):
return "AugmentedActiveDirectory: groupsQuery requires as dictionnary."
isGroupDNOnly = (config.get('groupUIDAttribute').strip() == 'dn')
return validate_ldap_query(groups_qry, isDNOnly=isGroupDNOnly)
def determine_ldap_scope(scope):
if scope in ("", "sub"):
return ldap.SCOPE_SUBTREE
elif scope == 'base':
return ldap.SCOPE_BASE
elif scope == 'one':
return ldap.SCOPE_ONELEVEL
return None
def determine_deref_aliases(derefAlias):
mapping = {
"never": ldap.DEREF_NEVER,
"search": ldap.DEREF_SEARCHING,
"base": ldap.DEREF_FINDING,
"always": ldap.DEREF_ALWAYS,
}
result = None
if derefAlias in mapping:
result = mapping.get(derefAlias)
return result
def openshift_ldap_build_base_query(config):
qry = {}
if config.get('baseDN'):
qry['base'] = config.get('baseDN')
scope = determine_ldap_scope(config.get('scope'))
if scope:
qry['scope'] = scope
pageSize = config.get('pageSize')
if pageSize and int(pageSize) > 0:
qry['sizelimit'] = int(pageSize)
timeout = config.get('timeout')
if timeout and int(timeout) > 0:
qry['timeout'] = int(timeout)
filter = config.get('filter')
if filter:
qry['filterstr'] = filter
derefAlias = determine_deref_aliases(config.get('derefAliases'))
if derefAlias:
qry['derefAlias'] = derefAlias
return qry
def openshift_ldap_get_attribute_for_entry(entry, attribute):
attributes = [attribute]
if isinstance(attribute, list):
attributes = attribute
for k in attributes:
if k.lower() == 'dn':
return entry[0]
v = entry[1].get(k, None)
if v:
if isinstance(v, list):
result = []
for x in v:
if hasattr(x, 'decode'):
result.append(x.decode('utf-8'))
else:
result.append(x)
return result
else:
return v.decode('utf-8') if hasattr(v, 'decode') else v
return ""
def ldap_split_host_port(hostport):
"""
ldap_split_host_port splits a network address of the form "host:port",
"host%zone:port", "[host]:port" or "[host%zone]:port" into host or
host%zone and port.
"""
result = dict(
scheme=None, netlocation=None, host=None, port=None
)
if not hostport:
return result, None
# Extract Scheme
netlocation = hostport
scheme_l = "://"
if "://" in hostport:
idx = hostport.find(scheme_l)
result["scheme"] = hostport[:idx]
netlocation = hostport[idx + len(scheme_l):]
result["netlocation"] = netlocation
if netlocation[-1] == ']':
# ipv6 literal (with no port)
result["host"] = netlocation
v = netlocation.rsplit(":", 1)
if len(v) != 1:
try:
result["port"] = int(v[1])
except ValueError:
return None, "Invalid value specified for port: %s" % v[1]
result["host"] = v[0]
return result, None
def openshift_ldap_query_for_entries(connection, qry, unique_entry=True):
# set deref alias (TODO: need to set a default value to reset for each transaction)
derefAlias = qry.pop('derefAlias', None)
if derefAlias:
ldap.set_option(ldap.OPT_DEREF, derefAlias)
try:
result = connection.search_ext_s(**qry)
if not result or len(result) == 0:
return None, "Entry not found for base='{0}' and filter='{1}'".format(qry['base'], qry['filterstr'])
if len(result) > 1 and unique_entry:
if qry.get('scope') == ldap.SCOPE_BASE:
return None, "multiple entries found matching dn={0}: {1}".format(qry['base'], result)
else:
return None, "multiple entries found matching filter {0}: {1}".format(qry['filterstr'], result)
return result, None
except ldap.NO_SUCH_OBJECT:
return None, "search for entry with base dn='{0}' refers to a non-existent entry".format(qry['base'])
def openshift_equal_dn_objects(dn_obj, other_dn_obj):
if len(dn_obj) != len(other_dn_obj):
return False
for k, v in enumerate(dn_obj):
if len(v) != len(other_dn_obj[k]):
return False
for j, item in enumerate(v):
if not (item == other_dn_obj[k][j]):
return False
return True
def openshift_equal_dn(dn, other):
dn_obj = ldap.dn.str2dn(dn)
other_dn_obj = ldap.dn.str2dn(other)
return openshift_equal_dn_objects(dn_obj, other_dn_obj)
def openshift_ancestorof_dn(dn, other):
dn_obj = ldap.dn.str2dn(dn)
other_dn_obj = ldap.dn.str2dn(other)
if len(dn_obj) >= len(other_dn_obj):
return False
# Take the last attribute from the other DN to compare against
return openshift_equal_dn_objects(dn_obj, other_dn_obj[len(other_dn_obj) - len(dn_obj):])
class OpenshiftLDAPQueryOnAttribute(object):
def __init__(self, qry, attribute):
# qry retrieves entries from an LDAP server
self.qry = copy.deepcopy(qry)
# query_attributes is the attribute for a specific filter that, when conjoined with the common filter,
# retrieves the specific LDAP entry from the LDAP server. (e.g. "cn", when formatted with "aGroupName"
# and conjoined with "objectClass=groupOfNames", becomes (&(objectClass=groupOfNames)(cn=aGroupName))")
self.query_attribute = attribute
@staticmethod
def escape_filter(buffer):
"""
escapes from the provided LDAP filter string the special
characters in the set '(', ')', '*', \\ and those out of the range 0 < c < 0x80, as defined in RFC4515.
"""
output = []
hex_string = "0123456789abcdef"
for c in buffer:
if ord(c) > 0x7f or c in ('(', ')', '\\', '*') or c == 0:
first = ord(c) >> 4
second = ord(c) & 0xf
output += ['\\', hex_string[first], hex_string[second]]
else:
output.append(c)
return ''.join(output)
def build_request(self, ldapuid, attributes):
params = copy.deepcopy(self.qry)
if self.query_attribute.lower() == 'dn':
if ldapuid:
if not openshift_equal_dn(ldapuid, params['base']) and not openshift_ancestorof_dn(params['base'], ldapuid):
return None, LDAP_SEARCH_OUT_OF_SCOPE_ERROR
params['base'] = ldapuid
params['scope'] = ldap.SCOPE_BASE
# filter that returns all values
params['filterstr'] = "(objectClass=*)"
params['attrlist'] = attributes
else:
# Builds the query containing a filter that conjoins the common filter given
# in the configuration with the specific attribute filter for which the attribute value is given
specificFilter = "%s=%s" % (self.escape_filter(self.query_attribute), self.escape_filter(ldapuid))
qry_filter = params.get('filterstr', None)
if qry_filter:
params['filterstr'] = "(&%s(%s))" % (qry_filter, specificFilter)
params['attrlist'] = attributes
return params, None
def ldap_search(self, connection, ldapuid, required_attributes, unique_entry=True):
query, error = self.build_request(ldapuid, required_attributes)
if error:
return None, error
# set deref alias (TODO: need to set a default value to reset for each transaction)
derefAlias = query.pop('derefAlias', None)
if derefAlias:
ldap.set_option(ldap.OPT_DEREF, derefAlias)
try:
result = connection.search_ext_s(**query)
if not result or len(result) == 0:
return None, "Entry not found for base='{0}' and filter='{1}'".format(query['base'], query['filterstr'])
if unique_entry:
if len(result) > 1:
return None, "Multiple Entries found matching search criteria: %s (%s)" % (query, result)
result = result[0]
return result, None
except ldap.NO_SUCH_OBJECT:
return None, "Entry not found for base='{0}' and filter='{1}'".format(query['base'], query['filterstr'])
except Exception as err:
return None, "Request %s failed due to: %s" % (query, err)
class OpenshiftLDAPQuery(object):
def __init__(self, qry):
# Query retrieves entries from an LDAP server
self.qry = qry
def build_request(self, attributes):
params = copy.deepcopy(self.qry)
params['attrlist'] = attributes
return params
def ldap_search(self, connection, required_attributes):
query = self.build_request(required_attributes)
# set deref alias (TODO: need to set a default value to reset for each transaction)
derefAlias = query.pop('derefAlias', None)
if derefAlias:
ldap.set_option(ldap.OPT_DEREF, derefAlias)
try:
result = connection.search_ext_s(**query)
if not result or len(result) == 0:
return None, "Entry not found for base='{0}' and filter='{1}'".format(query['base'], query['filterstr'])
return result, None
except ldap.NO_SUCH_OBJECT:
return None, "search for entry with base dn='{0}' refers to a non-existent entry".format(query['base'])
class OpenshiftLDAPInterface(object):
def __init__(self, connection, groupQuery, groupNameAttributes, groupMembershipAttributes,
userQuery, userNameAttributes, config):
self.connection = connection
self.groupQuery = copy.deepcopy(groupQuery)
self.groupNameAttributes = groupNameAttributes
self.groupMembershipAttributes = groupMembershipAttributes
self.userQuery = copy.deepcopy(userQuery)
self.userNameAttributes = userNameAttributes
self.config = config
self.tolerate_not_found = boolean(config.get('tolerateMemberNotFoundErrors', False))
self.tolerate_out_of_scope = boolean(config.get('tolerateMemberOutOfScopeErrors', False))
self.required_group_attributes = [self.groupQuery.query_attribute]
for x in self.groupNameAttributes + self.groupMembershipAttributes:
if x not in self.required_group_attributes:
self.required_group_attributes.append(x)
self.required_user_attributes = [self.userQuery.query_attribute]
for x in self.userNameAttributes:
if x not in self.required_user_attributes:
self.required_user_attributes.append(x)
self.cached_groups = {}
self.cached_users = {}
def get_group_entry(self, uid):
"""
get_group_entry returns an LDAP group entry for the given group UID by searching the internal cache
of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry.
"""
if uid in self.cached_groups:
return self.cached_groups.get(uid), None
group, err = self.groupQuery.ldap_search(self.connection, uid, self.required_group_attributes)
if err:
return None, err
self.cached_groups[uid] = group
return group, None
def get_user_entry(self, uid):
"""
get_user_entry returns an LDAP group entry for the given user UID by searching the internal cache
of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry.
"""
if uid in self.cached_users:
return self.cached_users.get(uid), None
entry, err = self.userQuery.ldap_search(self.connection, uid, self.required_user_attributes)
if err:
return None, err
self.cached_users[uid] = entry
return entry, None
def exists(self, ldapuid):
group, error = self.get_group_entry(ldapuid)
return bool(group), error
def list_groups(self):
group_qry = copy.deepcopy(self.groupQuery.qry)
group_qry['attrlist'] = self.required_group_attributes
groups, err = openshift_ldap_query_for_entries(
connection=self.connection,
qry=group_qry,
unique_entry=False
)
if err:
return None, err
group_uids = []
for entry in groups:
uid = openshift_ldap_get_attribute_for_entry(entry, self.groupQuery.query_attribute)
if not uid:
return None, "Unable to find LDAP group uid for entry %s" % entry
self.cached_groups[uid] = entry
group_uids.append(uid)
return group_uids, None
def extract_members(self, uid):
"""
returns the LDAP member entries for a group specified with a ldapGroupUID
"""
# Get group entry from LDAP
group, err = self.get_group_entry(uid)
if err:
return None, err
# Extract member UIDs from group entry
member_uids = []
for attribute in self.groupMembershipAttributes:
member_uids += openshift_ldap_get_attribute_for_entry(group, attribute)
members = []
for user_uid in member_uids:
entry, err = self.get_user_entry(user_uid)
if err:
if self.tolerate_not_found and err.startswith("Entry not found"):
continue
elif err == LDAP_SEARCH_OUT_OF_SCOPE_ERROR:
continue
return None, err
members.append(entry)
return members, None
class OpenshiftLDAPRFC2307(object):
def __init__(self, config, ldap_connection):
self.config = config
self.ldap_interface = self.create_ldap_interface(ldap_connection)
def create_ldap_interface(self, connection):
segment = self.config.get("rfc2307")
groups_base_qry = openshift_ldap_build_base_query(segment['groupsQuery'])
users_base_qry = openshift_ldap_build_base_query(segment['usersQuery'])
groups_query = OpenshiftLDAPQueryOnAttribute(groups_base_qry, segment['groupUIDAttribute'])
users_query = OpenshiftLDAPQueryOnAttribute(users_base_qry, segment['userUIDAttribute'])
params = dict(
connection=connection,
groupQuery=groups_query,
groupNameAttributes=segment['groupNameAttributes'],
groupMembershipAttributes=segment['groupMembershipAttributes'],
userQuery=users_query,
userNameAttributes=segment['userNameAttributes'],
config=segment
)
return OpenshiftLDAPInterface(**params)
def get_username_for_entry(self, entry):
username = openshift_ldap_get_attribute_for_entry(entry, self.ldap_interface.userNameAttributes)
if not username:
return None, "The user entry (%s) does not map to a OpenShift User name with the given mapping" % entry
return username, None
def get_group_name_for_uid(self, uid):
# Get name from User defined mapping
groupuid_name_mapping = self.config.get("groupUIDNameMapping")
if groupuid_name_mapping and uid in groupuid_name_mapping:
return groupuid_name_mapping.get(uid), None
elif self.ldap_interface.groupNameAttributes:
group, err = self.ldap_interface.get_group_entry(uid)
if err:
return None, err
group_name = openshift_ldap_get_attribute_for_entry(group, self.ldap_interface.groupNameAttributes)
if not group_name:
error = "The group entry (%s) does not map to an OpenShift Group name with the given name attribute (%s)" % (
group, self.ldap_interface.groupNameAttributes
)
return None, error
if isinstance(group_name, list):
group_name = group_name[0]
return group_name, None
else:
return None, "No OpenShift Group name defined for LDAP group UID: %s" % uid
def is_ldapgroup_exists(self, uid):
group, err = self.ldap_interface.get_group_entry(uid)
if err:
if err == LDAP_SEARCH_OUT_OF_SCOPE_ERROR or err.startswith("Entry not found") or "non-existent entry" in err:
return False, None
return False, err
if group:
return True, None
return False, None
def list_groups(self):
return self.ldap_interface.list_groups()
def extract_members(self, uid):
return self.ldap_interface.extract_members(uid)
class OpenshiftLDAP_ADInterface(object):
def __init__(self, connection, user_query, group_member_attr, user_name_attr):
self.connection = connection
self.userQuery = user_query
self.groupMembershipAttributes = group_member_attr
self.userNameAttributes = user_name_attr
self.required_user_attributes = self.userNameAttributes or []
for attr in self.groupMembershipAttributes:
if attr not in self.required_user_attributes:
self.required_user_attributes.append(attr)
self.cache = {}
self.cache_populated = False
def is_entry_present(self, cache_item, entry):
for item in cache_item:
if item[0] == entry[0]:
return True
return False
def populate_cache(self):
if not self.cache_populated:
self.cache_populated = True
entries, err = self.userQuery.ldap_search(self.connection, self.required_user_attributes)
if err:
return err
for entry in entries:
for group_attr in self.groupMembershipAttributes:
uids = openshift_ldap_get_attribute_for_entry(entry, group_attr)
if not isinstance(uids, list):
uids = [uids]
for uid in uids:
if uid not in self.cache:
self.cache[uid] = []
if not self.is_entry_present(self.cache[uid], entry):
self.cache[uid].append(entry)
return None
def list_groups(self):
err = self.populate_cache()
if err:
return None, err
result = []
if self.cache:
result = self.cache.keys()
return result, None
def extract_members(self, uid):
# ExtractMembers returns the LDAP member entries for a group specified with a ldapGroupUID
# if we already have it cached, return the cached value
if uid in self.cache:
return self.cache[uid], None
# This happens in cases where we did not list out every group.
# In that case, we're going to be asked about specific groups.
users_in_group = []
for attr in self.groupMembershipAttributes:
query_on_attribute = OpenshiftLDAPQueryOnAttribute(self.userQuery.qry, attr)
entries, error = query_on_attribute.ldap_search(self.connection, uid, self.required_user_attributes, unique_entry=False)
if error and "not found" not in error:
return None, error
if not entries:
continue
for entry in entries:
if not self.is_entry_present(users_in_group, entry):
users_in_group.append(entry)
self.cache[uid] = users_in_group
return users_in_group, None
class OpenshiftLDAPActiveDirectory(object):
def __init__(self, config, ldap_connection):
self.config = config
self.ldap_interface = self.create_ldap_interface(ldap_connection)
def create_ldap_interface(self, connection):
segment = self.config.get("activeDirectory")
base_query = openshift_ldap_build_base_query(segment['usersQuery'])
user_query = OpenshiftLDAPQuery(base_query)
return OpenshiftLDAP_ADInterface(
connection=connection,
user_query=user_query,
group_member_attr=segment["groupMembershipAttributes"],
user_name_attr=segment["userNameAttributes"],
)
def get_username_for_entry(self, entry):
username = openshift_ldap_get_attribute_for_entry(entry, self.ldap_interface.userNameAttributes)
if not username:
return None, "The user entry (%s) does not map to a OpenShift User name with the given mapping" % entry
return username, None
def get_group_name_for_uid(self, uid):
return uid, None
def is_ldapgroup_exists(self, uid):
members, error = self.extract_members(uid)
if error:
return False, error
exists = members and len(members) > 0
return exists, None
def list_groups(self):
return self.ldap_interface.list_groups()
def extract_members(self, uid):
return self.ldap_interface.extract_members(uid)
class OpenshiftLDAP_AugmentedADInterface(OpenshiftLDAP_ADInterface):
def __init__(self, connection, user_query, group_member_attr, user_name_attr, group_qry, group_name_attr):
super(OpenshiftLDAP_AugmentedADInterface, self).__init__(
connection, user_query, group_member_attr, user_name_attr
)
self.groupQuery = copy.deepcopy(group_qry)
self.groupNameAttributes = group_name_attr
self.required_group_attributes = [self.groupQuery.query_attribute]
for x in self.groupNameAttributes:
if x not in self.required_group_attributes:
self.required_group_attributes.append(x)
self.cached_groups = {}
def get_group_entry(self, uid):
"""
get_group_entry returns an LDAP group entry for the given group UID by searching the internal cache
of the LDAPInterface first, then sending an LDAP query if the cache did not contain the entry.
"""
if uid in self.cached_groups:
return self.cached_groups.get(uid), None
group, err = self.groupQuery.ldap_search(self.connection, uid, self.required_group_attributes)
if err:
return None, err
self.cached_groups[uid] = group
return group, None
def exists(self, ldapuid):
# Get group members
members, error = self.extract_members(ldapuid)
if error:
return False, error
group_exists = bool(members)
# Check group Existence
entry, error = self.get_group_entry(ldapuid)
if error:
if "not found" in error:
return False, None
else:
return False, error
else:
return group_exists and bool(entry), None
class OpenshiftLDAPAugmentedActiveDirectory(OpenshiftLDAPRFC2307):
def __init__(self, config, ldap_connection):
self.config = config
self.ldap_interface = self.create_ldap_interface(ldap_connection)
def create_ldap_interface(self, connection):
segment = self.config.get("augmentedActiveDirectory")
user_base_query = openshift_ldap_build_base_query(segment['usersQuery'])
groups_base_qry = openshift_ldap_build_base_query(segment['groupsQuery'])
user_query = OpenshiftLDAPQuery(user_base_query)
groups_query = OpenshiftLDAPQueryOnAttribute(groups_base_qry, segment['groupUIDAttribute'])
return OpenshiftLDAP_AugmentedADInterface(
connection=connection,
user_query=user_query,
group_member_attr=segment["groupMembershipAttributes"],
user_name_attr=segment["userNameAttributes"],
group_qry=groups_query,
group_name_attr=segment["groupNameAttributes"]
)
def is_ldapgroup_exists(self, uid):
return self.ldap_interface.exists(uid)

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import os
import traceback
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
from ansible.module_utils._text import to_native
try:
from kubernetes.dynamic.exceptions import DynamicApiError, NotFoundError
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
try:
from kubernetes.dynamic.exceptions import DynamicApiError, NotFoundError
except ImportError:
pass
class OpenShiftProcess(K8sAnsibleMixin):
def __init__(self, module):
self.module = module
self.fail_json = self.module.fail_json
self.exit_json = self.module.exit_json
if not HAS_KUBERNETES_COLLECTION:
self.module.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception),
)
super(OpenShiftProcess, self).__init__(self.module)
self.params = self.module.params
self.check_mode = self.module.check_mode
self.client = get_api_client(self.module)
def execute_module(self):
v1_templates = self.find_resource(
"templates", "template.openshift.io/v1", fail=True
)
v1_processed_templates = self.find_resource(
"processedtemplates", "template.openshift.io/v1", fail=True
)
name = self.params.get("name")
namespace = self.params.get("namespace")
namespace_target = self.params.get("namespace_target")
definition = self.params.get("resource_definition")
src = self.params.get("src")
state = self.params.get("state")
parameters = self.params.get("parameters") or {}
parameter_file = self.params.get("parameter_file")
if (name and definition) or (name and src) or (src and definition):
self.fail_json("Only one of src, name, or definition may be provided")
if name and not namespace:
self.fail_json("namespace is required when name is set")
template = None
if src or definition:
self.set_resource_definitions(self.module)
if len(self.resource_definitions) < 1:
self.fail_json(
"Unable to load a Template resource from src or resource_definition"
)
elif len(self.resource_definitions) > 1:
self.fail_json(
"Multiple Template resources found in src or resource_definition, only one Template may be processed at a time"
)
template = self.resource_definitions[0]
template_namespace = template.get("metadata", {}).get("namespace")
namespace = template_namespace or namespace or namespace_target or "default"
elif name and namespace:
try:
template = v1_templates.get(name=name, namespace=namespace).to_dict()
except DynamicApiError as exc:
self.fail_json(
msg="Failed to retrieve Template with name '{0}' in namespace '{1}': {2}".format(
name, namespace, exc.body
),
error=exc.status,
status=exc.status,
reason=exc.reason,
)
except Exception as exc:
self.module.fail_json(
msg="Failed to retrieve Template with name '{0}' in namespace '{1}': {2}".format(
name, namespace, to_native(exc)
),
error="",
status="",
reason="",
)
else:
self.fail_json(
"One of resource_definition, src, or name and namespace must be provided"
)
if parameter_file:
parameters = self.parse_dotenv_and_merge(parameters, parameter_file)
for k, v in parameters.items():
template = self.update_template_param(template, k, v)
result = {"changed": False}
try:
response = v1_processed_templates.create(
body=template, namespace=namespace
).to_dict()
except DynamicApiError as exc:
self.fail_json(
msg="Server failed to render the Template: {0}".format(exc.body),
error=exc.status,
status=exc.status,
reason=exc.reason,
)
except Exception as exc:
self.module.fail_json(
msg="Server failed to render the Template: {0}".format(to_native(exc)),
error="",
status="",
reason="",
)
result["message"] = ""
if "message" in response:
result["message"] = response["message"]
result["resources"] = response["objects"]
if state != "rendered":
self.resource_definitions = response["objects"]
self.kind = self.api_version = self.name = None
self.namespace = self.params.get("namespace_target")
self.append_hash = False
self.apply = False
self.params["validate"] = None
self.params["merge_type"] = None
super(OpenShiftProcess, self).execute_module()
self.module.exit_json(**result)
def update_template_param(self, template, k, v):
for i, param in enumerate(template["parameters"]):
if param["name"] == k:
template["parameters"][i]["value"] = v
return template
return template
def parse_dotenv_and_merge(self, parameters, parameter_file):
import re
DOTENV_PARSER = re.compile(
r"(?x)^(\s*(\#.*|\s*|(export\s+)?(?P<key>[A-z_][A-z0-9_.]*)=(?P<value>.+?)?)\s*)[\r\n]*$"
)
path = os.path.normpath(parameter_file)
if not os.path.exists(path):
self.fail(msg="Error accessing {0}. Does the file exist?".format(path))
try:
with open(path, "r") as f:
multiline = ""
for line in f.readlines():
line = line.strip()
if line.endswith("\\"):
multiline += " ".join(line.rsplit("\\", 1))
continue
if multiline:
line = multiline + line
multiline = ""
match = DOTENV_PARSER.search(line)
if not match:
continue
match = match.groupdict()
if match.get("key"):
if match["key"] in parameters:
self.fail_json(
msg="Duplicate value for '{0}' detected in parameter file".format(
match["key"]
)
)
parameters[match["key"]] = match["value"]
except IOError as exc:
self.fail(msg="Error loading parameter file: {0}".format(exc))
return parameters

View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import traceback
from urllib.parse import urlparse
from ansible.module_utils._text import to_native
try:
from ansible_collections.kubernetes.core.plugins.module_utils.common import (
K8sAnsibleMixin,
get_api_client,
)
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
from ansible_collections.community.okd.plugins.module_utils.openshift_docker_image import (
parse_docker_image_ref,
)
try:
from requests import request
from requests.auth import HTTPBasicAuth
HAS_REQUESTS_MODULE = True
except ImportError as e:
HAS_REQUESTS_MODULE = False
requests_import_exception = e
REQUESTS_MODULE_ERROR = traceback.format_exc()
class OpenShiftRegistry(K8sAnsibleMixin):
def __init__(self, module):
self.module = module
self.fail_json = self.module.fail_json
self.exit_json = self.module.exit_json
if not HAS_KUBERNETES_COLLECTION:
self.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception),
)
super(OpenShiftRegistry, self).__init__(self.module)
self.params = self.module.params
self.check_mode = self.module.check_mode
self.client = get_api_client(self.module)
self.check = self.params.get("check")
def list_image_streams(self, namespace=None):
kind = "ImageStream"
api_version = "image.openshift.io/v1"
params = dict(
kind=kind,
api_version=api_version,
namespace=namespace
)
result = self.kubernetes_facts(**params)
imagestream = []
if len(result["resources"]) > 0:
imagestream = result["resources"]
return imagestream
def find_registry_info(self):
def _determine_registry(image_stream):
public, internal = None, None
docker_repo = image_stream["status"].get("publicDockerImageRepository")
if docker_repo:
ref, err = parse_docker_image_ref(docker_repo, self.module)
public = ref["hostname"]
docker_repo = image_stream["status"].get("dockerImageRepository")
if docker_repo:
ref, err = parse_docker_image_ref(docker_repo, self.module)
internal = ref["hostname"]
return internal, public
# Try to determine registry hosts from Image Stream from 'openshift' namespace
for stream in self.list_image_streams(namespace="openshift"):
internal, public = _determine_registry(stream)
if not public and not internal:
self.fail_json(msg="The integrated registry has not been configured")
return internal, public
# Unable to determine registry from 'openshift' namespace, trying with all namespace
for stream in self.list_image_streams():
internal, public = _determine_registry(stream)
if not public and not internal:
self.fail_json(msg="The integrated registry has not been configured")
return internal, public
self.fail_json(msg="No Image Streams could be located to retrieve registry info.")
def info(self):
result = {}
result["internal_hostname"], result["public_hostname"] = self.find_registry_info()
if self.check:
public_registry = result["public_hostname"]
if not public_registry:
result["check"] = dict(
reached=False,
msg="Registry does not have a public hostname."
)
else:
headers = {
'Content-Type': 'application/json'
}
params = {
'method': 'GET',
'verify': False
}
if self.client.configuration.api_key:
headers.update(self.client.configuration.api_key)
elif self.client.configuration.username and self.client.configuration.password:
if not HAS_REQUESTS_MODULE:
result["check"] = dict(
reached=False,
msg="The requests python package is missing, try `pip install requests`",
error=requests_import_exception
)
self.exit_json(**result)
params.update(
dict(auth=HTTPBasicAuth(self.client.configuration.username, self.client.configuration.password))
)
# verify ssl
host = urlparse(public_registry)
if len(host.scheme) == 0:
registry_url = "https://" + public_registry
if registry_url.startswith("https://") and self.client.configuration.ssl_ca_cert:
params.update(
dict(verify=self.client.configuration.ssl_ca_cert)
)
params.update(
dict(headers=headers)
)
last_bad_status, last_bad_reason = None, None
for path in ("/", "/healthz"):
params.update(
dict(url=registry_url + path)
)
response = request(**params)
if response.status_code == 200:
result["check"] = dict(
reached=True,
msg="The local client can contact the integrated registry."
)
self.exit_json(**result)
last_bad_reason = response.reason
last_bad_status = response.status_code
result["check"] = dict(
reached=False,
msg="Unable to contact the integrated registry using local client. Status=%d, Reason=%s" % (
last_bad_status, last_bad_reason
)
)
self.exit_json(**result)

View File

@@ -0,0 +1,318 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Chris Houseknecht <@chouseknecht>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
# STARTREMOVE (downstream)
DOCUMENTATION = r'''
module: k8s
short_description: Manage OpenShift objects
author:
- "Chris Houseknecht (@chouseknecht)"
- "Fabian von Feilitzsch (@fabianvf)"
description:
- Use the Kubernetes Python client to perform CRUD operations on K8s objects.
- Pass the object definition from a source file or inline. See examples for reading
files and using Jinja templates or vault-encrypted files.
- Access to the full range of K8s APIs.
- Use the M(kubernetes.core.k8s_info) module to obtain a list of items about an object of type C(kind).
- Authenticate using either a config file, certificates, password or token.
- Supports check mode.
- Optimized for OKD/OpenShift Kubernetes flavors.
extends_documentation_fragment:
- kubernetes.core.k8s_name_options
- kubernetes.core.k8s_resource_options
- kubernetes.core.k8s_auth_options
- kubernetes.core.k8s_wait_options
- kubernetes.core.k8s_delete_options
options:
state:
description:
- Determines if an object should be created, patched, or deleted. When set to C(present), an object will be
created, if it does not already exist. If set to C(absent), an existing object will be deleted. If set to
C(present), an existing object will be patched, if its attributes differ from those specified using
I(resource_definition) or I(src).
- C(patched) state is an existing resource that has a given patch applied. If the resource doesn't exist, silently skip it (do not raise an error).
type: str
default: present
choices: [ absent, present, patched ]
force:
description:
- If set to C(yes), and I(state) is C(present), an existing object will be replaced.
type: bool
default: no
merge_type:
description:
- Whether to override the default patch merge approach with a specific type. By default, the strategic
merge will typically be used.
- For example, Custom Resource Definitions typically aren't updatable by the usual strategic merge. You may
want to use C(merge) if you see "strategic merge patch format is not supported"
- See U(https://kubernetes.io/docs/tasks/run-application/update-api-object-kubectl-patch/#use-a-json-merge-patch-to-update-a-deployment)
- If more than one merge_type is given, the merge_types will be tried in order
- Defaults to C(['strategic-merge', 'merge']), which is ideal for using the same parameters
on resource kinds that combine Custom Resources and built-in resources.
- mutually exclusive with C(apply)
- I(merge_type=json) is deprecated and will be removed in version 3.0.0. Please use M(kubernetes.core.k8s_json_patch) instead.
choices:
- json
- merge
- strategic-merge
type: list
elements: str
validate:
description:
- how (if at all) to validate the resource definition against the kubernetes schema.
Requires the kubernetes-validate python module
suboptions:
fail_on_error:
description: whether to fail on validation errors.
type: bool
version:
description: version of Kubernetes to validate against. defaults to Kubernetes server version
type: str
strict:
description: whether to fail when passing unexpected properties
default: True
type: bool
type: dict
append_hash:
description:
- Whether to append a hash to a resource name for immutability purposes
- Applies only to ConfigMap and Secret resources
- The parameter will be silently ignored for other resource kinds
- The full definition of an object is needed to generate the hash - this means that deleting an object created with append_hash
will only work if the same object is passed with state=absent (alternatively, just use state=absent with the name including
the generated hash and append_hash=no)
type: bool
default: false
apply:
description:
- C(apply) compares the desired resource definition with the previously supplied resource definition,
ignoring properties that are automatically generated
- C(apply) works better with Services than 'force=yes'
- mutually exclusive with C(merge_type)
type: bool
default: false
template:
description:
- Provide a valid YAML template definition file for an object when creating or updating.
- Value can be provided as string or dictionary.
- Mutually exclusive with C(src) and C(resource_definition).
- Template files needs to be present on the Ansible Controller's file system.
- Additional parameters can be specified using dictionary.
- 'Valid additional parameters - '
- 'C(newline_sequence) (str): Specify the newline sequence to use for templating files.
valid choices are "\n", "\r", "\r\n". Default value "\n".'
- 'C(block_start_string) (str): The string marking the beginning of a block.
Default value "{%".'
- 'C(block_end_string) (str): The string marking the end of a block.
Default value "%}".'
- 'C(variable_start_string) (str): The string marking the beginning of a print statement.
Default value "{{".'
- 'C(variable_end_string) (str): The string marking the end of a print statement.
Default value "}}".'
- 'C(trim_blocks) (bool): Determine when newlines should be removed from blocks. When set to C(yes) the first newline
after a block is removed (block, not variable tag!). Default value is true.'
- 'C(lstrip_blocks) (bool): Determine when leading spaces and tabs should be stripped.
When set to C(yes) leading spaces and tabs are stripped from the start of a line to a block.
This functionality requires Jinja 2.7 or newer. Default value is false.'
type: raw
version_added: '2.0.0'
continue_on_error:
description:
- Whether to continue on creation/deletion errors when multiple resources are defined.
- This has no effect on the validation step which is controlled by the C(validate.fail_on_error) parameter.
type: bool
default: False
version_added: 2.0.0
requirements:
- "python >= 3.6"
- "kubernetes >= 12.0.0"
- "PyYAML >= 3.11"
'''
EXAMPLES = r'''
- name: Create a k8s namespace
community.okd.k8s:
name: testing
api_version: v1
kind: Namespace
state: present
- name: Create a Service object from an inline definition
community.okd.k8s:
state: present
definition:
apiVersion: v1
kind: Service
metadata:
name: web
namespace: testing
labels:
app: galaxy
service: web
spec:
selector:
app: galaxy
service: web
ports:
- protocol: TCP
targetPort: 8000
name: port-8000-tcp
port: 8000
- name: Remove an existing Service object
community.okd.k8s:
state: absent
api_version: v1
kind: Service
namespace: testing
name: web
# Passing the object definition from a file
- name: Create a Deployment by reading the definition from a local file
community.okd.k8s:
state: present
src: /testing/deployment.yml
- name: >-
Read definition file from the Ansible controller file system.
If the definition file has been encrypted with Ansible Vault it will automatically be decrypted.
community.okd.k8s:
state: present
definition: "{{ lookup('file', '/testing/deployment.yml') | from_yaml }}"
- name: Read definition file from the Ansible controller file system after Jinja templating
community.okd.k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
- name: fail on validation errors
community.okd.k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
validate:
fail_on_error: yes
- name: warn on validation errors, check for unexpected properties
community.okd.k8s:
state: present
definition: "{{ lookup('template', '/testing/deployment.yml') | from_yaml }}"
validate:
fail_on_error: no
strict: yes
'''
RETURN = r'''
result:
description:
- The created, patched, or otherwise present object. Will be empty in the case of a deletion.
returned: success
type: complex
contains:
api_version:
description: The versioned schema of this representation of an object.
returned: success
type: str
kind:
description: Represents the REST resource this object represents.
returned: success
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
type: complex
spec:
description: Specific attributes of the object. Will vary based on the I(api_version) and I(kind).
returned: success
type: complex
status:
description: Current status details for the object.
returned: success
type: complex
items:
description: Returned only when multiple yaml documents are passed to src or resource_definition
returned: when resource_definition or src contains list of objects
type: list
duration:
description: elapsed time of task in seconds
returned: when C(wait) is true
type: int
sample: 48
error:
description: error while trying to create/delete the object.
returned: error
type: complex
'''
# ENDREMOVE (downstream)
try:
from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import AnsibleModule
except ImportError:
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
NAME_ARG_SPEC, RESOURCE_ARG_SPEC, AUTH_ARG_SPEC, WAIT_ARG_SPEC, DELETE_OPTS_ARG_SPEC)
def validate_spec():
return dict(
fail_on_error=dict(type='bool'),
version=dict(),
strict=dict(type='bool', default=True)
)
def argspec():
argument_spec = {}
argument_spec.update(NAME_ARG_SPEC)
argument_spec.update(RESOURCE_ARG_SPEC)
argument_spec.update(AUTH_ARG_SPEC)
argument_spec.update(WAIT_ARG_SPEC)
argument_spec['merge_type'] = dict(type='list', elements='str', choices=['json', 'merge', 'strategic-merge'])
argument_spec['validate'] = dict(type='dict', default=None, options=validate_spec())
argument_spec['append_hash'] = dict(type='bool', default=False)
argument_spec['apply'] = dict(type='bool', default=False)
argument_spec['template'] = dict(type='raw', default=None)
argument_spec['delete_options'] = dict(type='dict', default=None, options=DELETE_OPTS_ARG_SPEC)
argument_spec['continue_on_error'] = dict(type='bool', default=False)
argument_spec['state'] = dict(default='present', choices=['present', 'absent', 'patched'])
argument_spec['force'] = dict(type='bool', default=False)
return argument_spec
def main():
mutually_exclusive = [
('resource_definition', 'src'),
('merge_type', 'apply'),
('template', 'resource_definition'),
('template', 'src'),
]
module = AnsibleModule(argument_spec=argspec(), supports_check_mode=True, mutually_exclusive=mutually_exclusive)
from ansible_collections.community.okd.plugins.module_utils.k8s import OKDRawModule
okdraw_module = OKDRawModule(module)
# remove_aliases from kubernetes.core's common requires the argspec attribute. Ideally, it should
# read that throught the module class, but we cannot change that.
okdraw_module.argspec = argspec()
okdraw_module.execute_module()
if __name__ == '__main__':
main()

Some files were not shown because too many files have changed in this diff Show More