From f49b79fe40867dae2373bfb9ac7a306dd13fb49d Mon Sep 17 00:00:00 2001
From: Mohammed Naser <mnaser@vexxhost.com>
Date: Sat, 25 Apr 2020 09:06:57 -0400
Subject: [PATCH] Add service CRD

This introduces a CRD for enabling/removing services from Keystone,
it also introduces a basic DevStack plugin framework so that we can
test things from it.  It also adds a framework for the operator to
setup SDK clients.

Change-Id: I183e560c6b32de2ce7adefeb1daa26def675bbe3
---
 .gitignore                                    |  1 +
 bindep.txt                                    |  2 +
 .../crds/identity.openstack.org_services.yaml | 24 ++++++
 chart/templates/clusterrole.yaml              | 20 +++++
 chart/templates/crds.yaml                     |  8 +-
 chart/templates/deployment.yaml               |  7 ++
 chart/test-values.yaml                        |  2 +
 .../samples/identity_v1alpha1_services.yaml   |  8 ++
 devstack/override-defaults                    | 31 ++++++++
 devstack/plugin.sh                            | 51 ++++++++++++
 devstack/settings                             | 20 +++++
 openstack_operator/openstack/__init__.py      |  0
 .../openstack/identity/__init__.py            |  0
 .../openstack/identity/services.py            | 79 +++++++++++++++++++
 openstack_operator/utils.py                   |  9 +++
 playbooks/functional/devstack.yaml            |  3 +
 playbooks/functional/run.yaml                 | 19 +++++
 requirements.txt                              |  1 +
 tox.ini                                       |  2 +
 zuul.d/functional-jobs.yaml                   |  2 +
 20 files changed, 284 insertions(+), 5 deletions(-)
 create mode 100644 bindep.txt
 create mode 100644 chart/crds/identity.openstack.org_services.yaml
 create mode 100644 chart/test-values.yaml
 create mode 100644 config/samples/identity_v1alpha1_services.yaml
 create mode 100644 devstack/override-defaults
 create mode 100755 devstack/plugin.sh
 create mode 100644 devstack/settings
 create mode 100644 openstack_operator/openstack/__init__.py
 create mode 100644 openstack_operator/openstack/identity/__init__.py
 create mode 100644 openstack_operator/openstack/identity/services.py

diff --git a/.gitignore b/.gitignore
index 109fa9c3..ccae98f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,4 @@ doc/build
 __pycache__
 *.egg*
 .stestr
+openrc
diff --git a/bindep.txt b/bindep.txt
new file mode 100644
index 00000000..64b038ba
--- /dev/null
+++ b/bindep.txt
@@ -0,0 +1,2 @@
+gcc [compile]
+libc-dev [compile]
diff --git a/chart/crds/identity.openstack.org_services.yaml b/chart/crds/identity.openstack.org_services.yaml
new file mode 100644
index 00000000..9a4d9d71
--- /dev/null
+++ b/chart/crds/identity.openstack.org_services.yaml
@@ -0,0 +1,24 @@
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: services.identity.openstack.org
+spec:
+  group: identity.openstack.org
+  names:
+    kind: Service
+    listKind: ServiceList
+    plural: services
+    singular: service
+  scope: Cluster
+  version: v1alpha1
+  versions:
+  - name: v1alpha1
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/chart/templates/clusterrole.yaml b/chart/templates/clusterrole.yaml
index 7bea1980..23314dad 100644
--- a/chart/templates/clusterrole.yaml
+++ b/chart/templates/clusterrole.yaml
@@ -109,6 +109,26 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - identity.openstack.org
+  resources:
+  - services
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - identity.openstack.org
+  resources:
+  - services/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - infrastructure.vexxhost.cloud
   resources:
diff --git a/chart/templates/crds.yaml b/chart/templates/crds.yaml
index d9956984..441b6d19 100644
--- a/chart/templates/crds.yaml
+++ b/chart/templates/crds.yaml
@@ -8,8 +8,6 @@
     {{- end }}
 {{- end -}}
 
-{{- if .Values.crd.dns }}
-    {{- range $path, $bytes := .Files.Glob "crds/dns.openstack.org*.yaml" }}
-    {{ $.Files.Get $path }}
-    {{- end }}
-{{- end -}}
\ No newline at end of file
+{{- range $path, $bytes := .Files.Glob "crds/*.openstack.org_*.yaml" }}
+{{ $.Files.Get $path }}
+{{- end }}
diff --git a/chart/templates/deployment.yaml b/chart/templates/deployment.yaml
index 80e7f188..1bd8bc2e 100644
--- a/chart/templates/deployment.yaml
+++ b/chart/templates/deployment.yaml
@@ -21,6 +21,11 @@ spec:
       - name: operator
         image: vexxhost/openstack-operator:latest
         command: ["/usr/local/bin/kopf"]
+{{- with .Values.secretName }}
+        envFrom:
+        - secretRef:
+            name: {{ . }}
+{{- end }}
         args:
         - run
         - -m
@@ -28,6 +33,8 @@ spec:
         - -m
         - openstack_operator.memcached
         - -m
+        - openstack_operator.openstack.identity.services
+        - -m
         - openstack_operator.rabbitmq
         resources:
           limits:
diff --git a/chart/test-values.yaml b/chart/test-values.yaml
new file mode 100644
index 00000000..6916598d
--- /dev/null
+++ b/chart/test-values.yaml
@@ -0,0 +1,2 @@
+---
+secretName: devstack
diff --git a/config/samples/identity_v1alpha1_services.yaml b/config/samples/identity_v1alpha1_services.yaml
new file mode 100644
index 00000000..3efddd40
--- /dev/null
+++ b/config/samples/identity_v1alpha1_services.yaml
@@ -0,0 +1,8 @@
+---
+apiVersion: identity.openstack.org/v1alpha1
+kind: Service
+metadata:
+  name: heat
+spec:
+  type: orchestration
+  description: Heat Orchestration Service
diff --git a/devstack/override-defaults b/devstack/override-defaults
new file mode 100644
index 00000000..a7ef98ef
--- /dev/null
+++ b/devstack/override-defaults
@@ -0,0 +1,31 @@
+#!/bin/bash
+#
+# Copyright 2020 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+# Gets or creates service
+# Usage: get_or_create_service <name> <type> <description>
+function get_or_create_service {
+	cat <<EOF | kubectl apply -f-
+---
+apiVersion: identity.openstack.org/v1alpha1
+kind: Service
+metadata:
+  name: ${1//_/-}
+spec:
+  type: $2
+  description: $3
+EOF
+}
+export -f get_or_create_service
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
new file mode 100755
index 00000000..6d1a4987
--- /dev/null
+++ b/devstack/plugin.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# Copyright 2020 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+function copy_minikube_config {
+	mkdir ~stack/.kube
+
+	sudo cp ~zuul/.kube/config ~stack/.kube/config
+	sudo cp ~zuul/.minikube/ca.crt ~stack/.kube/ca.crt
+	sudo cp ~zuul/.minikube/profiles/minikube/client.crt ~stack/.kube/client.crt
+	sudo cp ~zuul/.minikube/profiles/minikube/client.key ~stack/.kube/client.key
+	sudo chown -Rv stack:stack ~stack/.kube
+
+	sed -i s%/home/zuul/.minikube/profiles/minikube%/opt/stack/.kube% ~/.kube/config
+	sed -i s%/home/zuul/.minikube/ca.crt%/opt/stack/.kube/ca.crt% ~/.kube/config
+
+	kubectl cluster-info
+}
+
+if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then
+	copy_minikube_config
+
+elif [[ "$1" == "stack" && "$2" == "install" ]]; then
+	:
+
+elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
+	:
+
+elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
+	:
+fi
+
+if [[ "$1" == "unstack" ]]; then
+	:
+fi
+
+if [[ "$1" == "clean" ]]; then
+	:
+fi
diff --git a/devstack/settings b/devstack/settings
new file mode 100644
index 00000000..f1ee5635
--- /dev/null
+++ b/devstack/settings
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# Copyright 2020 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+define_plugin openstack-operator
+
+disable_service etcd3
+disable_service rabbit
diff --git a/openstack_operator/openstack/__init__.py b/openstack_operator/openstack/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/openstack_operator/openstack/identity/__init__.py b/openstack_operator/openstack/identity/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/openstack_operator/openstack/identity/services.py b/openstack_operator/openstack/identity/services.py
new file mode 100644
index 00000000..a1f4a741
--- /dev/null
+++ b/openstack_operator/openstack/identity/services.py
@@ -0,0 +1,79 @@
+# Copyright 2020 VEXXHOST, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Services Operator
+
+This operator helps manage the creation and removal of services inside
+Keystone using custom resources.
+"""
+
+import kopf
+
+from openstack_operator import utils
+
+
+def _get_service(conn, name, service_type):
+    """Get a service from Keystone
+
+    This method will retrieve the service from Keystone, raise an error if it
+    found more than one or return None if it couldn't find it
+    """
+
+    services = conn.search_services(name_or_id=name,
+                                    filters={"type": service_type})
+
+    if len(services) > 1:
+        raise RuntimeError("Found multiple services with name and type")
+    if len(services) == 0:
+        return None
+    return services[0]
+
+
+@kopf.on.resume('identity.openstack.org', 'v1alpha1', 'services')
+@kopf.on.create('identity.openstack.org', 'v1alpha1', 'services')
+def create_or_resume(name, spec, **_):
+    """Create or resume controller
+
+    This function runs when a new resource is created or when the controller
+    is first started.  It creates or updates the appropriate service.
+    """
+
+    conn = utils.get_openstack_connection()
+    service = _get_service(conn, name, spec["type"])
+
+    if service:
+        service = conn.update_service(service.id, name=name,
+                                      type=spec["type"],
+                                      description=spec["description"])
+        return
+
+    service = conn.create_service(name=name, type=spec["type"],
+                                  description=spec["description"])
+
+
+@kopf.on.delete('identity.openstack.org', 'v1alpha1', 'services')
+def delete(name, spec, **_):
+    """Delete a service
+
+    This function runs when the servce CR is deleted and removes the record
+    from Keystone.
+    """
+
+    conn = utils.get_openstack_connection()
+    service = _get_service(conn, name, spec["type"])
+
+    if not service:
+        return
+
+    conn.delete_service(service)
diff --git a/openstack_operator/utils.py b/openstack_operator/utils.py
index 6aaee014..e7ae264f 100644
--- a/openstack_operator/utils.py
+++ b/openstack_operator/utils.py
@@ -23,7 +23,9 @@ import operator
 import os
 
 import jinja2
+import openstack
 import kopf
+from pbr import version
 import pykube
 import yaml
 
@@ -31,6 +33,7 @@ from openstack_operator import objects
 
 
 DIR_PATH = os.path.dirname(os.path.realpath(__file__))
+VERSION = version.VersionInfo('openstack_operator').version_string()
 
 
 def to_yaml(value):
@@ -147,3 +150,9 @@ def get_ready_pod_ips(namespace, selector):
     servers = sorted([p.obj["status"]["podIP"] for p in ready_pods])
 
     return servers
+
+
+def get_openstack_connection():
+    """Get an instance of OpenStack SDK."""
+    return openstack.connect(cloud="envvars", app_name='openstack-operator',
+                             app_version=VERSION)
diff --git a/playbooks/functional/devstack.yaml b/playbooks/functional/devstack.yaml
index 4205f206..e7c2b5d0 100644
--- a/playbooks/functional/devstack.yaml
+++ b/playbooks/functional/devstack.yaml
@@ -55,6 +55,9 @@
         name: write-devstack-local-conf
       vars:
         devstack_localrc: "{{ _devstack_localrc | combine(_devstack_localrc_extra) }}"
+    - name: Copy Zuul repo into devstack working directory
+      become: true
+      command: rsync -av src/opendev.org/vexxhost/openstack-operator /opt/stack
 
 # Changes that run through devstack-tempest are likely to have an impact on
 # the devstack part of the job, so we keep devstack in the main play to
diff --git a/playbooks/functional/run.yaml b/playbooks/functional/run.yaml
index 4abd9a18..96820e42 100755
--- a/playbooks/functional/run.yaml
+++ b/playbooks/functional/run.yaml
@@ -15,11 +15,30 @@
 # limitations under the License.
 
 - hosts: all
+  pre_tasks:
+    - name: Create secret for DevStack credentials
+      shell: |
+        cat <<EOF | kubectl apply -f-
+        apiVersion: v1
+        kind: Secret
+        metadata:
+          name: devstack
+        stringData:
+          OS_INSECURE: "true"
+          OS_AUTH_URL: http://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity
+          OS_AUTH_TYPE: password
+          OS_PROJECT_DOMAIN_ID: default
+          OS_PROJECT_NAME: admin
+          OS_USER_DOMAIN_ID: default
+          OS_USERNAME: admin
+          OS_PASSWORD: secretadmin
+        EOF
   roles:
     - role: helm-template
       vars:
         helm_release_name: openstack-operator
         helm_chart: ./chart
+        helm_values_file: ./chart/test-values.yaml
   tasks:
     # TODO(mnaser): Generate all manifests and ensure git is not dirty
     - include_tasks: tests/memcached.yaml
diff --git a/requirements.txt b/requirements.txt
index 48fc86de..7717aee0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 kopf
 Jinja2
+openstacksdk
diff --git a/tox.ini b/tox.ini
index 471937a5..aa3bcfbf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,6 +4,8 @@ envlist = py37
 
 [testenv]
 usedevelop = True
+passenv =
+  OS_*
 deps =
   -rtest-requirements.txt
   -rrequirements.txt
diff --git a/zuul.d/functional-jobs.yaml b/zuul.d/functional-jobs.yaml
index 88e12675..a3c1ca33 100644
--- a/zuul.d/functional-jobs.yaml
+++ b/zuul.d/functional-jobs.yaml
@@ -10,6 +10,8 @@
       devstack_services:
         etcd3: false
         rabbit: false
+      devstack_plugins:
+        openstack-operator: https://opendev.org/vexxhost/openstack-operator
       docker_use_buildset_registry: true
       minikube_dns_resolvers: [1.1.1.1, 8.8.8.8]