diff --git a/api/bases/watcher.openstack.org_watcherapis.yaml b/api/bases/watcher.openstack.org_watcherapis.yaml index a7e0f63..6a1ae8e 100644 --- a/api/bases/watcher.openstack.org_watcherapis.yaml +++ b/api/bases/watcher.openstack.org_watcherapis.yaml @@ -49,6 +49,11 @@ spec: MariaDB instance name Required to use the mariadb-operator instance to create the DB and user type: string + memcachedInstance: + default: memcached + description: MemcachedInstance is the name of the Memcached CR that + all watcher service will use. + type: string passwordSelectors: default: service: WatcherPassword diff --git a/api/bases/watcher.openstack.org_watchers.yaml b/api/bases/watcher.openstack.org_watchers.yaml index 28bae46..f11f730 100644 --- a/api/bases/watcher.openstack.org_watchers.yaml +++ b/api/bases/watcher.openstack.org_watchers.yaml @@ -49,6 +49,11 @@ spec: MariaDB instance name Required to use the mariadb-operator instance to create the DB and user type: string + memcachedInstance: + default: memcached + description: MemcachedInstance is the name of the Memcached CR that + all watcher service will use. + type: string passwordSelectors: default: service: WatcherPassword diff --git a/api/v1beta1/common_types.go b/api/v1beta1/common_types.go index 611cbc6..d95da78 100644 --- a/api/v1beta1/common_types.go +++ b/api/v1beta1/common_types.go @@ -38,6 +38,11 @@ type WatcherCommon struct { // +kubebuilder:default=watcher // DatabaseAccount - MariaDBAccount CR name used for watcher DB, defaults to watcher DatabaseAccount string `json:"databaseAccount"` + + // +kubebuilder:validation:Optional + // +kubebuilder:default=memcached + // MemcachedInstance is the name of the Memcached CR that all watcher service will use. + MemcachedInstance string `json:"memcachedInstance"` } // WatcherTemplate defines the fields used in the top level CR diff --git a/api/v1beta1/watcherapi_types.go b/api/v1beta1/watcherapi_types.go index 90cacf2..de1b64f 100644 --- a/api/v1beta1/watcherapi_types.go +++ b/api/v1beta1/watcherapi_types.go @@ -27,6 +27,7 @@ type WatcherAPISpec struct { // Important: Run "make" to regenerate code after modifying this file WatcherCommon `json:",inline"` + // +kubebuilder:validation:Required // Secret containing all passwords / keys needed Secret string `json:"secret"` diff --git a/config/crd/bases/watcher.openstack.org_watcherapis.yaml b/config/crd/bases/watcher.openstack.org_watcherapis.yaml index a7e0f63..6a1ae8e 100644 --- a/config/crd/bases/watcher.openstack.org_watcherapis.yaml +++ b/config/crd/bases/watcher.openstack.org_watcherapis.yaml @@ -49,6 +49,11 @@ spec: MariaDB instance name Required to use the mariadb-operator instance to create the DB and user type: string + memcachedInstance: + default: memcached + description: MemcachedInstance is the name of the Memcached CR that + all watcher service will use. + type: string passwordSelectors: default: service: WatcherPassword diff --git a/config/crd/bases/watcher.openstack.org_watchers.yaml b/config/crd/bases/watcher.openstack.org_watchers.yaml index 28bae46..f11f730 100644 --- a/config/crd/bases/watcher.openstack.org_watchers.yaml +++ b/config/crd/bases/watcher.openstack.org_watchers.yaml @@ -49,6 +49,11 @@ spec: MariaDB instance name Required to use the mariadb-operator instance to create the DB and user type: string + memcachedInstance: + default: memcached + description: MemcachedInstance is the name of the Memcached CR that + all watcher service will use. + type: string passwordSelectors: default: service: WatcherPassword diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3a073f5..5272bb1 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -28,6 +28,14 @@ rules: - patch - update - watch +- apiGroups: + - keystone.openstack.org + resources: + - keystoneapis + verbs: + - get + - list + - watch - apiGroups: - keystone.openstack.org resources: @@ -82,6 +90,23 @@ rules: - patch - update - watch +- apiGroups: + - memcached.openstack.org + resources: + - memcacheds + verbs: + - get + - list + - patch + - update + - watch +- apiGroups: + - memcached.openstack.org + resources: + - memcacheds/finalizers + verbs: + - patch + - update - apiGroups: - rabbitmq.openstack.org resources: diff --git a/controllers/watcher_common.go b/controllers/watcher_common.go index 6331edc..51681eb 100644 --- a/controllers/watcher_common.go +++ b/controllers/watcher_common.go @@ -15,7 +15,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/env" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + "github.com/openstack-k8s-operators/lib-common/modules/common/secret" "github.com/openstack-k8s-operators/lib-common/modules/common/util" ) @@ -188,3 +192,78 @@ func ensureSecret( return hash, ctrl.Result{}, *secret, nil } + +func GenerateConfigsGeneric( + ctx context.Context, helper *helper.Helper, + instance client.Object, + envVars *map[string]env.Setter, + templateParameters map[string]interface{}, + customData map[string]string, + cmLabels map[string]string, + scripts bool, +) error { + + cms := []util.Template{ + // Templates where the watcher config is stored + { + Name: fmt.Sprintf("%s-config-data", instance.GetName()), + Namespace: instance.GetNamespace(), + Type: util.TemplateTypeConfig, + InstanceType: instance.GetObjectKind().GroupVersionKind().Kind, + ConfigOptions: templateParameters, + CustomData: customData, + Labels: cmLabels, + }, + } + if scripts { + cms = append(cms, util.Template{ + Name: fmt.Sprintf("%s-scripts", instance.GetName()), + Namespace: instance.GetNamespace(), + Type: util.TemplateTypeScripts, + InstanceType: instance.GetObjectKind().GroupVersionKind().Kind, + ConfigOptions: templateParameters, + Labels: cmLabels, + }) + } + return secret.EnsureSecrets(ctx, helper, instance, cms, envVars) +} + +// ensureMemcached - gets the Memcached instance used for watcher services cache backend +func ensureMemcached( + ctx context.Context, + helper *helper.Helper, + namespaceName string, + memcachedName string, + conditionUpdater conditionUpdater, +) (*memcachedv1.Memcached, error) { + memcached, err := memcachedv1.GetMemcachedByName(ctx, helper, memcachedName, namespaceName) + if err != nil { + if k8s_errors.IsNotFound(err) { + conditionUpdater.Set(condition.FalseCondition( + condition.MemcachedReadyCondition, + condition.RequestedReason, + condition.SeverityInfo, + condition.MemcachedReadyWaitingMessage)) + return nil, fmt.Errorf("memcached %s not found", memcachedName) + } + conditionUpdater.Set(condition.FalseCondition( + condition.MemcachedReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.MemcachedReadyErrorMessage, + err.Error())) + return nil, err + } + + if !memcached.IsReady() { + conditionUpdater.Set(condition.FalseCondition( + condition.MemcachedReadyCondition, + condition.RequestedReason, + condition.SeverityInfo, + condition.MemcachedReadyWaitingMessage)) + return nil, fmt.Errorf("memcached %s is not ready", memcachedName) + } + conditionUpdater.MarkTrue(condition.MemcachedReadyCondition, condition.MemcachedReadyMessage) + + return memcached, err +} diff --git a/controllers/watcherapi_controller.go b/controllers/watcherapi_controller.go index 19fee10..e3fd48d 100644 --- a/controllers/watcherapi_controller.go +++ b/controllers/watcherapi_controller.go @@ -30,9 +30,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/go-logr/logr" + memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" + keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" + "github.com/openstack-k8s-operators/lib-common/modules/common/endpoint" "github.com/openstack-k8s-operators/lib-common/modules/common/env" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + "github.com/openstack-k8s-operators/lib-common/modules/common/labels" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" watcherv1beta1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" @@ -59,6 +64,11 @@ func (r *WatcherAPIReconciler) GetLogger(ctx context.Context) logr.Logger { //+kubebuilder:rbac:groups=watcher.openstack.org,resources=watcherapis/status,verbs=get;update;patch //+kubebuilder:rbac:groups=watcher.openstack.org,resources=watcherapis/finalizers,verbs=update //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete; +//+kubebuilder:rbac:groups=keystone.openstack.org,resources=keystoneapis,verbs=get;list;watch; +//+kubebuilder:rbac:groups=keystone.openstack.org,resources=keystoneservices,verbs=get;list;watch;create;update;patch;delete; +//+kubebuilder:rbac:groups=keystone.openstack.org,resources=keystoneendpoints,verbs=get;list;watch;create;update;patch;delete; +//+kubebuilder:rbac:groups=memcached.openstack.org,resources=memcacheds,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=memcached.openstack.org,resources=memcacheds/finalizers,verbs=update;patch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -138,6 +148,7 @@ func (r *WatcherAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request) types.NamespacedName{Namespace: instance.Namespace, Name: instance.Spec.Secret}, []string{ instance.Spec.PasswordSelectors.Service, + TransportURLSelector, }, helper.GetClient(), &instance.Status.Conditions, @@ -163,7 +174,26 @@ func (r *WatcherAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request) // all our input checks out so report InputReady instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage) - err = r.generateServiceConfigs(ctx, instance, secret, db, helper, &configVars) + memcached, err := ensureMemcached(ctx, helper, instance.Namespace, instance.Spec.MemcachedInstance, &instance.Status.Conditions) + + if err != nil { + return ctrl.Result{}, err + } + // Add finalizer to Memcached to prevent it from being deleted now that we're using it + if controllerutil.AddFinalizer(memcached, helper.GetFinalizer()) { + err := helper.GetClient().Update(ctx, memcached) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.MemcachedReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.MemcachedReadyErrorMessage, + err.Error())) + return ctrl.Result{}, err + } + } + + err = r.generateServiceConfigs(ctx, instance, secret, db, memcached, helper, &configVars) if err != nil { return ctrl.Result{}, err } @@ -181,25 +211,65 @@ func (r *WatcherAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // generateServiceConfigs - create Secret which holds the service configuration -// NOTE - jgilaber this function is WIP, currently implements a fraction of its -// functionality and will be expanded of further iteration to actually generate -// the service configs func (r *WatcherAPIReconciler) generateServiceConfigs( ctx context.Context, instance *watcherv1beta1.WatcherAPI, secret corev1.Secret, db *mariadbv1.Database, + memcachedInstance *memcachedv1.Memcached, helper *helper.Helper, envVars *map[string]env.Setter, ) error { Log := r.GetLogger(ctx) Log.Info("generateServiceConfigs - reconciling") - // replace by actual usage in future iterations - _ = db - _ = helper - _ = instance - _ = secret - _ = envVars + labels := labels.GetLabels(instance, labels.GetGroupLabel(watcher.ServiceName), map[string]string{}) + // jgilaber this might be wrong? we should probably get keystonapi in the + // watcher controller and set the url in the spec eventually? + keystoneAPI, err := keystonev1.GetKeystoneAPI(ctx, helper, instance.Namespace, map[string]string{}) + // KeystoneAPI not available we should not aggregate the error and continue + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.ServiceConfigReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.ServiceConfigReadyErrorMessage, + "keystoneAPI not found")) + return err + } + keystoneInternalURL, err := keystoneAPI.GetEndpoint(endpoint.EndpointInternal) + if err != nil { + return err + } + // customData hold any customization for the service. + // NOTE jgilaber making an empty map for now, we'll probably want to + // implement CustomServiceConfig later + customData := map[string]string{} + + databaseAccount := db.GetAccount() + databaseSecret := db.GetSecret() + templateParameters := map[string]interface{}{ + "DatabaseConnection": fmt.Sprintf("mysql+pymysql://%s:%s@%s/%s?charset=utf8", + databaseAccount.Spec.UserName, + string(databaseSecret.Data[mariadbv1.DatabasePasswordSelector]), + db.GetDatabaseHostname(), + watcher.DatabaseName, + ), + "KeystoneAuthURL": keystoneInternalURL, + "ServicePassword": string(secret.Data[instance.Spec.PasswordSelectors.Service]), + "ServiceUser": instance.Spec.ServiceUser, + "TransportURL": string(secret.Data[TransportURLSelector]), + "MemcachedServers": memcachedInstance.GetMemcachedServerListString(), + } - return nil + // create httpd vhost template parameters + httpdVhostConfig := map[string]interface{}{} + for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} { + endptConfig := map[string]interface{}{} + endptConfig["ServerName"] = fmt.Sprintf("%s-%s.%s.svc", watcher.ServiceName, endpt.String(), instance.Namespace) + endptConfig["TLS"] = false // default TLS to false, and set it below when implemented + httpdVhostConfig[endpt.String()] = endptConfig + } + templateParameters["VHosts"] = httpdVhostConfig + + return GenerateConfigsGeneric(ctx, helper, instance, envVars, templateParameters, customData, labels, false) } func (r *WatcherAPIReconciler) reconcileDelete(ctx context.Context, instance *watcherv1beta1.WatcherAPI, helper *helper.Helper) (ctrl.Result, error) { @@ -221,6 +291,7 @@ func (r *WatcherAPIReconciler) initStatus(instance *watcherv1beta1.WatcherAPI) e condition.UnknownCondition(condition.ReadyCondition, condition.InitReason, condition.ReadyInitMessage), condition.UnknownCondition(condition.InputReadyCondition, condition.InitReason, condition.InputReadyInitMessage), condition.UnknownCondition(condition.ServiceConfigReadyCondition, condition.InitReason, condition.ServiceConfigReadyMessage), + condition.UnknownCondition(condition.MemcachedReadyCondition, condition.InitReason, condition.MemcachedReadyInitMessage), ) instance.Status.Conditions.Init(&cl) diff --git a/go.mod b/go.mod index 4d93654..a350da6 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( k8s.io/api v0.29.10 k8s.io/apimachinery v0.29.10 k8s.io/client-go v0.29.10 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/controller-runtime v0.17.6 ) @@ -75,7 +76,6 @@ require ( k8s.io/component-base v0.29.10 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/main.go b/main.go index 084ea8c..ad482cf 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" @@ -58,6 +59,7 @@ func init() { utilruntime.Must(mariadbv1.AddToScheme(scheme)) utilruntime.Must(rabbitmqv1.AddToScheme(scheme)) utilruntime.Must(keystonev1.AddToScheme(scheme)) + utilruntime.Must(memcachedv1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } diff --git a/templates/watcher/config/00-default.conf b/templates/watcher/config/00-default.conf new file mode 100644 index 0000000..8bf3025 --- /dev/null +++ b/templates/watcher/config/00-default.conf @@ -0,0 +1,48 @@ +[DEFAULT] +state_path = /var/lib/watcher +transport_url = {{ .TransportURL }} +control_exchange = watcher +debug = True + +[database] +connection = {{ .DatabaseConnection }} + +[oslo_policy] +policy_file = /etc/watcher/policy.yaml.sample + +[oslo_messaging_notifications] +driver = messagingv2 + +[keystone_authtoken] +memcached_servers = {{ .MemcachedServers }} +# TODO jgilaber implement handling this option when we add tls support +# cafile = /var/lib/ca-bundle.pem +project_domain_name = Default +project_name = service +user_domain_name = Default +password = {{ .ServicePassword }} +username = {{ .ServiceUser }} +auth_url = {{ .KeystoneAuthURL }} +interface = internal +auth_type = password + +[watcher_clients_auth] +# TODO jgilaber implement handling this option when we add tls support +# cafile = /var/lib/ca-bundle.pem +project_domain_name = Default +project_name = service +user_domain_name = Default +password = {{ .ServicePassword }} +username = {{ .ServiceUser }} +auth_url = {{ .KeystoneAuthURL }} +interface = internal +auth_type = password + +[oslo_concurrency] +lock_path = /var/lib/watcher/tmp + +[watcher_datasources] +datasources = ceilometer + +[cache] +memcached_servers = {{ .MemcachedServers }} diff --git a/templates/watcher/config/10-watcher-wsgi-main.conf b/templates/watcher/config/10-watcher-wsgi-main.conf new file mode 100644 index 0000000..044929f --- /dev/null +++ b/templates/watcher/config/10-watcher-wsgi-main.conf @@ -0,0 +1,39 @@ +{{ if (index . "VHosts") }} +{{ range $endpt, $vhost := .VHosts }} +# {{ $endpt }} vhost {{ $vhost.ServerName }} configuration + + ServerName {{ $vhost.ServerName }} + + ## Vhost docroot + DocumentRoot "/var/www/cgi-bin/watcher" + + ## Directories, there should at least be a declaration for /var/www/cgi-bin/watcher + + + Options -Indexes +FollowSymLinks +MultiViews + AllowOverride None + Require all granted + + + ## Logging + ErrorLog "/var/log/watcher/error.log" + ServerSignature Off + CustomLog "/var/log/watcher/access.log" combined env=!forwarded + +{{- if $vhost.TLS }} + SetEnvIf X-Forwarded-Proto https HTTPS=1 + + ## SSL directives + SSLEngine on + SSLCertificateFile "{{ $vhost.SSLCertificateFile }}" + SSLCertificateKeyFile "{{ $vhost.SSLCertificateKeyFile }}" +{{- end }} + + ## WSGI configuration + WSGIApplicationGroup %{GLOBAL} + WSGIDaemonProcess {{ $endpt }} display-name={{ $endpt }} group=watcher processes=8 threads=1 user=watcher + WSGIProcessGroup {{ $endpt }} + WSGIScriptAlias / "/var/www/cgi-bin/watcher/main" + +{{ end }} +{{ end }} diff --git a/templates/watcher/config/httpd.conf b/templates/watcher/config/httpd.conf new file mode 100644 index 0000000..31f97aa --- /dev/null +++ b/templates/watcher/config/httpd.conf @@ -0,0 +1,45 @@ +ServerTokens Prod +ServerSignature Off +TraceEnable Off + +ServerName "watcher.openstack.svc" +ServerRoot "/etc/httpd" + +PidFile run/httpd.pid +Timeout 90 +KeepAlive On +MaxKeepAliveRequests 100 +KeepAliveTimeout 15 +LimitRequestFieldSize 8190 +LimitRequestFields 100 + +User apache +Group apache +Listen 9311 + +AccessFileName .htaccess + + Require all denied + + + + Options FollowSymLinks + AllowOverride None + + + + HostnameLookups Off + LogLevel debug + EnableSendfile On + + Include "/etc/httpd/conf.modules.d/*.conf" + + LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%a %l %u %t \"%r\" %>s %b" common + LogFormat "%{Referer}i -> %U" referer + LogFormat "%{User-agent}i" agent + LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-agent}i\"" forwarded + + CustomLog "/var/log/watcher/access.log" combined env=!forwarded + ErrorLog "/var/log/watcher/error.log" + IncludeOptional "/etc/httpd/conf.d/*.conf" diff --git a/templates/watcher/config/kolla_extend_start b/templates/watcher/config/kolla_extend_start new file mode 100755 index 0000000..89d740b --- /dev/null +++ b/templates/watcher/config/kolla_extend_start @@ -0,0 +1,10 @@ +#!/bin/bash + +# Bootstrap and exit if KOLLA_BOOTSTRAP variable is set. This catches all cases +# of the KOLLA_BOOTSTRAP variable being set, including empty. +if [[ "${!KOLLA_BOOTSTRAP[@]}" ]]; then + barbican-db-manage upgrade + exit 0 + fi + +. /usr/local/bin/kolla_httpd_setup diff --git a/templates/watcher/config/main b/templates/watcher/config/main new file mode 100755 index 0000000..3b2e937 --- /dev/null +++ b/templates/watcher/config/main @@ -0,0 +1,52 @@ +#!/usr/bin/python3 +#PBR Generated from 'wsgi_scripts' + +import threading + +from watcher.api.wsgi import initialize_wsgi_app + +if __name__ == "__main__": + import argparse + import socket + import sys + import wsgiref.simple_server as wss + + parser = argparse.ArgumentParser( + description=initialize_wsgi_app.__doc__, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + usage='%(prog)s [-h] [--port PORT] [--host IP] -- [passed options]') + parser.add_argument('--port', '-p', type=int, default=8000, + help='TCP port to listen on') + parser.add_argument('--host', '-b', default='', + help='IP to bind the server to') + parser.add_argument('args', + nargs=argparse.REMAINDER, + metavar='-- [passed options]', + help="'--' is the separator of the arguments used " + "to start the WSGI server and the arguments passed " + "to the WSGI application.") + args = parser.parse_args() + if args.args: + if args.args[0] == '--': + args.args.pop(0) + else: + parser.error("unrecognized arguments: %s" % ' '.join(args.args)) + sys.argv[1:] = args.args + server = wss.make_server(args.host, args.port, initialize_wsgi_app()) + + print("*" * 80) + print("STARTING test server watcher.api.wsgi.initialize_wsgi_app") + url = "http://%s:%d/" % (server.server_name, server.server_port) + print("Available at %s" % url) + print("DANGER! For testing only, do not use in production") + print("*" * 80) + sys.stdout.flush() + + server.serve_forever() +else: + application = None + app_lock = threading.Lock() + + with app_lock: + if application is None: + application = initialize_wsgi_app() diff --git a/templates/watcher/config/mime.conf b/templates/watcher/config/mime.conf new file mode 100644 index 0000000..ac91a1c --- /dev/null +++ b/templates/watcher/config/mime.conf @@ -0,0 +1,38 @@ +TypesConfig /etc/mime.types + +AddType application/x-compress .Z +AddType application/x-gzip .gz .tgz +AddType application/x-bzip2 .bz2 + +AddLanguage ca .ca +AddLanguage cs .cz .cs +AddLanguage da .dk +AddLanguage de .de +AddLanguage el .el +AddLanguage en .en +AddLanguage eo .eo +AddLanguage es .es +AddLanguage et .et +AddLanguage fr .fr +AddLanguage he .he +AddLanguage hr .hr +AddLanguage it .it +AddLanguage ja .ja +AddLanguage ko .ko +AddLanguage ltz .ltz +AddLanguage nl .nl +AddLanguage nn .nn +AddLanguage no .no +AddLanguage pl .po +AddLanguage pt .pt +AddLanguage pt-BR .pt-br +AddLanguage ru .ru +AddLanguage sv .sv +AddLanguage zh-CN .zh-cn +AddLanguage zh-TW .zh-tw + +AddHandler type-map var + +AddOutputFilter INCLUDES .shtml + +AddType text/html .shtml diff --git a/templates/watcher/config/watcher-api-config.json b/templates/watcher/config/watcher-api-config.json new file mode 100644 index 0000000..049d9e8 --- /dev/null +++ b/templates/watcher/config/watcher-api-config.json @@ -0,0 +1,53 @@ +{ + "command": "/usr/sbin/httpd -DFOREGROUND", + "config_files": [ + { + "source": "/var/lib/config-data/default/00-default.conf", + "dest": "/etc/watcher/watcher.conf.d/00-default.conf", + "owner": "watcher", + "perm": "0600" + }, + { + "source": "/var/lib/config-data/default/10-watcher_wsgi_main.conf", + "dest": "/etc/httpd/conf.d/10-watcher_wsgi_main.conf", + "owner": "root", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/httpd.conf", + "dest": "/etc/httpd/conf/httpd.conf", + "owner": "root", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/main", + "dest": "/var/www/cgi-bin/watcher/main", + "owner": "watcher", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/mime.conf", + "dest": "/etc/httpd/conf.modules.d/mime.conf", + "owner": "root", + "perm": "0640", + "optional": true + }, + { + "source": "/var/lib/config-data/default/kolla_extend_start", + "dest": "/usr/local/bin/kolla_extend_start", + "owner": "root", + "perm": "0755", + "optional": true + } + ], + "permissions": [ + { + "path": "/var/log/watcher", + "owner": "watcher:watcher", + "recurse": true + } + ] +} diff --git a/templates/watcherapi b/templates/watcherapi new file mode 120000 index 0000000..cba20eb --- /dev/null +++ b/templates/watcherapi @@ -0,0 +1 @@ +watcher/ \ No newline at end of file diff --git a/tests/functional/base_test.go b/tests/functional/base_test.go index 69f4a92..5f09642 100644 --- a/tests/functional/base_test.go +++ b/tests/functional/base_test.go @@ -38,8 +38,9 @@ func GetDefaultWatcherSpec() map[string]interface{} { func GetDefaultWatcherAPISpec() map[string]interface{} { return map[string]interface{}{ - "databaseInstance": "openstack", - "secret": SecretName, + "databaseInstance": "openstack", + "secret": SecretName, + "memcachedInstance": "memcached", } } diff --git a/tests/functional/sample_test.go b/tests/functional/sample_test.go index 4efea79..91d6c57 100644 --- a/tests/functional/sample_test.go +++ b/tests/functional/sample_test.go @@ -72,7 +72,7 @@ var _ = Describe("Samples", func() { When("watcher_v1beta1_watcherapi.yaml sample is applied", func() { It("WatcherAPI is created", func() { - name := CreateWatcherAPIFromSample("watcher_v1beta1_watcherapi.yaml", watcherTest.Instance) + name := CreateWatcherAPIFromSample("watcher_v1beta1_watcherapi.yaml", watcherTest.WatcherAPI) GetWatcherAPI(name) }) }) diff --git a/tests/functional/suite_test.go b/tests/functional/suite_test.go index 84a41d2..63d0e88 100644 --- a/tests/functional/suite_test.go +++ b/tests/functional/suite_test.go @@ -28,6 +28,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" test "github.com/openstack-k8s-operators/lib-common/modules/test" @@ -67,6 +68,8 @@ const ( SecretName = "test-osp-secret" interval = time.Millisecond * 200 + + MemcachedInstance = "memcached" ) func TestAPIs(t *testing.T) { @@ -123,12 +126,16 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = mariadbv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = keystonev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) err = corev1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) err = rabbitmqv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) err = keystonev1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = memcachedv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) logger = ctrl.Log.WithName("---Test---") //+kubebuilder:scaffold:scheme diff --git a/tests/functional/watcher_test_data.go b/tests/functional/watcher_test_data.go index 18a5666..d04950b 100644 --- a/tests/functional/watcher_test_data.go +++ b/tests/functional/watcher_test_data.go @@ -40,6 +40,8 @@ type WatcherTestData struct { InternalTopLevelSecretName types.NamespacedName WatcherTransportURL types.NamespacedName KeystoneServiceName types.NamespacedName + WatcherAPI types.NamespacedName + MemcachedNamespace types.NamespacedName } // GetWatcherTestData is a function that initialize the WatcherTestData @@ -80,5 +82,13 @@ func GetWatcherTestData(watcherName types.NamespacedName) WatcherTestData { Namespace: watcherName.Namespace, Name: "watcher", }, + WatcherAPI: types.NamespacedName{ + Namespace: watcherName.Namespace, + Name: "watcher-api", + }, + MemcachedNamespace: types.NamespacedName{ + Namespace: watcherName.Namespace, + Name: "memcached", + }, } } diff --git a/tests/functional/watcherapi_controller_test.go b/tests/functional/watcherapi_controller_test.go index e1ef701..332fa95 100644 --- a/tests/functional/watcherapi_controller_test.go +++ b/tests/functional/watcherapi_controller_test.go @@ -7,37 +7,41 @@ import ( . "github.com/onsi/gomega" //revive:disable:dot-imports //revive:disable-next-line:dot-imports + memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" watcherv1beta1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" "github.com/openstack-k8s-operators/watcher-operator/pkg/watcher" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" ) var ( MinimalWatcherAPISpec = map[string]interface{}{ - "secret": "osp-secret", - "databaseInstance": "openstack", + "secret": "osp-secret", + "databaseInstance": "openstack", + "memcachedInstance": "memcached", } ) var _ = Describe("WatcherAPI controller with minimal spec values", func() { When("A Watcher instance is created from minimal spec", func() { BeforeEach(func() { - DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.Instance, MinimalWatcherAPISpec)) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, MinimalWatcherAPISpec)) }) It("should have the Spec fields defaulted", func() { - WatcherAPI := GetWatcherAPI(watcherTest.Instance) + WatcherAPI := GetWatcherAPI(watcherTest.WatcherAPI) Expect(WatcherAPI.Spec.DatabaseInstance).Should(Equal("openstack")) Expect(WatcherAPI.Spec.DatabaseAccount).Should(Equal("watcher")) Expect(WatcherAPI.Spec.Secret).Should(Equal("osp-secret")) + Expect(WatcherAPI.Spec.MemcachedInstance).Should(Equal("memcached")) Expect(WatcherAPI.Spec.PasswordSelectors).Should(Equal(watcherv1beta1.PasswordSelector{Service: "WatcherPassword"})) }) It("should have the Status fields initialized", func() { - WatcherAPI := GetWatcherAPI(watcherTest.Instance) + WatcherAPI := GetWatcherAPI(watcherTest.WatcherAPI) Expect(WatcherAPI.Status.ObservedGeneration).To(Equal(int64(0))) }) @@ -45,7 +49,7 @@ var _ = Describe("WatcherAPI controller with minimal spec values", func() { // the reconciler loop adds the finalizer so we have to wait for // it to run Eventually(func() []string { - return GetWatcherAPI(watcherTest.Instance).Finalizers + return GetWatcherAPI(watcherTest.WatcherAPI).Finalizers }, timeout, interval).Should(ContainElement("openstack.org/watcherapi")) }) @@ -55,24 +59,25 @@ var _ = Describe("WatcherAPI controller with minimal spec values", func() { var _ = Describe("WatcherAPI controller", func() { When("A WatcherAPI instance is created", func() { BeforeEach(func() { - DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.Instance, GetDefaultWatcherAPISpec())) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) }) It("should have the Spec fields defaulted", func() { - WatcherAPI := GetWatcherAPI(watcherTest.Instance) + WatcherAPI := GetWatcherAPI(watcherTest.WatcherAPI) Expect(WatcherAPI.Spec.DatabaseInstance).Should(Equal("openstack")) Expect(WatcherAPI.Spec.DatabaseAccount).Should(Equal("watcher")) Expect(WatcherAPI.Spec.Secret).Should(Equal("test-osp-secret")) + Expect(WatcherAPI.Spec.MemcachedInstance).Should(Equal("memcached")) }) It("should have the Status fields initialized", func() { - WatcherAPI := GetWatcherAPI(watcherTest.Instance) + WatcherAPI := GetWatcherAPI(watcherTest.WatcherAPI) Expect(WatcherAPI.Status.ObservedGeneration).To(Equal(int64(0))) }) It("should have ReadyCondition false", func() { th.ExpectCondition( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.ReadyCondition, corev1.ConditionFalse, @@ -81,7 +86,7 @@ var _ = Describe("WatcherAPI controller", func() { It("should have input not ready", func() { th.ExpectCondition( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.InputReadyCondition, corev1.ConditionFalse, @@ -90,7 +95,7 @@ var _ = Describe("WatcherAPI controller", func() { It("should have service config input unknown", func() { th.ExpectCondition( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.ServiceConfigReadyCondition, corev1.ConditionUnknown, @@ -101,16 +106,17 @@ var _ = Describe("WatcherAPI controller", func() { // the reconciler loop adds the finalizer so we have to wait for // it to run Eventually(func() []string { - return GetWatcherAPI(watcherTest.Instance).Finalizers + return GetWatcherAPI(watcherTest.WatcherAPI).Finalizers }, timeout, interval).Should(ContainElement("openstack.org/watcherapi")) }) }) - When("the secret is created with all the expected fields", func() { + When("the secret is created with all the expected fields and has all the required infra", func() { BeforeEach(func() { secret := th.CreateSecret( watcherTest.InternalTopLevelSecretName, map[string][]byte{ "WatcherPassword": []byte("service-password"), + "transport_url": []byte("url"), }, ) DeferCleanup(k8sClient.Delete, ctx, secret) @@ -122,19 +128,35 @@ var _ = Describe("WatcherAPI controller", func() { watcherTest.WatcherDatabaseAccount, mariadbv1.MariaDBAccountSpec{}) DeferCleanup(k8sClient.Delete, ctx, apiMariaDBAccount) DeferCleanup(k8sClient.Delete, ctx, apiMariaDBSecret) - DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.Instance, GetDefaultWatcherAPISpec())) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(watcherTest.WatcherAPI.Namespace)) + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(1)), + }, + } + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(watcherTest.WatcherAPI.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(watcherTest.MemcachedNamespace) }) It("should have input ready", func() { th.ExpectCondition( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.InputReadyCondition, corev1.ConditionTrue, ) }) + It("should have memcached ready true", func() { + th.ExpectCondition( + watcherTest.WatcherAPI, + ConditionGetterFunc(WatcherAPIConditionGetter), + condition.MemcachedReadyCondition, + corev1.ConditionTrue, + ) + }) It("should have config service input ready", func() { th.ExpectCondition( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.ServiceConfigReadyCondition, corev1.ConditionTrue, @@ -156,7 +178,7 @@ var _ = Describe("WatcherAPI controller", func() { watcherTest.WatcherDatabaseAccount, mariadbv1.MariaDBAccountSpec{}) DeferCleanup(k8sClient.Delete, ctx, apiMariaDBAccount) DeferCleanup(k8sClient.Delete, ctx, apiMariaDBSecret) - DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.Instance, GetDefaultWatcherAPISpec())) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) }) It("should have input false", func() { errorString := fmt.Sprintf( @@ -164,7 +186,7 @@ var _ = Describe("WatcherAPI controller", func() { "field 'WatcherPassword' not found in secret/test-osp-secret", ) th.ExpectConditionWithDetails( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.InputReadyCondition, corev1.ConditionFalse, @@ -174,7 +196,7 @@ var _ = Describe("WatcherAPI controller", func() { }) It("should have config service input unknown", func() { th.ExpectCondition( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.ServiceConfigReadyCondition, corev1.ConditionUnknown, @@ -183,11 +205,11 @@ var _ = Describe("WatcherAPI controller", func() { }) When("A WatcherAPI instance without secret is created", func() { BeforeEach(func() { - DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.Instance, GetDefaultWatcherAPISpec())) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) }) It("is missing the secret", func() { th.ExpectConditionWithDetails( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.InputReadyCondition, corev1.ConditionFalse, @@ -202,13 +224,14 @@ var _ = Describe("WatcherAPI controller", func() { watcherTest.InternalTopLevelSecretName, map[string][]byte{ "WatcherPassword": []byte("service-password"), + "transport_url": []byte("url"), }, ) DeferCleanup(k8sClient.Delete, ctx, secret) - DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.Instance, GetDefaultWatcherAPISpec())) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) }) It("should have input not ready", func() { - WatcherAPI := GetWatcherAPI(watcherTest.Instance) + WatcherAPI := GetWatcherAPI(watcherTest.WatcherAPI) customErrorString := fmt.Sprintf( "couldn't get database %s and account %s", watcher.DatabaseCRName, @@ -219,7 +242,7 @@ var _ = Describe("WatcherAPI controller", func() { customErrorString, ) th.ExpectConditionWithDetails( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.InputReadyCondition, corev1.ConditionFalse, @@ -229,11 +252,109 @@ var _ = Describe("WatcherAPI controller", func() { }) It("should have config service unknown", func() { th.ExpectCondition( - watcherTest.Instance, + watcherTest.WatcherAPI, ConditionGetterFunc(WatcherAPIConditionGetter), condition.ServiceConfigReadyCondition, corev1.ConditionUnknown, ) }) }) + When("secret and db are created, but there is no memcached", func() { + BeforeEach(func() { + secret := th.CreateSecret( + watcherTest.InternalTopLevelSecretName, + map[string][]byte{ + "WatcherPassword": []byte("service-password"), + "transport_url": []byte("url"), + }, + ) + DeferCleanup(k8sClient.Delete, ctx, secret) + mariadb.CreateMariaDBDatabase(watcherTest.WatcherDatabaseName.Namespace, watcherTest.WatcherDatabaseName.Name, mariadbv1.MariaDBDatabaseSpec{}) + DeferCleanup(k8sClient.Delete, ctx, mariadb.GetMariaDBDatabase(watcherTest.WatcherDatabaseName)) + + mariadb.SimulateMariaDBTLSDatabaseCompleted(watcherTest.WatcherDatabaseName) + apiMariaDBAccount, apiMariaDBSecret := mariadb.CreateMariaDBAccountAndSecret( + watcherTest.WatcherDatabaseAccount, mariadbv1.MariaDBAccountSpec{}) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBAccount) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBSecret) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) + }) + It("should have input ready true", func() { + th.ExpectCondition( + watcherTest.WatcherAPI, + ConditionGetterFunc(WatcherAPIConditionGetter), + condition.InputReadyCondition, + corev1.ConditionTrue, + ) + }) + It("should have memcached ready false", func() { + th.ExpectConditionWithDetails( + watcherTest.WatcherAPI, + ConditionGetterFunc(WatcherAPIConditionGetter), + condition.MemcachedReadyCondition, + corev1.ConditionFalse, + condition.RequestedReason, + condition.MemcachedReadyWaitingMessage, + ) + }) + }) + When("secret, db and memcached are created, but there is no keystoneapi", func() { + BeforeEach(func() { + secret := th.CreateSecret( + watcherTest.InternalTopLevelSecretName, + map[string][]byte{ + "WatcherPassword": []byte("service-password"), + "transport_url": []byte("url"), + }, + ) + DeferCleanup(k8sClient.Delete, ctx, secret) + mariadb.CreateMariaDBDatabase(watcherTest.WatcherDatabaseName.Namespace, watcherTest.WatcherDatabaseName.Name, mariadbv1.MariaDBDatabaseSpec{}) + DeferCleanup(k8sClient.Delete, ctx, mariadb.GetMariaDBDatabase(watcherTest.WatcherDatabaseName)) + + mariadb.SimulateMariaDBTLSDatabaseCompleted(watcherTest.WatcherDatabaseName) + apiMariaDBAccount, apiMariaDBSecret := mariadb.CreateMariaDBAccountAndSecret( + watcherTest.WatcherDatabaseAccount, mariadbv1.MariaDBAccountSpec{}) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBAccount) + DeferCleanup(k8sClient.Delete, ctx, apiMariaDBSecret) + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(1)), + }, + } + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(watcherTest.WatcherAPI.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(watcherTest.MemcachedNamespace) + DeferCleanup(th.DeleteInstance, CreateWatcherAPI(watcherTest.WatcherAPI, GetDefaultWatcherAPISpec())) + + }) + It("should have input ready true", func() { + th.ExpectCondition( + watcherTest.WatcherAPI, + ConditionGetterFunc(WatcherAPIConditionGetter), + condition.InputReadyCondition, + corev1.ConditionTrue, + ) + }) + It("should have memcached ready true", func() { + th.ExpectCondition( + watcherTest.WatcherAPI, + ConditionGetterFunc(WatcherAPIConditionGetter), + condition.MemcachedReadyCondition, + corev1.ConditionTrue, + ) + }) + It("should have config service input unknown", func() { + errorString := fmt.Sprintf( + condition.ServiceConfigReadyErrorMessage, + "keystoneAPI not found", + ) + th.ExpectConditionWithDetails( + watcherTest.WatcherAPI, + ConditionGetterFunc(WatcherAPIConditionGetter), + condition.ServiceConfigReadyCondition, + corev1.ConditionFalse, + condition.ErrorReason, + errorString, + ) + }) + }) }) diff --git a/tests/kuttl/test-suites/default/watcher-api/03-assert.yaml b/tests/kuttl/test-suites/default/watcher-api/03-assert.yaml index 84ab971..a524eee 100644 --- a/tests/kuttl/test-suites/default/watcher-api/03-assert.yaml +++ b/tests/kuttl/test-suites/default/watcher-api/03-assert.yaml @@ -20,7 +20,17 @@ status: reason: Ready status: "True" type: InputReady + - message: " Memcached instance has been provisioned" + reason: Ready + status: "True" + type: MemcachedReady - message: Service config create completed reason: Ready status: "True" type: ServiceConfigReady +--- +apiVersion: v1 +kind: Secret +metadata: + name: watcherapi-kuttl-config-data +type: Opaque diff --git a/tests/kuttl/test-suites/default/watcher-api/03-deploy-watcher-api.yaml b/tests/kuttl/test-suites/default/watcher-api/03-deploy-watcher-api.yaml index 38e6684..1e0af9c 100644 --- a/tests/kuttl/test-suites/default/watcher-api/03-deploy-watcher-api.yaml +++ b/tests/kuttl/test-suites/default/watcher-api/03-deploy-watcher-api.yaml @@ -5,6 +5,7 @@ metadata: type: Opaque stringData: WatcherPassword: password + transport_url: rabbitmq-transport-url-watcher-kuttl-watcher-transport --- apiVersion: watcher.openstack.org/v1beta1 kind: WatcherAPI @@ -13,3 +14,4 @@ metadata: spec: databaseInstance: openstack secret: watcherapi-secret + memcachedInstance: "memcached"