Compare commits

...

14 Commits

Author SHA1 Message Date
Elijah DeLee
6a2b42cdde allow configuration of uwsgi timeout
This is important when you have proxies in front of the service
that may have stricter timeouts, as you need at least as strict a
timeout at uwsgi to get meaningful traceback and see source of problem.
2025-02-19 14:38:54 -05:00
Christian Adams
bb4f4c2eb4 Fail early if postgres_configuration_secret is specified by does not exist (#2015) 2025-02-17 12:38:06 -05:00
Christian Adams
97efcab2a2 Accepts new status conditions from the operator on the CR object (#2016) 2025-02-17 12:36:43 -05:00
aknochow
c08c1027a1 idle_deployment - Scale down deployments to put AWX into an idle state (#2012)
- separating database_configuration and deployment tasks into separate files to add ability to call configuration independently
2025-02-11 11:01:18 -05:00
Yuval Lahav
3d1ecc19f4 AAP-38745 Increase limits in manager.py (#2006)
* AAP-38745 Increase limits in manager.py

Closes https://issues.redhat.com/browse/AAP-38745

* Update manager.yaml
2025-01-20 11:32:49 -05:00
aknochow
5d0f91ec13 adding conditional to checksum template and removing default for public_base_url to fix undefined variable 2024-12-02 11:54:13 -05:00
Christian Adams
6ab32a42cf Add up.sh and down.sh development scripts for consistency with other ansible operators (#1991) 2024-11-26 11:25:24 -05:00
Chris Meyers
9718424483 Point at awx devel collection
* awx.awx collection on galaxy is ooooold at this point. Releases are
  paused, so point at awx collection in devel to get that new bleeding
  edge hotness.
2024-11-07 12:53:53 -05:00
aknochow
d5683adaf8 adding redirect page (#1982) 2024-10-29 17:47:41 -04:00
Christian Adams
1bc342258a Only set upgradedFrom status if previous_version is explicitly set (#1980) 2024-10-23 13:51:18 -04:00
Hao Liu
79ab6f0b5e Fix disable reverse sync on mgmt command (#1977) 2024-10-18 15:02:28 +00:00
Christian Adams
3822e32755 Add upgradedFrom status for check-version changes (#1975) 2024-10-17 19:28:43 +00:00
Christian Adams
c30d4c174d Compare gating version against existing deployment versions (#1972)
* Compare gating version against existing deployment versions and set upgradeFrom status
* Add quotes to default version

Co-authored-by: Dimitri Savineau <savineau.dimitri@gmail.com>
2024-10-16 17:43:26 -04:00
Christian Adams
8a5ec6e19c Fix Label PR check by using python venv for requests library (#1973) 2024-10-16 13:00:50 -04:00
30 changed files with 744 additions and 186 deletions

View File

@@ -13,9 +13,17 @@ jobs:
name: Label PR - Community name: Label PR - Community
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
- name: Install python requests
run: pip install requests - name: Create a virtual environment
run: python3 -m venv venv
- name: Activate virtual environment and install dependencies
run: |
source venv/bin/activate
pip3 install requests
- name: Check if user is a member of Ansible org - name: Check if user is a member of Ansible org
uses: jannekem/run-python-script-action@v1 uses: jannekem/run-python-script-action@v1
id: check_user id: check_user
@@ -32,6 +40,7 @@ jobs:
print("User is member") print("User is member")
else: else:
print("User is community") print("User is community")
- name: Add community label if not a member - name: Add community label if not a member
if: contains(steps.check_user.outputs.stdout, 'community') if: contains(steps.check_user.outputs.stdout, 'community')
uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90

View File

@@ -10,6 +10,7 @@ Have questions about this document or anything not covered here? Please file a n
- [Table of contents](#table-of-contents) - [Table of contents](#table-of-contents)
- [Things to know prior to submitting code](#things-to-know-prior-to-submitting-code) - [Things to know prior to submitting code](#things-to-know-prior-to-submitting-code)
- [Submmiting your work](#submmiting-your-work) - [Submmiting your work](#submmiting-your-work)
- [Development](#development)
- [Testing](#testing) - [Testing](#testing)
- [Testing in Kind](#testing-in-kind) - [Testing in Kind](#testing-in-kind)
- [Testing in Minikube](#testing-in-minikube) - [Testing in Minikube](#testing-in-minikube)
@@ -42,7 +43,8 @@ Have questions about this document or anything not covered here? Please file a n
**Note**: If you have multiple commits, make sure to `squash` your commits into a single commit which will facilitate our release process. **Note**: If you have multiple commits, make sure to `squash` your commits into a single commit which will facilitate our release process.
## Development
The development environment consists of running an [`up.sh`](./up.sh) and a [`down.sh`](./down.sh) script, which applies or deletes yaml on the Openshift or K8s cluster you are connected to. See the [development.md](docs/development.md) for information on how to deploy and test changes from your branch.
## Testing ## Testing

View File

@@ -20,6 +20,8 @@ Please visit [our contributing guidelines](https://github.com/ansible/awx-operat
For docs changes, create PRs on the appropriate files in the `/docs` folder. For docs changes, create PRs on the appropriate files in the `/docs` folder.
The development environment consists of running an [`up.sh`](https://github.com/ansible/awx-operator/blob/devel/up.sh) and a [`down.sh`](https://github.com/ansible/awx-operator/blob/devel/down.sh) script, which applies or deletes yaml on the Openshift or K8s cluster you are connected to. See the [development.md](https://github.com/ansible/awx-operator/blob/devel/docs/development.md) for information on how to deploy and test changes from your branch.
## Author ## Author
This operator was originally built in 2019 by [Jeff Geerling](https://www.jeffgeerling.com) and is now maintained by the Ansible Team This operator was originally built in 2019 by [Jeff Geerling](https://www.jeffgeerling.com) and is now maintained by the Ansible Team

View File

@@ -1730,6 +1730,9 @@ spec:
uwsgi_listen_queue_size: uwsgi_listen_queue_size:
description: Set the socket listen queue size for uwsgi description: Set the socket listen queue size for uwsgi
type: integer type: integer
uwsgi_timeout:
description: Set the timeout for requests served by uwsgi. (note, graceful exit signal sent 2 seconds prior to timeout)
type: integer
nginx_worker_processes: nginx_worker_processes:
description: Set the number of workers for nginx description: Set the number of workers for nginx
type: integer type: integer
@@ -1965,6 +1968,9 @@ spec:
description: Disable web container's nginx ipv6 listener description: Disable web container's nginx ipv6 listener
type: boolean type: boolean
default: false default: false
idle_deployment:
description: Scale down deployments to put AWX into an idle state
type: boolean
metrics_utility_enabled: metrics_utility_enabled:
description: Enable metrics utility description: Enable metrics utility
type: boolean type: boolean
@@ -2009,8 +2015,12 @@ spec:
description: Enable metrics utility shipping to Red Hat Hybrid Cloud Console description: Enable metrics utility shipping to Red Hat Hybrid Cloud Console
type: boolean type: boolean
default: false default: false
public_base_url:
description: Public base URL
type: string
type: object type: object
status: status:
x-kubernetes-preserve-unknown-fields: true
properties: properties:
URL: URL:
description: URL to access the deployed instance description: URL to access the deployed instance
@@ -2042,6 +2052,9 @@ spec:
image: image:
description: URL of the image used for the deployed instance description: URL of the image used for the deployed instance
type: string type: string
upgradedFrom:
description: Last gated version
type: string
conditions: conditions:
description: The resulting conditions when a Service Telemetry is instantiated description: The resulting conditions when a Service Telemetry is instantiated
items: items:
@@ -2056,5 +2069,6 @@ spec:
type: string type: string
type: object type: object
type: array type: array
x-kubernetes-preserve-unknown-fields: true
type: object type: object
type: object type: object

View File

@@ -73,8 +73,8 @@ spec:
memory: "32Mi" memory: "32Mi"
cpu: "50m" cpu: "50m"
limits: limits:
memory: "960Mi" memory: "4000Mi"
cpu: "1500m" cpu: "2000m"
serviceAccountName: controller-manager serviceAccountName: controller-manager
imagePullSecrets: imagePullSecrets:
- name: redhat-operators-pull-secret - name: redhat-operators-pull-secret

View File

@@ -173,6 +173,12 @@ spec:
path: db_management_pod_node_selector path: db_management_pod_node_selector
x-descriptors: x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:advanced - urn:alm:descriptor:com.tectonic.ui:advanced
- displayName: Public Base URL
path: public_base_url
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:text
- urn:alm:descriptor:com.tectonic.ui:advanced
- urn:alm:descriptor:com.tectonic.ui:hidden
statusDescriptors: statusDescriptors:
- description: Persistent volume claim name used during backup - description: Persistent volume claim name used during backup
displayName: Backup Claim displayName: Backup Claim
@@ -548,6 +554,12 @@ spec:
- urn:alm:descriptor:com.tectonic.ui:advanced - urn:alm:descriptor:com.tectonic.ui:advanced
- urn:alm:descriptor:com.tectonic.ui:number - urn:alm:descriptor:com.tectonic.ui:number
- urn:alm:descriptor:com.tectonic.ui:hidden - urn:alm:descriptor:com.tectonic.ui:hidden
- displayName: Uwsgi Timeout
path: uwsgi_timeout
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:advanced
- urn:alm:descriptor:com.tectonic.ui:number
- urn:alm:descriptor:com.tectonic.ui:hidden
- displayName: Uwsgi Processes - displayName: Uwsgi Processes
path: uwsgi_processes path: uwsgi_processes
x-descriptors: x-descriptors:
@@ -1149,6 +1161,13 @@ spec:
- urn:alm:descriptor:com.tectonic.ui:advanced - urn:alm:descriptor:com.tectonic.ui:advanced
- urn:alm:descriptor:com.tectonic.ui:booleanSwitch - urn:alm:descriptor:com.tectonic.ui:booleanSwitch
- urn:alm:descriptor:com.tectonic.ui:fieldDependency:metrics_utility_enabled:true - urn:alm:descriptor:com.tectonic.ui:fieldDependency:metrics_utility_enabled:true
- description: Scale down deployments to put AWX into an idle state
displayName: Idle AWX
path: idle_deployment
x-descriptors:
- urn:alm:descriptor:com.tectonic.ui:advanced
- urn:alm:descriptor:com.tectonic.ui:booleanSwitch
- urn:alm:descriptor:com.tectonic.ui:hidden
statusDescriptors: statusDescriptors:
- description: Route to access the instance deployed - description: Route to access the instance deployed
displayName: URL displayName: URL

View File

@@ -0,0 +1,24 @@
---
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: awx
spec:
service_type: clusterip
ingress_type: route
no_log: false
# Secrets
admin_password_secret: custom-admin-password
postgres_configuration_secret: custom-pg-configuration
secret_key_secret: custom-secret-key
# Resource Requirements
postgres_storage_requirements:
requests:
storage: 10Gi
# Extra Settings
extra_settings:
- setting: MAX_PAGE_SIZE
value: "500"

View File

@@ -0,0 +1,13 @@
---
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: awx
spec:
service_type: nodeport
ingress_type: ingress
# Secrets
admin_password_secret: custom-admin-password
postgres_configuration_secret: custom-pg-configuration
secret_key_secret: custom-secret-key

View File

@@ -0,0 +1,13 @@
---
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: awx
spec:
service_type: clusterip
ingress_type: Route
# Secrets
admin_password_secret: custom-admin-password
postgres_configuration_secret: custom-pg-configuration
secret_key_secret: custom-secret-key

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Secret
metadata:
name: custom-admin-password
stringData:
password: 'password'

View File

@@ -0,0 +1,7 @@
---
apiVersion: v1
kind: Secret
metadata:
name: custom-secret-key
stringData:
secret_key: 'awxsecret'

View File

@@ -0,0 +1,12 @@
---
apiVersion: v1
kind: Secret
metadata:
name: external-pg-secret
stringData:
database: 'awx'
host: 'awx-postgres'
password: 'test'
port: '5432'
type: 'managed'
username: 'awx'

58
docs/development.md Normal file
View File

@@ -0,0 +1,58 @@
# Development Guide
There are development scripts and yaml exaples in the [`dev/`](../dev) directory that, along with the up.sh and down.sh scripts in the root of the repo, can be used to build, deploy and test changes made to the awx-operator.
## Build and Deploy
If you clone the repo, and make sure you are logged in at the CLI with oc and your cluster, you can run:
```
export QUAY_USER=username
export NAMESPACE=awx
export TAG=test
./up.sh
```
You can add those variables to your .bashrc file so that you can just run `./up.sh` in the future.
> Note: the first time you run this, it will create quay.io repos on your fork. You will need to either make those public, or create a global pull secret on your Openshift cluster.
To get the URL, if on **Openshift**, run:
```
$ oc get route
```
On **k8s with ingress**, run:
```
$ kubectl get ing
```
On **k8s with nodeport**, run:
```
$ kubectl get svc
```
The URL is then `http://<Node-IP>:<NodePort>`
> Note: NodePort will only work if you expose that port on your underlying k8s node, or are accessing it from localhost.
By default, the usename and password will be admin and password if using the `up.sh` script because it pre-creates a custom admin password k8s secret and specifies it on the AWX custom resource spec. Without that, a password would have been generated and stored in a k8s secret named <deployment-name>-admin-password.
## Clean up
Same thing for cleanup, just run ./down.sh and it will clean up your namespace on that cluster
```
./down.sh
```
## Running CI tests locally
More tests coming soon...

View File

@@ -70,15 +70,16 @@ spec:
## Custom UWSGI Configuration ## Custom UWSGI Configuration
We allow the customization of two UWSGI parameters: We allow the customization of three UWSGI parameters:
* [processes](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#processes) with `uwsgi_processes` (default 5) * [processes](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#processes) with `uwsgi_processes` (default 5)
* [listen](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#listen) with `uwsgi_listen_queue_size` (default 128) * [listen](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#listen) with `uwsgi_listen_queue_size` (default 128)
* [harakiri](https://uwsgi-docs.readthedocs.io/en/latest/Options.html#harakiri) with `uwsgi_timeout` (default 30)
**Note:** Increasing the listen queue beyond 128 requires that the sysctl setting net.core.somaxconn be set to an equal value or higher. **Note:** Increasing the listen queue beyond 128 requires that the sysctl setting net.core.somaxconn be set to an equal value or higher.
The operator will set the appropriate securityContext sysctl value for you, but it is a required that this sysctl be added to an allowlist on the kubelet level. [See kubernetes docs about allowing this sysctl setting](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/#enabling-unsafe-sysctls). The operator will set the appropriate securityContext sysctl value for you, but it is a required that this sysctl be added to an allowlist on the kubelet level. [See kubernetes docs about allowing this sysctl setting](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/#enabling-unsafe-sysctls).
These vars relate to the vertical and horizontal scalibility of the web service. The `processes` and `listen` vars relate to the vertical and horizontal scalibility of the web service.
Increasing the number of processes allows more requests to be actively handled Increasing the number of processes allows more requests to be actively handled
per web pod, but will consume more CPU and Memory and the resource requests per web pod, but will consume more CPU and Memory and the resource requests
@@ -89,6 +90,12 @@ requests (more than 128) tend to come in a short period of time, but can all be
handled before any other time outs may apply. Also see related nginx handled before any other time outs may apply. Also see related nginx
configuration. configuration.
The `uwsgi_timeout` variable determines after how many seconds a request will
be forecibly killed by uwsgi. A "graceful" timeout signal is sent to the worker
2 seconds prior to attempt to get a traceback of what may be causing the
request to hang.
## Custom Nginx Configuration ## Custom Nginx Configuration
Using the [extra_volumes feature](#custom-volume-and-volume-mount-options), it is possible to extend the nginx.conf. Using the [extra_volumes feature](#custom-volume-and-volume-mount-options), it is possible to extend the nginx.conf.

36
down.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# AWX Operator down.sh
# Purpose:
# Cleanup and delete the namespace you deployed in
# -- Usage
# NAMESPACE=awx ./down.sh
# -- Variables
TAG=${TAG:-dev}
AWX_CR=${AWX_CR:-awx}
CLEAN_DB=${CLEAN_DB:-false}
# -- Check for required variables
# Set the following environment variables
# export NAMESPACE=awx
if [ -z "$NAMESPACE" ]; then
echo "Error: NAMESPACE env variable is not set. Run the following with your namespace:"
echo " export NAMESPACE=developer"
exit 1
fi
# -- Delete Backups
kubectl delete awxbackup --all
# -- Delete Restores
kubectl delete awxrestore --all
# Deploy Operator
make undeploy NAMESPACE=$NAMESPACE
# Remove PVCs
kubectl delete pvc postgres-15-$AWX_CR-postgres-15-0

View File

@@ -4,4 +4,6 @@ collections:
- name: kubernetes.core - name: kubernetes.core
- name: operator_sdk.util - name: operator_sdk.util
- name: community.docker - name: community.docker
- name: awx.awx - name: https://github.com/ansible/awx.git#/awx_collection/
type: git
version: devel

View File

@@ -497,6 +497,7 @@ uwsgi_processes: 5
# Also see https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/#enabling-unsafe-sysctls for how # Also see https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/#enabling-unsafe-sysctls for how
# to allow setting this sysctl, which requires kubelet configuration to add to allowlist # to allow setting this sysctl, which requires kubelet configuration to add to allowlist
uwsgi_listen_queue_size: 128 uwsgi_listen_queue_size: 128
uwsgi_timeout: 30
# NGINX default values # NGINX default values
nginx_worker_processes: 1 nginx_worker_processes: 1
@@ -505,3 +506,6 @@ nginx_worker_cpu_affinity: 'auto'
nginx_listen_queue_size: "{{ uwsgi_listen_queue_size }}" nginx_listen_queue_size: "{{ uwsgi_listen_queue_size }}"
extra_settings_files: {} extra_settings_files: {}
# idle_deployment - Scale down deployments to put AWX into an idle state
idle_deployment: false

View File

@@ -0,0 +1,35 @@
---
- name: Check for presence of Deployment
kubernetes.core.k8s_info:
api_version: apps/v1
kind: Deployment
namespace: "{{ ansible_operator_meta.namespace }}"
label_selectors:
- 'app.kubernetes.io/part-of={{ ansible_operator_meta.name }}'
- 'app.kubernetes.io/managed-by={{ deployment_type }}-operator'
- 'app.kubernetes.io/component={{ deployment_type }}'
register: _deployments
- name: Set previous_version if deployment exists
when: _deployments.resources | length > 0
block:
- name: Check for existing deployment
kubernetes.core.k8s_info:
api_version: "{{ api_version }}"
kind: "{{ kind }}"
namespace: "{{ ansible_operator_meta.namespace }}"
name: "{{ ansible_operator_meta.name }}"
register: existing_cr
- name: Set previous_version version based on AWX CR version status
ansible.builtin.set_fact:
previous_version: "{{ existing_cr.resources[0].status.version }}"
when: existing_cr['resources'] | length
- name: If previous_version is less than or equal to gating_version, set upgraded_from to previous_version
ansible.builtin.set_fact:
upgraded_from: "{{ previous_version }}"
when:
- previous_version is defined
- previous_version is version_compare(gating_version, '<')

View File

@@ -0,0 +1,167 @@
---
- name: Get database configuration
include_tasks: database_configuration.yml
# It is possible that N-2 postgres pods may still be present in the namespace from previous upgrades.
# So we have to take that into account and preferentially set the most recent one.
- name: Get the old postgres pod (N-1)
k8s_info:
kind: Pod
namespace: "{{ ansible_operator_meta.namespace }}"
field_selectors:
- status.phase=Running
register: _running_pods
- block:
- name: Filter pods by name
set_fact:
filtered_old_postgres_pods: "{{ _running_pods.resources |
selectattr('metadata.name', 'match', ansible_operator_meta.name + '-postgres.*-0') |
rejectattr('metadata.name', 'search', '-' + supported_pg_version | string + '-0') |
list }}"
# Sort pods by name in reverse order (most recent PG version first) and set
- name: Set info for previous postgres pod
set_fact:
sorted_old_postgres_pods: "{{ filtered_old_postgres_pods |
sort(attribute='metadata.name') |
reverse | list }}"
when: filtered_old_postgres_pods | length
- name: Set info for previous postgres pod
set_fact:
old_postgres_pod: "{{ sorted_old_postgres_pods | first }}"
when: filtered_old_postgres_pods | length
when: _running_pods.resources | length
- name: Look up details for this deployment
k8s_info:
api_version: "{{ api_version }}"
kind: "{{ kind }}"
name: "{{ ansible_operator_meta.name }}"
namespace: "{{ ansible_operator_meta.namespace }}"
register: this_awx
# If this deployment has been upgraded before or if upgrade has already been started, set this var
- name: Set previous PG version var
set_fact:
_previous_upgraded_pg_version: "{{ this_awx['resources'][0]['status']['upgradedPostgresVersion'] | default(false) }}"
when:
- this_awx['resources'][0] is defined
- "'upgradedPostgresVersion' in this_awx['resources'][0]['status']"
- name: Check if postgres pod is running an older version
block:
- name: Get old PostgreSQL version
k8s_exec:
namespace: "{{ ansible_operator_meta.namespace }}"
pod: "{{ old_postgres_pod['metadata']['name'] }}"
command: |
bash -c """
if [ -f "{{ _postgres_data_path }}/PG_VERSION" ]; then
cat "{{ _postgres_data_path }}/PG_VERSION"
elif [ -f '/var/lib/postgresql/data/pgdata/PG_VERSION' ]; then
cat '/var/lib/postgresql/data/pgdata/PG_VERSION'
fi
"""
register: _old_pg_version
- debug:
msg: "--- Upgrading from {{ old_postgres_pod['metadata']['name'] | default('NONE')}} Pod ---"
- name: Upgrade data dir from old Postgres to {{ supported_pg_version }} if applicable
include_tasks: upgrade_postgres.yml
when:
- (_old_pg_version.stdout | default(0) | int ) < supported_pg_version
when:
- managed_database
- (_previous_upgraded_pg_version | default(false)) | ternary(_previous_upgraded_pg_version | int < supported_pg_version, true)
- old_postgres_pod | length # If empty, then old pg pod has been removed and we can assume the upgrade is complete
- block:
- name: Create Database if no database is specified
k8s:
apply: true
definition: "{{ lookup('template', 'statefulsets/postgres.yaml.j2') }}"
register: create_statefulset_result
- name: Scale down Deployment for migration
include_tasks: scale_down_deployment.yml
when: create_statefulset_result.changed
rescue:
- name: Scale down Deployment for migration
include_tasks: scale_down_deployment.yml
- name: Scale down PostgreSQL statefulset for migration
kubernetes.core.k8s_scale:
api_version: apps/v1
kind: StatefulSet
name: "{{ ansible_operator_meta.name }}-postgres-{{ supported_pg_version }}"
namespace: "{{ ansible_operator_meta.namespace }}"
replicas: 0
wait: yes
- name: Remove PostgreSQL statefulset for upgrade
k8s:
state: absent
api_version: apps/v1
kind: StatefulSet
name: "{{ ansible_operator_meta.name }}-postgres-{{ supported_pg_version }}"
namespace: "{{ ansible_operator_meta.namespace }}"
wait: yes
when: create_statefulset_result.error == 422
- name: Recreate PostgreSQL statefulset with updated values
k8s:
apply: true
definition: "{{ lookup('template', 'statefulsets/postgres.yaml.j2') }}"
when: managed_database
- name: Set Default label selector for custom resource generated postgres
set_fact:
postgres_label_selector: "app.kubernetes.io/instance=postgres-{{ supported_pg_version }}-{{ ansible_operator_meta.name }}"
when: postgres_label_selector is not defined
- name: Get the postgres pod information
k8s_info:
kind: Pod
namespace: "{{ ansible_operator_meta.namespace }}"
label_selectors:
- "{{ postgres_label_selector }}"
field_selectors:
- status.phase=Running
register: postgres_pod
- name: Wait for Database to initialize if managed DB
k8s_info:
kind: Pod
namespace: '{{ ansible_operator_meta.namespace }}'
label_selectors:
- "{{ postgres_label_selector }}"
field_selectors:
- status.phase=Running
register: postgres_pod
until:
- "postgres_pod['resources'] | length"
- "postgres_pod['resources'][0]['status']['phase'] == 'Running'"
- "postgres_pod['resources'][0]['status']['containerStatuses'][0]['ready'] == true"
delay: 5
retries: 60
when: managed_database
- name: Look up details for this deployment
k8s_info:
api_version: "{{ api_version }}"
kind: "{{ kind }}"
name: "{{ ansible_operator_meta.name }}"
namespace: "{{ ansible_operator_meta.namespace }}"
register: this_awx
- name: Migrate data from old Openshift instance
import_tasks: migrate_data.yml
when:
- old_pg_config['resources'] is defined
- old_pg_config['resources'] | length
- this_awx['resources'][0]['status']['migratedFromSecret'] is not defined

View File

@@ -51,6 +51,14 @@
set_fact: set_fact:
_default_postgres_image: "{{ _postgres_image }}:{{_postgres_image_version }}" _default_postgres_image: "{{ _postgres_image }}:{{_postgres_image_version }}"
- name: Fail if PostgreSQL secret is specified, but not found
fail:
msg: "PostgreSQL configuration {{ postgres_configuration_secret }} not found in namespace {{ ansible_operator_meta.namespace }}"
when:
- postgres_configuration_secret | length
- _custom_pg_config_resources is defined
- _custom_pg_config_resources['resources'] | length == 0
- name: Set PostgreSQL configuration - name: Set PostgreSQL configuration
set_fact: set_fact:
_pg_config: '{{ _custom_pg_config_resources["resources"] | default([]) | length | ternary(_custom_pg_config_resources, _default_pg_config_resources) }}' _pg_config: '{{ _custom_pg_config_resources["resources"] | default([]) | length | ternary(_custom_pg_config_resources, _default_pg_config_resources) }}'
@@ -106,167 +114,3 @@
- name: Set database as managed - name: Set database as managed
set_fact: set_fact:
managed_database: "{{ pg_config['resources'][0]['data']['type'] | default('') | b64decode == 'managed' }}" managed_database: "{{ pg_config['resources'][0]['data']['type'] | default('') | b64decode == 'managed' }}"
# It is possible that N-2 postgres pods may still be present in the namespace from previous upgrades.
# So we have to take that into account and preferentially set the most recent one.
- name: Get the old postgres pod (N-1)
k8s_info:
kind: Pod
namespace: "{{ ansible_operator_meta.namespace }}"
field_selectors:
- status.phase=Running
register: _running_pods
- block:
- name: Filter pods by name
set_fact:
filtered_old_postgres_pods: "{{ _running_pods.resources |
selectattr('metadata.name', 'match', ansible_operator_meta.name + '-postgres.*-0') |
rejectattr('metadata.name', 'search', '-' + supported_pg_version | string + '-0') |
list }}"
# Sort pods by name in reverse order (most recent PG version first) and set
- name: Set info for previous postgres pod
set_fact:
sorted_old_postgres_pods: "{{ filtered_old_postgres_pods |
sort(attribute='metadata.name') |
reverse | list }}"
when: filtered_old_postgres_pods | length
- name: Set info for previous postgres pod
set_fact:
old_postgres_pod: "{{ sorted_old_postgres_pods | first }}"
when: filtered_old_postgres_pods | length
when: _running_pods.resources | length
- name: Look up details for this deployment
k8s_info:
api_version: "{{ api_version }}"
kind: "{{ kind }}"
name: "{{ ansible_operator_meta.name }}"
namespace: "{{ ansible_operator_meta.namespace }}"
register: this_awx
# If this deployment has been upgraded before or if upgrade has already been started, set this var
- name: Set previous PG version var
set_fact:
_previous_upgraded_pg_version: "{{ this_awx['resources'][0]['status']['upgradedPostgresVersion'] | default(false) }}"
when:
- this_awx['resources'][0] is defined
- "'upgradedPostgresVersion' in this_awx['resources'][0]['status']"
- name: Check if postgres pod is running an older version
block:
- name: Get old PostgreSQL version
k8s_exec:
namespace: "{{ ansible_operator_meta.namespace }}"
pod: "{{ old_postgres_pod['metadata']['name'] }}"
command: |
bash -c """
if [ -f "{{ _postgres_data_path }}/PG_VERSION" ]; then
cat "{{ _postgres_data_path }}/PG_VERSION"
elif [ -f '/var/lib/postgresql/data/pgdata/PG_VERSION' ]; then
cat '/var/lib/postgresql/data/pgdata/PG_VERSION'
fi
"""
register: _old_pg_version
- debug:
msg: "--- Upgrading from {{ old_postgres_pod['metadata']['name'] | default('NONE')}} Pod ---"
- name: Upgrade data dir from old Postgres to {{ supported_pg_version }} if applicable
include_tasks: upgrade_postgres.yml
when:
- (_old_pg_version.stdout | default(0) | int ) < supported_pg_version
when:
- managed_database
- (_previous_upgraded_pg_version | default(false)) | ternary(_previous_upgraded_pg_version | int < supported_pg_version, true)
- old_postgres_pod | length # If empty, then old pg pod has been removed and we can assume the upgrade is complete
- block:
- name: Create Database if no database is specified
k8s:
apply: true
definition: "{{ lookup('template', 'statefulsets/postgres.yaml.j2') }}"
register: create_statefulset_result
- name: Scale down Deployment for migration
include_tasks: scale_down_deployment.yml
when: create_statefulset_result.changed
rescue:
- name: Scale down Deployment for migration
include_tasks: scale_down_deployment.yml
- name: Scale down PostgreSQL statefulset for migration
kubernetes.core.k8s_scale:
api_version: apps/v1
kind: StatefulSet
name: "{{ ansible_operator_meta.name }}-postgres-{{ supported_pg_version }}"
namespace: "{{ ansible_operator_meta.namespace }}"
replicas: 0
wait: yes
- name: Remove PostgreSQL statefulset for upgrade
k8s:
state: absent
api_version: apps/v1
kind: StatefulSet
name: "{{ ansible_operator_meta.name }}-postgres-{{ supported_pg_version }}"
namespace: "{{ ansible_operator_meta.namespace }}"
wait: yes
when: create_statefulset_result.error == 422
- name: Recreate PostgreSQL statefulset with updated values
k8s:
apply: true
definition: "{{ lookup('template', 'statefulsets/postgres.yaml.j2') }}"
when: managed_database
- name: Set Default label selector for custom resource generated postgres
set_fact:
postgres_label_selector: "app.kubernetes.io/instance=postgres-{{ supported_pg_version }}-{{ ansible_operator_meta.name }}"
when: postgres_label_selector is not defined
- name: Get the postgres pod information
k8s_info:
kind: Pod
namespace: "{{ ansible_operator_meta.namespace }}"
label_selectors:
- "{{ postgres_label_selector }}"
field_selectors:
- status.phase=Running
register: postgres_pod
- name: Wait for Database to initialize if managed DB
k8s_info:
kind: Pod
namespace: '{{ ansible_operator_meta.namespace }}'
label_selectors:
- "{{ postgres_label_selector }}"
field_selectors:
- status.phase=Running
register: postgres_pod
until:
- "postgres_pod['resources'] | length"
- "postgres_pod['resources'][0]['status']['phase'] == 'Running'"
- "postgres_pod['resources'][0]['status']['containerStatuses'][0]['ready'] == true"
delay: 5
retries: 60
when: managed_database
- name: Look up details for this deployment
k8s_info:
api_version: "{{ api_version }}"
kind: "{{ kind }}"
name: "{{ ansible_operator_meta.name }}"
namespace: "{{ ansible_operator_meta.namespace }}"
register: this_awx
- name: Migrate data from old Openshift instance
import_tasks: migrate_data.yml
when:
- old_pg_config['resources'] is defined
- old_pg_config['resources'] | length
- this_awx['resources'][0]['status']['migratedFromSecret'] is not defined

View File

@@ -0,0 +1,34 @@
---
- name: Scale down AWX Deployments
kubernetes.core.k8s:
state: present
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: "{{ item }}"
namespace: "{{ ansible_operator_meta.namespace }}"
spec:
replicas: 0
loop:
- '{{ ansible_operator_meta.name }}-task'
- '{{ ansible_operator_meta.name }}-web'
- name: Get database configuration
include_tasks: database_configuration.yml
- name: Scale down PostgreSQL Statefulset
kubernetes.core.k8s:
state: present
definition:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: "{{ ansible_operator_meta.name }}-postgres-{{ supported_pg_version }}"
namespace: "{{ ansible_operator_meta.namespace }}"
spec:
replicas: 0
when: managed_database
- name: End Playbook
ansible.builtin.meta: end_play

View File

@@ -18,13 +18,11 @@
namespace: "{{ ansible_operator_meta.namespace }}" namespace: "{{ ansible_operator_meta.namespace }}"
pod: "{{ awx_web_pod_name }}" pod: "{{ awx_web_pod_name }}"
container: "{{ ansible_operator_meta.name }}-web" container: "{{ ansible_operator_meta.name }}-web"
command: awx-manage createsuperuser --username={{ admin_user | quote }} --email={{ admin_email | quote }} --noinput command: bash -c "ANSIBLE_REVERSE_RESOURCE_SYNC=false awx-manage createsuperuser --username={{ admin_user | quote }} --email={{ admin_email | quote }} --noinput"
register: result register: result
changed_when: "'That username is already taken' not in result.stderr" changed_when: "'That username is already taken' not in result.stderr"
failed_when: "'That username is already taken' not in result.stderr and 'Superuser created successfully' not in result.stdout" failed_when: "'That username is already taken' not in result.stderr and 'Superuser created successfully' not in result.stdout"
no_log: "{{ no_log }}" no_log: "{{ no_log }}"
environment:
ANSIBLE_REVERSE_RESOURCE_SYNC: "false"
when: users_result.return_code > 0 when: users_result.return_code > 0
- name: Update Django super user password - name: Update Django super user password
@@ -116,9 +114,7 @@
pod: "{{ awx_web_pod_name }}" pod: "{{ awx_web_pod_name }}"
container: "{{ ansible_operator_meta.name }}-web" container: "{{ ansible_operator_meta.name }}-web"
command: >- command: >-
bash -c "awx-manage create_preload_data" bash -c "ANSIBLE_REVERSE_RESOURCE_SYNC=false awx-manage create_preload_data"
register: cdo register: cdo
changed_when: "'added' in cdo.stdout" changed_when: "'added' in cdo.stdout"
environment:
ANSIBLE_REVERSE_RESOURCE_SYNC: "false"
when: create_preload_data | bool when: create_preload_data | bool

View File

@@ -44,6 +44,12 @@
- name: Include secret key configuration tasks - name: Include secret key configuration tasks
include_tasks: secret_key_configuration.yml include_tasks: secret_key_configuration.yml
- name: Apply Redirect Page Configmap
k8s:
apply: true
definition: "{{ lookup('template', 'configmaps/redirect-page.configmap.html.j2') }}"
when: public_base_url is defined
- name: Load LDAP CAcert certificate (Deprecated) - name: Load LDAP CAcert certificate (Deprecated)
include_tasks: load_ldap_cacert_secret.yml include_tasks: load_ldap_cacert_secret.yml
when: when:
@@ -68,8 +74,8 @@
- name: Include set_images tasks - name: Include set_images tasks
include_tasks: set_images.yml include_tasks: set_images.yml
- name: Include database configuration tasks - name: Include Database tasks
include_tasks: database_configuration.yml include_tasks: database.yml
- name: Load Route TLS certificate - name: Load Route TLS certificate
include_tasks: load_route_tls_secret.yml include_tasks: load_route_tls_secret.yml

View File

@@ -1,4 +1,8 @@
--- ---
- name: Idle AWX
include_tasks: idle_deployment.yml
when: idle_deployment | bool
- name: Check for presence of old awx Deployment - name: Check for presence of old awx Deployment
k8s_info: k8s_info:
api_version: apps/v1 api_version: apps/v1
@@ -23,6 +27,10 @@
namespace: "{{ ansible_operator_meta.namespace }}" namespace: "{{ ansible_operator_meta.namespace }}"
register: awx_web_deployment register: awx_web_deployment
- name: Check for existing deployment for previous version
include_tasks: check_existing.yml
when: gating_version | length
- name: Start installation if auto_upgrade is true - name: Start installation if auto_upgrade is true
include_tasks: install.yml include_tasks: install.yml
when: when:

View File

@@ -55,6 +55,16 @@
changed_when: false changed_when: false
when: awx_web_pod_name != '' when: awx_web_pod_name != ''
- name: Update upgradedFrom status
operator_sdk.util.k8s_status:
api_version: '{{ api_version }}'
kind: "{{ kind }}"
name: "{{ ansible_operator_meta.name }}"
namespace: "{{ ansible_operator_meta.namespace }}"
status:
upgradedFrom: "{{ upgraded_from }}"
when: upgraded_from is defined
- name: Update version status - name: Update version status
operator_sdk.util.k8s_status: operator_sdk.util.k8s_status:
api_version: '{{ api_version }}' api_version: '{{ api_version }}'

View File

@@ -304,8 +304,8 @@ data:
max-requests = 1000 max-requests = 1000
buffer-size = 32768 buffer-size = 32768
harakiri = 120 harakiri = {{ uwsgi_timeout|int }}
harakiri-graceful-timeout = 115 harakiri-graceful-timeout = {{ [(uwsgi_timeout|int - 2), 1] | max }}
harakiri-graceful-signal = 6 harakiri-graceful-signal = 6
py-call-osafterfork = true py-call-osafterfork = true

View File

@@ -0,0 +1,76 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ ansible_operator_meta.name }}-redirect-page
namespace: {{ ansible_operator_meta.namespace }}
data:
redirect-page.html: |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="15; url={{ public_base_url }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Redirecting to Ansible Automation Platform</title>
<!-- Favicon links -->
<link rel="icon" type="image/x-icon" href="static/media/favicon.ico">
<!-- Link to DRF's CSS -->
<link rel="stylesheet" type="text/css" href="static/rest_framework/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="static/rest_framework/css/bootstrap-theme.min.css">
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding-top: 0px;
/* background-color: rgb(34, 34, 34); */
}
.banner {
background-color: #151414;
color: rgb(255, 255, 255);
padding: 20px;
margin-bottom: 20px;
min-height: 70px; /* Ensure the banner is tall enough to fit the logo */
text-align: left;
}
.logo {
width: 150px;
margin-bottom: 20px;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.doc-note {
font-size: 0.7em; /* Makes the text smaller */
color: #555; /* Optional: Change text color to a lighter shade */
background-color: #f9f9f9; /* Optional: Light background color */
padding: 10px; /* Optional: Add some padding */
margin: 10px 0; /* Optional: Add some margin */
}
</style>
</head>
<body>
<!-- Banner Section with Brand Logo -->
<div class="banner">
<img src="/static/media/aap-logo.svg" alt="Brand Logo">
</div>
<h2>Redirecting to Ansible Automation Platform...</h2>
<p>If you are not redirected automatically, <a href="{{ public_base_url }}">click here</a> to go to AAP.</p>
<p class="doc-note">
The API endpoints for this platform service will temporarily remain available at the URL for this service.
Please use the Ansible Automation Platform API endpoints corresponding to this component in the future.
These can be found at <a href="{{ public_base_url }}/api/{{ deployment_type }}" target="_blank">{{ public_base_url }}/api/{{ deployment_type }}</a>.
</p>
<!-- Include any additional scripts if needed -->
<script src="static/rest_framework/js/jquery-3.5.1.min.js"></script>
<script src="static/rest_framework/js/bootstrap.min.js"></script>
</body>
</html>

View File

@@ -28,12 +28,15 @@ spec:
annotations: annotations:
kubectl.kubernetes.io/default-container: '{{ ansible_operator_meta.name }}-web' kubectl.kubernetes.io/default-container: '{{ ansible_operator_meta.name }}-web'
{% for template in [ {% for template in [
"configmaps/config", "configmaps/config.yaml",
"secrets/app_credentials", "secrets/app_credentials.yaml",
"storage/persistent", "storage/persistent.yaml",
] %} ] %}
checksum-{{ template | replace('/', '-') }}: "{{ lookup('template', template + '.yaml.j2') | sha1 }}" checksum-{{ template | replace('/', '-') }}: "{{ lookup('template', template + '.j2') | sha1 }}"
{% endfor %} {% endfor %}
{% if public_base_url is defined %}
checksum-configmaps-redirect-page.configmap.html: "{{ lookup('template', 'configmaps/redirect-page.configmap.html.j2') | sha1 }}"
{% endif %}
{% for secret in [ {% for secret in [
"bundle_cacert", "bundle_cacert",
"route_tls", "route_tls",
@@ -197,6 +200,11 @@ spec:
timeoutSeconds: {{ web_readiness_timeout }} timeoutSeconds: {{ web_readiness_timeout }}
{% endif %} {% endif %}
volumeMounts: volumeMounts:
{% if public_base_url is defined %}
- name: redirect-page
mountPath: '/var/lib/awx/venv/awx/lib/python3.11/site-packages/awx/ui/build/index.html'
subPath: redirect-page.html
{% endif %}
{% if bundle_ca_crt %} {% if bundle_ca_crt %}
- name: "ca-trust-extracted" - name: "ca-trust-extracted"
mountPath: "/etc/pki/ca-trust/extracted" mountPath: "/etc/pki/ca-trust/extracted"
@@ -375,6 +383,14 @@ spec:
{{ security_context_settings | to_nice_yaml | indent(8) }} {{ security_context_settings | to_nice_yaml | indent(8) }}
{% endif %} {% endif %}
volumes: volumes:
{% if public_base_url is defined %}
- name: redirect-page
configMap:
name: '{{ ansible_operator_meta.name }}-redirect-page'
items:
- key: redirect-page.html
path: redirect-page.html
{% endif %}
- name: "{{ ansible_operator_meta.name }}-receptor-ca" - name: "{{ ansible_operator_meta.name }}-receptor-ca"
secret: secret:
secretName: "{{ ansible_operator_meta.name }}-receptor-ca" secretName: "{{ ansible_operator_meta.name }}-receptor-ca"

View File

@@ -21,3 +21,6 @@ _metrics_utility_pvc_claim: "{{ metrics_utility_pvc_claim | default(deployment_t
_metrics_utility_pvc_claim_size: "{{ metrics_utility_pvc_claim_size | default('5Gi') }}" _metrics_utility_pvc_claim_size: "{{ metrics_utility_pvc_claim_size | default('5Gi') }}"
_metrics_utility_cronjob_gather_schedule: "{{ metrics_utility_cronjob_gather_schedule | default('@hourly') }}" _metrics_utility_cronjob_gather_schedule: "{{ metrics_utility_cronjob_gather_schedule | default('@hourly') }}"
_metrics_utility_cronjob_report_schedule: "{{ metrics_utility_cronjob_report_schedule | default('@monthly') }}" _metrics_utility_cronjob_report_schedule: "{{ metrics_utility_cronjob_report_schedule | default('@monthly') }}"
# version check
gating_version: ''

134
up.sh Executable file
View File

@@ -0,0 +1,134 @@
#!/bin/bash
# AWX Operator up.sh
# Purpose:
# Build operator image from your local checkout, push to quay.io/youruser/awx-operator:dev, and deploy operator
# -- Usage
# NAMESPACE=awx TAG=dev QUAY_USER=developer ./up.sh
# -- User Variables
NAMESPACE=${NAMESPACE:-awx}
QUAY_USER=${QUAY_USER:-developer}
TAG=${TAG:-$(git rev-parse --short HEAD)}
DEV_TAG=${DEV_TAG:-dev}
DEV_TAG_PUSH=${DEV_TAG_PUSH:-true}
# -- Check for required variables
# Set the following environment variables
# export NAMESPACE=awx
# export QUAY_USER=developer
if [ -z "$QUAY_USER" ]; then
echo "Error: QUAY_USER env variable is not set."
echo " export QUAY_USER=developer"
exit 1
fi
if [ -z "$NAMESPACE" ]; then
echo "Error: NAMESPACE env variable is not set. Run the following with your namespace:"
echo " export NAMESPACE=developer"
exit 1
fi
# -- Container Build Engine (podman or docker)
ENGINE=${ENGINE:-podman}
# -- Variables
IMG=quay.io/$QUAY_USER/awx-operator
KUBE_APPLY="kubectl apply -n $NAMESPACE -f"
# -- Wait for existing project to be deleted
# Function to check if the namespace is in terminating state
is_namespace_terminating() {
kubectl get namespace $NAMESPACE 2>/dev/null | grep -q 'Terminating'
return $?
}
# Check if the namespace exists and is in terminating state
if kubectl get namespace $NAMESPACE 2>/dev/null; then
echo "Namespace $NAMESPACE exists."
if is_namespace_terminating; then
echo "Namespace $NAMESPACE is in terminating state. Waiting for it to be fully terminated..."
while is_namespace_terminating; do
sleep 5
done
echo "Namespace $NAMESPACE has been terminated."
fi
fi
# -- Create namespace
kubectl create namespace $NAMESPACE
# -- Prepare
# Set imagePullPolicy to Always
files=(
config/manager/manager.yaml
)
for file in "${files[@]}"; do
if grep -qF 'imagePullPolicy: IfNotPresent' ${file}; then
sed -i -e "s|imagePullPolicy: IfNotPresent|imagePullPolicy: Always|g" ${file};
fi
done
# Delete old operator deployment
kubectl delete deployment awx-operator-controller-manager
# Create secrets
$KUBE_APPLY dev/secrets/custom-secret-key.yml
$KUBE_APPLY dev/secrets/admin-password-secret.yml
# (Optional) Create external-pg-secret
# $KUBE_APPLY dev/secrets/external-pg-secret.yml
# -- Login to Quay.io
$ENGINE login quay.io
if [ $ENGINE = 'podman' ]; then
if [ -f "$XDG_RUNTIME_DIR/containers/auth.json" ] ; then
REGISTRY_AUTH_CONFIG=$XDG_RUNTIME_DIR/containers/auth.json
echo "Found registry auth config: $REGISTRY_AUTH_CONFIG"
elif [ -f $HOME/.config/containers/auth.json ] ; then
REGISTRY_AUTH_CONFIG=$HOME/.config/containers/auth.json
echo "Found registry auth config: $REGISTRY_AUTH_CONFIG"
elif [ -f "/home/$USER/.docker/config.json" ] ; then
REGISTRY_AUTH_CONFIG=/home/$USER/.docker/config.json
echo "Found registry auth config: $REGISTRY_AUTH_CONFIG"
else
echo "No Podman configuration files were found."
fi
fi
if [ $ENGINE = 'docker' ]; then
if [ -f "/home/$USER/.docker/config.json" ] ; then
REGISTRY_AUTH_CONFIG=/home/$USER/.docker/config.json
echo "Found registry auth config: $REGISTRY_AUTH_CONFIG"
else
echo "No Docker configuration files were found."
fi
fi
# -- Build & Push Operator Image
echo "Preparing to build $IMG:$TAG ($IMG:$DEV_TAG) with $ENGINE..."
sleep 3
make docker-build docker-push IMG=$IMG:$TAG
# Tag and Push DEV_TAG Image when DEV_TAG_PUSH is 'True'
if $DEV_TAG_PUSH ; then
$ENGINE tag $IMG:$TAG $IMG:$DEV_TAG
make docker-push IMG=$IMG:$DEV_TAG
fi
# -- Deploy Operator
make deploy IMG=$IMG:$TAG NAMESPACE=$NAMESPACE
# -- Create CR
# uncomment the CR you want to use
$KUBE_APPLY dev/awx-cr/awx-openshift-cr.yml
# $KUBE_APPLY dev/awx-cr/awx-cr-settings.yml
# $KUBE_APPLY dev/awx-cr/awx-k8s-ingress.yml