Initial commit.

This commit is contained in:
Jeff Geerling
2019-10-30 12:00:17 -05:00
commit 80895d628a
29 changed files with 738 additions and 0 deletions

9
.travis.yml Normal file
View File

@@ -0,0 +1,9 @@
sudo: required
services: docker
language: python
install:
- pip3 install docker molecule openshift jmespath
script:
- molecule test -s test-local

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Tower Operator
[![Build Status](https://travis-ci.com/geerlingguy/tower-operator.svg?branch=master)](https://travis-ci.com/geerlingguy/tower-operator)
A Tower operator for Kubernetes built with Operator SDK.

6
build/Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM quay.io/operator-framework/ansible-operator:v0.10.0
COPY watches.yaml ${HOME}/watches.yaml
COPY main.yml ${HOME}/main.yml
COPY roles/ ${HOME}/roles/

View File

@@ -0,0 +1,13 @@
ARG BASEIMAGE
FROM ${BASEIMAGE}
USER 0
RUN yum install -y python-devel gcc libffi-devel
RUN pip install molecule==2.20.1
ARG NAMESPACEDMAN
ADD $NAMESPACEDMAN /namespaced.yaml
ADD build/test-framework/ansible-test.sh /ansible-test.sh
RUN chmod +x /ansible-test.sh
USER 1001
ADD . /opt/ansible/project

View File

@@ -0,0 +1,7 @@
#!/bin/sh
export WATCH_NAMESPACE=${TEST_NAMESPACE}
(/usr/local/bin/entrypoint)&
trap "kill $!" SIGINT SIGTERM EXIT
cd ${HOME}/project
exec molecule test -s test-cluster

View File

@@ -0,0 +1,13 @@
apiVersion: tower.ansible.com/v1alpha1
kind: Tower
metadata:
name: example-tower
namespace: example-tower
spec:
tower_task_image: awx_task:1.0.0.8
tower_web_image: awx_web:1.0.0.8
tower_memcached_image: memcached:alpine
tower_rabbitmq_image: rabbitmq:3
tower_postgres_pass: awxpass
tower_postgres_image: postgres:9.6
tower_postgres_storage_request: 8Gi

View File

@@ -0,0 +1,19 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: towers.tower.ansible.com
spec:
group: tower.ansible.com
names:
kind: Tower
listKind: TowerList
plural: towers
singular: tower
scope: Namespaced
subresources:
status: {}
version: v1alpha1
versions:
- name: v1alpha1
served: true
storage: true

46
deploy/operator.yaml Normal file
View File

@@ -0,0 +1,46 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: tower-operator
spec:
replicas: 1
selector:
matchLabels:
name: tower-operator
template:
metadata:
labels:
name: tower-operator
spec:
serviceAccountName: tower-operator
containers:
- name: ansible
command:
- /usr/local/bin/ao-logs
- /tmp/ansible-operator/runner
- stdout
image: "{{ operator_image }}"
imagePullPolicy: "{{ pull_policy|default('Always') }}"
volumeMounts:
- mountPath: /tmp/ansible-operator/runner
name: runner
readOnly: true
- name: operator
image: "{{ operator_image }}"
imagePullPolicy: "{{ pull_policy|default('Always') }}"
volumeMounts:
- mountPath: /tmp/ansible-operator/runner
name: runner
env:
# Watch all namespaces (cluster-scoped).
- name: WATCH_NAMESPACE
value: ""
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: OPERATOR_NAME
value: "tower-operator"
volumes:
- name: runner
emptyDir: {}

62
deploy/role.yaml Normal file
View File

@@ -0,0 +1,62 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
creationTimestamp: null
name: tower-operator
rules:
- apiGroups:
- ""
resources:
- pods
- services
- services/finalizers
- endpoints
- persistentvolumeclaims
- events
- configmaps
- secrets
verbs:
- '*'
- apiGroups:
- apps
- extensions
resources:
- deployments
- daemonsets
- replicasets
- statefulsets
verbs:
- '*'
- apiGroups:
- monitoring.coreos.com
resources:
- servicemonitors
verbs:
- get
- create
- apiGroups:
- apps
resourceNames:
- tower-operator
resources:
- deployments/finalizers
verbs:
- update
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- apiGroups:
- apps
resources:
- replicasets
verbs:
- get
- apiGroups:
- tower.ansible.com
resources:
- '*'
verbs:
- '*'

12
deploy/role_binding.yaml Normal file
View File

@@ -0,0 +1,12 @@
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: tower-operator
subjects:
- kind: ServiceAccount
name: tower-operator
namespace: default
roleRef:
kind: ClusterRole
name: tower-operator
apiGroup: rbac.authorization.k8s.io

View File

@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: tower-operator
namespace: default

4
main.yml Normal file
View File

@@ -0,0 +1,4 @@
- hosts: localhost
gather_facts: no
roles:
- tower

View File

@@ -0,0 +1,27 @@
---
- name: Verify cluster resources
hosts: localhost
connection: local
vars:
ansible_python_interpreter: '{{ ansible_playbook_python }}'
tasks:
- name: Get tower Pod data
k8s_facts:
kind: Pod
namespace: example-tower
label_selectors:
- app=tower
register: tower_pods
- name: Verify there are two tower pods
assert:
that: '{{ (tower_pods.resources | length) == 2 }}'
- name: Verify tower functionality
hosts: k8s
vars: []
tasks: []

View File

@@ -0,0 +1,42 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
enabled: False
platforms:
- name: kind-default
groups:
- k8s
image: bsycorp/kind:latest-1.14
privileged: True
override_command: no
exposed_ports:
- 8443/tcp
- 10080/tcp
published_ports:
- 0.0.0.0:${TEST_CLUSTER_PORT:-9443}:8443/tcp
pre_build_image: yes
provisioner:
name: ansible
log: True
lint:
name: ansible-lint
enabled: False
inventory:
group_vars:
all:
operator_namespace: ${TEST_NAMESPACE:-default}
env:
K8S_AUTH_KUBECONFIG: /tmp/molecule/kind-default/kubeconfig
KUBECONFIG: /tmp/molecule/kind-default/kubeconfig
ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles
KIND_PORT: '${TEST_CLUSTER_PORT:-9443}'
scenario:
name: default
verifier:
name: testinfra
lint:
name: flake8

View File

@@ -0,0 +1,10 @@
---
- name: Converge
hosts: localhost
connection: local
vars:
ansible_python_interpreter: '{{ ansible_playbook_python }}'
roles:
- tower
- import_playbook: '{{ playbook_dir }}/asserts.yml'

View File

@@ -0,0 +1,35 @@
---
- name: Prepare
hosts: k8s
gather_facts: no
vars:
kubeconfig: "{{ lookup('env', 'KUBECONFIG') }}"
tasks:
- name: delete the kubeconfig if present
file:
path: '{{ kubeconfig }}'
state: absent
delegate_to: localhost
- name: Fetch the kubeconfig
fetch:
dest: '{{ kubeconfig }}'
flat: yes
src: /root/.kube/config
- name: Change the kubeconfig port to the proper value
replace:
regexp: 8443
replace: "{{ lookup('env', 'KIND_PORT') }}"
path: '{{ kubeconfig }}'
delegate_to: localhost
- name: Wait for the Kubernetes API to become available (this could take a minute)
uri:
url: "http://localhost:10080/kubernetes-ready"
status_code: 200
validate_certs: no
register: result
until: (result.status|default(-1)) == 200
retries: 60
delay: 5

View File

@@ -0,0 +1,43 @@
---
dependency:
name: galaxy
driver:
name: delegated
options:
managed: False
ansible_connection_options: {}
lint:
name: yamllint
enabled: False
platforms:
- name: test-cluster
groups:
- k8s
provisioner:
name: ansible
inventory:
group_vars:
all:
namespace: ${TEST_NAMESPACE:-default}
lint:
name: ansible-lint
enabled: False
env:
ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles
scenario:
name: test-cluster
test_sequence:
- lint
- destroy
- dependency
- syntax
- create
- prepare
- converge
- side_effect
- verify
- destroy
verifier:
name: testinfra
lint:
name: flake8

View File

@@ -0,0 +1,33 @@
---
- name: Converge
hosts: localhost
connection: local
vars:
ansible_python_interpreter: '{{ ansible_playbook_python }}'
deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy"
image_name: tower.ansible.com/tower-operator:testing
custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/tower_v1alpha1_tower_cr.yaml'])) | from_yaml }}"
tasks:
- name: Create the tower.ansible.com/v1alpha1.Tower
k8s:
namespace: '{{ namespace }}'
definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/tower_v1alpha1_tower_cr.yaml'])) }}"
- name: Get the newly created Custom Resource
debug:
msg: "{{ lookup('k8s', group='tower.ansible.com', api_version='v1alpha1', kind='Tower', namespace=namespace, resource_name=custom_resource.metadata.name) }}"
- name: Wait 60s for reconciliation to run
k8s_facts:
api_version: 'v1alpha1'
kind: 'Tower'
namespace: '{{ namespace }}'
name: '{{ custom_resource.metadata.name }}'
register: reconcile_cr
until:
- "'Successful' in (reconcile_cr | json_query('resources[].status.conditions[].reason'))"
delay: 6
retries: 10
- import_playbook: '{{ playbook_dir }}/../default/asserts.yml'

View File

@@ -0,0 +1,55 @@
---
dependency:
name: galaxy
driver:
name: docker
lint:
name: yamllint
enabled: False
platforms:
- name: kind-test-local
groups:
- k8s
image: bsycorp/kind:latest-1.15
privileged: True
override_command: no
exposed_ports:
- 8443/tcp
- 10080/tcp
published_ports:
- 0.0.0.0:${TEST_CLUSTER_PORT:-10443}:8443/tcp
pre_build_image: yes
volumes:
- ${MOLECULE_PROJECT_DIRECTORY}:/build:Z
provisioner:
name: ansible
log: True
lint:
name: ansible-lint
enabled: False
inventory:
group_vars:
all:
operator_namespace: ${TEST_NAMESPACE:-default}
env:
K8S_AUTH_KUBECONFIG: /tmp/molecule/kind-test-local/kubeconfig
KUBECONFIG: /tmp/molecule/kind-test-local/kubeconfig
ANSIBLE_ROLES_PATH: ${MOLECULE_PROJECT_DIRECTORY}/roles
KIND_PORT: '${TEST_CLUSTER_PORT:-10443}'
scenario:
name: test-local
test_sequence:
- lint
- destroy
- dependency
- syntax
- create
- prepare
- converge
- side_effect
- verify
- destroy
verifier:
name: testinfra
lint:
name: flake8

View File

@@ -0,0 +1,126 @@
---
- name: Build Operator in Kubernetes docker container
hosts: k8s
vars:
image_name: tower.ansible.com/tower-operator:testing
tasks:
# using command so we don't need to install any dependencies
- name: Get existing image hash
command: docker images -q {{ image_name }}
register: prev_hash
changed_when: false
- name: Build Operator Image
command: docker build -f /build/build/Dockerfile -t {{ image_name }} /build
register: build_cmd
changed_when: not prev_hash.stdout or (prev_hash.stdout and prev_hash.stdout not in ''.join(build_cmd.stdout_lines[-2:]))
- name: Converge
hosts: localhost
connection: local
vars:
ansible_python_interpreter: '{{ ansible_playbook_python }}'
deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy"
pull_policy: Never
operator_image: tower.ansible.com/tower-operator:testing
custom_resource: "{{ lookup('file', '/'.join([deploy_dir, 'crds/tower_v1alpha1_tower_cr.yaml'])) | from_yaml }}"
tasks:
- block:
- name: Delete the Operator Deployment
k8s:
state: absent
namespace: '{{ operator_namespace }}'
definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}"
register: delete_deployment
when: hostvars[groups.k8s.0].build_cmd.changed
- name: Wait 30s for Operator Deployment to terminate
k8s_facts:
api_version: '{{ definition.apiVersion }}'
kind: '{{ definition.kind }}'
namespace: '{{ operator_namespace }}'
name: '{{ definition.metadata.name }}'
vars:
definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}"
register: deployment
until: not deployment.resources
delay: 3
retries: 10
when: delete_deployment.changed
- name: Create the Operator Deployment
k8s:
namespace: '{{ operator_namespace }}'
definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) }}"
- name: Ensure the Tower custom_resource namespace exists
k8s:
state: present
name: '{{ custom_resource.metadata.namespace }}'
kind: Namespace
api_version: v1
- name: Create the tower.ansible.com/v1alpha1.Tower
k8s:
state: present
namespace: '{{ custom_resource.metadata.namespace }}'
definition: '{{ custom_resource }}'
- name: Wait 5m for reconciliation to run
k8s_facts:
api_version: '{{ custom_resource.apiVersion }}'
kind: '{{ custom_resource.kind }}'
namespace: '{{ custom_resource.metadata.namespace }}'
name: '{{ custom_resource.metadata.name }}'
register: cr
until:
- "'Successful' in (cr | json_query('resources[].status.conditions[].reason'))"
delay: 6
retries: 50
rescue:
- name: debug cr
ignore_errors: yes
failed_when: false
debug:
var: debug_cr
vars:
debug_cr: '{{ lookup("k8s",
kind=custom_resource.kind,
api_version=custom_resource.apiVersion,
namespace=custom_resource.metadata.namespace,
resource_name=custom_resource.metadata.name
)}}'
- name: debug tower deployment
ignore_errors: yes
failed_when: false
debug:
var: deploy
vars:
deploy: '{{ lookup("k8s",
kind="Deployment",
api_version="apps/v1",
namespace=custom_resource.metadata.namespace,
label_selector="app=tower"
)}}'
- name: get operator logs
ignore_errors: yes
failed_when: false
command: kubectl logs deployment/{{ definition.metadata.name }} -n {{ operator_namespace }} -c operator
environment:
KUBECONFIG: '{{ lookup("env", "KUBECONFIG") }}'
vars:
definition: "{{ lookup('template', '/'.join([deploy_dir, 'operator.yaml'])) | from_yaml }}"
register: log
- debug: var=log.stdout_lines
- fail:
msg: "Failed on action: converge"
- import_playbook: '{{ playbook_dir }}/../default/asserts.yml'

View File

@@ -0,0 +1,28 @@
---
- import_playbook: ../default/prepare.yml
- name: Prepare operator resources
hosts: localhost
connection: local
vars:
ansible_python_interpreter: '{{ ansible_playbook_python }}'
deploy_dir: "{{ lookup('env', 'MOLECULE_PROJECT_DIRECTORY') }}/deploy"
tasks:
- name: Create Custom Resource Definition
k8s:
definition: "{{ lookup('file', '/'.join([deploy_dir, 'crds/tower_v1alpha1_tower_crd.yaml'])) }}"
- name: Ensure specified namespace is present
k8s:
api_version: v1
kind: Namespace
name: '{{ operator_namespace }}'
- name: Create RBAC resources
k8s:
definition: "{{ lookup('template', '/'.join([deploy_dir, item])) }}"
namespace: '{{ operator_namespace }}'
with_items:
- role.yaml
- role_binding.yaml
- service_account.yaml

32
roles/tower/README.md Normal file
View File

@@ -0,0 +1,32 @@
Tower
=======
This role builds and maintains an Ansible Tower instance inside of Kubernetes.
Requirements
------------
TODO.
Role Variables
--------------
TODO.
Dependencies
------------
N/A
Example Playbook
----------------
- hosts: localhost
connection: local
roles:
- tower
License
-------
MIT / BSD

View File

@@ -0,0 +1,2 @@
---
# defaults file for tower

19
roles/tower/meta/main.yml Normal file
View File

@@ -0,0 +1,19 @@
galaxy_info:
author: Jeff Geerling
description: Tower role for Tower Operator for Kubernetes.
company: Midwestern Mac, LLC
license: MIT
min_ansible_version: 2.8
galaxy_tags:
- tower
- awx
- ansible
- automation
- ci
- cd
- deployment
dependencies: []

View File

@@ -0,0 +1,8 @@
---
- name: Ensure configured Tower Postgres resources exist in the cluster.
k8s:
definition: "{{ lookup('template', item) | from_yaml }}"
with_items:
- tower_postgres_secret.yaml.j2
- tower_postgres_statefulset.yaml.j2
- tower_postgres_service.yaml.j2

View File

@@ -0,0 +1,8 @@
---
apiVersion: v1
kind: Secret
metadata:
name: '{{ meta.name }}-postgres-pass'
namespace: {{ meta.namespace }}
data:
password: {{ tower_postgres_pass | b64encode }}

View File

@@ -0,0 +1,14 @@
---
apiVersion: v1
kind: Service
metadata:
name: '{{ meta.name }}-postgres'
namespace: '{{ meta.namespace }}'
labels:
app: tower-postgres
spec:
ports:
- port: 5432
clusterIP: None
selector:
app: tower-postgres

View File

@@ -0,0 +1,50 @@
---
apiVersion: v1
kind: StatefulSet
metadata:
name: '{{ meta.name }}-postgres'
namespace: '{{ meta.namespace }}'
labels:
app: tower-postgres
spec:
selector:
matchLabels:
app: tower-postgres
serviceName: '{{ meta.name }}'
replicas: 1
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
app: tower-postgres
spec:
containers:
- image: '{{ mariadb_image }}'
name: postgres
env:
- name: POSTGRES_DB
value: awx
- name: POSTGRES_USER
value: awx
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: '{{ meta.name }}-postgres-pass'
key: password
ports:
- containerPort: 3306
name: postgres
volumeMounts:
- name: postgres
mountPath: /var/lib/postgresql/data
subPath: data
volumeClaimTemplates:
- metadata:
name: postgres
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: '{{ tower_postgres_storage_request }}'

5
watches.yaml Normal file
View File

@@ -0,0 +1,5 @@
---
- version: v1alpha1
group: tower.ansible.com
kind: Tower
playbook: /opt/ansible/main.yml