Repository: EnMasseProject/enmasse
Pull request #2181 (2019-01-09 17:13:53)

Implement non-namespaced versions for listing users and addresses (#2101)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21a06c991..9e2f32e8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,27 @@
+## 0.26.0
+* Support for switching address space plans
+* Support for switching address plans for `anycast`, `multicast`, and `queue` address types in standard address space
+* Readiness on initial configuration for routers in standard address space. This ensures that routers can only be connected to once they have receive initial configuration, and will allow a HA configuration (> 1) routers to upgrade without downtime for the service.
+* Router configuration now enforces authenticatePeer: yes, requiring clients to perform SASL handshake
+* Allow querying all EnMasse resources cluster wide
+* Allow listing connections by container id by clicking on link in Addresses pane in the console
+* Update all API versions from v1alpha1 to v1beta1. v1alpha1 is now deprecated for all EnMasse custom resources and will get removed.
+* Bugfix: allow address full policy to be set for brokers in standard address space
+* Allow clients to authenticate as service account users by supplying @@serviceaccount@@ as the username and the token as the password.
+
+## 0.25.0
+* Prometheus metrics and alerts for address health are now exposed
+* Pooled broker deployments now use N deployments with 1 broker instead of 1 deployment with N brokers. This is to handle a resize bug in previous versions. When upgrading, take care to drain all queues first to avoid message loss.
+* Improved command line output of addresses and address spaces with more tabular information about the resource(s)
+* Allow configuring storageClass used for broker volumes
+* Add support for serviceaccount authentication type for users
+* Allow resizing volumes if configured (and underlying provider supports it)
+* Add support for nodeAffinity preferred labels for operators and messaging infra
+* Add support for configuring network network policies for address space infrastructure
+* Add support for changing address space plans
+* Split broker image into a 'broker' and 'broker-plugin' image run as initContainer image
+* Bugfixes in address handling in standard-controller
+
 ## 0.24.0 (...)
 * Support automatic upgrades of EnMasse based on infrastructure configuration versions
 * Update default set of address space plans and address plans to better show their use
diff --git a/Makefile.common b/Makefile.common
index 3ffaa95d1..82d75b369 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -38,7 +38,7 @@ docker_tag:
 docker_push:
 	if [ -f Dockerfile ] ; \
 	then \
-		bash ../scripts/docker_push.sh "$(DOCKER) push $(DOCKER_REGISTRY)/$(DOCKER_ORG)/$(PROJECT_NAME):$(TAG)" 10 10 ; \
+		bash $(TOPDIR)/scripts/docker_push.sh "$(DOCKER) push $(DOCKER_REGISTRY)/$(DOCKER_ORG)/$(PROJECT_NAME):$(TAG)" 10 10 ; \
 	fi
 	
 
diff --git a/address-space-controller/src/main/java/io/enmasse/controller/EndpointController.java b/address-space-controller/src/main/java/io/enmasse/controller/EndpointController.java
index 0971c518d..006ed8845 100644
--- a/address-space-controller/src/main/java/io/enmasse/controller/EndpointController.java
+++ b/address-space-controller/src/main/java/io/enmasse/controller/EndpointController.java
@@ -106,6 +106,13 @@ public class EndpointController implements Controller {
 
             if (endpoint.endpointSpec.getExposeSpec().isPresent()) {
                 exposedStatuses.add(exposeEndpoint(addressSpace, endpoint, endpoint.endpointSpec.getExposeSpec().get()));
+            } else {
+                EndpointStatus.Builder statusBuilder = new EndpointStatus.Builder(endpoint.endpointStatus);
+                Secret certSecret = client.secrets().inNamespace(namespace).withName(KubeUtil.getExternalCertSecretName(endpoint.endpointSpec.getService(), addressSpace)).get();
+                if (certSecret != null) {
+                    statusBuilder.setCertificate(certSecret.getData().get("tls.crt"));
+                }
+                exposedStatuses.add(endpoint.endpointStatus);
             }
         }
 
diff --git a/agent/lib/router_config.js b/agent/lib/router_config.js
index d68439fed..42f6ed852 100644
--- a/agent/lib/router_config.js
+++ b/agent/lib/router_config.js
@@ -351,6 +351,8 @@ function desired_address_config(high_level_address_definitions) {
                     var brokerStatus = def.allocated_to[j];
                     if (brokerStatus.state === 'Active') {
                         config.add_autolink_pair({addr:def.address, containerId: brokerStatus.containerId});
+                    } else if (brokerStatus.state === 'Migrating') {
+                        config.add_autolink_pair({addr:def.address, containerId: brokerStatus.containerId});
                     } else if (brokerStatus.state === 'Draining') {
                         config.add_autolink_in({addr:def.address, containerId: brokerStatus.containerId});
                     }
diff --git a/agent/npm-shrinkwrap.json b/agent/npm-shrinkwrap.json
index 505dd09ab..f016b54f3 100644
--- a/agent/npm-shrinkwrap.json
+++ b/agent/npm-shrinkwrap.json
@@ -147,19 +147,6 @@
             "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
             "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
         },
-        "caller-path": {
-            "version": "0.1.0",
-            "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
-            "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
-            "requires": {
-                "callsites": "0.2.0"
-            }
-        },
-        "callsites": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
-            "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo="
-        },
         "chalk": {
             "version": "2.4.1",
             "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
@@ -323,15 +310,15 @@
             }
         },
         "eslint": {
-            "version": "5.10.0",
-            "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.10.0.tgz",
-            "integrity": "sha512-HpqzC+BHULKlnPwWae9MaVZ5AXJKpkxCVXQHrFaRw3hbDj26V/9ArYM4Rr/SQ8pi6qUPLXSSXC4RBJlyq2Z2OQ==",
+            "version": "5.12.0",
+            "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.12.0.tgz",
+            "integrity": "sha512-LntwyPxtOHrsJdcSwyQKVtHofPHdv+4+mFwEe91r2V13vqpM8yLr7b1sW+Oo/yheOPkWYsYlYJCkzlFAt8KV7g==",
             "requires": {
                 "@babel/code-frame": "7.0.0",
                 "ajv": "6.5.4",
                 "chalk": "2.4.1",
                 "cross-spawn": "6.0.5",
-                "debug": "4.1.0",
+                "debug": "4.1.1",
                 "doctrine": "2.1.0",
                 "eslint-scope": "4.0.0",
                 "eslint-utils": "1.3.1",
@@ -344,6 +331,7 @@
                 "glob": "7.1.3",
                 "globals": "11.8.0",
                 "ignore": "4.0.6",
+                "import-fresh": "3.0.0",
                 "imurmurhash": "0.1.4",
                 "inquirer": "6.2.0",
                 "js-yaml": "3.12.0",
@@ -358,7 +346,6 @@
                 "pluralize": "7.0.0",
                 "progress": "2.0.1",
                 "regexpp": "2.0.1",
-                "require-uncached": "1.0.3",
                 "semver": "5.6.0",
                 "strip-ansi": "4.0.0",
                 "strip-json-comments": "2.0.1",
@@ -367,9 +354,9 @@
             },
             "dependencies": {
                 "debug": {
-                    "version": "4.1.0",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
-                    "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
+                    "version": "4.1.1",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+                    "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
                     "requires": {
                         "ms": "2.1.1"
                     }
@@ -608,6 +595,22 @@
             "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
             "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
         },
+        "import-fresh": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz",
+            "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==",
+            "requires": {
+                "parent-module": "1.0.0",
+                "resolve-from": "4.0.0"
+            },
+            "dependencies": {
+                "resolve-from": {
+                    "version": "4.0.0",
+                    "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+                    "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+                }
+            }
+        },
         "imurmurhash": {
             "version": "0.1.4",
             "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -863,7 +866,7 @@
             "dependencies": {
                 "commander": {
                     "version": "2.15.1",
-                    "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+                    "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
                     "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
                 },
                 "debug": {
@@ -1012,6 +1015,21 @@
             "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
             "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
         },
+        "parent-module": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.0.tgz",
+            "integrity": "sha512-8Mf5juOMmiE4FcmzYc4IaiS9L3+9paz2KOiXzkRviCP6aDmN49Hz6EMWz0lGNp9pX80GvvAuLADtyGfW/Em3TA==",
+            "requires": {
+                "callsites": "3.0.0"
+            },
+            "dependencies": {
+                "callsites": {
+                    "version": "3.0.0",
+                    "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.0.0.tgz",
+                    "integrity": "sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw=="
+                }
+            }
+        },
         "path-is-absolute": {
             "version": "1.0.1",
             "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -1070,25 +1088,11 @@
             "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
             "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw=="
         },
-        "require-uncached": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
-            "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
-            "requires": {
-                "caller-path": "0.1.0",
-                "resolve-from": "1.0.1"
-            }
-        },
         "resolve": {
             "version": "1.1.7",
             "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
             "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
         },
-        "resolve-from": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
-            "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY="
-        },
         "restore-cursor": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
diff --git a/api-common/pom.xml b/api-common/pom.xml
index 6b4ec0550..cd038d20b 100644
--- a/api-common/pom.xml
+++ b/api-common/pom.xml
@@ -97,6 +97,7 @@
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
@@ -111,7 +112,7 @@
     <dependency>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-launcher</artifactId>
-      <scope>compile</scope>
+      <scope>test</scope>
     </dependency>
   </dependencies>
 </project>
diff --git a/api-model/pom.xml b/api-model/pom.xml
index 388efc216..9f474be8a 100644
--- a/api-model/pom.xml
+++ b/api-model/pom.xml
@@ -36,6 +36,11 @@
       <artifactId>junit-jupiter-engine</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-params</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-launcher</artifactId>
diff --git a/api-model/src/main/java/io/enmasse/address/model/BrokerState.java b/api-model/src/main/java/io/enmasse/address/model/BrokerState.java
index 1950f1d30..cd0682686 100644
--- a/api-model/src/main/java/io/enmasse/address/model/BrokerState.java
+++ b/api-model/src/main/java/io/enmasse/address/model/BrokerState.java
@@ -6,5 +6,6 @@ package io.enmasse.address.model;
 
 public enum BrokerState {
     Active,
+    Migrating,
     Draining
 }
diff --git a/api-model/src/main/java/io/enmasse/address/model/KubeUtil.java b/api-model/src/main/java/io/enmasse/address/model/KubeUtil.java
index 7b860952f..4c31f59c5 100644
--- a/api-model/src/main/java/io/enmasse/address/model/KubeUtil.java
+++ b/api-model/src/main/java/io/enmasse/address/model/KubeUtil.java
@@ -4,15 +4,20 @@
  */
 package io.enmasse.address.model;
 
+import java.util.regex.Pattern;
+
 import io.enmasse.config.AnnotationKeys;
 
 /**
  * Various static utilities that don't belong in a specific place
  */
-public class KubeUtil {
-    private static int MAX_KUBE_NAME = 63 - 3; // max length of identifier - space for pod identifier
-    private static final String addressPattern = "[^a-z0-9\\-]";
-    private static final String usernamePattern = "[^a-z0-9\\-.@_]";
+public final class KubeUtil {
+    private static final int MAX_KUBE_NAME = 63 - 3; // max length of identifier - space for pod identifier
+    private static final Pattern addressPattern = Pattern.compile("[^a-z0-9\\-]");
+    private static final Pattern usernamePattern = Pattern.compile("[^a-z0-9\\-.@_]");
+
+    public KubeUtil() {
+    }
 
     public static String sanitizeName(String name) {
         return sanitizeWithPattern(name, addressPattern);
@@ -22,12 +27,15 @@ public class KubeUtil {
         return sanitizeWithPattern(name, usernamePattern);
     }
 
-    private static String sanitizeWithPattern(String value, String pattern) {
+    private static String sanitizeWithPattern(String value, Pattern pattern) {
         if (value == null) {
             return null;
         }
 
-        String clean = value.toLowerCase().replaceAll(pattern, "");
+        String clean = pattern
+                        .matcher(value.toLowerCase())
+                        .replaceAll("");
+
         if (clean.startsWith("-")) {
             clean = clean.replaceFirst("-", "1");
         }
@@ -39,6 +47,7 @@ public class KubeUtil {
         if (clean.endsWith("-")) {
             clean = clean.substring(0, clean.length() - 1) + "1";
         }
+
         return clean;
     }
 
@@ -88,7 +97,7 @@ public class KubeUtil {
             throw new IllegalArgumentException("Name length is longer than " + MAX_KUBE_NAME + " characters");
         }
 
-        if (name.matches(addressPattern)) {
+        if (addressPattern.matcher(name).matches()) {
             throw new IllegalArgumentException("Illegal characters found in " + name + ". Must not match " + addressPattern);
         }
     }
diff --git a/api-model/src/main/java/io/enmasse/address/model/Status.java b/api-model/src/main/java/io/enmasse/address/model/Status.java
index 6bcd06c7f..1e223c821 100644
--- a/api-model/src/main/java/io/enmasse/address/model/Status.java
+++ b/api-model/src/main/java/io/enmasse/address/model/Status.java
@@ -23,7 +23,10 @@ public class Status {
         this.isReady = other.isReady();
         this.phase = other.getPhase();
         this.messages = new HashSet<>(other.getMessages());
-        this.brokerStatuses = new ArrayList<>(other.getBrokerStatuses());
+        this.brokerStatuses = new ArrayList<>();
+        for (BrokerStatus brokerStatus : other.getBrokerStatuses()) {
+            brokerStatuses.add(new BrokerStatus(brokerStatus.getClusterId(), brokerStatus.getContainerId(), brokerStatus.getState()));
+        }
     }
 
     public boolean isReady() {
diff --git a/api-model/src/main/java/io/enmasse/user/api/UserApi.java b/api-model/src/main/java/io/enmasse/user/api/UserApi.java
index 1f6c129e3..e901620b4 100644
--- a/api-model/src/main/java/io/enmasse/user/api/UserApi.java
+++ b/api-model/src/main/java/io/enmasse/user/api/UserApi.java
@@ -20,5 +20,8 @@ public interface UserApi {
 
     UserList listUsers(String namespace);
     UserList listUsersWithLabels(String namespace, Map<String, String> labels);
+    UserList listAllUsers();
+    UserList listAllUsersWithLabels(Map<String, String> labels);
+
     void deleteUsers(String namespace);
 }
diff --git a/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java b/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java
index a5c07a0ee..d18f888f7 100644
--- a/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java
+++ b/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java
@@ -101,11 +101,13 @@ public class HTTPServer extends AbstractVerticle {
         deployment.getRegistry().addSingletonResource(new HttpOpenApiService());
         deployment.getRegistry().addSingletonResource(new HttpNestedAddressService(addressSpaceApi, schemaProvider, clock));
         deployment.getRegistry().addSingletonResource(new HttpAddressService(addressSpaceApi, schemaProvider, clock));
+        deployment.getRegistry().addSingletonResource(new HttpClusterAddressService(addressSpaceApi, schemaProvider, clock));
         deployment.getRegistry().addSingletonResource(new HttpSchemaService(schemaProvider));
         deployment.getRegistry().addSingletonResource(new HttpAddressSpaceService(addressSpaceApi, schemaProvider, clock));
         deployment.getRegistry().addSingletonResource(new HttpClusterAddressSpaceService(addressSpaceApi, clock));
         if (userApi != null) {
             deployment.getRegistry().addSingletonResource(new HttpUserService(addressSpaceApi, userApi, clock));
+            deployment.getRegistry().addSingletonResource(new HttpClusterUserService(userApi, clock));
         } else {
             log.info("User API not available, disabling");
         }
diff --git a/api-server/src/main/java/io/enmasse/api/v1/AddressApiHelper.java b/api-server/src/main/java/io/enmasse/api/v1/AddressApiHelper.java
index 1c607840f..bf9c3c670 100644
--- a/api-server/src/main/java/io/enmasse/api/v1/AddressApiHelper.java
+++ b/api-server/src/main/java/io/enmasse/api/v1/AddressApiHelper.java
@@ -5,6 +5,7 @@
 package io.enmasse.api.v1;
 
 import java.util.*;
+import java.util.function.BiFunction;
 import java.util.stream.Collectors;
 
 import javax.ws.rs.BadRequestException;
@@ -31,30 +32,38 @@ public class AddressApiHelper {
         this.schemaProvider = schemaProvider;
     }
 
-    public AddressList getAddresses(String namespace, String addressSpaceId) throws Exception {
-        AddressList list = new AddressList();
-        if (addressSpaceId == null) {
-            for (AddressSpace addressSpace : addressSpaceApi.listAddressSpaces(namespace)) {
-                list.addAll(addressSpaceApi.withAddressSpace(addressSpace).listAddresses(namespace));
-            }
-        } else {
-            AddressSpace addressSpace = getAddressSpace(namespace, addressSpaceId);
-            list.addAll(addressSpaceApi.withAddressSpace(addressSpace).listAddresses(namespace));
+    protected AddressList queryAddresses(Collection<AddressSpace> addressSpaces, BiFunction<String, AddressApi, Collection<Address>> lister) throws Exception {
+        final AddressList list = new AddressList();
+
+        for (final AddressSpace addressSpace : addressSpaces) {
+            list.addAll(lister.apply(addressSpace.getNamespace(), addressSpaceApi.withAddressSpace(addressSpace)));
         }
+
         return list;
     }
 
-    public AddressList getAddressesWithLabels(String namespace, String addressSpaceId, Map<String, String> labels) throws Exception {
-        AddressList list = new AddressList();
+    protected Collection<AddressSpace> getAddressSpaces(String namespace, String addressSpaceId) throws Exception {
         if (addressSpaceId == null) {
-            for (AddressSpace addressSpace : addressSpaceApi.listAddressSpaces(namespace)) {
-                list.addAll(addressSpaceApi.withAddressSpace(addressSpace).listAddressesWithLabels(namespace, labels));
-            }
+            return addressSpaceApi.listAddressSpaces(namespace);
         } else {
-            AddressSpace addressSpace = getAddressSpace(namespace, addressSpaceId);
-            list.addAll(addressSpaceApi.withAddressSpace(addressSpace).listAddressesWithLabels(namespace, labels));
+            return Collections.singleton(getAddressSpace(namespace, addressSpaceId));
         }
-        return list;
+    }
+    
+    public AddressList getAddresses(String namespace, String addressSpaceId) throws Exception {
+        return queryAddresses(getAddressSpaces(namespace, addressSpaceId), (ns, api) -> api.listAddresses(ns));
+    }
+
+    public AddressList getAddressesWithLabels(String namespace, String addressSpaceId, Map<String, String> labels) throws Exception {
+        return queryAddresses(getAddressSpaces(namespace, addressSpaceId), (ns, api) -> api.listAddressesWithLabels(ns, labels));
+    }
+
+    public AddressList getAllAddresses() throws Exception {
+        return queryAddresses(addressSpaceApi.listAllAddressSpaces(), (ns, api) -> api.listAddresses(ns));
+    }
+
+    public AddressList getAllAddressesWithLabels(final Map<String, String> labels) throws Exception {
+        return queryAddresses(addressSpaceApi.listAllAddressSpaces(), (ns, api) -> api.listAddressesWithLabels(ns, labels));
     }
 
     private void validateAddress(AddressSpace addressSpace, Address address) {
diff --git a/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterAddressService.java b/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterAddressService.java
new file mode 100644
index 000000000..0bd5a48e9
--- /dev/null
+++ b/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterAddressService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017-2018, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.api.v1.http;
+
+import java.time.Clock;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import io.enmasse.k8s.api.AddressSpaceApi;
+import io.enmasse.k8s.api.SchemaProvider;
+
+@Path(HttpClusterAddressService.BASE_URI)
+public class HttpClusterAddressService extends HttpAddressServiceBase {
+
+    private static final String RESOURCE_NAME = "addresses";
+
+    static final String BASE_URI = "/apis/enmasse.io/{version:v1alpha1|v1beta1}/" + RESOURCE_NAME;
+
+    public HttpClusterAddressService(AddressSpaceApi addressSpaceApi, SchemaProvider schemaProvider, Clock clock) {
+        super(addressSpaceApi, schemaProvider, clock);
+    }
+
+    @GET
+    @Produces({MediaType.APPLICATION_JSON})
+    @Consumes({MediaType.APPLICATION_JSON})
+    public Response getAddressList(@Context SecurityContext securityContext, @HeaderParam("Accept") String acceptHeader, @QueryParam("address") String address, @QueryParam("labelSelector") String labelSelector) throws Exception {
+        return super.getAddressList(securityContext, acceptHeader, "", null, address, labelSelector);
+    }
+}
diff --git a/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterAddressSpaceService.java b/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterAddressSpaceService.java
index 113b8e352..a43530474 100644
--- a/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterAddressSpaceService.java
+++ b/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterAddressSpaceService.java
@@ -4,44 +4,37 @@
  */
 package io.enmasse.api.v1.http;
 
-import io.enmasse.address.model.*;
-import io.enmasse.api.auth.RbacSecurityContext;
-import io.enmasse.api.auth.ResourceVerb;
-import io.enmasse.api.common.Exceptions;
-import io.enmasse.api.common.Status;
-import io.enmasse.api.common.UuidGenerator;
-import io.enmasse.api.v1.AddressApiHelper;
-import io.enmasse.config.AnnotationKeys;
-import io.enmasse.config.LabelKeys;
-import io.enmasse.k8s.api.AddressSpaceApi;
-import io.enmasse.k8s.api.SchemaProvider;
-import io.enmasse.k8s.model.v1beta1.PartialObjectMetadata;
-import io.enmasse.k8s.model.v1beta1.Table;
-import io.enmasse.k8s.model.v1beta1.TableColumnDefinition;
-import io.enmasse.k8s.model.v1beta1.TableRow;
-import io.enmasse.k8s.util.TimeUtil;
-import io.fabric8.kubernetes.api.model.ListMeta;
-import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import static io.enmasse.api.v1.http.HttpAddressSpaceService.formatResponse;
+import static io.enmasse.api.v1.http.HttpAddressSpaceService.removeSecrets;
 
-import javax.validation.constraints.NotNull;
-import javax.ws.rs.*;
-import javax.ws.rs.core.*;
 import java.time.Clock;
-import java.time.Duration;
 import java.time.Instant;
-import java.util.*;
+import java.util.Map;
 import java.util.concurrent.Callable;
-import java.util.stream.Collectors;
 
-import static io.enmasse.api.v1.http.HttpAddressSpaceService.formatResponse;
-import static io.enmasse.api.v1.http.HttpAddressSpaceService.removeSecrets;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.enmasse.api.auth.RbacSecurityContext;
+import io.enmasse.api.auth.ResourceVerb;
+import io.enmasse.api.common.Exceptions;
+import io.enmasse.api.v1.AddressApiHelper;
+import io.enmasse.k8s.api.AddressSpaceApi;
 
 @Path(HttpClusterAddressSpaceService.BASE_URI)
 public class HttpClusterAddressSpaceService {
 
-    static final String BASE_URI = "/apis/enmasse.io/v1beta1/addressspaces";
+    static final String BASE_URI = "/apis/enmasse.io/{version:v1alpha1|v1beta1}/addressspaces";
 
     private static final Logger log = LoggerFactory.getLogger(HttpClusterAddressSpaceService.class.getName());
 
diff --git a/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterUserService.java b/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterUserService.java
new file mode 100644
index 000000000..abebd7baa
--- /dev/null
+++ b/api-server/src/main/java/io/enmasse/api/v1/http/HttpClusterUserService.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017-2018, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.api.v1.http;
+
+
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.enmasse.api.auth.RbacSecurityContext;
+import io.enmasse.api.auth.ResourceVerb;
+import io.enmasse.api.common.Exceptions;
+import io.enmasse.api.v1.AddressApiHelper;
+import io.enmasse.user.api.UserApi;
+import io.enmasse.user.model.v1.User;
+import io.enmasse.user.model.v1.UserList;
+
+@Path(HttpClusterUserService.BASE_URI)
+public class HttpClusterUserService {
+
+    private static final String RESOURCE_NAME = "messagingusers";
+
+    static final String BASE_URI = "/apis/" + User.GROUP + "/{version:v1alpha1|v1beta1}/" + RESOURCE_NAME;
+
+    private static final Logger log = LoggerFactory.getLogger(HttpClusterUserService.class.getName());
+
+    private final UserApi userApi;
+    private final Clock clock;
+
+    public HttpClusterUserService(UserApi userApi, Clock clock) {
+        this.userApi = userApi;
+        this.clock = clock;
+    }
+
+    private Response doRequest(String errorMessage, Callable<Response> request) throws Exception {
+        try {
+            return request.call();
+        } catch (Exception e) {
+            log.error(errorMessage, e);
+            throw e;
+        }
+    }
+
+    private static void verifyAuthorized(SecurityContext securityContext, ResourceVerb verb) {
+        if (!securityContext.isUserInRole(RbacSecurityContext.rbacToRole("", verb, RESOURCE_NAME, User.GROUP))) {
+            throw Exceptions.notAuthorizedException();
+        }
+    }
+
+    @GET
+    @Produces({MediaType.APPLICATION_JSON})
+    public Response getUserList(@Context SecurityContext securityContext, @HeaderParam("Accept") String acceptHeader, @QueryParam("labelSelector") String labelSelector) throws Exception {
+        return doRequest("Error getting user list", () -> {
+            verifyAuthorized(securityContext, ResourceVerb.list);
+
+            Instant now = clock.instant();
+            UserList userList = new UserList();
+
+            if (labelSelector != null) {
+                Map<String, String> labels = AddressApiHelper.parseLabelSelector(labelSelector);
+                userList.getItems().addAll(userApi.listAllUsersWithLabels(labels).getItems());
+            } else {
+                userList.getItems().addAll(userApi.listAllUsers().getItems());
+            }
+
+            return Response.ok(HttpUserService.formatResponse(acceptHeader, now, userList)).build();
+        });
+    }
+}
diff --git a/api-server/src/main/java/io/enmasse/api/v1/http/HttpUserService.java b/api-server/src/main/java/io/enmasse/api/v1/http/HttpUserService.java
index ba187c5bc..f5d81deb1 100644
--- a/api-server/src/main/java/io/enmasse/api/v1/http/HttpUserService.java
+++ b/api-server/src/main/java/io/enmasse/api/v1/http/HttpUserService.java
@@ -94,6 +94,7 @@ public class HttpUserService {
         return doRequest("Error getting user list", () -> {
             verifyAuthorized(securityContext, namespace, ResourceVerb.list);
 
+            Instant now = clock.instant();
             UserList userList = new UserList();
             if (labelSelector != null) {
                 Map<String, String> labels = AddressApiHelper.parseLabelSelector(labelSelector);
@@ -101,7 +102,7 @@ public class HttpUserService {
             } else {
                 userList.getItems().addAll(userApi.listUsers(namespace).getItems());
             }
-            return Response.ok(formatResponse(acceptHeader, userList)).build();
+            return Response.ok(formatResponse(acceptHeader, now, userList)).build();
         });
     }
 
@@ -112,6 +113,7 @@ public class HttpUserService {
         return doRequest("Error getting user " + userNameWithAddressSpace, () -> {
             verifyAuthorized(securityContext, namespace, ResourceVerb.get);
 
+            Instant now = clock.instant();
             String addressSpaceName = parseAddressSpace(userNameWithAddressSpace);
             checkAddressSpaceName(userNameWithAddressSpace, addressSpaceName);
             AddressSpace addressSpace = addressSpaceApi.getAddressSpaceWithName(namespace, addressSpaceName).orElse(null);
@@ -122,7 +124,7 @@ public class HttpUserService {
             log.debug("Retrieving user {} in realm {} namespace {}", userNameWithAddressSpace, realm, namespace);
 
             return userApi.getUserWithName(realm, userNameWithAddressSpace)
-                    .map(user -> Response.ok(formatResponse(acceptHeader, user)).build())
+                    .map(user -> Response.ok(formatResponse(acceptHeader, now, user)).build())
                     .orElseGet(() -> Response.status(404).entity(Status.notFound("MessagingUser", userNameWithAddressSpace)).build());
         });
     }
@@ -265,30 +267,29 @@ public class HttpUserService {
                     0,
                     "string"));
 
-    private Object formatResponse(String headerParam, UserList userList) {
+    static Object formatResponse(String headerParam, Instant now, UserList userList) {
         if (isTableFormat(headerParam)) {
-            return new Table(new ListMeta(), tableColumnDefinitions, createRows(userList));
+            return new Table(new ListMeta(), tableColumnDefinitions, createRows(userList, now));
         } else {
             return userList;
         }
     }
 
-    private Object formatResponse(String headerParam, User user) {
+    static Object formatResponse(String headerParam, Instant now, User user) {
         if (isTableFormat(headerParam)) {
             UserList list = new UserList();
             list.getItems().add(user);
-            return new Table(new ListMeta(), tableColumnDefinitions, createRows(list));
+            return new Table(new ListMeta(), tableColumnDefinitions, createRows(list, now));
         } else {
             return user;
         }
     }
 
-    private boolean isTableFormat(String acceptHeader) {
+    static boolean isTableFormat(String acceptHeader) {
         return acceptHeader != null && acceptHeader.contains("as=Table") && acceptHeader.contains("g=meta.k8s.io") && acceptHeader.contains("v=v1beta1");
     }
 
-    private List<TableRow> createRows(UserList userList) {
-        Instant now = clock.instant();
+    static List<TableRow> createRows(UserList userList, Instant now) {
         return userList.getItems().stream()
                 .map(user -> new TableRow(
                         Arrays.asList(
diff --git a/broker-plugin/plugin/src/main/resources/standard/colocated/broker.xml b/broker-plugin/plugin/src/main/resources/standard/colocated/broker.xml
index 4804012f1..ad30e3a0e 100644
--- a/broker-plugin/plugin/src/main/resources/standard/colocated/broker.xml
+++ b/broker-plugin/plugin/src/main/resources/standard/colocated/broker.xml
@@ -68,7 +68,7 @@ under the License.
       <global-max-size>${GLOBAL_MAX_SIZE}</global-max-size>
 
       <acceptors>
-         <acceptor name="artemis">tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576</acceptor>
+         <acceptor name="artemis">tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;protocols=CORE</acceptor>
          <acceptor name="amqp">tcp://0.0.0.0:5673?protocols=AMQP;sslEnabled=true;keyStorePath=${KEYSTORE_PATH};keyStorePassword=enmasse;trustStorePath=${TRUSTSTORE_PATH};trustStorePassword=enmasse;verifyHost=false;needClientAuth=true</acceptor>
       </acceptors>
 
diff --git a/broker-plugin/plugin/src/main/resources/standard/sharded-queue/broker.xml b/broker-plugin/plugin/src/main/resources/standard/sharded-queue/broker.xml
index 644738610..3d64835af 100644
--- a/broker-plugin/plugin/src/main/resources/standard/sharded-queue/broker.xml
+++ b/broker-plugin/plugin/src/main/resources/standard/sharded-queue/broker.xml
@@ -70,7 +70,7 @@ under the License.
       <global-max-size>${GLOBAL_MAX_SIZE}</global-max-size>
 
       <acceptors>
-         <acceptor name="artemis">tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576</acceptor>
+         <acceptor name="artemis">tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;protocols=CORE</acceptor>
          <acceptor name="amqp">tcp://0.0.0.0:5673?protocols=AMQP;sslEnabled=true;keyStorePath=${KEYSTORE_PATH};keyStorePassword=enmasse;trustStorePath=${TRUSTSTORE_PATH};trustStorePassword=enmasse;verifyHost=false;needClientAuth=true</acceptor>
       </acceptors>
 
diff --git a/broker-plugin/plugin/src/main/resources/standard/sharded-topic/broker.xml b/broker-plugin/plugin/src/main/resources/standard/sharded-topic/broker.xml
index dc85b1e27..258de8811 100644
--- a/broker-plugin/plugin/src/main/resources/standard/sharded-topic/broker.xml
+++ b/broker-plugin/plugin/src/main/resources/standard/sharded-topic/broker.xml
@@ -71,7 +71,7 @@ under the License.
       <global-max-size>${GLOBAL_MAX_SIZE}</global-max-size>
 
       <acceptors>
-         <acceptor name="artemis">tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576</acceptor>
+         <acceptor name="artemis">tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;protocols=CORE</acceptor>
          <acceptor name="amqp">tcp://0.0.0.0:5673?protocols=AMQP;sslEnabled=true;keyStorePath=${KEYSTORE_PATH};keyStorePassword=enmasse;trustStorePath=${TRUSTSTORE_PATH};trustStorePassword=enmasse;verifyHost=false;needClientAuth=true</acceptor>
       </acceptors>
 
diff --git a/documentation/common/attributes.adoc b/documentation/common/attributes.adoc
index 84694f608..808aa6c96 100644
--- a/documentation/common/attributes.adoc
+++ b/documentation/common/attributes.adoc
@@ -4,7 +4,7 @@
 :doctype: book
 :experimental:
 :idprefix:
-:imagesdir: images
+:imagesdir: ../images
 :numbered:
 :sectanchors!:
 :sectnums:
diff --git a/documentation/modules/proc-logging-in-console.adoc b/documentation/modules/proc-logging-in-console.adoc
index 6457ae223..c03c2e663 100644
--- a/documentation/modules/proc-logging-in-console.adoc
+++ b/documentation/modules/proc-logging-in-console.adoc
@@ -13,5 +13,5 @@
 
 . Log in with your OpenShift user credentials. The {ProductName} console opens.
 
-image::../images/console-screenshot.png[{ProductName} console]
+image::{imagesdir}/console-screenshot.png[{ProductName} console]
 
diff --git a/documentation/modules/ref-view-message-conn-stats-table.adoc b/documentation/modules/ref-view-message-conn-stats-table.adoc
index bca102e13..5a7b50a24 100644
--- a/documentation/modules/ref-view-message-conn-stats-table.adoc
+++ b/documentation/modules/ref-view-message-conn-stats-table.adoc
@@ -33,4 +33,4 @@
 |Total number of messages delivered |For the desired connection, expand the twisty on the left to show the *Senders* and *Receivers* tables; see the *Deliveries* columns.
 |Standard address space only: Username used by the client to connect |The third column
 |===
-
+NOTE: For the brokered address space only, on the Connections page, the number of senders is either `0` or `1`. As soon as one or more senders exist, `1` is displayed rather than reflecting the actual number of senders.
diff --git a/keycloak-controller/pom.xml b/keycloak-controller/pom.xml
index ca841eb0a..d3f27eb58 100644
--- a/keycloak-controller/pom.xml
+++ b/keycloak-controller/pom.xml
@@ -46,6 +46,7 @@
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
@@ -60,7 +61,7 @@
     <dependency>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-launcher</artifactId>
-      <scope>compile</scope>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.mockito</groupId>
diff --git a/keycloak-user-api/src/main/java/io/enmasse/user/keycloak/KeycloakUserApi.java b/keycloak-user-api/src/main/java/io/enmasse/user/keycloak/KeycloakUserApi.java
index 9474941eb..e2c5632c3 100644
--- a/keycloak-user-api/src/main/java/io/enmasse/user/keycloak/KeycloakUserApi.java
+++ b/keycloak-user-api/src/main/java/io/enmasse/user/keycloak/KeycloakUserApi.java
@@ -5,6 +5,8 @@
 package io.enmasse.user.keycloak;
 
 
+import static java.util.Optional.empty;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
@@ -13,7 +15,16 @@ import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.Duration;
 import java.time.Instant;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import javax.ws.rs.WebApplicationException;
@@ -31,25 +42,6 @@ import org.keycloak.representations.idm.UserRepresentation;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
 import io.enmasse.k8s.util.TimeUtil;
 import io.enmasse.user.api.UserApi;
 import io.enmasse.user.model.v1.Operation;
@@ -443,26 +435,103 @@ public class KeycloakUserApi implements UserApi {
         return withKeycloak(kc -> getRealmResource(kc, realmName) != null);
     }
 
-    @Override
-    public UserList listUsers(String namespace) {
+    private UserList queryUsers(final Predicate<RealmRepresentation> realmPredicate, final Predicate<UserRepresentation> userPredicate) {
         return withKeycloak(keycloak -> {
+
             List<RealmRepresentation> realmReps = keycloak.realms().findAll();
             UserList userList = new UserList();
+
             for (RealmRepresentation realmRep : realmReps) {
-                String realmNs = realmRep.getAttributes().get("namespace");
-                if (realmNs != null && realmNs.equals(namespace)) {
+                if (realmPredicate.test(realmRep)) {
                     String realm = realmRep.getRealm();
-                    List<UserRepresentation> userReps = keycloak.realm(realm).users().list();
-                    for (UserRepresentation userRep : userReps) {
-                        List<GroupRepresentation> groupReps = keycloak.realm(realm).users().get(userRep.getId()).groups();
-                        userList.getItems().add(buildUser(userRep, groupReps));
-                    }
+
+                    keycloak.realm(realm).users().list()
+                                    .stream()
+                                    .filter(userPredicate)
+                                    .forEachOrdered(userRep -> {
+                                        List<GroupRepresentation> groupReps = keycloak.realm(realm).users().get(userRep.getId()).groups();
+                                        userList.getItems().add(buildUser(userRep, groupReps));
+                                    });
                 }
             }
+
             return userList;
         });
     }
 
+    /**
+     * List all users, from all namespaces.
+     */
+    @Override
+    public UserList listAllUsers() {
+        return queryUsers(
+                        realm -> getAttribute(realm, "namespace").isPresent(),
+                        user -> true);
+    }
+
+    /**
+     * List users from a single namespace.
+     */
+    @Override
+    public UserList listUsers(final String namespace) {
+        return queryUsers(
+                        realm -> hasAttribute(realm, "namespace", namespace),
+                        user -> true);
+    }
+
+    static Optional<String> getAttribute(final RealmRepresentation realm, final String attributeName) {
+
+        if (realm == null || attributeName == null) {
+            // if either the realm is null or the attribute name, then we simply return nothing
+            return empty();
+        }
+
+        final Map<String, String> attributes = realm.getAttributes();
+        if (attributes == null) {
+            // there are no attributes, so no chance for "true"
+            return empty();
+        }
+
+        return Optional.ofNullable(attributes.get(attributeName));
+    }
+
+    /**
+     * Test if a realm as has specific attribute.
+     * 
+     * @param realm The realm to test.
+     * @param attributeName The attribute to test.
+     * @param attributeValue The expected value. May be {@code null}, in which case the attribute value
+     *        is to be expected {@code null} or not set.
+     * @return The methods return {@code true} either if the value attributeName parameter is
+     *         {@code null} and the attribute is either missing or {@code null}, or if the attribute
+     *         value is set and equal to the parameter "attributeValue".
+     */
+    static boolean hasAttribute(final RealmRepresentation realm, final String attributeName, final String attributeValue) {
+
+        if (realm == null || attributeName == null) {
+            return false;
+        }
+
+        final Optional<String> value = getAttribute(realm, attributeName);
+
+        final String v = value.orElse(null);
+
+        if (v == attributeValue) {
+            // do a check on the object reference, catches "null == null"
+            return true;
+        }
+
+        if (v == null) {
+            // attribute value is null, but due to the previous check we know
+            // that the required attribute value is non-null, so it doesn't match
+            return false;
+        }
+
+        // do a proper check
+
+        return v.equals(attributeValue);
+
+    }
 
     static User buildUser(UserRepresentation userRep, List<GroupRepresentation> groupReps) {
         log.debug("Creating user from user representation id {}, name {} part of groups {}", userRep.getId(), userRep.getUsername(), userRep.getGroups());
@@ -524,34 +593,27 @@ public class KeycloakUserApi implements UserApi {
                 .build();
     }
 
+    private static boolean matchesLabels (final UserRepresentation userRep, final Map<String,String> labels) {
+        for (final Map.Entry<String, String> label : labels.entrySet()) {
+            if (userRep.getAttributes().get(label.getKey()) == null || !label.getValue().equals(userRep.getAttributes().get(label.getKey()).get(0))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     @Override
     public UserList listUsersWithLabels(String namespace, Map<String, String> labels) {
-        return withKeycloak(keycloak -> {
+        return queryUsers(
+                        realm -> hasAttribute(realm, "namespace", namespace),
+                        user -> matchesLabels(user, labels));
+    }
 
-            List<RealmRepresentation> realmReps = keycloak.realms().findAll();
-            UserList userList = new UserList();
-            for (RealmRepresentation realmRep : realmReps) {
-                String realmNs = realmRep.getAttributes().get("namespace");
-                if (realmNs != null && realmNs.equals(namespace)) {
-                    String realm = realmRep.getRealm();
-                    List<UserRepresentation> userReps = keycloak.realm(realm).users().list().stream()
-                            .filter(userRep -> {
-                                for (Map.Entry<String, String> label : labels.entrySet()) {
-                                    if (userRep.getAttributes().get(label.getKey()) == null || !label.getValue().equals(userRep.getAttributes().get(label.getKey()).get(0))) {
-                                        return false;
-                                    }
-                                }
-                                return true;
-                            }).collect(Collectors.toList());
-
-                    for (UserRepresentation userRep : userReps) {
-                        List<GroupRepresentation> groupReps = keycloak.realm(realm).users().get(userRep.getId()).groups();
-                        userList.getItems().add(buildUser(userRep, groupReps));
-                    }
-                }
-            }
-            return userList;
-        });
+    @Override
+    public UserList listAllUsersWithLabels(final Map<String, String> labels) {
+        return queryUsers(
+                        realm -> getAttribute(realm, "namespace").isPresent(),
+                        user -> matchesLabels(user, labels));
     }
 
     @Override
diff --git a/mqtt-lwt/pom.xml b/mqtt-lwt/pom.xml
index c333ca09e..6bd169794 100644
--- a/mqtt-lwt/pom.xml
+++ b/mqtt-lwt/pom.xml
@@ -31,6 +31,7 @@
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-api</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
@@ -45,7 +46,7 @@
     <dependency>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-launcher</artifactId>
-      <scope>compile</scope>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>io.vertx</groupId>
diff --git a/pom.xml b/pom.xml
index 9eb31a0a4..749e0ea36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -534,6 +534,11 @@
                 <artifactId>junit-jupiter-engine</artifactId>
                 <version>${junit.jupiter.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.junit.jupiter</groupId>
+                <artifactId>junit-jupiter-params</artifactId>
+                <version>${junit.jupiter.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.junit.platform</groupId>
                 <artifactId>junit-platform-launcher</artifactId>
diff --git a/router/Dockerfile b/router/Dockerfile
index dfe4104e5..fa3be7f7c 100644
--- a/router/Dockerfile
+++ b/router/Dockerfile
@@ -1,4 +1,4 @@
-FROM enmasseproject/qdrouterd-base:1.5-SNAPSHOT-DISPATCH-1181-2
+FROM enmasseproject/qdrouterd-base:1.5-rc2
 ARG version
 ARG commit
 ENV VERSION=${version} COMMIT=${commit} TZ=GMT0
diff --git a/standard-controller/pom.xml b/standard-controller/pom.xml
index 5050eabe1..5646c3b48 100644
--- a/standard-controller/pom.xml
+++ b/standard-controller/pom.xml
@@ -66,6 +66,7 @@
     <dependency>
       <groupId>org.hamcrest</groupId>
       <artifactId>hamcrest-all</artifactId>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.junit.platform</groupId>
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
index c380bd5f9..7cf4d75d9 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
@@ -100,6 +100,10 @@ public class AddressController implements Watcher<Address> {
 
         AddressProvisioner provisioner = new AddressProvisioner(addressSpaceResolver, addressResolver, addressSpacePlan, clusterGenerator, kubernetes, eventLogger, options.getInfraUuid(), brokerIdGenerator);
 
+        for (Address address : addressList) {
+            address.getStatus().setReady(true).clearMessages();
+        }
+
         Map<Status.Phase, Long> countByPhase = countPhases(addressSet);
         log.info("Total: {}, Active: {}, Configuring: {}, Pending: {}, Terminating: {}, Failed: {}", addressSet.size(), countByPhase.get(Active), countByPhase.get(Configuring), countByPhase.get(Pending), countByPhase.get(Terminating), countByPhase.get(Failed));
 
@@ -134,6 +138,7 @@ public class AddressController implements Watcher<Address> {
             }
         }
 
+        checkAndMoveMigratingBrokersToDraining(addressSet, clusterList);
         checkAndRemoveDrainingBrokers(addressSet);
 
         deprovisionUnused(clusterList, filterByNotPhases(addressSet, EnumSet.of(Terminating)));
@@ -316,6 +321,32 @@ public class AddressController implements Watcher<Address> {
         }
     }
 
+    private void checkAndMoveMigratingBrokersToDraining(Set<Address> addresses, List<BrokerCluster> brokerList) throws Exception {
+        for (Address address : addresses) {
+            int numActive = 0;
+            int numReadyActive = 0;
+            for (BrokerStatus brokerStatus : address.getStatus().getBrokerStatuses()) {
+                if (BrokerState.Active.equals(brokerStatus.getState())) {
+                    numActive++;
+                    for (BrokerCluster cluster : brokerList) {
+                        if (brokerStatus.getClusterId().equals(cluster.getClusterId()) && cluster.getReadyReplicas() > 0) {
+                            numReadyActive++;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            if (numActive == numReadyActive) {
+                for (BrokerStatus brokerStatus : address.getStatus().getBrokerStatuses()) {
+                    if (BrokerState.Migrating.equals(brokerStatus.getState())) {
+                        brokerStatus.setState(BrokerState.Draining);
+                    }
+                }
+            }
+        }
+    }
+
     private void checkAndRemoveDrainingBrokers(Set<Address> addresses) throws Exception {
         BrokerStatusCollector brokerStatusCollector = new BrokerStatusCollector(kubernetes, brokerClientFactory);
         for (Address address : addresses) {
@@ -344,9 +375,6 @@ public class AddressController implements Watcher<Address> {
         if (addresses.isEmpty()) {
             return numOk;
         }
-        for (Address address : addresses) {
-            address.getStatus().setReady(true).clearMessages();
-        }
         RouterStatusCollector routerStatusCollector = new RouterStatusCollector(vertx, options.getCertDir());
         List<RouterStatus> routerStatusList = new ArrayList<>();
         for (Pod router : kubernetes.listRouters()) {
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java
index d91ed7486..057bb3ec1 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java
@@ -148,11 +148,23 @@ public class AddressProvisioner {
             Map<String, Map<String, UsageInfo>> newUsageMap, Map<String, Double> limits) {
         for (Address address : pending) {
             if (!Status.Phase.Configuring.equals(address.getStatus().getPhase())) {
+                Status previousStatus = new Status(address.getStatus());
+                String previousBrokerId = address.getAnnotation(AnnotationKeys.BROKER_ID);
+                String previousClusterId = address.getAnnotation(AnnotationKeys.CLUSTER_ID);
+
                 Map<String, Map<String, UsageInfo>> neededMap = checkQuotaForAddress(limits, newUsageMap, address, all);
                 if (neededMap != null) {
                     newUsageMap = neededMap;
                     address.getStatus().setPhase(Status.Phase.Configuring);
                     address.putAnnotation(AnnotationKeys.APPLIED_PLAN, address.getPlan());
+                } else {
+                    if (previousBrokerId != null) {
+                        address.putAnnotation(AnnotationKeys.BROKER_ID, previousBrokerId);
+                    }
+                    if (previousClusterId != null) {
+                        address.putAnnotation(AnnotationKeys.CLUSTER_ID, previousClusterId);
+                    }
+                    address.getStatus().setBrokerStatuses(previousStatus.getBrokerStatuses());
                 }
             }
         }
@@ -299,11 +311,7 @@ public class AddressProvisioner {
                         address.removeAnnotation(AnnotationKeys.BROKER_ID);
                     }
                     String clusterId = getShardedClusterId(address);
-                    UsageInfo info = resourceUsage.get(clusterId);
-                    if (info != null && address.getAnnotation(AnnotationKeys.APPLIED_PLAN) != null) {
-                        throw new IllegalArgumentException("Found unexpected conflicting usage for address " + address.getName());
-                    }
-                    info = new UsageInfo();
+                    UsageInfo info = new UsageInfo();
                     info.addUsed(resourceRequest.getCredit());
                     resourceUsage.put(clusterId, info);
 
@@ -339,7 +347,6 @@ public class AddressProvisioner {
             double resourceNeeded = sumNeeded(resourceUsage);
             if (resourceNeeded > limits.get(resourceName)) {
                 log.info("address {} for {} needed {} > limit {}", address.getAddress(), resourceName, resourceNeeded, limits.get(resourceRequest.getName()));
-                address.getStatus().setPhase(Status.Phase.Pending);
                 address.getStatus().appendMessage("Quota exceeded");
                 return null;
             }
@@ -350,7 +357,6 @@ public class AddressProvisioner {
         double totalNeeded = sumTotalNeeded(needed);
         if (totalNeeded > limits.get("aggregate")) {
             log.info("address {} usage {}, total needed {} > limit {}", address.getAddress(), usage, totalNeeded, limits.get("aggregate"));
-            address.getStatus().setPhase(Status.Phase.Pending);
             address.getStatus().appendMessage("Quota exceeded");
             return null;
         }
@@ -405,7 +411,7 @@ public class AddressProvisioner {
                 }
             }
             if (!found) {
-                brokerStatus.setState(BrokerState.Draining);
+                brokerStatus.setState(BrokerState.Migrating);
             }
         }
 
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/BrokerCluster.java b/standard-controller/src/main/java/io/enmasse/controller/standard/BrokerCluster.java
index 6ac559236..29ef8f3b3 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/BrokerCluster.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/BrokerCluster.java
@@ -11,12 +11,15 @@ import io.enmasse.config.AnnotationKeys;
 import io.fabric8.kubernetes.api.model.HasMetadata;
 import io.fabric8.kubernetes.api.model.KubernetesList;
 import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.DeploymentStatus;
 import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.fabric8.kubernetes.api.model.apps.StatefulSetStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Represents a cluster of resources for a given destination.
@@ -52,14 +55,27 @@ public class BrokerCluster {
     private final int replicas;
     private KubernetesList resources;
     private int newReplicas;
+    private final int readyReplicas;
 
     public BrokerCluster(String clusterId, KubernetesList resources) {
         this.clusterId = clusterId;
         this.resources = resources;
         this.replicas = findReplicas(resources.getItems());
+        this.readyReplicas = findReadyReplicas(resources.getItems());
         this.newReplicas = replicas;
     }
 
+    private int findReadyReplicas(List<HasMetadata> items) {
+        for (HasMetadata item : items) {
+            if (item instanceof StatefulSet) {
+                return Optional.ofNullable(((StatefulSet)item).getStatus()).map(StatefulSetStatus::getReadyReplicas).orElse(0);
+            } else if (item instanceof Deployment) {
+                return Optional.ofNullable(((Deployment)item).getStatus()).map(DeploymentStatus::getReadyReplicas).orElse(0);
+            }
+        }
+        return 0;
+    }
+
     private int findReplicas(List<HasMetadata> items) {
         for (HasMetadata item : items) {
             if (item instanceof StatefulSet) {
@@ -123,4 +139,8 @@ public class BrokerCluster {
             }
         }
     }
+
+    public int getReadyReplicas() {
+        return readyReplicas;
+    }
 }


Test patch:
diff --git a/api-model/src/test/java/io/enmasse/address/model/v1/address/KubeUtilTest.java b/api-model/src/test/java/io/enmasse/address/model/v1/address/KubeUtilTest.java
index 0c1925299..b4e4187d8 100644
--- a/api-model/src/test/java/io/enmasse/address/model/v1/address/KubeUtilTest.java
+++ b/api-model/src/test/java/io/enmasse/address/model/v1/address/KubeUtilTest.java
@@ -5,13 +5,28 @@
 package io.enmasse.address.model.v1.address;
 
 import io.enmasse.address.model.KubeUtil;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.params.provider.Arguments.of;
+
+import java.util.stream.Stream;
+
+import org.hamcrest.Matchers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
+import static java.util.stream.Stream.concat;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 
 public class KubeUtilTest {
+
     @Test
     public void testLeaveSpaceForPodIdentifier() {
         String address = "receiver-round-robincli_rhearatherlongaddresswhichcanbeverylongblablabla";
@@ -20,4 +35,70 @@ public class KubeUtilTest {
         String id2 = KubeUtil.sanitizeName(id);
         assertThat(id, is(id2));
     }
+
+    @Test
+    public void testNull() {
+        assertNull(KubeUtil.sanitizeName(null));
+        assertNull(KubeUtil.sanitizeUserName(null));
+    }
+
+    protected static Stream<Arguments> commonNames() {
+        return Stream.of(
+                        of("foo", "foo"),
+                        of("-foo", "1foo"),
+                        of("foo-", "foo1"),
+                        of("-foo-", "1foo1"),
+                        of("--foo--", "1-foo-1"),
+                        of("--f-o-o--", "1-f-o-o-1"),
+
+                        of("foo#bar", "foobar"),
+                        of("#foo#bar", "foobar"),
+                        of("foo#bar#", "foobar"),
+                        of("#foo#bar#", "foobar"),
+
+                        of("foo##bar", "foobar"),
+                        of("foo#&&#bar", "foobar"),
+                        of("foo bar", "foobar"),
+
+                        of("###", ""),
+
+                        of("", "")
+
+                        );
+    }
+    
+    protected static Stream<Arguments> addressNames() {
+        return concat(
+                        commonNames(),
+                        Stream.of(
+                                        of("foo@bar", "foobar"),
+                                        of("foo.bar", "foobar")
+                                        ));
+    }
+
+    @ParameterizedTest
+    @MethodSource("addressNames")
+    public void testSanitizeAddressName(final String input, final String output) {
+        final String result = KubeUtil.sanitizeName(input);
+
+        assertThat(result.length(), Matchers.lessThanOrEqualTo(60));
+        assertEquals(output, result);
+
+    }
+
+    protected static Stream<Arguments> userNames() {
+        return concat(
+                        commonNames(),
+                        Stream.of(
+                                        of("foo@bar", "foo@bar"),
+                                        of("foo.bar", "foo.bar")
+                                        ));
+    }
+
+    @ParameterizedTest
+    @MethodSource("userNames")
+    public void testSanitizeUserName(final String input, final String output) {
+        assertEquals(output, KubeUtil.sanitizeUserName(input));
+    }
+
 }
diff --git a/api-server/src/test/java/io/enmasse/api/server/HTTPServerTest.java b/api-server/src/test/java/io/enmasse/api/server/HTTPServerTest.java
index 6d8fc53c1..149d5825c 100644
--- a/api-server/src/test/java/io/enmasse/api/server/HTTPServerTest.java
+++ b/api-server/src/test/java/io/enmasse/api/server/HTTPServerTest.java
@@ -50,8 +50,7 @@ public class HTTPServerTest {
     public void setup(VertxTestContext context) throws InterruptedException {
         vertx = Vertx.vertx();
         addressSpaceApi = new TestAddressSpaceApi();
-        String addressSpaceName = "myinstance";
-        addressSpace = createAddressSpace(addressSpaceName);
+        addressSpace = createAddressSpace("ns", "myinstance");
         addressSpaceApi.createAddressSpace(addressSpace);
 
         AuthApi authApi = mock(AuthApi.class);
@@ -92,10 +91,10 @@ public class HTTPServerTest {
         vertx.close(context.succeeding(arg -> context.completeNow()));
     }
 
-    private AddressSpace createAddressSpace(String name) {
+    private AddressSpace createAddressSpace(String namespace, String name) {
         return new AddressSpace.Builder()
                 .setName(name)
-                .setNamespace(name)
+                .setNamespace(namespace)
                 .setType("type1")
                 .setPlan("myplan")
                 .setStatus(new io.enmasse.address.model.AddressSpaceStatus(false))
@@ -136,6 +135,7 @@ public class HTTPServerTest {
                         JsonObject data = buffer.toJsonObject();
                         context.verify(() -> {
                             assertTrue(data.containsKey("items"));
+                            assertEquals(1, data.getJsonArray("items").size());
                             assertEquals("myinstance.addr1", data.getJsonArray("items").getJsonObject(0).getJsonObject("metadata").getString("name"));
                         });
                         context.completeNow();
diff --git a/api-server/src/test/java/io/enmasse/api/v1/http/HttpNestedAddressServiceTest.java b/api-server/src/test/java/io/enmasse/api/v1/http/HttpNestedAddressServiceTest.java
index 0505b4703..7783ac05a 100644
--- a/api-server/src/test/java/io/enmasse/api/v1/http/HttpNestedAddressServiceTest.java
+++ b/api-server/src/test/java/io/enmasse/api/v1/http/HttpNestedAddressServiceTest.java
@@ -51,6 +51,7 @@ public class HttpNestedAddressServiceTest {
 
         AddressSpace addressSpace = new AddressSpace.Builder()
                 .setName("myspace")
+                .setNamespace("ns")
                 .setType("type1")
                 .setPlan("myplan")
                 .build();
diff --git a/api-server/src/test/java/io/enmasse/api/v1/http/TestUserApi.java b/api-server/src/test/java/io/enmasse/api/v1/http/TestUserApi.java
index 17772036e..60f09321e 100644
--- a/api-server/src/test/java/io/enmasse/api/v1/http/TestUserApi.java
+++ b/api-server/src/test/java/io/enmasse/api/v1/http/TestUserApi.java
@@ -92,6 +92,26 @@ public class TestUserApi implements UserApi {
         return listUsers(namespace);
     }
 
+    @Override
+    public UserList listAllUsers() {
+        if (throwException) {
+            throw new RuntimeException("exception");
+        }
+        UserList list = new UserList();
+        for (Map<String, User> users : userMap.values()) {
+            list.getItems().addAll(users.values());
+        }
+        return list;
+    }
+
+    @Override
+    public UserList listAllUsersWithLabels(Map<String, String> labels) {
+        if (throwException) {
+            throw new RuntimeException("exception");
+        }
+        return listAllUsers();
+    }
+
     @Override
     public void deleteUsers(String namespace) {
         if (throwException) {
diff --git a/keycloak-controller/src/test/java/io/enmasse/keycloak/controller/KeycloakManagerTest.java b/keycloak-controller/src/test/java/io/enmasse/keycloak/controller/KeycloakManagerTest.java
index a4c087b14..49b272d78 100644
--- a/keycloak-controller/src/test/java/io/enmasse/keycloak/controller/KeycloakManagerTest.java
+++ b/keycloak-controller/src/test/java/io/enmasse/keycloak/controller/KeycloakManagerTest.java
@@ -92,6 +92,16 @@ public class KeycloakManagerTest {
                 return null;
             }
 
+            @Override
+            public UserList listAllUsers() {
+                return null;
+            }
+
+            @Override
+            public UserList listAllUsersWithLabels(Map<String, String> labels) {
+                return null;
+            }
+
             @Override
             public void deleteUsers(String namespace) {
 
diff --git a/keycloak-user-api/src/test/java/io/enmasse/user/keycloak/KeycloakUserApiTest.java b/keycloak-user-api/src/test/java/io/enmasse/user/keycloak/KeycloakUserApiTest.java
index ac7db504d..af1adf932 100644
--- a/keycloak-user-api/src/test/java/io/enmasse/user/keycloak/KeycloakUserApiTest.java
+++ b/keycloak-user-api/src/test/java/io/enmasse/user/keycloak/KeycloakUserApiTest.java
@@ -10,6 +10,7 @@ import static java.util.Collections.emptyList;
 import static java.util.stream.Collectors.toList;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -22,6 +23,7 @@ import java.util.Set;
 
 import org.junit.jupiter.api.Test;
 import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 
 import io.enmasse.user.model.v1.Operation;
@@ -317,4 +319,34 @@ public class KeycloakUserApiTest {
         assertSorted(expected, actual.toArray(new String[actual.size()]));
     }
 
+    @Test
+    public void testHasAttribute1() {
+        final RealmRepresentation realm = new RealmRepresentation();
+        realm.setAttributes(new HashMap<>());
+        realm.getAttributes().put("foo", "bar");
+
+        assertTrue(KeycloakUserApi.hasAttribute(realm, "foo", "bar"));
+        assertFalse(KeycloakUserApi.hasAttribute(realm, "foo", null));
+        assertFalse(KeycloakUserApi.hasAttribute(realm, "foo2", "bar"));
+        assertTrue(KeycloakUserApi.hasAttribute(realm, "foo2", null));
+
+        assertFalse(KeycloakUserApi.hasAttribute(realm, null, null));
+
+    }
+
+    @Test
+    public void testHasAttribute2() {
+        assertFalse(KeycloakUserApi.hasAttribute(null, "foo", "bar"));
+        assertFalse(KeycloakUserApi.hasAttribute(null, "foo", null));
+        assertFalse(KeycloakUserApi.hasAttribute(null, null, null));
+    }
+
+    @Test
+    public void testHasAttribute3() {
+        final RealmRepresentation realm = new RealmRepresentation();
+
+        assertFalse(KeycloakUserApi.hasAttribute(realm, "foo", "bar"));
+        assertTrue(KeycloakUserApi.hasAttribute(realm, "foo", null));
+        assertFalse(KeycloakUserApi.hasAttribute(realm, null, null));
+    }
 }
diff --git a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
index 3cc67f590..0aa5bb3a3 100644
--- a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
+++ b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
@@ -16,6 +16,7 @@ import io.enmasse.metrics.api.Metrics;
 import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
 import io.fabric8.kubernetes.api.model.KubernetesList;
 import io.fabric8.kubernetes.api.model.KubernetesListBuilder;
+import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
 import io.fabric8.openshift.client.OpenShiftClient;
 import io.vertx.core.Vertx;
 import org.junit.jupiter.api.BeforeEach;
@@ -110,15 +111,93 @@ public class AddressControllerTest {
                         .endMetadata()
                         .build())
                 .build();
+
+        KubernetesList newList = new KubernetesListBuilder()
+                .addToStatefulSetItems(new StatefulSetBuilder()
+                        .editOrNewMetadata()
+                        .withName("broker-infra-0")
+                        .endMetadata()
+                        .editOrNewSpec()
+                        .withReplicas(1)
+                        .endSpec()
+                        .editOrNewStatus()
+                        .withReadyReplicas(1)
+                        .endStatus()
+                        .build())
+                .build();
+
         when(mockHelper.listClusters()).thenReturn(Arrays.asList(
-                new BrokerCluster("broker-infra-0", new KubernetesList()),
+                new BrokerCluster("broker-infra-0", newList),
                 new BrokerCluster("broker-infra-1", oldList)));
 
         controller.onUpdate(Arrays.asList(alive));
 
+        assertEquals(1, alive.getStatus().getBrokerStatuses().size());
+        assertEquals("broker-infra-0", alive.getStatus().getBrokerStatuses().get(0).getClusterId());
         verify(mockHelper).delete(any());
         verify(mockHelper).delete(eq(oldList));
-        assertEquals(1, alive.getStatus().getBrokerStatuses().size());
+    }
+
+    @Test
+    public void testMovesBrokersToDrained() throws Exception {
+        Address alive = new Address.Builder()
+                .setName("q1")
+                .setAddress("q1")
+                .setAddressSpace("myspace")
+                .setNamespace("ns")
+                .setType("queue")
+                .setPlan("small-queue")
+                .setStatus(new Status(true).setPhase(Status.Phase.Active)
+                        .appendBrokerStatus(new BrokerStatus("broker-infra-0", "broker-infra-0-0", BrokerState.Migrating))
+                        .appendBrokerStatus(new BrokerStatus("broker-infra-1", "broker-infra-1-0", BrokerState.Active)))
+                .build();
+
+        KubernetesList oldList = new KubernetesListBuilder()
+                .addToStatefulSetItems(new StatefulSetBuilder()
+                        .editOrNewMetadata()
+                        .withName("broker-infra-0")
+                        .endMetadata()
+                        .editOrNewSpec()
+                        .withReplicas(1)
+                        .endSpec()
+                        .editOrNewStatus()
+                        .withReadyReplicas(0)
+                        .endStatus()
+                        .build())
+                .build();
+
+        when(mockHelper.listClusters()).thenReturn(Arrays.asList(
+                new BrokerCluster("broker-infra-0", oldList),
+                new BrokerCluster("broker-infra-1", oldList)));
+
+        controller.onUpdate(Arrays.asList(alive));
+
+        assertEquals(2, alive.getStatus().getBrokerStatuses().size());
         assertEquals("broker-infra-0", alive.getStatus().getBrokerStatuses().get(0).getClusterId());
+        assertEquals(BrokerState.Migrating, alive.getStatus().getBrokerStatuses().get(0).getState());
+
+        oldList = new KubernetesListBuilder()
+                .addToStatefulSetItems(new StatefulSetBuilder()
+                        .editOrNewMetadata()
+                        .withName("broker-infra-0")
+                        .endMetadata()
+                        .editOrNewSpec()
+                        .withReplicas(1)
+                        .endSpec()
+                        .editOrNewStatus()
+                        .withReadyReplicas(1)
+                        .endStatus()
+                        .build())
+                .build();
+
+        when(mockHelper.listClusters()).thenReturn(Arrays.asList(
+                new BrokerCluster("broker-infra-0", oldList),
+                new BrokerCluster("broker-infra-1", oldList)));
+
+        controller.onUpdate(Arrays.asList(alive));
+
+        assertEquals(1, alive.getStatus().getBrokerStatuses().size());
+        assertEquals("broker-infra-1", alive.getStatus().getBrokerStatuses().get(0).getClusterId());
+        assertEquals(BrokerState.Active, alive.getStatus().getBrokerStatuses().get(0).getState());
     }
 }
diff --git a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
index cf0b65c37..8e198b6e6 100644
--- a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
+++ b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
@@ -18,6 +18,7 @@ import org.mockito.internal.util.collections.Sets;
 import java.util.*;
 import java.util.function.Consumer;
 
+import static io.enmasse.address.model.Status.Phase.Active;
 import static io.enmasse.address.model.Status.Phase.Configuring;
 import static io.enmasse.address.model.Status.Phase.Pending;
 import static java.util.Collections.singleton;
@@ -129,6 +130,7 @@ public class AddressProvisionerTest {
 
         assertThat(neededMap, is(usageMap));
         assertThat(largeQueue.getStatus().getPhase(), is(Pending));
+        assertTrue(largeQueue.getStatus().getMessages().contains("Quota exceeded"));
 
         Address smallQueue = createQueue("q4", "small-queue");
         neededMap = provisioner.checkQuota(usageMap, Sets.newSet(smallQueue), Sets.newSet(smallQueue));
@@ -355,6 +357,85 @@ public class AddressProvisionerTest {
         assertEquals(q1.getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
     }
 
+    @Test
+    public void testSwitchShardedAddressPlan() throws Exception {
+
+        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+                new ResourceAllowance("broker", 3),
+                new ResourceAllowance("router", 1),
+                new ResourceAllowance("aggregate", 4)));
+
+        Address q1 = createQueue("q1", "large-queue");
+        Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(Collections.emptySet());
+
+        provisioner.checkQuota(usageMap, Sets.newSet(q1), Sets.newSet(q1));
+        assertEquals(q1.getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+
+        q1 = new Address.Builder(q1)
+                .setPlan("xlarge-queue")
+                .build();
+
+        q1.getStatus().setPhase(Active);
+
+        usageMap = provisioner.checkUsage(Sets.newSet(q1));
+        Map<String, Map<String, UsageInfo>> neededMap = provisioner.checkQuota(usageMap, Sets.newSet(q1), Sets.newSet(q1));
+        assertEquals(q1.getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+    }
+
+    @Test
+    public void testSwitchPooledToShardedQuotaCheck() throws Exception {
+
+        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+                new ResourceAllowance("broker", 1),
+                new ResourceAllowance("router", 1),
+                new ResourceAllowance("aggregate", 4)));
+
+        Address q1 = createQueue("q1", "small-queue");
+        Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(Collections.emptySet());
+
+        provisioner.checkQuota(usageMap, Sets.newSet(q1), Sets.newSet(q1));
+        assertEquals(q1.getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+
+        q1.getStatus().setPhase(Active);
+        q1 = new Address.Builder(q1)
+                .setPlan("large-queue")
+                .build();
+
+
+        usageMap = provisioner.checkUsage(Sets.newSet(q1));
+        provisioner.checkQuota(usageMap, Sets.newSet(q1), Sets.newSet(q1));
+        assertTrue(q1.getStatus().getMessages().contains("Quota exceeded"));
+        assertThat(q1.getStatus().getPhase(), is(Active));
+        assertNotEquals(q1.getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+    }
+
+    @Test
+    public void testSwitchShardedToShardedQuotaCheck() throws Exception {
+
+        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+                new ResourceAllowance("broker", 2),
+                new ResourceAllowance("router", 1),
+                new ResourceAllowance("aggregate", 4)));
+
+        Address q1 = createQueue("q1", "large-queue");
+        Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(Collections.emptySet());
+
+        provisioner.checkQuota(usageMap, Sets.newSet(q1), Sets.newSet(q1));
+        assertEquals(q1.getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+
+        q1.getStatus().setPhase(Active);
+        q1 = new Address.Builder(q1)
+                .setPlan("xlarge-queue")
+                .build();
+
+
+        usageMap = provisioner.checkUsage(Sets.newSet(q1));
+        provisioner.checkQuota(usageMap, Sets.newSet(q1), Sets.newSet(q1));
+        assertTrue(q1.getStatus().getMessages().isEmpty());
+        assertThat(q1.getStatus().getPhase(), is(Configuring));
+        assertEquals(q1.getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+    }
+
     @Test
     public void testProvisioningShardedWithClusterId() throws Exception {
         final Set<Address> addresses = new HashSet<>();
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/AddressSpace.java b/systemtests/src/main/java/io/enmasse/systemtest/AddressSpace.java
index bac6f1985..51438ada7 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/AddressSpace.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/AddressSpace.java
@@ -23,6 +23,7 @@ public class AddressSpace {
     private AuthService authService;
     private List<AddressSpaceEndpoint> endpoints = new ArrayList<>();
     private Map<String, String> annotations = new HashMap<>();
+    private boolean useEndpointsInJson;
 
     public AddressSpace(String name) {
         this(name, name, AddressSpaceType.STANDARD, AuthService.NONE);
@@ -137,6 +138,11 @@ public class AddressSpace {
         this.endpoints = endpoints;
     }
 
+    public void setEndpoints(List<AddressSpaceEndpoint> endpoints, boolean useEndpointsInJson) {
+        this.endpoints = endpoints;
+        this.useEndpointsInJson = useEndpointsInJson;
+    }
+
     public String getName() {
         return name;
     }
@@ -220,19 +226,15 @@ public class AddressSpace {
         return addressSpaceString.toString();
     }
 
-    public JsonObject toJson(String version, boolean useEndpoints) {
+    public JsonObject toJson(String version) {
         JsonObject entry = new JsonObject();
         entry.put("apiVersion", version);
         entry.put("kind", "AddressSpace");
         entry.put("metadata", this.jsonMetadata());
-        entry.put("spec", this.jsonSpec(useEndpoints));
+        entry.put("spec", this.jsonSpec());
         return entry;
     }
 
-    public JsonObject toJson(String version) {
-        return toJson(version, false);
-    }
-
     public JsonObject jsonMetadata() {
         JsonObject metadata = new JsonObject();
         metadata.put("name", this.getName());
@@ -246,14 +248,14 @@ public class AddressSpace {
         return annotations;
     }
 
-    public JsonObject jsonSpec(boolean useEndpoints) {
+    public JsonObject jsonSpec() {
         JsonObject spec = new JsonObject();
         spec.put("type", this.getType().toString().toLowerCase());
         spec.put("plan", this.getPlan());
         JsonObject authService = new JsonObject();
         authService.put("type", this.getAuthService().toString());
         spec.put("authenticationService", authService);
-        if (!this.getEndpoints().isEmpty() && useEndpoints) {
+        if (!this.getEndpoints().isEmpty() && this.useEndpointsInJson) {
             spec.put("endpoints", this.jsonEndpoints());
         }
         return spec;
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java b/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java
index 49c06f97b..48f9bbcd2 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java
@@ -412,7 +412,7 @@ public class TestUtils {
         TimeoutBudget budget = null;
 
         boolean isReady = false;
-        budget = new TimeoutBudget(5, TimeUnit.MINUTES);
+        budget = new TimeoutBudget(10, TimeUnit.MINUTES);
         while (budget.timeLeft() >= 0 && !isReady) {
             addressSpaceObject = apiClient.getAddressSpace(addressSpace.getName());
             isReady = isAddressSpaceReady(addressSpaceObject);
@@ -439,7 +439,7 @@ public class TestUtils {
      */
     public static void waitForAddressSpacePlanApplied(AddressApiClient apiClient, AddressSpace addressSpace) throws Exception {
         JsonObject addressSpaceObject = null;
-        TimeoutBudget budget = new TimeoutBudget(5, TimeUnit.MINUTES);
+        TimeoutBudget budget = new TimeoutBudget(10, TimeUnit.MINUTES);
 
         boolean isPlanApplied = false;
         while (budget.timeLeft() >= 0 && !isPlanApplied) {
@@ -466,7 +466,7 @@ public class TestUtils {
      * @throws Exception
      */
     public static void waitForServiceInstanceReady(OSBApiClient apiClient, String username, String instanceId) throws Exception {
-        TimeoutBudget budget = new TimeoutBudget(5, TimeUnit.MINUTES);
+        TimeoutBudget budget = new TimeoutBudget(10, TimeUnit.MINUTES);
         boolean isReady = false;
         while (budget.timeLeft() >= 0 && !isReady) {
             isReady = isServiceInstanceReady(apiClient.getLastOperation(username, instanceId));
@@ -935,7 +935,7 @@ public class TestUtils {
      */
     public static void waitForAddressSpaceDeleted(Kubernetes kubernetes, AddressSpace addressSpace) throws Exception {
         log.info("Waiting for AddressSpace {} to be deleted", addressSpace);
-        TimeoutBudget budget = new TimeoutBudget(5, TimeUnit.MINUTES);
+        TimeoutBudget budget = new TimeoutBudget(10, TimeUnit.MINUTES);
         waitForItems(addressSpace, budget, () -> kubernetes.listPods(Collections.singletonMap("infraUuid", addressSpace.getInfraUuid())).size());
         waitForItems(addressSpace, budget, () -> kubernetes.listConfigMaps(Collections.singletonMap("infraUuid", addressSpace.getInfraUuid())).size());
         waitForItems(addressSpace, budget, () -> kubernetes.listServices(Collections.singletonMap("infraUuid", addressSpace.getInfraUuid())).size());
@@ -962,7 +962,7 @@ public class TestUtils {
      * @param namespace  project/namespace to remove
      */
     public static void waitForNamespaceDeleted(Kubernetes kubernetes, String namespace) throws Exception {
-        TimeoutBudget budget = new TimeoutBudget(5, TimeUnit.MINUTES);
+        TimeoutBudget budget = new TimeoutBudget(10, TimeUnit.MINUTES);
         while (budget.timeLeft() >= 0 && kubernetes.listNamespaces().contains(namespace)) {
             Thread.sleep(1000);
         }
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/apiclients/AddressApiClient.java b/systemtests/src/main/java/io/enmasse/systemtest/apiclients/AddressApiClient.java
index 238f58a40..0a133424f 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/apiclients/AddressApiClient.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/apiclients/AddressApiClient.java
@@ -331,9 +331,9 @@ public class AddressApiClient extends ApiClient {
 
     public void replaceAddress(String addressSpace, Destination destination, int expectedCode) throws Exception {
         String path = getAddressPath(addressSpace) + "/" + destination.getAddressName(addressSpace);
-        log.info("UPDATE-address: path {}", path);
         CompletableFuture<JsonObject> responsePromise = new CompletableFuture<>();
         JsonObject payload = destination.toJson(apiVersion, addressSpace);
+        log.info("UPDATE-address: path {}: {}", path, payload.toString());
         doRequestNTimes(initRetry, () -> {
                     client.put(endpoint.getPort(), endpoint.getHost(), path)
                             .timeout(20_000)
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java b/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java
index f073bbaec..6a58e057b 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java
@@ -38,8 +38,7 @@ import java.util.stream.IntStream;
 import static io.enmasse.systemtest.TestTag.isolated;
 import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
 import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.*;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
 
@@ -100,7 +99,8 @@ class ApiServerTest extends TestBase {
         addressSpace.setEndpoints(Arrays.asList(
                 new AddressSpaceEndpoint(endpointPrefix + "messaging", "messaging", "amqps"),
                 new AddressSpaceEndpoint(endpointPrefix + "console", "console", "https"),
-                new AddressSpaceEndpoint(endpointPrefix + "mqtt", "mqtt", "secure-mqtt")));
+                new AddressSpaceEndpoint(endpointPrefix + "mqtt", "mqtt", "secure-mqtt")),
+                true);
         createAddressSpace(addressSpace);
 
         UserCredentials luckyUser = new UserCredentials("lucky", "luckyPswd");
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/standard/QueueTest.java b/systemtests/src/test/java/io/enmasse/systemtest/standard/QueueTest.java
index 5df2d4203..c0b6f7fdb 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/standard/QueueTest.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/standard/QueueTest.java
@@ -203,6 +203,8 @@ public class QueueTest extends TestBaseWithShared implements ITestBaseStandard {
         final int totalNumMessages = numMessages * prefixes.size();
         final int numReceiveBeforeDraining = numMessages / 2;
         final int numReceivedAfterScaled = totalNumMessages - numReceiveBeforeDraining;
+        final int numReceivedAfterScaledPhase1 = numReceivedAfterScaled / 2;
+        final int numReceivedAfterScaledPhase2 = numReceivedAfterScaled - numReceivedAfterScaledPhase1;
 
         List<Future<Integer>> sent = prefixes.stream().map(prefix -> client.sendMessages(before.getAddress(), TestUtils.generateMessages(prefix, numMessages))).collect(Collectors.toList());
 
@@ -224,7 +226,12 @@ public class QueueTest extends TestBaseWithShared implements ITestBaseStandard {
 
         replaceAddress(getSharedAddressSpace(), after);
         // Receive messages sent before address was replaced
-        assertThat("Wrong count of messages received", client.recvMessages(after.getAddress(), numReceivedAfterScaled).get(1, TimeUnit.MINUTES).size(), is(numReceivedAfterScaled));
+        assertThat("Wrong count of messages received", client.recvMessages(after.getAddress(), numReceivedAfterScaledPhase1).get(1, TimeUnit.MINUTES).size(), is(numReceivedAfterScaledPhase1));
+
+        Thread.sleep(30_000);
+
+        // Give system a chance to do something stupid
+        assertThat("Wrong count of messages received", client.recvMessages(after.getAddress(), numReceivedAfterScaledPhase2).get(1, TimeUnit.MINUTES).size(), is(numReceivedAfterScaledPhase2));
 
         // Ensure send and receive works after address was replaced
         assertThat("Wrong count of messages sent", client.sendMessages(after.getAddress(), TestUtils.generateMessages(prefixes.get(0), numMessages)).get(1, TimeUnit.MINUTES), is(numMessages));