Repository: EnMasseProject/enmasse
Pull request #3219 (2019-09-19 15:07:39)

Cleanups for the managed strategy

ctron: @enmasse-ci run tests profile=shared-iot
enmasse-ci: :heavy_exclamation_mark: **Build Failed** :heavy_exclamation_mark:
ctron: @enmasse-ci run tests profile=shared-iot
enmasse-ci: ### :x: Test Summary :x:
**TEST_PROFILE**: shared-iot
**TEST_CASE:** 
**TOTAL:** 31
**PASS:** 26
**FAIL:** 2
**SKIP:** 3
### :heavy_exclamation_mark: Test Failures :heavy_exclamation_mark:
- testChangeAddressSpace in io.enmasse.systemtest.iot.project.ManagedTest
- testCreate in io.enmasse.systemtest.iot.IoTProjectManagedTest
ctron: @enmasse-ci run tests profile=shared-iot
enmasse-ci: ### :x: Test Summary :x:
**TEST_PROFILE**: shared-iot
**TEST_CASE:** 
**TOTAL:** 31
**PASS:** 27
**FAIL:** 1
**SKIP:** 3
### :heavy_exclamation_mark: Test Failures :heavy_exclamation_mark:
- testChangeAddressSpace in io.enmasse.systemtest.iot.project.ManagedTest
ctron: @enmasse-ci run tests profile=shared-iot
enmasse-ci: ### :heavy_check_mark: Test Summary :heavy_check_mark:
**TEST_PROFILE**: shared-iot
**TEST_CASE:** 
**TOTAL:** 30
**PASS:** 27
**FAIL:** 0
**SKIP:** 3

ctron: @enmasse-ci run tests profile=shared-iot
enmasse-ci: ### :heavy_check_mark: Test Summary :heavy_check_mark:
**TEST_PROFILE**: shared-iot
**TEST_CASE:** 
**TOTAL:** 30
**PASS:** 27
**FAIL:** 0
**SKIP:** 3


Patch:
diff --git a/api-model/pom.xml b/api-model/pom.xml
index a9e31938c..ed99e522c 100644
--- a/api-model/pom.xml
+++ b/api-model/pom.xml
@@ -50,6 +50,11 @@
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
diff --git a/api-model/src/main/java/io/enmasse/address/model/Address.java b/api-model/src/main/java/io/enmasse/address/model/Address.java
index 34a7cd791..2ba546601 100644
--- a/api-model/src/main/java/io/enmasse/address/model/Address.java
+++ b/api-model/src/main/java/io/enmasse/address/model/Address.java
@@ -6,7 +6,6 @@ package io.enmasse.address.model;
 
 import java.nio.charset.StandardCharsets;
 import java.util.*;
-
 import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
 
diff --git a/api-model/src/main/java/io/enmasse/address/model/AuthenticationService.java b/api-model/src/main/java/io/enmasse/address/model/AuthenticationService.java
index 3d5fedd93..18f5c7413 100644
--- a/api-model/src/main/java/io/enmasse/address/model/AuthenticationService.java
+++ b/api-model/src/main/java/io/enmasse/address/model/AuthenticationService.java
@@ -5,6 +5,8 @@
 package io.enmasse.address.model;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.google.common.base.MoreObjects.ToStringHelper;
+
 import io.enmasse.admin.model.v1.AbstractWithAdditionalProperties;
 import io.fabric8.kubernetes.api.model.Doneable;
 import io.sundr.builder.annotations.Buildable;
@@ -78,4 +80,12 @@ public class AuthenticationService extends AbstractWithAdditionalProperties {
     public int hashCode() {
         return Objects.hash(name, type, overrides);
     }
+
+    @Override
+    protected ToStringHelper toStringHelper() {
+        return super.toStringHelper()
+                .add("name", this.name)
+                .add("type", this.type)
+                .add("overrides", this.overrides);
+    }
 }
diff --git a/api-model/src/main/java/io/enmasse/address/model/AuthenticationServiceSettings.java b/api-model/src/main/java/io/enmasse/address/model/AuthenticationServiceSettings.java
index 15ecacb1c..d951ea4ae 100644
--- a/api-model/src/main/java/io/enmasse/address/model/AuthenticationServiceSettings.java
+++ b/api-model/src/main/java/io/enmasse/address/model/AuthenticationServiceSettings.java
@@ -5,6 +5,8 @@
 package io.enmasse.address.model;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.google.common.base.MoreObjects.ToStringHelper;
+
 import io.enmasse.admin.model.v1.AbstractWithAdditionalProperties;
 import io.fabric8.kubernetes.api.model.Doneable;
 import io.fabric8.kubernetes.api.model.SecretReference;
@@ -89,13 +91,13 @@ public class AuthenticationServiceSettings extends AbstractWithAdditionalPropert
     }
 
     @Override
-    public String toString() {
-        return "AuthenticationServiceOverrides{" +
-                "host='" + host + '\'' +
-                ", port=" + port +
-                ", realm='" + realm + '\'' +
-                ", caCertSecret='" + caCertSecret + '\'' +
-                ", clientCertSecret='" + realm + '\'' +
-                '}';
+    protected ToStringHelper toStringHelper() {
+        return super.toStringHelper()
+                .add("host", this.host)
+                .add("port", this.port)
+                .add("realm", this.realm)
+                .add("caCertSecert", this.caCertSecret)
+                .add("clientCertSecret", this.clientCertSecret);
     }
+
 }
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 01e74e721..2753d9d7f 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,6 +4,7 @@
  */
 package io.enmasse.address.model;
 
+import java.nio.charset.StandardCharsets;
 import java.util.*;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -17,6 +18,9 @@ import io.fabric8.kubernetes.api.model.apps.StatefulSet;
  * Various static utilities that don't belong in a specific place
  */
 public final class KubeUtil {
+
+    private static final byte [] GO_NAMESPACE = new byte []{(byte)0x15,(byte)0x16,(byte)0xb2,(byte)0x46,(byte)0x23,(byte)0xaa,(byte)0x11,(byte)0xe9,(byte)0xb6,(byte)0x15,(byte)0xc8,(byte)0x5b,(byte)0x76,(byte)0x2e,(byte)0x5a,(byte)0x2c};
+
     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\\-.@_]");
@@ -32,7 +36,7 @@ public final class KubeUtil {
         return sanitizeWithPattern(name, usernamePattern);
     }
 
-    private static String sanitizeWithPattern(String value, Pattern pattern) {
+    public static String sanitizeWithPattern(String value, Pattern pattern) {
         if (value == null) {
             return null;
         }
@@ -56,8 +60,8 @@ public final class KubeUtil {
         return clean;
     }
 
-    public static String sanitizeWithUuid(String name, String uuid) {
-        name = sanitizeName(name);
+    public static String sanitizeWithUuid(String name, String uuid, Pattern pattern) {
+        name = sanitizeWithPattern(name, pattern);
         if (name.length() + uuid.length() + 1 > MAX_KUBE_NAME) {
             name = name.substring(0, MAX_KUBE_NAME - uuid.length() - 1);
         }
@@ -65,6 +69,33 @@ public final class KubeUtil {
         return name;
     }
 
+    public static String sanitizeWithUuid(String name, String uuid) {
+        return sanitizeWithUuid(name, uuid, addressPattern);
+    }
+
+    /**
+     * Create the address name, aligned with the Go logic of creating names.
+     * <br>
+     * The main difference between Go and the Java method {@link #generateName(String, String)} is, that in Go
+     * you must use a namespace prefix for a type 3 UUID. This method uses the same prefix as the Go code.
+     *
+     * @param addressSpace The name of the address space.
+     * @param address The name of the address.
+     * @return The encoded name, compatible with the logic in Go.
+     */
+    public static String sanitizeForGo(final String addressSpace, final String address) {
+
+        byte [] addressBytes = address.getBytes(StandardCharsets.UTF_8);
+        byte [] data = new byte[addressBytes.length + GO_NAMESPACE.length];
+
+        System.arraycopy(GO_NAMESPACE, 0, data, 0, GO_NAMESPACE.length);
+        System.arraycopy(addressBytes, 0, data, GO_NAMESPACE.length, addressBytes.length);
+
+        String uuid = UUID.nameUUIDFromBytes(data).toString();
+
+        return KubeUtil.sanitizeName(addressSpace) + "." + KubeUtil.sanitizeWithUuid(address, uuid, addressPattern);
+    }
+
     public static String getAddressSpaceCaSecretName(AddressSpace addressSpace) {
         return sanitizeName("ca-" + addressSpace.getMetadata().getName() + "." + addressSpace.getAnnotation(AnnotationKeys.INFRA_UUID));
     }
diff --git a/api-model/src/main/java/io/enmasse/admin/model/v1/AbstractWithAdditionalProperties.java b/api-model/src/main/java/io/enmasse/admin/model/v1/AbstractWithAdditionalProperties.java
index 7ddf8b52c..5be0d29f1 100644
--- a/api-model/src/main/java/io/enmasse/admin/model/v1/AbstractWithAdditionalProperties.java
+++ b/api-model/src/main/java/io/enmasse/admin/model/v1/AbstractWithAdditionalProperties.java
@@ -10,6 +10,8 @@ import java.util.Map;
 
 import com.fasterxml.jackson.annotation.JsonAnyGetter;
 import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.MoreObjects.ToStringHelper;
 
 import io.fabric8.kubernetes.api.model.Doneable;
 import io.sundr.builder.annotations.Buildable;
@@ -42,4 +44,13 @@ public abstract class AbstractWithAdditionalProperties implements WithAdditional
         this.additionalProperties.put(name, value);
     }
 
+    protected ToStringHelper toStringHelper() {
+        return MoreObjects.toStringHelper(this)
+                .add("additionalProperties", this.additionalProperties);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper().toString();
+    }
 }
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/OperatorConfig.java b/api-model/src/main/java/io/enmasse/iot/model/v1/OperatorConfig.java
new file mode 100644
index 000000000..bf4437233
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/OperatorConfig.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package io.enmasse.iot.model.v1;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.Inline;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        inline = @Inline(
+                type = Doneable.class,
+                prefix = "Doneable",
+                value = "done"))
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class OperatorConfig extends ServiceConfig {
+    private ContainerConfig container;
+
+    public ContainerConfig getContainer() {
+        return container;
+    }
+    public void setContainer(ContainerConfig container) {
+        this.container = container;
+    }
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/ServicesConfig.java b/api-model/src/main/java/io/enmasse/iot/model/v1/ServicesConfig.java
index fe5e4c890..4e31c99f7 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/ServicesConfig.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/ServicesConfig.java
@@ -27,6 +27,7 @@ public class ServicesConfig {
     private AuthenticationServiceConfig authentication;
     private TenantServiceConfig tenant;
     private CollectorConfig collector;
+    private OperatorConfig operator;
 
     public DeviceRegistryServiceConfig getDeviceRegistry() {
         return deviceRegistry;
@@ -56,5 +57,11 @@ public class ServicesConfig {
         this.collector = collector;
     }
 
+    public OperatorConfig getOperator() {
+        return operator;
+    }
+    public void setOperator(OperatorConfig operator) {
+        this.operator = operator;
+    }
 
 }
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 191016867..e8a858e3f 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
@@ -49,7 +49,6 @@ import io.enmasse.user.model.v1.User;
 import io.enmasse.user.model.v1.UserBuilder;
 import io.enmasse.user.model.v1.UserCrd;
 import io.enmasse.user.model.v1.UserList;
-import io.enmasse.user.model.v1.UserSpecBuilder;
 import io.fabric8.kubernetes.api.model.ListMeta;
 import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
 
@@ -195,9 +194,9 @@ public class HttpUserService {
     private User setUserDefaults(User user, String namespace) {
         if (user.getMetadata().getNamespace() == null) {
             user = new UserBuilder(user)
-                    .withMetadata(new ObjectMetaBuilder(user.getMetadata())
-                            .withNamespace(namespace)
-                            .build())
+                    .editOrNewMetadata()
+                    .withNamespace(namespace)
+                    .endMetadata()
                     .build();
         }
         return user;
@@ -301,13 +300,20 @@ public class HttpUserService {
     }
 
     private User overrideNameAndNamespace(User existingUser, User replacement) {
-        replacement = new UserBuilder(replacement)
-                .withSpec(new UserSpecBuilder(replacement.getSpec())
-                        .withUsername(existingUser.getSpec().getUsername())
-                        .build())
-                .withMetadata(new ObjectMetaBuilder(existingUser.getMetadata())
-                        .withNamespace(existingUser.getMetadata().getNamespace()).build()).build();
-        return replacement;
+
+        return new UserBuilder(replacement)
+
+                .editOrNewSpec()
+                .withUsername(existingUser.getSpec().getUsername())
+                .endSpec()
+
+                .editOrNewMetadata()
+                .withName(existingUser.getMetadata().getName())
+                .withNamespace(existingUser.getMetadata().getNamespace())
+                .endMetadata()
+
+                .build();
+
     }
 
     @DELETE
diff --git a/cmd/iot-proxy-configurator/configurator.go b/cmd/iot-proxy-configurator/configurator.go
index 3fc0b822b..0b718019f 100644
--- a/cmd/iot-proxy-configurator/configurator.go
+++ b/cmd/iot-proxy-configurator/configurator.go
@@ -205,19 +205,33 @@ func (c *Configurator) syncHandler(key string) error {
 
 			log.Info("Item got deleted. Deleting configuration.")
 
-			err = c.deleteProject(&metav1.ObjectMeta{
+			// if something went wrong deleting, then returning
+			// and error will re-queue the item
+
+			return c.deleteProject(&metav1.ObjectMeta{
 				Namespace: namespace,
 				Name:      name,
 			})
 
-			// if something went wrong deleting, then returning
-			// and error will re-queue the item
-			return err
 		}
 
 		return err
 	}
 
+	// check if we are scheduled for deletion ...
+	if project.DeletionTimestamp != nil {
+
+		// ... yes we are, go ahead and delete
+
+		log.Info("Item scheduled for deletion. Deleting configuration.")
+
+		return c.deleteProject(&metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      name,
+		})
+
+	}
+
 	if !project.Status.IsReady || project.Status.DownstreamEndpoint == nil {
 		// project is not ready yet
 		return nil
diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh
index 0407a99c5..2f69d978b 100755
--- a/hack/update-codegen.sh
+++ b/hack/update-codegen.sh
@@ -15,6 +15,6 @@ GENERATOR_BASE=${SCRIPTPATH}/../vendor/k8s.io/code-generator
 "$GENERATOR_BASE/generate-groups.sh" "all" \
     github.com/enmasseproject/enmasse/pkg/client \
     github.com/enmasseproject/enmasse/pkg/apis \
-    "admin:v1beta1 enmasse:v1beta1 user:v1beta1 iot:v1alpha1" \
+    "admin:v1beta1 admin:v1beta2 enmasse:v1beta1 user:v1beta1 iot:v1alpha1" \
     --go-header-file "${SCRIPTPATH}/header.txt" \
     "$@"
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 498123628..3e58c4039 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,7 @@
 package io.enmasse.user.keycloak;
 
 
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import io.enmasse.admin.model.v1.AuthenticationService;
 import io.enmasse.k8s.util.TimeUtil;
@@ -40,6 +41,9 @@ import static java.util.Optional.empty;
 
 public class KeycloakUserApi implements UserApi, RealmApi {
 
+    private static final String ATTR_ANNOTATIONS = "annotations";
+    private static final String ATTR_OWNER_REFERENCES = "ownerReferences";
+
     private static final Logger log = LoggerFactory.getLogger(KeycloakUserApi.class);
 
     private final Clock clock;
@@ -160,16 +164,37 @@ public class KeycloakUserApi implements UserApi, RealmApi {
         attributes.put("authenticationType", Collections.singletonList(user.getSpec().getAuthentication().getType().name()));
 
         final List<String> ownerReferences = ownerReferencesToString(user.getMetadata().getOwnerReferences());
-        attributes.put("ownerReferences", ownerReferences);
+        attributes.put(ATTR_OWNER_REFERENCES, ownerReferences);
 
         Instant now = clock.instant();
         attributes.put("creationTimestamp", Collections.singletonList(TimeUtil.formatRfc3339(now)));
+        attributes.put(ATTR_ANNOTATIONS, annotationsToString(user.getMetadata().getAnnotations()));
 
         userRep.setAttributes(attributes);
 
         return userRep;
     }
 
+    static List<String> annotationsToString(final Map<String, String> annotations) {
+
+        if (annotations == null) {
+            return null;
+        }
+
+        final ObjectMapper mapper = new ObjectMapper();
+
+        return annotations.entrySet().stream()
+                .map(entry -> {
+                    try {
+                        return mapper.writeValueAsString(entry);
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                })
+                .collect(Collectors.toList());
+
+    }
+
     private static List<String> ownerReferencesToString(final List<OwnerReference> references) {
 
         if (references == null) {
@@ -178,7 +203,7 @@ public class KeycloakUserApi implements UserApi, RealmApi {
 
         final ObjectMapper mapper = new ObjectMapper();
 
-        final List<String> ownerReferences = references
+        return references
                 .stream()
                 .map(ownerReference -> {
                     try {
@@ -189,12 +214,31 @@ public class KeycloakUserApi implements UserApi, RealmApi {
                 })
                 .collect(Collectors.toList());
 
-        return ownerReferences;
+    }
+
+    static Map<String, String> annotationsFromString(final List<String> annotationValues) {
+
+        if (annotationValues == null || annotationValues.isEmpty()) {
+            return null;
+        }
+
+        final ObjectMapper mapper = new ObjectMapper();
+
+        return annotationValues
+                .stream().<Map.Entry<String, String>>map(ref -> {
+                    try {
+                        return mapper.readValue(ref, new TypeReference<Map.Entry<String, String>>() {});
+                    } catch (IOException e) {
+                        throw new RuntimeException(e);
+                    }
+                })
+                .collect(Collectors.<Map.Entry<String, String>, String, String>toMap(Map.Entry::getKey, Map.Entry::getValue));
+
     }
 
     private static List<OwnerReference> ownerReferencesFromString(final List<String> attributeValues) {
 
-        if (attributeValues == null) {
+        if (attributeValues == null || attributeValues.isEmpty() ) {
             return null;
         }
 
@@ -390,10 +434,47 @@ public class KeycloakUserApi implements UserApi, RealmApi {
                 }
             }
             applyAuthorizationRules(realm, user, realm.users().get(userRep.getId()));
+            applyAttributes(realm, user, userRep);
             return true;
         });
     }
 
+    private void applyAttributes(RealmResource realm, User user, UserRepresentation userRep) {
+        boolean changed = false;
+        if (updateAttribute(userRep, ATTR_ANNOTATIONS, annotationsToString(user.getMetadata().getAnnotations()))) {
+            changed = true;
+        }
+        if (updateAttribute(userRep, ATTR_OWNER_REFERENCES, ownerReferencesToString(user.getMetadata().getOwnerReferences()))) {
+            changed = true;
+        }
+        if (changed) {
+            realm.users().get(userRep.getId()).update(userRep);
+        }
+    }
+
+    private static boolean updateAttribute(final UserRepresentation userRep, final String key, List<String> value) {
+
+        final String[] currentValue = Optional.ofNullable(userRep.getAttributes().get(key))
+                .map(v -> v.toArray(String[]::new))
+                .orElseGet(()-> new String[] {});
+
+        final String[] newValue = Optional.ofNullable(value)
+                .map(v-> v.toArray(String[]::new))
+                .orElseGet(()-> new String[] {});
+
+        if (Arrays.equals(currentValue, newValue)) {
+            return false;
+        }
+
+        if (value.isEmpty()) {
+            userRep.getAttributes().remove(key);
+        } else {
+            userRep.getAttributes().put(key, value);
+        }
+
+        return true;
+    }
+
     private Optional<String> getGroupId(List<GroupRepresentation> groupRepresentations, String groupName) {
         for (GroupRepresentation groupRepresentation : groupRepresentations) {
             if (groupName.equals(groupRepresentation.getName())) {
@@ -622,7 +703,8 @@ public class KeycloakUserApi implements UserApi, RealmApi {
                         .withNamespace(namespace)
                         .withSelfLink("/apis/user.enmasse.io/v1beta1/namespaces/" + namespace + "/messagingusers/" + name)
                         .withCreationTimestamp(userRep.getAttributes().get("creationTimestamp").get(0))
-                        .withOwnerReferences(ownerReferencesFromString(userRep.getAttributes().get("ownerReferences")))
+                        .withOwnerReferences(ownerReferencesFromString(userRep.getAttributes().get(ATTR_OWNER_REFERENCES)))
+                        .withAnnotations(annotationsFromString(userRep.getAttributes().get(ATTR_ANNOTATIONS)))
                         .build())
                 .withSpec(new UserSpecBuilder()
                         .withUsername(userRep.getUsername())
diff --git a/pkg/apis/admin/v1beta2/doc.go b/pkg/apis/admin/v1beta2/doc.go
new file mode 100644
index 000000000..bdc6a64e4
--- /dev/null
+++ b/pkg/apis/admin/v1beta2/doc.go
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// +k8s:deepcopy-gen=package
+// +groupName=admin.enmasse.io
+
+package v1beta2
diff --git a/pkg/apis/admin/v1beta2/register.go b/pkg/apis/admin/v1beta2/register.go
new file mode 100644
index 000000000..d4897e6ef
--- /dev/null
+++ b/pkg/apis/admin/v1beta2/register.go
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package v1beta2
+
+import (
+	"github.com/enmasseproject/enmasse/pkg/apis/admin"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+const GroupVersion = "v1beta2"
+
+var SchemeGroupVersion = schema.GroupVersion{Group: admin.GroupName, Version: GroupVersion}
+
+func Kind(kind string) schema.GroupKind {
+	return SchemeGroupVersion.WithKind(kind).GroupKind()
+}
+
+func Resource(resource string) schema.GroupResource {
+	return SchemeGroupVersion.WithResource(resource).GroupResource()
+}
+
+var (
+	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+	AddToScheme   = SchemeBuilder.AddToScheme
+)
+
+func addKnownTypes(scheme *runtime.Scheme) error {
+	scheme.AddKnownTypes(SchemeGroupVersion,
+		&AddressPlan{},
+		&AddressPlanList{},
+		&AddressSpacePlan{},
+		&AddressSpacePlanList{},
+	)
+
+	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+	return nil
+}
diff --git a/pkg/apis/admin/v1beta2/types.go b/pkg/apis/admin/v1beta2/types.go
new file mode 100644
index 000000000..1d8f94b58
--- /dev/null
+++ b/pkg/apis/admin/v1beta2/types.go
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package v1beta2
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+/*
+ * Be careful with the comments in this file. The prefix "+" indicates that this is being processed
+ * by the client generator. The location, empty lines, and other comments in this file may confuse
+ * the generator, and produce a non-version version.
+ */
+
+// ** AddressPlan
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +genclient:noStatus
+
+type AddressPlan struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec AddressPlanSpec `json:"spec"`
+}
+
+type AddressPlanSpec struct {
+	AddressType      string             `json:"addressType"`
+	DisplayName      string             `json:"displayName,omitempty"`
+	LongDescription  string             `json:"longDescription,omitempty"`
+	ShortDescription string             `json:"shortDescription,omitempty"`
+	DisplayOrder     int                `json:"displayOrder,omitempty"`
+	Resources        map[string]float64 `json:"resources,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+type AddressPlanList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+
+	Items []AddressPlan `json:"items"`
+}
+
+// ** AddressSpacePlan
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +genclient:noStatus
+
+type AddressSpacePlan struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec AddressSpacePlanSpec `json:"spec"`
+}
+
+type AddressSpacePlanSpec struct {
+	AddressPlans     []string           `json:"addressPlans"`
+	AddressSpaceType string             `json:"addressSpaceType"`
+	DisplayName      string             `json:"displayName,omitempty"`
+	LongDescription  string             `json:"longDescription,omitempty"`
+	ShortDescription string             `json:"shortDescription,omitempty"`
+	InfraConfigRef   string             `json:"infraConfigRef,omitempty"`
+	DisplayOrder     int                `json:"displayOrder,omitempty"`
+	ResourceLimits   map[string]float64 `json:"resourceLimits,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+type AddressSpacePlanList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+
+	Items []AddressSpacePlan `json:"items"`
+}
diff --git a/pkg/apis/admin/v1beta2/zz_generated.deepcopy.go b/pkg/apis/admin/v1beta2/zz_generated.deepcopy.go
new file mode 100644
index 000000000..a0f055884
--- /dev/null
+++ b/pkg/apis/admin/v1beta2/zz_generated.deepcopy.go
@@ -0,0 +1,185 @@
+// +build !ignore_autogenerated
+
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AddressPlan) DeepCopyInto(out *AddressPlan) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressPlan.
+func (in *AddressPlan) DeepCopy() *AddressPlan {
+	if in == nil {
+		return nil
+	}
+	out := new(AddressPlan)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AddressPlan) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AddressPlanList) DeepCopyInto(out *AddressPlanList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	out.ListMeta = in.ListMeta
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]AddressPlan, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressPlanList.
+func (in *AddressPlanList) DeepCopy() *AddressPlanList {
+	if in == nil {
+		return nil
+	}
+	out := new(AddressPlanList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AddressPlanList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AddressPlanSpec) DeepCopyInto(out *AddressPlanSpec) {
+	*out = *in
+	if in.Resources != nil {
+		in, out := &in.Resources, &out.Resources
+		*out = make(map[string]float64, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressPlanSpec.
+func (in *AddressPlanSpec) DeepCopy() *AddressPlanSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(AddressPlanSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AddressSpacePlan) DeepCopyInto(out *AddressSpacePlan) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressSpacePlan.
+func (in *AddressSpacePlan) DeepCopy() *AddressSpacePlan {
+	if in == nil {
+		return nil
+	}
+	out := new(AddressSpacePlan)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AddressSpacePlan) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AddressSpacePlanList) DeepCopyInto(out *AddressSpacePlanList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	out.ListMeta = in.ListMeta
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]AddressSpacePlan, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressSpacePlanList.
+func (in *AddressSpacePlanList) DeepCopy() *AddressSpacePlanList {
+	if in == nil {
+		return nil
+	}
+	out := new(AddressSpacePlanList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *AddressSpacePlanList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AddressSpacePlanSpec) DeepCopyInto(out *AddressSpacePlanSpec) {
+	*out = *in
+	if in.AddressPlans != nil {
+		in, out := &in.AddressPlans, &out.AddressPlans
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.ResourceLimits != nil {
+		in, out := &in.ResourceLimits, &out.ResourceLimits
+		*out = make(map[string]float64, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddressSpacePlanSpec.
+func (in *AddressSpacePlanSpec) DeepCopy() *AddressSpacePlanSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(AddressSpacePlanSpec)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/pkg/apis/iot/v1alpha1/types_project.go b/pkg/apis/iot/v1alpha1/types_project.go
index 2c64028f0..2ee374893 100644
--- a/pkg/apis/iot/v1alpha1/types_project.go
+++ b/pkg/apis/iot/v1alpha1/types_project.go
@@ -29,9 +29,14 @@ type IoTProjectSpec struct {
 }
 
 type IoTProjectStatus struct {
-	IsReady            bool                        `json:"isReady"`
-	TenantName         string                      `json:"tenantName"`
-	DownstreamEndpoint *ExternalDownstreamStrategy `json:"downstreamEndpoint,omitempty"`
+	IsReady     bool   `json:"isReady"`
+	Phase       string `json:"phase"`
+	PhaseReason string `json:"phaseReason,omitempty"`
+
+	TenantName         string                 `json:"tenantName"`
+	DownstreamEndpoint *ConnectionInformation `json:"downstreamEndpoint,omitempty"`
+
+	Managed *ManagedStatus `json:"managed,omitempty"`
 
 	Conditions []ProjectCondition `json:"conditions"`
 }
@@ -89,7 +94,7 @@ type AddressConfig struct {
 	Type string `json:"type,omitempty"`
 }
 
-type ExternalDownstreamStrategy struct {
+type ConnectionInformation struct {
 	Host string `json:"host"`
 	Port uint16 `json:"port"`
 
@@ -99,6 +104,15 @@ type ExternalDownstreamStrategy struct {
 	Certificate []byte `json:"certificate,omitempty"`
 }
 
+type ExternalDownstreamStrategy struct {
+	ConnectionInformation `json:",inline"`
+}
+
+type ManagedStatus struct {
+	PasswordTime metav1.Time `json:"passwordTime,omitempty"`
+	AddressSpace string      `json:"addressSpace,omitempty"`
+}
+
 type Credentials struct {
 	Username string `json:"username"`
 	Password string `json:"password"`
diff --git a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
index b605ebe72..c1d087dbd 100644
--- a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
@@ -316,6 +316,28 @@ func (in *CommonStatus) DeepCopy() *CommonStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ConnectionInformation) DeepCopyInto(out *ConnectionInformation) {
+	*out = *in
+	out.Credentials = in.Credentials
+	if in.Certificate != nil {
+		in, out := &in.Certificate, &out.Certificate
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionInformation.
+func (in *ConnectionInformation) DeepCopy() *ConnectionInformation {
+	if in == nil {
+		return nil
+	}
+	out := new(ConnectionInformation)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ContainerConfig) DeepCopyInto(out *ContainerConfig) {
 	*out = *in
@@ -446,12 +468,7 @@ func (in *ExternalCacheNames) DeepCopy() *ExternalCacheNames {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalDownstreamStrategy) DeepCopyInto(out *ExternalDownstreamStrategy) {
 	*out = *in
-	out.Credentials = in.Credentials
-	if in.Certificate != nil {
-		in, out := &in.Certificate, &out.Certificate
-		*out = make([]byte, len(*in))
-		copy(*out, *in)
-	}
+	in.ConnectionInformation.DeepCopyInto(&out.ConnectionInformation)
 	return
 }
 
@@ -811,7 +828,12 @@ func (in *IoTProjectStatus) DeepCopyInto(out *IoTProjectStatus) {
 	*out = *in
 	if in.DownstreamEndpoint != nil {
 		in, out := &in.DownstreamEndpoint, &out.DownstreamEndpoint
-		*out = new(ExternalDownstreamStrategy)
+		*out = new(ConnectionInformation)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Managed != nil {
+		in, out := &in.Managed, &out.Managed
+		*out = new(ManagedStatus)
 		(*in).DeepCopyInto(*out)
 	}
 	if in.Conditions != nil {
@@ -937,6 +959,23 @@ func (in *ManagedDownstreamStrategy) DeepCopy() *ManagedDownstreamStrategy {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ManagedStatus) DeepCopyInto(out *ManagedStatus) {
+	*out = *in
+	in.PasswordTime.DeepCopyInto(&out.PasswordTime)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedStatus.
+func (in *ManagedStatus) DeepCopy() *ManagedStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ManagedStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MqttAdapterConfig) DeepCopyInto(out *MqttAdapterConfig) {
 	*out = *in
diff --git a/pkg/client/clientset/versioned/clientset.go b/pkg/client/clientset/versioned/clientset.go
index acc50c9d0..2f59687e5 100644
--- a/pkg/client/clientset/versioned/clientset.go
+++ b/pkg/client/clientset/versioned/clientset.go
@@ -9,6 +9,7 @@ package versioned
 
 import (
 	adminv1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta1"
+	adminv1beta2 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta2"
 	enmassev1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/enmasse/v1beta1"
 	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/iot/v1alpha1"
 	userv1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/user/v1beta1"
@@ -20,8 +21,9 @@ import (
 type Interface interface {
 	Discovery() discovery.DiscoveryInterface
 	AdminV1beta1() adminv1beta1.AdminV1beta1Interface
+	AdminV1beta2() adminv1beta2.AdminV1beta2Interface
 	// Deprecated: please explicitly pick a version if possible.
-	Admin() adminv1beta1.AdminV1beta1Interface
+	Admin() adminv1beta2.AdminV1beta2Interface
 	EnmasseV1beta1() enmassev1beta1.EnmasseV1beta1Interface
 	// Deprecated: please explicitly pick a version if possible.
 	Enmasse() enmassev1beta1.EnmasseV1beta1Interface
@@ -38,6 +40,7 @@ type Interface interface {
 type Clientset struct {
 	*discovery.DiscoveryClient
 	adminV1beta1   *adminv1beta1.AdminV1beta1Client
+	adminV1beta2   *adminv1beta2.AdminV1beta2Client
 	enmasseV1beta1 *enmassev1beta1.EnmasseV1beta1Client
 	iotV1alpha1    *iotv1alpha1.IotV1alpha1Client
 	userV1beta1    *userv1beta1.UserV1beta1Client
@@ -48,10 +51,15 @@ func (c *Clientset) AdminV1beta1() adminv1beta1.AdminV1beta1Interface {
 	return c.adminV1beta1
 }
 
+// AdminV1beta2 retrieves the AdminV1beta2Client
+func (c *Clientset) AdminV1beta2() adminv1beta2.AdminV1beta2Interface {
+	return c.adminV1beta2
+}
+
 // Deprecated: Admin retrieves the default version of AdminClient.
 // Please explicitly pick a version.
-func (c *Clientset) Admin() adminv1beta1.AdminV1beta1Interface {
-	return c.adminV1beta1
+func (c *Clientset) Admin() adminv1beta2.AdminV1beta2Interface {
+	return c.adminV1beta2
 }
 
 // EnmasseV1beta1 retrieves the EnmasseV1beta1Client
@@ -107,6 +115,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 	if err != nil {
 		return nil, err
 	}
+	cs.adminV1beta2, err = adminv1beta2.NewForConfig(&configShallowCopy)
+	if err != nil {
+		return nil, err
+	}
 	cs.enmasseV1beta1, err = enmassev1beta1.NewForConfig(&configShallowCopy)
 	if err != nil {
 		return nil, err
@@ -132,6 +144,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) {
 func NewForConfigOrDie(c *rest.Config) *Clientset {
 	var cs Clientset
 	cs.adminV1beta1 = adminv1beta1.NewForConfigOrDie(c)
+	cs.adminV1beta2 = adminv1beta2.NewForConfigOrDie(c)
 	cs.enmasseV1beta1 = enmassev1beta1.NewForConfigOrDie(c)
 	cs.iotV1alpha1 = iotv1alpha1.NewForConfigOrDie(c)
 	cs.userV1beta1 = userv1beta1.NewForConfigOrDie(c)
@@ -144,6 +157,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset {
 func New(c rest.Interface) *Clientset {
 	var cs Clientset
 	cs.adminV1beta1 = adminv1beta1.New(c)
+	cs.adminV1beta2 = adminv1beta2.New(c)
 	cs.enmasseV1beta1 = enmassev1beta1.New(c)
 	cs.iotV1alpha1 = iotv1alpha1.New(c)
 	cs.userV1beta1 = userv1beta1.New(c)
diff --git a/pkg/client/clientset/versioned/fake/clientset_generated.go b/pkg/client/clientset/versioned/fake/clientset_generated.go
index 2e357f2c9..9a08d60c6 100644
--- a/pkg/client/clientset/versioned/fake/clientset_generated.go
+++ b/pkg/client/clientset/versioned/fake/clientset_generated.go
@@ -11,6 +11,8 @@ import (
 	clientset "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned"
 	adminv1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta1"
 	fakeadminv1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta1/fake"
+	adminv1beta2 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta2"
+	fakeadminv1beta2 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta2/fake"
 	enmassev1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/enmasse/v1beta1"
 	fakeenmassev1beta1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/enmasse/v1beta1/fake"
 	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/iot/v1alpha1"
@@ -71,9 +73,14 @@ func (c *Clientset) AdminV1beta1() adminv1beta1.AdminV1beta1Interface {
 	return &fakeadminv1beta1.FakeAdminV1beta1{Fake: &c.Fake}
 }
 
-// Admin retrieves the AdminV1beta1Client
-func (c *Clientset) Admin() adminv1beta1.AdminV1beta1Interface {
-	return &fakeadminv1beta1.FakeAdminV1beta1{Fake: &c.Fake}
+// AdminV1beta2 retrieves the AdminV1beta2Client
+func (c *Clientset) AdminV1beta2() adminv1beta2.AdminV1beta2Interface {
+	return &fakeadminv1beta2.FakeAdminV1beta2{Fake: &c.Fake}
+}
+
+// Admin retrieves the AdminV1beta2Client
+func (c *Clientset) Admin() adminv1beta2.AdminV1beta2Interface {
+	return &fakeadminv1beta2.FakeAdminV1beta2{Fake: &c.Fake}
 }
 
 // EnmasseV1beta1 retrieves the EnmasseV1beta1Client
diff --git a/pkg/client/clientset/versioned/fake/register.go b/pkg/client/clientset/versioned/fake/register.go
index 7a270763c..f816446ad 100644
--- a/pkg/client/clientset/versioned/fake/register.go
+++ b/pkg/client/clientset/versioned/fake/register.go
@@ -9,6 +9,7 @@ package fake
 
 import (
 	adminv1beta1 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta1"
+	adminv1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
 	enmassev1beta1 "github.com/enmasseproject/enmasse/pkg/apis/enmasse/v1beta1"
 	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
 	userv1beta1 "github.com/enmasseproject/enmasse/pkg/apis/user/v1beta1"
@@ -24,6 +25,7 @@ var codecs = serializer.NewCodecFactory(scheme)
 var parameterCodec = runtime.NewParameterCodec(scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
 	adminv1beta1.AddToScheme,
+	adminv1beta2.AddToScheme,
 	enmassev1beta1.AddToScheme,
 	iotv1alpha1.AddToScheme,
 	userv1beta1.AddToScheme,
diff --git a/pkg/client/clientset/versioned/scheme/register.go b/pkg/client/clientset/versioned/scheme/register.go
index db9f0f838..77ecf0b5e 100644
--- a/pkg/client/clientset/versioned/scheme/register.go
+++ b/pkg/client/clientset/versioned/scheme/register.go
@@ -9,6 +9,7 @@ package scheme
 
 import (
 	adminv1beta1 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta1"
+	adminv1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
 	enmassev1beta1 "github.com/enmasseproject/enmasse/pkg/apis/enmasse/v1beta1"
 	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
 	userv1beta1 "github.com/enmasseproject/enmasse/pkg/apis/user/v1beta1"
@@ -24,6 +25,7 @@ var Codecs = serializer.NewCodecFactory(Scheme)
 var ParameterCodec = runtime.NewParameterCodec(Scheme)
 var localSchemeBuilder = runtime.SchemeBuilder{
 	adminv1beta1.AddToScheme,
+	adminv1beta2.AddToScheme,
 	enmassev1beta1.AddToScheme,
 	iotv1alpha1.AddToScheme,
 	userv1beta1.AddToScheme,
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/addressplan.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/addressplan.go
new file mode 100644
index 000000000..cab9ab6ed
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/addressplan.go
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	scheme "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// AddressPlansGetter has a method to return a AddressPlanInterface.
+// A group's client should implement this interface.
+type AddressPlansGetter interface {
+	AddressPlans(namespace string) AddressPlanInterface
+}
+
+// AddressPlanInterface has methods to work with AddressPlan resources.
+type AddressPlanInterface interface {
+	Create(*v1beta2.AddressPlan) (*v1beta2.AddressPlan, error)
+	Update(*v1beta2.AddressPlan) (*v1beta2.AddressPlan, error)
+	Delete(name string, options *v1.DeleteOptions) error
+	DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
+	Get(name string, options v1.GetOptions) (*v1beta2.AddressPlan, error)
+	List(opts v1.ListOptions) (*v1beta2.AddressPlanList, error)
+	Watch(opts v1.ListOptions) (watch.Interface, error)
+	Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta2.AddressPlan, err error)
+	AddressPlanExpansion
+}
+
+// addressPlans implements AddressPlanInterface
+type addressPlans struct {
+	client rest.Interface
+	ns     string
+}
+
+// newAddressPlans returns a AddressPlans
+func newAddressPlans(c *AdminV1beta2Client, namespace string) *addressPlans {
+	return &addressPlans{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the addressPlan, and returns the corresponding addressPlan object, and an error if there is any.
+func (c *addressPlans) Get(name string, options v1.GetOptions) (result *v1beta2.AddressPlan, err error) {
+	result = &v1beta2.AddressPlan{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("addressplans").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of AddressPlans that match those selectors.
+func (c *addressPlans) List(opts v1.ListOptions) (result *v1beta2.AddressPlanList, err error) {
+	result = &v1beta2.AddressPlanList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("addressplans").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested addressPlans.
+func (c *addressPlans) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("addressplans").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Watch()
+}
+
+// Create takes the representation of a addressPlan and creates it.  Returns the server's representation of the addressPlan, and an error, if there is any.
+func (c *addressPlans) Create(addressPlan *v1beta2.AddressPlan) (result *v1beta2.AddressPlan, err error) {
+	result = &v1beta2.AddressPlan{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("addressplans").
+		Body(addressPlan).
+		Do().
+		Into(result)
+	return
+}
+
+// Update takes the representation of a addressPlan and updates it. Returns the server's representation of the addressPlan, and an error, if there is any.
+func (c *addressPlans) Update(addressPlan *v1beta2.AddressPlan) (result *v1beta2.AddressPlan, err error) {
+	result = &v1beta2.AddressPlan{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("addressplans").
+		Name(addressPlan.Name).
+		Body(addressPlan).
+		Do().
+		Into(result)
+	return
+}
+
+// Delete takes name of the addressPlan and deletes it. Returns an error if one occurs.
+func (c *addressPlans) Delete(name string, options *v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("addressplans").
+		Name(name).
+		Body(options).
+		Do().
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *addressPlans) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("addressplans").
+		VersionedParams(&listOptions, scheme.ParameterCodec).
+		Body(options).
+		Do().
+		Error()
+}
+
+// Patch applies the patch and returns the patched addressPlan.
+func (c *addressPlans) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta2.AddressPlan, err error) {
+	result = &v1beta2.AddressPlan{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("addressplans").
+		SubResource(subresources...).
+		Name(name).
+		Body(data).
+		Do().
+		Into(result)
+	return
+}
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/addressspaceplan.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/addressspaceplan.go
new file mode 100644
index 000000000..028f40098
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/addressspaceplan.go
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	scheme "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/scheme"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// AddressSpacePlansGetter has a method to return a AddressSpacePlanInterface.
+// A group's client should implement this interface.
+type AddressSpacePlansGetter interface {
+	AddressSpacePlans(namespace string) AddressSpacePlanInterface
+}
+
+// AddressSpacePlanInterface has methods to work with AddressSpacePlan resources.
+type AddressSpacePlanInterface interface {
+	Create(*v1beta2.AddressSpacePlan) (*v1beta2.AddressSpacePlan, error)
+	Update(*v1beta2.AddressSpacePlan) (*v1beta2.AddressSpacePlan, error)
+	Delete(name string, options *v1.DeleteOptions) error
+	DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
+	Get(name string, options v1.GetOptions) (*v1beta2.AddressSpacePlan, error)
+	List(opts v1.ListOptions) (*v1beta2.AddressSpacePlanList, error)
+	Watch(opts v1.ListOptions) (watch.Interface, error)
+	Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta2.AddressSpacePlan, err error)
+	AddressSpacePlanExpansion
+}
+
+// addressSpacePlans implements AddressSpacePlanInterface
+type addressSpacePlans struct {
+	client rest.Interface
+	ns     string
+}
+
+// newAddressSpacePlans returns a AddressSpacePlans
+func newAddressSpacePlans(c *AdminV1beta2Client, namespace string) *addressSpacePlans {
+	return &addressSpacePlans{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the addressSpacePlan, and returns the corresponding addressSpacePlan object, and an error if there is any.
+func (c *addressSpacePlans) Get(name string, options v1.GetOptions) (result *v1beta2.AddressSpacePlan, err error) {
+	result = &v1beta2.AddressSpacePlan{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of AddressSpacePlans that match those selectors.
+func (c *addressSpacePlans) List(opts v1.ListOptions) (result *v1beta2.AddressSpacePlanList, err error) {
+	result = &v1beta2.AddressSpacePlanList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested addressSpacePlans.
+func (c *addressSpacePlans) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Watch()
+}
+
+// Create takes the representation of a addressSpacePlan and creates it.  Returns the server's representation of the addressSpacePlan, and an error, if there is any.
+func (c *addressSpacePlans) Create(addressSpacePlan *v1beta2.AddressSpacePlan) (result *v1beta2.AddressSpacePlan, err error) {
+	result = &v1beta2.AddressSpacePlan{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		Body(addressSpacePlan).
+		Do().
+		Into(result)
+	return
+}
+
+// Update takes the representation of a addressSpacePlan and updates it. Returns the server's representation of the addressSpacePlan, and an error, if there is any.
+func (c *addressSpacePlans) Update(addressSpacePlan *v1beta2.AddressSpacePlan) (result *v1beta2.AddressSpacePlan, err error) {
+	result = &v1beta2.AddressSpacePlan{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		Name(addressSpacePlan.Name).
+		Body(addressSpacePlan).
+		Do().
+		Into(result)
+	return
+}
+
+// Delete takes name of the addressSpacePlan and deletes it. Returns an error if one occurs.
+func (c *addressSpacePlans) Delete(name string, options *v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		Name(name).
+		Body(options).
+		Do().
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *addressSpacePlans) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		VersionedParams(&listOptions, scheme.ParameterCodec).
+		Body(options).
+		Do().
+		Error()
+}
+
+// Patch applies the patch and returns the patched addressSpacePlan.
+func (c *addressSpacePlans) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta2.AddressSpacePlan, err error) {
+	result = &v1beta2.AddressSpacePlan{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("addressspaceplans").
+		SubResource(subresources...).
+		Name(name).
+		Body(data).
+		Do().
+		Into(result)
+	return
+}
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/admin_client.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/admin_client.go
new file mode 100644
index 000000000..61f3eab21
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/admin_client.go
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	"github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/scheme"
+	serializer "k8s.io/apimachinery/pkg/runtime/serializer"
+	rest "k8s.io/client-go/rest"
+)
+
+type AdminV1beta2Interface interface {
+	RESTClient() rest.Interface
+	AddressPlansGetter
+	AddressSpacePlansGetter
+}
+
+// AdminV1beta2Client is used to interact with features provided by the admin.enmasse.io group.
+type AdminV1beta2Client struct {
+	restClient rest.Interface
+}
+
+func (c *AdminV1beta2Client) AddressPlans(namespace string) AddressPlanInterface {
+	return newAddressPlans(c, namespace)
+}
+
+func (c *AdminV1beta2Client) AddressSpacePlans(namespace string) AddressSpacePlanInterface {
+	return newAddressSpacePlans(c, namespace)
+}
+
+// NewForConfig creates a new AdminV1beta2Client for the given config.
+func NewForConfig(c *rest.Config) (*AdminV1beta2Client, error) {
+	config := *c
+	if err := setConfigDefaults(&config); err != nil {
+		return nil, err
+	}
+	client, err := rest.RESTClientFor(&config)
+	if err != nil {
+		return nil, err
+	}
+	return &AdminV1beta2Client{client}, nil
+}
+
+// NewForConfigOrDie creates a new AdminV1beta2Client for the given config and
+// panics if there is an error in the config.
+func NewForConfigOrDie(c *rest.Config) *AdminV1beta2Client {
+	client, err := NewForConfig(c)
+	if err != nil {
+		panic(err)
+	}
+	return client
+}
+
+// New creates a new AdminV1beta2Client for the given RESTClient.
+func New(c rest.Interface) *AdminV1beta2Client {
+	return &AdminV1beta2Client{c}
+}
+
+func setConfigDefaults(config *rest.Config) error {
+	gv := v1beta2.SchemeGroupVersion
+	config.GroupVersion = &gv
+	config.APIPath = "/apis"
+	config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
+
+	if config.UserAgent == "" {
+		config.UserAgent = rest.DefaultKubernetesUserAgent()
+	}
+
+	return nil
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *AdminV1beta2Client) RESTClient() rest.Interface {
+	if c == nil {
+		return nil
+	}
+	return c.restClient
+}
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/doc.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/doc.go
new file mode 100644
index 000000000..427be07af
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/doc.go
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// This package has the automatically generated typed clients.
+package v1beta2
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/doc.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/doc.go
new file mode 100644
index 000000000..94d68eb8f
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/doc.go
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+// Package fake has the automatically generated clients.
+package fake
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_addressplan.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_addressplan.go
new file mode 100644
index 000000000..d93b3c31e
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_addressplan.go
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeAddressPlans implements AddressPlanInterface
+type FakeAddressPlans struct {
+	Fake *FakeAdminV1beta2
+	ns   string
+}
+
+var addressplansResource = schema.GroupVersionResource{Group: "admin.enmasse.io", Version: "v1beta2", Resource: "addressplans"}
+
+var addressplansKind = schema.GroupVersionKind{Group: "admin.enmasse.io", Version: "v1beta2", Kind: "AddressPlan"}
+
+// Get takes name of the addressPlan, and returns the corresponding addressPlan object, and an error if there is any.
+func (c *FakeAddressPlans) Get(name string, options v1.GetOptions) (result *v1beta2.AddressPlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(addressplansResource, c.ns, name), &v1beta2.AddressPlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressPlan), err
+}
+
+// List takes label and field selectors, and returns the list of AddressPlans that match those selectors.
+func (c *FakeAddressPlans) List(opts v1.ListOptions) (result *v1beta2.AddressPlanList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(addressplansResource, addressplansKind, c.ns, opts), &v1beta2.AddressPlanList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta2.AddressPlanList{ListMeta: obj.(*v1beta2.AddressPlanList).ListMeta}
+	for _, item := range obj.(*v1beta2.AddressPlanList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested addressPlans.
+func (c *FakeAddressPlans) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(addressplansResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a addressPlan and creates it.  Returns the server's representation of the addressPlan, and an error, if there is any.
+func (c *FakeAddressPlans) Create(addressPlan *v1beta2.AddressPlan) (result *v1beta2.AddressPlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(addressplansResource, c.ns, addressPlan), &v1beta2.AddressPlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressPlan), err
+}
+
+// Update takes the representation of a addressPlan and updates it. Returns the server's representation of the addressPlan, and an error, if there is any.
+func (c *FakeAddressPlans) Update(addressPlan *v1beta2.AddressPlan) (result *v1beta2.AddressPlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(addressplansResource, c.ns, addressPlan), &v1beta2.AddressPlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressPlan), err
+}
+
+// Delete takes name of the addressPlan and deletes it. Returns an error if one occurs.
+func (c *FakeAddressPlans) Delete(name string, options *v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(addressplansResource, c.ns, name), &v1beta2.AddressPlan{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeAddressPlans) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(addressplansResource, c.ns, listOptions)
+
+	_, err := c.Fake.Invokes(action, &v1beta2.AddressPlanList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched addressPlan.
+func (c *FakeAddressPlans) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta2.AddressPlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(addressplansResource, c.ns, name, data, subresources...), &v1beta2.AddressPlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressPlan), err
+}
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_addressspaceplan.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_addressspaceplan.go
new file mode 100644
index 000000000..6af728e87
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_addressspaceplan.go
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakeAddressSpacePlans implements AddressSpacePlanInterface
+type FakeAddressSpacePlans struct {
+	Fake *FakeAdminV1beta2
+	ns   string
+}
+
+var addressspaceplansResource = schema.GroupVersionResource{Group: "admin.enmasse.io", Version: "v1beta2", Resource: "addressspaceplans"}
+
+var addressspaceplansKind = schema.GroupVersionKind{Group: "admin.enmasse.io", Version: "v1beta2", Kind: "AddressSpacePlan"}
+
+// Get takes name of the addressSpacePlan, and returns the corresponding addressSpacePlan object, and an error if there is any.
+func (c *FakeAddressSpacePlans) Get(name string, options v1.GetOptions) (result *v1beta2.AddressSpacePlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(addressspaceplansResource, c.ns, name), &v1beta2.AddressSpacePlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressSpacePlan), err
+}
+
+// List takes label and field selectors, and returns the list of AddressSpacePlans that match those selectors.
+func (c *FakeAddressSpacePlans) List(opts v1.ListOptions) (result *v1beta2.AddressSpacePlanList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(addressspaceplansResource, addressspaceplansKind, c.ns, opts), &v1beta2.AddressSpacePlanList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1beta2.AddressSpacePlanList{ListMeta: obj.(*v1beta2.AddressSpacePlanList).ListMeta}
+	for _, item := range obj.(*v1beta2.AddressSpacePlanList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested addressSpacePlans.
+func (c *FakeAddressSpacePlans) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(addressspaceplansResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a addressSpacePlan and creates it.  Returns the server's representation of the addressSpacePlan, and an error, if there is any.
+func (c *FakeAddressSpacePlans) Create(addressSpacePlan *v1beta2.AddressSpacePlan) (result *v1beta2.AddressSpacePlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(addressspaceplansResource, c.ns, addressSpacePlan), &v1beta2.AddressSpacePlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressSpacePlan), err
+}
+
+// Update takes the representation of a addressSpacePlan and updates it. Returns the server's representation of the addressSpacePlan, and an error, if there is any.
+func (c *FakeAddressSpacePlans) Update(addressSpacePlan *v1beta2.AddressSpacePlan) (result *v1beta2.AddressSpacePlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(addressspaceplansResource, c.ns, addressSpacePlan), &v1beta2.AddressSpacePlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressSpacePlan), err
+}
+
+// Delete takes name of the addressSpacePlan and deletes it. Returns an error if one occurs.
+func (c *FakeAddressSpacePlans) Delete(name string, options *v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(addressspaceplansResource, c.ns, name), &v1beta2.AddressSpacePlan{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeAddressSpacePlans) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(addressspaceplansResource, c.ns, listOptions)
+
+	_, err := c.Fake.Invokes(action, &v1beta2.AddressSpacePlanList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched addressSpacePlan.
+func (c *FakeAddressSpacePlans) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta2.AddressSpacePlan, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(addressspaceplansResource, c.ns, name, data, subresources...), &v1beta2.AddressSpacePlan{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1beta2.AddressSpacePlan), err
+}
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_admin_client.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_admin_client.go
new file mode 100644
index 000000000..1aed4f91a
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/fake/fake_admin_client.go
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/typed/admin/v1beta2"
+	rest "k8s.io/client-go/rest"
+	testing "k8s.io/client-go/testing"
+)
+
+type FakeAdminV1beta2 struct {
+	*testing.Fake
+}
+
+func (c *FakeAdminV1beta2) AddressPlans(namespace string) v1beta2.AddressPlanInterface {
+	return &FakeAddressPlans{c, namespace}
+}
+
+func (c *FakeAdminV1beta2) AddressSpacePlans(namespace string) v1beta2.AddressSpacePlanInterface {
+	return &FakeAddressSpacePlans{c, namespace}
+}
+
+// RESTClient returns a RESTClient that is used to communicate
+// with API server by this client implementation.
+func (c *FakeAdminV1beta2) RESTClient() rest.Interface {
+	var ret *rest.RESTClient
+	return ret
+}
diff --git a/pkg/client/clientset/versioned/typed/admin/v1beta2/generated_expansion.go b/pkg/client/clientset/versioned/typed/admin/v1beta2/generated_expansion.go
new file mode 100644
index 000000000..e64896a95
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/admin/v1beta2/generated_expansion.go
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1beta2
+
+type AddressPlanExpansion interface{}
+
+type AddressSpacePlanExpansion interface{}
diff --git a/pkg/client/informers/externalversions/admin/interface.go b/pkg/client/informers/externalversions/admin/interface.go
index d8ccc8439..4115202cb 100644
--- a/pkg/client/informers/externalversions/admin/interface.go
+++ b/pkg/client/informers/externalversions/admin/interface.go
@@ -9,6 +9,7 @@ package admin
 
 import (
 	v1beta1 "github.com/enmasseproject/enmasse/pkg/client/informers/externalversions/admin/v1beta1"
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/client/informers/externalversions/admin/v1beta2"
 	internalinterfaces "github.com/enmasseproject/enmasse/pkg/client/informers/externalversions/internalinterfaces"
 )
 
@@ -16,6 +17,8 @@ import (
 type Interface interface {
 	// V1beta1 provides access to shared informers for resources in V1beta1.
 	V1beta1() v1beta1.Interface
+	// V1beta2 provides access to shared informers for resources in V1beta2.
+	V1beta2() v1beta2.Interface
 }
 
 type group struct {
@@ -33,3 +36,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 func (g *group) V1beta1() v1beta1.Interface {
 	return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
 }
+
+// V1beta2 returns a new v1beta2.Interface.
+func (g *group) V1beta2() v1beta2.Interface {
+	return v1beta2.New(g.factory, g.namespace, g.tweakListOptions)
+}
diff --git a/pkg/client/informers/externalversions/admin/v1beta2/addressplan.go b/pkg/client/informers/externalversions/admin/v1beta2/addressplan.go
new file mode 100644
index 000000000..ad268c3c6
--- /dev/null
+++ b/pkg/client/informers/externalversions/admin/v1beta2/addressplan.go
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	time "time"
+
+	adminv1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	versioned "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/enmasseproject/enmasse/pkg/client/informers/externalversions/internalinterfaces"
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/client/listers/admin/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// AddressPlanInformer provides access to a shared informer and lister for
+// AddressPlans.
+type AddressPlanInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta2.AddressPlanLister
+}
+
+type addressPlanInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewAddressPlanInformer constructs a new informer for AddressPlan type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewAddressPlanInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredAddressPlanInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredAddressPlanInformer constructs a new informer for AddressPlan type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredAddressPlanInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AdminV1beta2().AddressPlans(namespace).List(options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AdminV1beta2().AddressPlans(namespace).Watch(options)
+			},
+		},
+		&adminv1beta2.AddressPlan{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *addressPlanInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredAddressPlanInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *addressPlanInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&adminv1beta2.AddressPlan{}, f.defaultInformer)
+}
+
+func (f *addressPlanInformer) Lister() v1beta2.AddressPlanLister {
+	return v1beta2.NewAddressPlanLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/informers/externalversions/admin/v1beta2/addressspaceplan.go b/pkg/client/informers/externalversions/admin/v1beta2/addressspaceplan.go
new file mode 100644
index 000000000..1a17c05ca
--- /dev/null
+++ b/pkg/client/informers/externalversions/admin/v1beta2/addressspaceplan.go
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	time "time"
+
+	adminv1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	versioned "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/enmasseproject/enmasse/pkg/client/informers/externalversions/internalinterfaces"
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/client/listers/admin/v1beta2"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// AddressSpacePlanInformer provides access to a shared informer and lister for
+// AddressSpacePlans.
+type AddressSpacePlanInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1beta2.AddressSpacePlanLister
+}
+
+type addressSpacePlanInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewAddressSpacePlanInformer constructs a new informer for AddressSpacePlan type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewAddressSpacePlanInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredAddressSpacePlanInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredAddressSpacePlanInformer constructs a new informer for AddressSpacePlan type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredAddressSpacePlanInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AdminV1beta2().AddressSpacePlans(namespace).List(options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.AdminV1beta2().AddressSpacePlans(namespace).Watch(options)
+			},
+		},
+		&adminv1beta2.AddressSpacePlan{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *addressSpacePlanInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredAddressSpacePlanInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *addressSpacePlanInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&adminv1beta2.AddressSpacePlan{}, f.defaultInformer)
+}
+
+func (f *addressSpacePlanInformer) Lister() v1beta2.AddressSpacePlanLister {
+	return v1beta2.NewAddressSpacePlanLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/informers/externalversions/admin/v1beta2/interface.go b/pkg/client/informers/externalversions/admin/v1beta2/interface.go
new file mode 100644
index 000000000..5ba10bbb3
--- /dev/null
+++ b/pkg/client/informers/externalversions/admin/v1beta2/interface.go
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	internalinterfaces "github.com/enmasseproject/enmasse/pkg/client/informers/externalversions/internalinterfaces"
+)
+
+// Interface provides access to all the informers in this group version.
+type Interface interface {
+	// AddressPlans returns a AddressPlanInformer.
+	AddressPlans() AddressPlanInformer
+	// AddressSpacePlans returns a AddressSpacePlanInformer.
+	AddressSpacePlans() AddressSpacePlanInformer
+}
+
+type version struct {
+	factory          internalinterfaces.SharedInformerFactory
+	namespace        string
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// New returns a new Interface.
+func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
+	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
+}
+
+// AddressPlans returns a AddressPlanInformer.
+func (v *version) AddressPlans() AddressPlanInformer {
+	return &addressPlanInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
+// AddressSpacePlans returns a AddressSpacePlanInformer.
+func (v *version) AddressSpacePlans() AddressSpacePlanInformer {
+	return &addressSpacePlanInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go
index 3d064a8cf..aee6400d8 100644
--- a/pkg/client/informers/externalversions/generic.go
+++ b/pkg/client/informers/externalversions/generic.go
@@ -11,6 +11,7 @@ import (
 	"fmt"
 
 	v1beta1 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta1"
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
 	enmassev1beta1 "github.com/enmasseproject/enmasse/pkg/apis/enmasse/v1beta1"
 	v1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
 	userv1beta1 "github.com/enmasseproject/enmasse/pkg/apis/user/v1beta1"
@@ -50,6 +51,12 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	case v1beta1.SchemeGroupVersion.WithResource("consoleservices"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Admin().V1beta1().ConsoleServices().Informer()}, nil
 
+		// Group=admin.enmasse.io, Version=v1beta2
+	case v1beta2.SchemeGroupVersion.WithResource("addressplans"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Admin().V1beta2().AddressPlans().Informer()}, nil
+	case v1beta2.SchemeGroupVersion.WithResource("addressspaceplans"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Admin().V1beta2().AddressSpacePlans().Informer()}, nil
+
 		// Group=enmasse.io, Version=v1beta1
 	case enmassev1beta1.SchemeGroupVersion.WithResource("addresses"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Enmasse().V1beta1().Addresses().Informer()}, nil
diff --git a/pkg/client/listers/admin/v1beta2/addressplan.go b/pkg/client/listers/admin/v1beta2/addressplan.go
new file mode 100644
index 000000000..d674f522e
--- /dev/null
+++ b/pkg/client/listers/admin/v1beta2/addressplan.go
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// AddressPlanLister helps list AddressPlans.
+type AddressPlanLister interface {
+	// List lists all AddressPlans in the indexer.
+	List(selector labels.Selector) (ret []*v1beta2.AddressPlan, err error)
+	// AddressPlans returns an object that can list and get AddressPlans.
+	AddressPlans(namespace string) AddressPlanNamespaceLister
+	AddressPlanListerExpansion
+}
+
+// addressPlanLister implements the AddressPlanLister interface.
+type addressPlanLister struct {
+	indexer cache.Indexer
+}
+
+// NewAddressPlanLister returns a new AddressPlanLister.
+func NewAddressPlanLister(indexer cache.Indexer) AddressPlanLister {
+	return &addressPlanLister{indexer: indexer}
+}
+
+// List lists all AddressPlans in the indexer.
+func (s *addressPlanLister) List(selector labels.Selector) (ret []*v1beta2.AddressPlan, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.AddressPlan))
+	})
+	return ret, err
+}
+
+// AddressPlans returns an object that can list and get AddressPlans.
+func (s *addressPlanLister) AddressPlans(namespace string) AddressPlanNamespaceLister {
+	return addressPlanNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// AddressPlanNamespaceLister helps list and get AddressPlans.
+type AddressPlanNamespaceLister interface {
+	// List lists all AddressPlans in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1beta2.AddressPlan, err error)
+	// Get retrieves the AddressPlan from the indexer for a given namespace and name.
+	Get(name string) (*v1beta2.AddressPlan, error)
+	AddressPlanNamespaceListerExpansion
+}
+
+// addressPlanNamespaceLister implements the AddressPlanNamespaceLister
+// interface.
+type addressPlanNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all AddressPlans in the indexer for a given namespace.
+func (s addressPlanNamespaceLister) List(selector labels.Selector) (ret []*v1beta2.AddressPlan, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.AddressPlan))
+	})
+	return ret, err
+}
+
+// Get retrieves the AddressPlan from the indexer for a given namespace and name.
+func (s addressPlanNamespaceLister) Get(name string) (*v1beta2.AddressPlan, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta2.Resource("addressplan"), name)
+	}
+	return obj.(*v1beta2.AddressPlan), nil
+}
diff --git a/pkg/client/listers/admin/v1beta2/addressspaceplan.go b/pkg/client/listers/admin/v1beta2/addressspaceplan.go
new file mode 100644
index 000000000..fac1d940d
--- /dev/null
+++ b/pkg/client/listers/admin/v1beta2/addressspaceplan.go
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta2
+
+import (
+	v1beta2 "github.com/enmasseproject/enmasse/pkg/apis/admin/v1beta2"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// AddressSpacePlanLister helps list AddressSpacePlans.
+type AddressSpacePlanLister interface {
+	// List lists all AddressSpacePlans in the indexer.
+	List(selector labels.Selector) (ret []*v1beta2.AddressSpacePlan, err error)
+	// AddressSpacePlans returns an object that can list and get AddressSpacePlans.
+	AddressSpacePlans(namespace string) AddressSpacePlanNamespaceLister
+	AddressSpacePlanListerExpansion
+}
+
+// addressSpacePlanLister implements the AddressSpacePlanLister interface.
+type addressSpacePlanLister struct {
+	indexer cache.Indexer
+}
+
+// NewAddressSpacePlanLister returns a new AddressSpacePlanLister.
+func NewAddressSpacePlanLister(indexer cache.Indexer) AddressSpacePlanLister {
+	return &addressSpacePlanLister{indexer: indexer}
+}
+
+// List lists all AddressSpacePlans in the indexer.
+func (s *addressSpacePlanLister) List(selector labels.Selector) (ret []*v1beta2.AddressSpacePlan, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.AddressSpacePlan))
+	})
+	return ret, err
+}
+
+// AddressSpacePlans returns an object that can list and get AddressSpacePlans.
+func (s *addressSpacePlanLister) AddressSpacePlans(namespace string) AddressSpacePlanNamespaceLister {
+	return addressSpacePlanNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// AddressSpacePlanNamespaceLister helps list and get AddressSpacePlans.
+type AddressSpacePlanNamespaceLister interface {
+	// List lists all AddressSpacePlans in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1beta2.AddressSpacePlan, err error)
+	// Get retrieves the AddressSpacePlan from the indexer for a given namespace and name.
+	Get(name string) (*v1beta2.AddressSpacePlan, error)
+	AddressSpacePlanNamespaceListerExpansion
+}
+
+// addressSpacePlanNamespaceLister implements the AddressSpacePlanNamespaceLister
+// interface.
+type addressSpacePlanNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all AddressSpacePlans in the indexer for a given namespace.
+func (s addressSpacePlanNamespaceLister) List(selector labels.Selector) (ret []*v1beta2.AddressSpacePlan, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1beta2.AddressSpacePlan))
+	})
+	return ret, err
+}
+
+// Get retrieves the AddressSpacePlan from the indexer for a given namespace and name.
+func (s addressSpacePlanNamespaceLister) Get(name string) (*v1beta2.AddressSpacePlan, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1beta2.Resource("addressspaceplan"), name)
+	}
+	return obj.(*v1beta2.AddressSpacePlan), nil
+}
diff --git a/pkg/client/listers/admin/v1beta2/expansion_generated.go b/pkg/client/listers/admin/v1beta2/expansion_generated.go
new file mode 100644
index 000000000..5756d1b28
--- /dev/null
+++ b/pkg/client/listers/admin/v1beta2/expansion_generated.go
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2018-2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1beta2
+
+// AddressPlanListerExpansion allows custom methods to be added to
+// AddressPlanLister.
+type AddressPlanListerExpansion interface{}
+
+// AddressPlanNamespaceListerExpansion allows custom methods to be added to
+// AddressPlanNamespaceLister.
+type AddressPlanNamespaceListerExpansion interface{}
+
+// AddressSpacePlanListerExpansion allows custom methods to be added to
+// AddressSpacePlanLister.
+type AddressSpacePlanListerExpansion interface{}
+
+// AddressSpacePlanNamespaceListerExpansion allows custom methods to be added to
+// AddressSpacePlanNamespaceLister.
+type AddressSpacePlanNamespaceListerExpansion interface{}
diff --git a/pkg/controller/iotproject/external.go b/pkg/controller/iotproject/external.go
index 7260e5108..426185eeb 100644
--- a/pkg/controller/iotproject/external.go
+++ b/pkg/controller/iotproject/external.go
@@ -8,12 +8,36 @@ package iotproject
 import (
 	"context"
 
+	"github.com/enmasseproject/enmasse/pkg/util/recon"
+
 	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
 func (r *ReconcileIoTProject) reconcileExternal(ctx context.Context, request *reconcile.Request, project *iotv1alpha1.IoTProject) (reconcile.Result, error) {
 
+	if project.Status.Managed != nil {
+
+		project.Status.Phase = "Reconfiguring"
+		project.Status.PhaseReason = "Change of downstream strategy"
+
+		rc := recon.ReconcileContext{}
+
+		rc.Process(func() (result reconcile.Result, e error) {
+			return cleanupManagedResources(ctx, r.client, project)
+		})
+		if rc.Error() == nil {
+			project.Status.Managed = nil
+			rc.ProcessSimple(func() error {
+				return r.client.Status().Update(ctx, project)
+			})
+		}
+
+		// always re-queue when the cleared out managed resources
+
+		return reconcile.Result{Requeue: true}, nil
+	}
+
 	// we simply copy over the external information
 	// so everything is expected to be ready
 
@@ -27,7 +51,7 @@ func (r *ReconcileIoTProject) reconcileExternal(ctx context.Context, request *re
 		GetProjectCondition(iotv1alpha1.ProjectConditionTypeReady).
 		SetStatusOk()
 
-	project.Status.DownstreamEndpoint = project.Spec.DownstreamStrategy.ExternalDownstreamStrategy.DeepCopy()
+	project.Status.DownstreamEndpoint = project.Spec.DownstreamStrategy.ExternalDownstreamStrategy.ConnectionInformation.DeepCopy()
 	project.Status.IsReady = true
 
 	return reconcile.Result{}, nil
diff --git a/pkg/controller/iotproject/finalizers.go b/pkg/controller/iotproject/finalizers.go
index 854be18a2..7d2c9ede9 100644
--- a/pkg/controller/iotproject/finalizers.go
+++ b/pkg/controller/iotproject/finalizers.go
@@ -8,7 +8,10 @@ package iotproject
 import (
 	"context"
 	"fmt"
-	"time"
+
+	"github.com/enmasseproject/enmasse/pkg/util"
+
+	"k8s.io/apimachinery/pkg/api/errors"
 
 	"github.com/enmasseproject/enmasse/pkg/util/install"
 
@@ -53,37 +56,86 @@ func deconstructManagedResources(project *iotv1alpha1.IoTProject, ctx finalizer.
 
 	rc := recon.ReconcileContext{}
 
+	if project.Status.Managed != nil {
+		rc.Process(func() (result reconcile.Result, e error) {
+			return cleanupManagedResources(ctx.Context, ctx.Client, project)
+		})
+	}
+
+	return rc.Result()
+
+}
+
+// delete all managed resources by the project
+func cleanupManagedResources(ctx context.Context, c client.Client, project *iotv1alpha1.IoTProject) (reconcile.Result, error) {
+
+	rc := recon.ReconcileContext{}
+
+	managed := project.Status.Managed
+
+	if managed == nil {
+		return rc.Result()
+	}
+
+	for _, a := range Addresses {
+		rc.Process(func() (result reconcile.Result, e error) {
+
+			addressName := util.AddressName(project, a)
+			addressMetaName := util.EncodeAddressSpaceAsMetaName(managed.AddressSpace, addressName)
+
+			return cleanupResource(ctx, c, project, client.ObjectKey{
+				Namespace: project.Namespace,
+				Name:      addressMetaName,
+			}, &v1beta1.Address{})
+
+		})
+	}
+
 	rc.Process(func() (result reconcile.Result, e error) {
-		return collectResources(ctx.Context, ctx.Client, project, &v1beta1.AddressList{})
-	})
-	rc.Process(func() (result reconcile.Result, e error) {
-		return collectResources(ctx.Context, ctx.Client, project, &v1beta1.AddressSpaceList{})
+		return cleanupResource(ctx, c, project, client.ObjectKey{
+			Namespace: project.Namespace,
+			Name:      managed.AddressSpace,
+		}, &v1beta1.AddressSpace{})
 	})
+
 	rc.Process(func() (result reconcile.Result, e error) {
-		return collectResources(ctx.Context, ctx.Client, project, &userv1beta1.MessagingUserList{})
+		return cleanupResource(ctx, c, project, client.ObjectKey{
+			Namespace: project.Namespace,
+			Name:      managed.AddressSpace + "." + string(project.UID),
+		}, &userv1beta1.MessagingUser{})
 	})
 
 	return rc.Result()
 
 }
 
-// collect all resource which are owned by this project
-func collectResources(ctx context.Context, c client.Client, project *iotv1alpha1.IoTProject, list runtime.Object) (reconcile.Result, error) {
+func cleanupResource(ctx context.Context, c client.Client, project *iotv1alpha1.IoTProject, key client.ObjectKey, obj runtime.Object) (reconcile.Result, error) {
 
-	n, err := install.BulkRemoveOwner(ctx, c, project, false, list, client.ListOptions{
-		Namespace: project.Namespace,
-	})
+	err := c.Get(ctx, key, obj)
+
+	if err != nil {
+
+		if errors.IsNotFound(err) {
+			return reconcile.Result{}, nil
+		} else {
+			return reconcile.Result{}, err
+		}
+
+	}
+
+	result, err := install.RemoveAsOwner(project, obj, false)
 
 	if err != nil {
 		return reconcile.Result{}, err
 	}
 
-	if n > 0 {
-		return reconcile.Result{
-			RequeueAfter: 10 * time.Second,
-		}, nil
-	} else {
-		return reconcile.Result{}, nil
+	switch result {
+	case install.Found:
+		err = c.Update(ctx, obj)
+	case install.FoundAndEmpty:
+		err = c.Delete(ctx, obj)
 	}
 
+	return reconcile.Result{}, err
+
 }
diff --git a/pkg/controller/iotproject/iotproject_controller.go b/pkg/controller/iotproject/iotproject_controller.go
index 31dd70507..958b7a1ef 100644
--- a/pkg/controller/iotproject/iotproject_controller.go
+++ b/pkg/controller/iotproject/iotproject_controller.go
@@ -8,7 +8,6 @@ package iotproject
 import (
 	"context"
 	"fmt"
-	"time"
 
 	corev1 "k8s.io/api/core/v1"
 
@@ -16,8 +15,6 @@ import (
 
 	"github.com/enmasseproject/enmasse/pkg/util/recon"
 
-	"github.com/enmasseproject/enmasse/pkg/util"
-
 	enmassev1beta1 "github.com/enmasseproject/enmasse/pkg/apis/enmasse/v1beta1"
 	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
 	userv1beta1 "github.com/enmasseproject/enmasse/pkg/apis/user/v1beta1"
@@ -145,37 +142,6 @@ type ReconcileIoTProject struct {
 	scheme *runtime.Scheme
 }
 
-func (r *ReconcileIoTProject) updateProjectStatusError(ctx context.Context, request *reconcile.Request, project *iotv1alpha1.IoTProject, err error) error {
-
-	newProject := project.DeepCopy()
-	newProject.Status.IsReady = false
-
-	readyCondition := newProject.Status.GetProjectCondition(iotv1alpha1.ProjectConditionTypeReady)
-	var reason = ""
-	var message = ""
-	if err != nil {
-		reason = "ProcessingError"
-		message = err.Error()
-	}
-	readyCondition.SetStatus(corev1.ConditionFalse, reason, message)
-
-	return r.client.Update(ctx, newProject)
-}
-
-func (r *ReconcileIoTProject) updateProjectStatusReady(ctx context.Context, request *reconcile.Request, project *iotv1alpha1.IoTProject, endpointStatus *iotv1alpha1.ExternalDownstreamStrategy) error {
-
-	newProject := project.DeepCopy()
-
-	newProject.Status.IsReady = true
-	newProject.Status.DownstreamEndpoint = endpointStatus.DeepCopy()
-
-	newProject.Status.
-		GetProjectCondition(iotv1alpha1.ProjectConditionTypeReady).
-		SetStatusOk()
-
-	return r.client.Update(ctx, newProject)
-}
-
 func (r *ReconcileIoTProject) updateProjectStatus(ctx context.Context, project *iotv1alpha1.IoTProject, currentError error) (reconcile.Result, error) {
 
 	newProject := project.DeepCopy()
@@ -204,6 +170,8 @@ func (r *ReconcileIoTProject) updateProjectStatus(ctx context.Context, project *
 	}
 	if newProject.Status.IsReady {
 		status = corev1.ConditionTrue
+		newProject.Status.Phase = "Ready"
+		newProject.Status.PhaseReason = ""
 	} else {
 		status = corev1.ConditionFalse
 	}
@@ -219,25 +187,6 @@ func (r *ReconcileIoTProject) updateProjectStatus(ctx context.Context, project *
 
 }
 
-func (r *ReconcileIoTProject) applyUpdate(ctx context.Context, status *iotv1alpha1.ExternalDownstreamStrategy, err error, request *reconcile.Request, project *iotv1alpha1.IoTProject) (reconcile.Result, error) {
-
-	if err != nil {
-
-		if util.IsNotReadyYetError(err) {
-			log.Info("Not ready yet, re-scheduling...")
-			// if the resource is not ready yet, then we retry after a delay
-			return reconcile.Result{Requeue: true, RequeueAfter: time.Minute}, nil
-		}
-
-		log.Error(err, "failed to reconcile")
-		_ = r.updateProjectStatusError(ctx, request, project, err)
-		return reconcile.Result{}, err
-	}
-
-	err = r.updateProjectStatusReady(ctx, request, project, status)
-	return reconcile.Result{}, err
-}
-
 // Reconcile by reading the IoT project spec and making required changes
 //
 // returning an error will get the request re-queued
diff --git a/pkg/controller/iotproject/managed.go b/pkg/controller/iotproject/managed.go
index ab5cd2e25..a085e5b8c 100644
--- a/pkg/controller/iotproject/managed.go
+++ b/pkg/controller/iotproject/managed.go
@@ -30,14 +30,25 @@ const AddressNameCommandLegacy = "control"
 const AddressNameCommand = "command"
 const AddressNameCommandResponse = "command_response"
 
+var Addresses = []string{
+	AddressNameTelemetry,
+	AddressNameEvent,
+	AddressNameCommandLegacy,
+	AddressNameCommand,
+	AddressNameCommandResponse,
+}
+
 const resourceTypeAddressSpace = "Address Space"
 const resourceTypeAdapterUser = "Adapter User"
 
+const annotationPasswordSyncTime = annotationBase + "/passwordSyncTime"
+const annotationProject = annotationBase + "/project.name"
+const annotationProjectUID = annotationBase + "/project.uid"
+
 type managedStatus struct {
-	remainingCreated   map[string]bool
-	remainingReady     map[string]bool
-	addressSpace       *enmassev1beta1.AddressSpace
-	adapterCredentials *iotv1alpha1.Credentials
+	remainingCreated map[string]bool
+	remainingReady   map[string]bool
+	addressSpace     *enmassev1beta1.AddressSpace
 }
 
 func updateFromMap(resources map[string]bool, condition *iotv1alpha1.CommonCondition, reason string) {
@@ -75,7 +86,7 @@ func updateManagedStatus(m *managedStatus, project *iotv1alpha1.IoTProject) {
 
 func (r *ReconcileIoTProject) reconcileManaged(ctx context.Context, request *reconcile.Request, project *iotv1alpha1.IoTProject) (reconcile.Result, error) {
 
-	log.Info("Reconcile project with managed strategy")
+	now := time.Now()
 
 	strategy := project.Spec.DownstreamStrategy.ManagedDownstreamStrategy
 
@@ -90,6 +101,7 @@ func (r *ReconcileIoTProject) reconcileManaged(ctx context.Context, request *rec
 	managedStatus.remainingReady[resourceTypeAdapterUser] = false
 
 	managedStatus.remainingCreated[resourceTypeAddressSpace] = false
+	managedStatus.remainingReady[resourceTypeAdapterUser] = false
 
 	// defer condition status update
 
@@ -99,6 +111,19 @@ func (r *ReconcileIoTProject) reconcileManaged(ctx context.Context, request *rec
 
 	rc := recon.ReconcileContext{}
 
+	// ensure adapter credentials are set
+
+	rc.Process(func() (result reconcile.Result, e error) {
+		return r.ensureAdapterCredentials(ctx, project)
+	})
+
+	if rc.NeedRequeue() || rc.Error() != nil {
+		// return early as we need to be persisted
+		return rc.Result()
+	}
+
+	project.Status.Phase = "Configuring"
+
 	// reconcile address space
 
 	rc.Process(func() (result reconcile.Result, e error) {
@@ -116,34 +141,144 @@ func (r *ReconcileIoTProject) reconcileManaged(ctx context.Context, request *rec
 	// create a new user for protocol adapters
 
 	rc.Process(func() (result reconcile.Result, e error) {
-		credentials, err := r.reconcileAdapterUser(ctx, project, strategy, managedStatus)
-		managedStatus.adapterCredentials = credentials
-		return reconcile.Result{}, err
+		return r.reconcileAdapterUser(ctx, project, strategy, managedStatus)
 	})
 
 	// extract endpoint information
 
-	project.Status.DownstreamEndpoint = nil
-	if managedStatus.addressSpace != nil && managedStatus.adapterCredentials != nil {
+	currentCredentials := project.Status.DownstreamEndpoint.Credentials.DeepCopy()
+	if managedStatus.addressSpace != nil && project.Status.IsReady {
 
 		forceTls := true
 		rc.Process(func() (result reconcile.Result, e error) {
-			endpoint, err := extractEndpointInformation("messaging", iotv1alpha1.Service, "amqps", managedStatus.adapterCredentials, managedStatus.addressSpace, &forceTls)
+			endpoint, err := extractEndpointInformation("messaging", iotv1alpha1.Service, "amqps", currentCredentials, managedStatus.addressSpace, &forceTls)
 
 			if endpoint != nil {
-				project.Status.DownstreamEndpoint = endpoint.DeepCopy()
+				project.Status.DownstreamEndpoint = endpoint.ConnectionInformation.DeepCopy()
 			}
 
 			return reconcile.Result{}, err
 		})
 	}
 
+	// check and queue password reset
+
+	rc.Process(func() (result reconcile.Result, e error) {
+
+		if val, ok := project.Annotations[annotationBase+"/resetPasswordAfter"]; ok {
+			expires, err := time.Parse(time.RFC3339, val)
+			if err != nil {
+				return reconcile.Result{}, err
+			}
+			delay := expires.Sub(now)
+			if delay.Seconds() > 0 {
+				log.Info("Password set to expire in", "delay", delay)
+				return reconcile.Result{RequeueAfter: delay}, nil
+			}
+		}
+		return reconcile.Result{}, nil
+	})
+
 	// return result
 
 	return rc.Result()
 
 }
 
+func (r *ReconcileIoTProject) ensureAdapterCredentials(ctx context.Context, project *iotv1alpha1.IoTProject) (reconcile.Result, error) {
+
+	if project.Status.DownstreamEndpoint == nil {
+		project.Status.DownstreamEndpoint = &iotv1alpha1.ConnectionInformation{}
+	}
+
+	if project.Status.Managed == nil {
+		project.Status.Managed = &iotv1alpha1.ManagedStatus{}
+	}
+
+	//
+
+	changed := false
+
+	// eval address space
+
+	if project.Status.Managed.AddressSpace != project.Spec.DownstreamStrategy.ManagedDownstreamStrategy.AddressSpace.Name {
+
+		if project.Status.Managed.AddressSpace == "" {
+
+			// record change, early store and requeue
+
+			project.Status.Managed.AddressSpace = project.Spec.DownstreamStrategy.ManagedDownstreamStrategy.AddressSpace.Name
+			changed = true
+
+		} else {
+
+			// cleanup old address space first
+
+			project.Status.Phase = "Reconfiguring"
+			project.Status.PhaseReason = "Address Space changed"
+
+			result, err := cleanupManagedResources(ctx, r.client, project)
+			if err != nil {
+				// failed to clean up ... return
+				return result, err
+			}
+
+			// clear out address space, will re-set in the next iteration
+
+			project.Status.Managed.AddressSpace = ""
+			changed = true
+
+		}
+
+	}
+
+	// eval password
+
+	if val, ok := project.Annotations[annotationBase+"/resetPasswordAfter"]; ok {
+		expires, err := time.Parse(time.RFC3339, val)
+		if err != nil {
+			return reconcile.Result{}, err
+		}
+		if time.Now().After(expires) && expires.After(project.Status.Managed.PasswordTime.Time) {
+			log.Info("Password expired")
+			// reset password
+			project.Status.DownstreamEndpoint.Password = ""
+		}
+	}
+
+	if project.Status.DownstreamEndpoint.Password == "" || project.Status.Managed.PasswordTime.Time.IsZero() {
+
+		project.Status.Managed.PasswordTime.Time = time.Now()
+
+		// ... we generate a new one ...
+		log.Info("Generating new password")
+		password, err := util.GeneratePassword(32)
+
+		if err != nil {
+			return reconcile.Result{}, err
+		}
+
+		project.Status.DownstreamEndpoint = &iotv1alpha1.ConnectionInformation{
+			Credentials: iotv1alpha1.Credentials{
+				Username: "adapter-" + string(project.UID),
+				Password: password,
+			},
+		}
+
+		// re-queue right now to ensure the password is stored
+
+		changed = true
+
+	}
+
+	// proceed
+
+	return reconcile.Result{
+		Requeue: changed,
+	}, nil
+
+}
+
 func (r *ReconcileIoTProject) reconcileAddressSpace(ctx context.Context, project *iotv1alpha1.IoTProject, strategy *iotv1alpha1.ManagedDownstreamStrategy, managedStatus *managedStatus) (*enmassev1beta1.AddressSpace, reconcile.Result, error) {
 
 	addressSpace := &enmassev1beta1.AddressSpace{
@@ -158,10 +293,6 @@ func (r *ReconcileIoTProject) reconcileAddressSpace(ctx context.Context, project
 	_, err := controllerutil.CreateOrUpdate(ctx, r.client, addressSpace, func(existing runtime.Object) error {
 		existingAddressSpace := existing.(*enmassev1beta1.AddressSpace)
 
-		if err := r.ensureOwnerIsSet(project, existingAddressSpace); err != nil {
-			return err
-		}
-
 		log.V(2).Info("Reconcile address space", "AddressSpace", existingAddressSpace)
 
 		managedStatus.remainingReady[resourceTypeAddressSpace] = existingAddressSpace.Status.IsReady
@@ -183,11 +314,13 @@ func (r *ReconcileIoTProject) reconcileAddressSpace(ctx context.Context, project
 
 }
 
-func (r *ReconcileIoTProject) reconcileAdapterUser(ctx context.Context, project *iotv1alpha1.IoTProject, strategy *iotv1alpha1.ManagedDownstreamStrategy, managedStatus *managedStatus) (*iotv1alpha1.Credentials, error) {
+func (r *ReconcileIoTProject) reconcileAdapterUser(ctx context.Context, project *iotv1alpha1.IoTProject, strategy *iotv1alpha1.ManagedDownstreamStrategy, managedStatus *managedStatus) (reconcile.Result, error) {
 
-	credentials := &iotv1alpha1.Credentials{
-		Username: "adapter",
-	}
+	// ensured that this is not nil
+
+	credentials := project.Status.DownstreamEndpoint.Credentials
+
+	// construct object
 
 	adapterUser := &userv1beta1.MessagingUser{
 		ObjectMeta: metav1.ObjectMeta{
@@ -209,7 +342,7 @@ func (r *ReconcileIoTProject) reconcileAdapterUser(ctx context.Context, project
 		managedStatus.remainingReady[resourceTypeAdapterUser] = true
 	}
 
-	return credentials, err
+	return reconcile.Result{}, err
 }
 
 func (r *ReconcileIoTProject) reconcileAddress(project *iotv1alpha1.IoTProject, strategy *iotv1alpha1.ManagedDownstreamStrategy, addressName string, plan string, typeName string, existing *enmassev1beta1.Address) error {
@@ -313,51 +446,89 @@ func (r *ReconcileIoTProject) reconcileAddressSet(ctx context.Context, project *
 
 func (r *ReconcileIoTProject) reconcileManagedAddressSpace(project *iotv1alpha1.IoTProject, strategy *iotv1alpha1.ManagedDownstreamStrategy, existing *enmassev1beta1.AddressSpace) error {
 
-	if existing.CreationTimestamp.IsZero() {
-		existing.ObjectMeta.Labels = project.Labels
+	// add ourselves to the list of owners
+
+	if err := r.ensureOwnerIsSet(project, existing); err != nil {
+		return err
 	}
 
+	// we must have a plan
+
 	if strategy.AddressSpace.Plan == "" {
 		return fmt.Errorf("'addressSpace.plan' must not be empty")
 	}
 
-	existing.Spec.Type = StringOrDefault(strategy.AddressSpace.Type, "standard")
-	existing.Spec.Plan = strategy.AddressSpace.Plan
+	// eval type information
+
+	t := StringOrDefault(strategy.AddressSpace.Type, "standard")
+
+	// check if we are the only one using this space
+
+	if len(existing.OwnerReferences) == 1 {
+
+		// if so, we simply set the information
+
+		existing.Spec.Type = t
+		existing.Spec.Plan = strategy.AddressSpace.Plan
+
+	} else {
+
+		// otherwise, we must ensure we have the same setting
+
+		if existing.Spec.Type != t {
+			return fmt.Errorf("address space is already created using a different type: %s", existing.Spec.Type)
+		}
+		if existing.Spec.Plan != strategy.AddressSpace.Plan {
+			return fmt.Errorf("address space is already created using a different plan: %s", existing.Spec.Plan)
+		}
+
+	}
 
 	return nil
 }
 
-func (r *ReconcileIoTProject) reconcileAdapterMessagingUser(project *iotv1alpha1.IoTProject, credentials *iotv1alpha1.Credentials, existing *userv1beta1.MessagingUser) error {
+func (r *ReconcileIoTProject) reconcileAdapterMessagingUser(project *iotv1alpha1.IoTProject, credentials iotv1alpha1.Credentials, existing *userv1beta1.MessagingUser) error {
 
 	if err := r.ensureControllerOwnerIsSet(project, existing); err != nil {
 		return err
 	}
 
+	managed := project.Status.Managed
+
 	existing.Spec.Username = credentials.Username
 	existing.Spec.Authentication = userv1beta1.AuthenticationSpec{
 		Type: "password",
 	}
 
-	// if the status has endpoint information ...
-	if project.Status.DownstreamEndpoint != nil {
-		// ... we extract the password and return it
-		credentials.Password = project.Status.DownstreamEndpoint.Password
+	if existing.Annotations == nil {
+		existing.Annotations = make(map[string]string)
 	}
 
-	// if the project is ready and password is not set yet ...
-	if (project.Status.IsReady && credentials.Password == "") || util.IsNewObject(existing) {
-		// ... we generate a new one ...
-		log.Info("Generating new password")
-		password, err := util.GeneratePassword(32)
-		if err != nil {
-			return err
-		}
-		// ... return it in the credentials structure
-		credentials.Password = password
-		// ... and set it to the messaging user
+	existing.Annotations[annotationProject] = project.Name
+	existing.Annotations[annotationProjectUID] = string(project.UID)
+
+	// get last password sync
+
+	var syncTime time.Time
+	syncTimeString := existing.Annotations[annotationPasswordSyncTime]
+	if syncTimeString != "" {
+		syncTime, _ = time.Parse(time.RFC3339, syncTimeString)
+	}
+
+	// if we missed an update
+
+	if managed.PasswordTime.After(syncTime) {
+
+		log.Info("Updating password for", "user", existing.Name)
+
+		// set the password
 		existing.Spec.Authentication.Password = []byte(credentials.Password)
+		existing.Annotations[annotationPasswordSyncTime] = managed.PasswordTime.UTC().Format(time.RFC3339)
+
 	}
 
+	// create access rules
+
 	telemetryName := util.AddressName(project, AddressNameTelemetry)
 	eventName := util.AddressName(project, AddressNameEvent)
 
diff --git a/pkg/controller/iotproject/provided.go b/pkg/controller/iotproject/provided.go
index fcb2e05d4..23f4a9b91 100644
--- a/pkg/controller/iotproject/provided.go
+++ b/pkg/controller/iotproject/provided.go
@@ -29,7 +29,7 @@ func (r *ReconcileIoTProject) reconcileProvided(ctx context.Context, request *re
 
 	project.Status.DownstreamEndpoint = nil
 	if endpoint != nil {
-		project.Status.DownstreamEndpoint = endpoint.DeepCopy()
+		project.Status.DownstreamEndpoint = endpoint.ConnectionInformation.DeepCopy()
 	}
 
 	return reconcile.Result{}, err
diff --git a/pkg/controller/iotproject/utils.go b/pkg/controller/iotproject/utils.go
index 59d44a671..4d23aea1c 100644
--- a/pkg/controller/iotproject/utils.go
+++ b/pkg/controller/iotproject/utils.go
@@ -17,6 +17,8 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
+const annotationBase = "iot.enmasse.io"
+
 // Ensure that a controller owner is set
 // As there may only be one, we only to this when the creation timestamp is zero
 func (r *ReconcileIoTProject) ensureControllerOwnerIsSet(owner, object v1.Object) error {
diff --git a/pkg/util/address.go b/pkg/util/address.go
index 8940ce8cb..f96111700 100644
--- a/pkg/util/address.go
+++ b/pkg/util/address.go
@@ -7,6 +7,7 @@ package util
 
 import (
 	"regexp"
+	"strings"
 
 	"github.com/google/uuid"
 
@@ -14,10 +15,10 @@ import (
 )
 
 var (
-	addressNameExpression  *regexp.Regexp = regexp.MustCompile("^[a-zA-Z]+$")
-	replaceExpression      *regexp.Regexp = regexp.MustCompile("[^a-zA-Z]")
-	replaceStartExpression *regexp.Regexp = regexp.MustCompile("^[^a-zA-Z]+")
-	operatorUuidNamespace  uuid.UUID      = uuid.MustParse("1516b246-23aa-11e9-b615-c85b762e5a2c")
+	addressNameExpression *regexp.Regexp = regexp.MustCompile("^[a-z0-9\\-]+$")
+	replaceExpression     *regexp.Regexp = regexp.MustCompile("[^a-z0-9\\-]")
+	replaceEndExpression  *regexp.Regexp = regexp.MustCompile("[^a-z0-9]+$")
+	operatorUuidNamespace uuid.UUID      = uuid.MustParse("1516b246-23aa-11e9-b615-c85b762e5a2c")
 )
 
 // Get an address name from an IoTProject
@@ -32,25 +33,28 @@ func EncodeAsMetaName(name string, maxLength int) string {
 		return name
 	}
 
-	newPrefix := replaceExpression.ReplaceAllString(name, "")
-	if len(newPrefix) > 0 {
-		newPrefix = newPrefix + "-"
-	}
+	name = strings.ToLower(name)
+
+	prefix := replaceExpression.ReplaceAllString(name, "")
 
-	name = newPrefix + uuid.NewMD5(operatorUuidNamespace, []byte(name)).String()
+	id := uuid.NewMD5(operatorUuidNamespace, []byte(name)).String()
 
 	if maxLength > 0 {
-		rname := []rune(name)
-		l := len(rname)
+		rprefix := []rune(prefix)
+		idlen := 1 + len(id)
+		l := len(rprefix) + idlen
 		if l > maxLength {
-			s := l - maxLength
-			rname = rname[s:l]
-			name = string(rname)
-			name = replaceStartExpression.ReplaceAllString(name, "")
+			rprefix = rprefix[:maxLength-idlen]
+			prefix = string(rprefix)
+			prefix = replaceEndExpression.ReplaceAllString(prefix, "")
 		}
 	}
 
-	return name
+	if len(prefix) > 0 {
+		prefix = prefix + "-"
+	}
+
+	return prefix + id
 }
 
 // Encode an address name so that it can be put inside the .metadata.name field of an Address object
diff --git a/pkg/util/install/delete.go b/pkg/util/install/delete.go
index 5e49a7002..58cbec98b 100644
--- a/pkg/util/install/delete.go
+++ b/pkg/util/install/delete.go
@@ -223,8 +223,7 @@ func BulkRemoveOwner(ctx context.Context, c client.Client, owner runtime.Object,
 
 		// process ownership
 
-		result, err := ProcessOwnedByObject(owner, i, controller, true)
-
+		result, err := RemoveAsOwner(owner, i, controller)
 		if err != nil {
 			return nil, err
 		}
@@ -247,3 +246,7 @@ func BulkRemoveOwner(ctx context.Context, c client.Client, owner runtime.Object,
 	})
 
 }
+
+func RemoveAsOwner(owner runtime.Object, object interface{}, controller bool) (OwnerResult, error) {
+	return ProcessOwnedByObject(owner, object, controller, true)
+}
diff --git a/templates/csv/enmasse.clusterserviceversion.yaml b/templates/csv/enmasse.clusterserviceversion.yaml
index d88314bce..0eb60caf1 100644
--- a/templates/csv/enmasse.clusterserviceversion.yaml
+++ b/templates/csv/enmasse.clusterserviceversion.yaml
@@ -1253,3 +1253,14 @@ spec:
             path: conditions
             x-descriptors:
               - 'urn:alm:descriptor:io.kubernetes.conditions'
+
+          - description: The phase of the project.
+            displayName: Phase
+            path: phase
+            x-descriptors:
+              - 'urn:alm:descriptor:io.kubernetes.phase'
+          - description: Description of the project phase.
+            displayName: Reason
+            path: phaseReason
+            x-descriptors:
+              - 'urn:alm:descriptor:io.kubernetes.phase:reason'
diff --git a/templates/iot/api/010-CustomResourceDefinition-iot-project.yaml b/templates/iot/api/010-CustomResourceDefinition-iot-project.yaml
index 6183b3972..7109cf074 100644
--- a/templates/iot/api/010-CustomResourceDefinition-iot-project.yaml
+++ b/templates/iot/api/010-CustomResourceDefinition-iot-project.yaml
@@ -32,9 +32,9 @@ spec:
     type: boolean
     description: If TLS is enabled
     JSONPath: .status.downstreamEndpoint.tls
-  - name: Ready
-    type: boolean
-    description: If the IoT project is ready
-    JSONPath: .status.isReady
+  - name: Phase
+    type: string
+    description: Phase of the IoT project
+    JSONPath: .status.phase
   subresources:
     status: {}
diff --git a/templates/iot/examples/infinispan/manual/030-Service.yaml b/templates/iot/examples/infinispan/manual/030-Service.yaml
index c216a27a6..a667fa144 100644
--- a/templates/iot/examples/infinispan/manual/030-Service.yaml
+++ b/templates/iot/examples/infinispan/manual/030-Service.yaml
@@ -3,7 +3,7 @@ kind: Service
 metadata:
   labels:
     app: infinispan
-  name: infinispan-server
+  name: infinispan # align name with operator version
 spec:
   ports:
   - port: 11222
@@ -13,5 +13,5 @@ spec:
     targetPort: 9990
     name: management
   selector:
-    name: infinispan-server
+    name: infinispan
     app: infinispan
\ No newline at end of file
diff --git a/templates/iot/examples/infinispan/manual/050-StatefulSet.yaml b/templates/iot/examples/infinispan/manual/050-StatefulSet.yaml
index 371d59f53..1d4f0155c 100644
--- a/templates/iot/examples/infinispan/manual/050-StatefulSet.yaml
+++ b/templates/iot/examples/infinispan/manual/050-StatefulSet.yaml
@@ -2,22 +2,22 @@ kind: StatefulSet
 apiVersion: apps/v1beta1
 metadata:
   labels:
-    name: infinispan-server
+    name: infinispan
     app: infinispan
-  name: infinispan-server
+  name: infinispan
 spec:
   replicas: 1
   serviceName: infinispan-server-headless
   selector:
     matchLabels:
-      name: infinispan-server
+      name: infinispan
       app: infinispan
   template:
     metadata:
       labels:
-        name: infinispan-server
+        name: infinispan
         app: infinispan
-      name: infinispan-server
+      name: infinispan
     spec:
       serviceAccount: infinispan
       containers:
@@ -26,7 +26,7 @@ spec:
         - -Djboss.default.jgroups.stack=kubernetes
         env:
         - name: KUBERNETES_LABELS
-          value: app=infinispan,name=infinispan-server
+          value: app=infinispan,name=infinispan
         - name: KUBERNETES_NAMESPACE
           valueFrom:
             fieldRef:


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 08d87112e..16751717b 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
@@ -291,4 +291,13 @@ public class KubeUtilTest {
     public void testSanitizeUuidName(final String name, final String uuid, final String output) {
         assertEquals(output, KubeUtil.sanitizeWithUuid(name, uuid));
     }
+
+    /**
+     * Test for encoding with Go logic.
+     */
+    @Test
+    public void testSanitizeForGo () {
+        assertEquals("as1.telemetryiot-project-ns-4cf002ae-37a5-38d6-9749-b7f85b29a385", KubeUtil.sanitizeForGo("as1", "telemetry/iot-project-ns.iot1"));
+    }
+
 }
diff --git a/api-model/src/test/java/io/enmasse/user/model/v1/UserModelTest.java b/api-model/src/test/java/io/enmasse/user/model/v1/UserModelTest.java
index 5857f5cbd..c2521d38d 100644
--- a/api-model/src/test/java/io/enmasse/user/model/v1/UserModelTest.java
+++ b/api-model/src/test/java/io/enmasse/user/model/v1/UserModelTest.java
@@ -24,6 +24,7 @@ public class UserModelTest {
                 .withMetadata(new ObjectMetaBuilder()
                         .withName("myspace.user1")
                         .withNamespace("ns1")
+                        .addToAnnotations("foo", "bar")
                         .build())
                 .withSpec(new UserSpecBuilder()
                         .withUsername("user1")
@@ -60,15 +61,20 @@ public class UserModelTest {
         UserList list = new UserList();
         list.getItems().add(user);
 
+        // serialize & deserialize
+
         serialized = mapper.writeValueAsBytes(list);
         UserList deserializedList = mapper.readValue(serialized, UserList.class);
 
+        // assert deserialized
+
         assertEquals(1, deserializedList.getItems().size());
 
         deserialized = deserializedList.getItems().get(0);
 
         assertEquals(user.getMetadata().getName(), deserialized.getMetadata().getName());
         assertEquals(user.getMetadata().getNamespace(), deserialized.getMetadata().getNamespace());
+        assertEquals("bar", deserialized.getMetadata().getAnnotations().get("foo"));
         assertEquals(user.getSpec().getUsername(), deserialized.getSpec().getUsername());
         assertEquals(user.getSpec().getAuthentication().getType(), deserialized.getSpec().getAuthentication().getType());
         assertEquals(user.getSpec().getAuthentication().getPassword(), deserialized.getSpec().getAuthentication().getPassword());
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 4fda9f260..d05def7b7 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
@@ -11,6 +11,7 @@ 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 static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -313,7 +314,7 @@ public class KeycloakUserApiTest {
      * Assert groups. <br>
      * A convenience method to assert group information. Compares the groups as
      * sorted arrays.
-     * 
+     *
      * @param actual   The actual result.
      * @param expected The expected result.
      */
@@ -351,4 +352,18 @@ public class KeycloakUserApiTest {
         assertTrue(KeycloakUserApi.hasAttribute(realm, "foo", null));
         assertFalse(KeycloakUserApi.hasAttribute(realm, null, null));
     }
+
+    @Test
+    public void testAnnotations() {
+        final Map<String,String> annotations = new HashMap<>();
+        annotations.put("foo", "bar");
+        annotations.put("bar", "baz");
+
+        final List<String> encoded = KeycloakUserApi.annotationsToString(annotations);
+        assertNotNull(encoded);
+        assertEquals(2,encoded.size());
+
+        final Map<String, String> actualAnnotations = KeycloakUserApi.annotationsFromString(encoded);
+        assertEquals(annotations, actualAnnotations);
+    }
 }
diff --git a/pkg/apis/admin/v1beta2/types_test.go b/pkg/apis/admin/v1beta2/types_test.go
new file mode 100644
index 000000000..09580c84a
--- /dev/null
+++ b/pkg/apis/admin/v1beta2/types_test.go
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package v1beta2
+
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestSerializeAddressPlan(t *testing.T) {
+	ap := AddressPlan{}
+	bytes, err := json.Marshal(ap)
+	if err != nil {
+		t.Fatalf("Failed to serialize")
+		return
+	}
+
+	fmt.Println(string(bytes))
+}
+
+func TestDeserializeAddressPlan(t *testing.T) {
+	data := []byte(`
+{
+        "spec": {
+        "addressType": "queue",
+        "displayName": "Brokered Queue",
+        "displayOrder": 0,
+        "longDescription": "Creates a queue on a broker.",
+        "resources": {
+            "foos": 1
+        },
+        "shortDescription": "Creates a queue on a broker."
+    }
+}`)
+	ap := AddressPlan{}
+	err := json.Unmarshal(data, &ap)
+
+	if err != nil {
+		t.Fatal("Failed to decode:", err)
+	}
+
+	if val, ok := ap.Spec.Resources["foo"]; ok {
+		if val != 1 {
+			t.Error("foo resources unequal")
+		}
+	}
+}
+
+func TestDeepEqualsAddressPlan(t *testing.T) {
+	data := []byte(`
+{
+        "spec": {
+        "addressType": "queue",
+        "displayName": "My queue",
+        "displayOrder": 0,
+        "longDescription": "My long description",
+        "shortDescription": "My short description",
+        "resources": {
+            "broker": 0
+        }
+    }
+}`)
+
+	ap1 := &AddressPlan{}
+	err := json.Unmarshal(data, ap1)
+
+	if err != nil {
+		t.Fatal("Failed to decode:", err)
+	}
+
+	ap2 := ap1.DeepCopyObject()
+	ap3 := ap1.DeepCopy()
+
+	if !reflect.DeepEqual(ap1, ap2) {
+		t.Error("a1 equals a2 failed")
+	}
+	if !reflect.DeepEqual(ap2, ap3) {
+		t.Error("a2 equals a3 failed")
+	}
+	if !reflect.DeepEqual(ap1, ap3) {
+		t.Error("a1 equals a3 failed")
+	}
+}
+func TestSerializeAddressSpacePlan(t *testing.T) {
+	asp := AddressSpacePlan{}
+	bytes, err := json.Marshal(asp)
+	if err != nil {
+		t.Fatalf("Failed to serialize")
+		return
+	}
+
+	fmt.Println(string(bytes))
+}
+
+func TestDeserializeAddressSpacePlan(t *testing.T) {
+	data := []byte(`
+{
+        "spec": {
+        "addressPlans": [
+            "foo-plan"
+        ],
+        "addressSpaceType": "mytype",
+        "displayName": "My space plan",
+        "displayOrder": 0,
+        "infraConfigRef": "my-infra-config",
+        "longDescription": "My long description",
+        "shortDescription": "My short description",
+        "resourceLimits": {
+            "foo": 500
+        }
+    }
+}`)
+	asp := AddressSpacePlan{}
+	err := json.Unmarshal(data, &asp)
+
+	if err != nil {
+		t.Fatal("Failed to decode:", err)
+	}
+
+	if val, ok := asp.Spec.ResourceLimits["foo"]; ok {
+		if val != 500 {
+			t.Error("foo resource limits unequal")
+		}
+	}
+}
+
+func TestDeepEqualsAddressSpacePlan(t *testing.T) {
+	data := []byte(`
+{
+        "spec": {
+        "addressPlans": [
+            "foo-plan"
+        ],
+        "addressSpaceType": "mytype",
+        "displayName": "My space plan",
+        "displayOrder": 0,
+        "infraConfigRef": "my-infra-config",
+        "longDescription": "My long description",
+        "shortDescription": "My short description",
+        "resourceLimits": {
+            "foo": 500
+        }
+    }
+}`)
+
+	asp1 := &AddressSpacePlan{}
+	err := json.Unmarshal(data, asp1)
+
+	if err != nil {
+		t.Fatal("Failed to decode:", err)
+	}
+
+	asp2 := asp1.DeepCopyObject()
+	asp3 := asp1.DeepCopy()
+
+	if !reflect.DeepEqual(asp1, asp2) {
+		t.Error("a1 equals a2 failed")
+	}
+	if !reflect.DeepEqual(asp2, asp3) {
+		t.Error("a2 equals a3 failed")
+	}
+	if !reflect.DeepEqual(asp1, asp3) {
+		t.Error("a1 equals a3 failed")
+	}
+}
diff --git a/pkg/util/address_test.go b/pkg/util/address_test.go
new file mode 100644
index 000000000..96b85fe52
--- /dev/null
+++ b/pkg/util/address_test.go
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package util
+
+import (
+	"testing"
+)
+
+func TestAddressName(t *testing.T) {
+	for _, v := range []struct {
+		addressSpaceName string
+		addressName      string
+		expected         string
+	}{
+		{addressSpaceName: "as1", addressName: "telemetry/iot-project-ns.iot1", expected: "as1.telemetryiot-project-ns-4cf002ae-37a5-38d6-9749-b7f85b29a385"},
+		{addressSpaceName: "as1", addressName: "event/iot-project-ns.iot1", expected: "as1.eventiot-project-nsiot1-82203f07-4081-392b-a834-dd1ffcaa2c5f"},
+	} {
+
+		out := EncodeAddressSpaceAsMetaName(v.addressSpaceName, v.addressName)
+		if out != v.expected {
+			t.Errorf("Encoding error - input: '%s', '%s' expected: '%s', actual: '%s'", v.addressSpaceName, v.addressName, v.expected, out)
+		}
+
+	}
+}
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/Environment.java b/systemtests/src/main/java/io/enmasse/systemtest/Environment.java
index c66f8c5db..12424d2ee 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/Environment.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/Environment.java
@@ -33,6 +33,7 @@ public class Environment {
     public static final String IS_OCP4_REGEXP = "4.*";
     private static final String DEFAULT_DEVICE_REGISTRY = "DEFAULT_DEVICE_REGISTRY";
     private static final String SKIP_SAVE_STATE = "SKIP_SAVE_STATE";
+    private static final String SKIP_DEPLOY_INFINISPAN = "SKIP_DEPLOY_INFINISPAN";
     private static Logger log = CustomLogger.getLogger();
     private static Environment instance;
     private final String namespace = System.getenv(K8S_NAMESPACE_ENV);
@@ -53,6 +54,7 @@ public class Environment {
     private final String appName = System.getenv().getOrDefault(APP_NAME_ENV, "enmasse");
     private final String defaultDeviceRegistry = System.getenv().getOrDefault(DEFAULT_DEVICE_REGISTRY, "file");
     private final boolean skipSaveState = Boolean.parseBoolean(System.getenv(SKIP_SAVE_STATE));
+    private final boolean skipDeployInfinispan = Boolean.parseBoolean(System.getenv(SKIP_DEPLOY_INFINISPAN));
     protected UserCredentials managementCredentials = new UserCredentials(null, null);
     protected UserCredentials defaultCredentials = new UserCredentials(null, null);
 
@@ -190,9 +192,15 @@ public class Environment {
     public void setDefaultCredentials(UserCredentials defaultCredentials) {
         this.defaultCredentials = defaultCredentials;
     }
+
     public boolean isSkipSaveState() {
-        return skipSaveState;
+        return this.skipSaveState;
+    }
+
+    public boolean isSkipDeployInfinispan() {
+        return this.skipDeployInfinispan;
     }
+
     public String getDefaultDeviceRegistry() {
         return defaultDeviceRegistry;
     }
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/platform/Kubernetes.java b/systemtests/src/main/java/io/enmasse/systemtest/platform/Kubernetes.java
index eba82b3bc..2a50a1c91 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/platform/Kubernetes.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/platform/Kubernetes.java
@@ -88,6 +88,7 @@ import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
@@ -275,6 +276,7 @@ public abstract class Kubernetes {
 
     public Endpoint getEndpoint(String serviceName, String namespace, String port) {
         Service service = client.services().inNamespace(namespace).withName(serviceName).get();
+        Objects.requireNonNull(service, () -> String.format("Unable to find service '%s' in namespace '%s'", serviceName, namespace));
         return new Endpoint(service.getSpec().getClusterIP(), getPort(service, port));
     }
 
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/platform/apps/SystemtestsKubernetesApps.java b/systemtests/src/main/java/io/enmasse/systemtest/platform/apps/SystemtestsKubernetesApps.java
index eeb4b32e5..25669dfae 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/platform/apps/SystemtestsKubernetesApps.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/platform/apps/SystemtestsKubernetesApps.java
@@ -61,7 +61,7 @@ public class SystemtestsKubernetesApps {
     public static final String SELENIUM_CONFIG_MAP = "rhea-configmap";
     public static final String OPENSHIFT_CERT_VALIDATOR = "systemtests-cert-validator";
     public static final String POSTGRES_APP = "postgres-app";
-    public static final String INFINISPAN_SERVER = "infinispan-server";
+    public static final String INFINISPAN_SERVER = "infinispan";
     private static final Path INFINISPAN_EXAMPLE_BASE = Paths.get("../templates/iot/examples/infinispan");
 
     public static String getMessagingAppPodName() throws Exception {
@@ -196,7 +196,11 @@ public class SystemtestsKubernetesApps {
         return in -> ReplaceValueStream.replaceValues(in, values);
     }
 
-    public static Endpoint deployInfinispanServer(String namespace) throws Exception {
+    public static Endpoint deployInfinispanServer(final String namespace) throws Exception {
+
+        if (Environment.getInstance().isSkipDeployInfinispan()) {
+            return getInfinispanEndpoint(namespace);
+        }
 
         final Kubernetes kubeCli = Kubernetes.getInstance();
         final KubernetesClient client = kubeCli.getClient();
@@ -217,17 +221,25 @@ public class SystemtestsKubernetesApps {
 
         // return hotrod enpoint
 
-        return kubeCli.getEndpoint(INFINISPAN_SERVER, namespace, "hotrod");
+        return getInfinispanEndpoint(namespace);
     }
 
+    private static Endpoint getInfinispanEndpoint(final String namespace) {
+        return Kubernetes.getInstance().getEndpoint(INFINISPAN_SERVER, namespace, "hotrod");
+    }
 
     public static void deleteInfinispanServer(final String namespace) throws Exception {
 
+        if (Environment.getInstance().isSkipDeployInfinispan()) {
+            return;
+        }
+
         // delete "common" and "manual" folders
 
         deleteDirectories(namespaceReplacer(namespace),
                 INFINISPAN_EXAMPLE_BASE.resolve("common"),
                 INFINISPAN_EXAMPLE_BASE.resolve("manual"));
+
     }
 
     public static void deployAMQBroker(String namespace, String name, String user, String password, BrokerCertBundle certBundle) throws Exception {
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/time/TimeoutBudget.java b/systemtests/src/main/java/io/enmasse/systemtest/time/TimeoutBudget.java
index 5768a62c7..b83648819 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/time/TimeoutBudget.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/time/TimeoutBudget.java
@@ -41,6 +41,14 @@ public class TimeoutBudget {
         }
     }
 
+    /**
+     * Get the remaining time of the budget.
+     * @return The remaining time.
+     */
+    public Duration remaining() {
+        return Duration.ofMillis(timeLeft());
+    }
+
     public boolean timeoutExpired() {
         return timeLeft() < 0;
     }
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/utils/AddressSpaceUtils.java b/systemtests/src/main/java/io/enmasse/systemtest/utils/AddressSpaceUtils.java
index 2b45e4240..f16408af7 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/utils/AddressSpaceUtils.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/utils/AddressSpaceUtils.java
@@ -29,6 +29,7 @@ import java.util.Optional;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.BooleanSupplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -58,6 +59,10 @@ public class AddressSpaceUtils {
                 .map(addressSpace -> addressSpace.getMetadata().getName()).collect(Collectors.toList()).contains(addressSpaceName);
     }
 
+    public static BooleanSupplier addressSpaceExists(final String namespace, final String name) {
+        return () -> existAddressSpace(namespace, name);
+    }
+
     public static boolean isAddressSpaceReady(AddressSpace addressSpace) {
         return addressSpace != null && addressSpace.getStatus().isReady();
     }
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/utils/IoTUtils.java b/systemtests/src/main/java/io/enmasse/systemtest/utils/IoTUtils.java
index 2edf525cf..ab2cf8b65 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/utils/IoTUtils.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/utils/IoTUtils.java
@@ -103,7 +103,9 @@ public class IoTUtils {
                 && project.getSpec().getDownstreamStrategy().getManagedStrategy().getAddressSpace().getName() != null
         ) {
             var addressSpaceName = project.getSpec().getDownstreamStrategy().getManagedStrategy().getAddressSpace().getName();
-            AddressSpaceUtils.waitForAddressSpaceReady(Kubernetes.getInstance().getAddressSpaceClient(project.getMetadata().getNamespace()).withName(addressSpaceName).get(), budget);
+            var addressSpace = Kubernetes.getInstance().getAddressSpaceClient(project.getMetadata().getNamespace()).withName(addressSpaceName).get();
+            Objects.requireNonNull(addressSpace, () -> String.format("Unable to find addressSpace: %s", addressSpaceName));
+            AddressSpaceUtils.waitForAddressSpaceReady(addressSpace, budget);
         }
     }
 
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java b/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java
index 09e42aaa6..958dee896 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java
@@ -32,12 +32,15 @@ import org.openqa.selenium.Capabilities;
 import org.openqa.selenium.chrome.ChromeOptions;
 import org.openqa.selenium.firefox.FirefoxOptions;
 import org.openqa.selenium.remote.RemoteWebDriver;
+import org.opentest4j.AssertionFailedError;
 import org.slf4j.Logger;
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.URL;
 import java.net.UnknownHostException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
@@ -50,12 +53,19 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.BooleanSupplier;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
 public class TestUtils {
+
+    public interface TimeoutHandler<X extends Throwable> {
+        void timeout() throws X;
+    }
+
     private static final Random R = new Random();
     private static Logger log = CustomLogger.getLogger();
 
@@ -368,34 +378,29 @@ public class TestUtils {
      * Check if endpoint is accessible
      */
     public static boolean resolvable(Endpoint endpoint) {
-        for (int i = 0; i < 10; i++) {
+        return waitUntilCondition(() -> {
             try {
                 InetAddress[] addresses = Inet4Address.getAllByName(endpoint.getHost());
                 return addresses.length > 0;
             } catch (UnknownHostException ignore) {
             }
-            try {
-                Thread.sleep(1_000);
-            } catch (InterruptedException e) {
-            }
-        }
-        return false;
+            return false;
+        }, Duration.ofSeconds(10), Duration.ofSeconds(1));
     }
 
     /**
      * Wait until Namespace will be removed
      *
      * @param kubernetes client for manipulation with kubernetes cluster
-     * @param namespace  project/namespace to remove
+     * @param namespace project/namespace to remove
      */
     public static void waitForNamespaceDeleted(Kubernetes kubernetes, String namespace) throws Exception {
-        TimeoutBudget budget = new TimeoutBudget(10, TimeUnit.MINUTES);
-        while (budget.timeLeft() >= 0 && kubernetes.listNamespaces().contains(namespace)) {
-            Thread.sleep(1000);
-        }
-        if (kubernetes.listNamespaces().contains(namespace)) {
-            throw new TimeoutException("Timed out waiting for namespace " + namespace + " to disappear");
-        }
+        waitUntilCondition(
+                () -> !kubernetes.listNamespaces().contains(namespace),
+                Duration.ofMinutes(10), Duration.ofSeconds(1),
+                () -> {
+                    throw new TimeoutException("Timed out waiting for namespace " + namespace + " to disappear");
+                });
     }
 
     /**
@@ -557,26 +562,131 @@ public class TestUtils {
     }
 
     public static void waitUntilCondition(final String forWhat, final Predicate<WaitPhase> condition, final TimeoutBudget budget) throws Exception {
+
         Objects.requireNonNull(condition);
         Objects.requireNonNull(budget);
 
         log.info("Waiting {} ms for - {}", budget.timeLeft(), forWhat);
 
-        while (!budget.timeoutExpired()) {
-            if (condition.test(WaitPhase.LOOP)) {
-                log.info("Successfully wait for: {} , it took {} ms", forWhat, budget.timeSpent());
-                return;
-            }
-            log.debug("next iteration, remaining time: {}", budget.timeLeft());
-            Thread.sleep(5000);
+        waitUntilCondition(
+
+                () -> condition.test(WaitPhase.LOOP),
+                budget.remaining(), Duration.ofSeconds(5),
+
+                () -> {
+                    // try once more
+                    if (condition.test(WaitPhase.LAST_TRY)) {
+                        log.info("Successfully wait for: {} , it passed on last try", forWhat);
+                        return;
+                    }
+
+                    throw new IllegalStateException("Failed to wait for: " + forWhat);
+                });
+
+        log.info("Successfully wait for: {}, it took {} ms", forWhat, budget.timeSpent());
+
+    }
+
+    /**
+     * Wait for a condition, fail otherwise.
+     *
+     * @param condition The condition to check, returning {@code true} means success.
+     * @param budget The remaining time budget for this step.
+     * @param delay The delay between checks.
+     * @param timeoutMessageSupplier The supplier of a timeout message.
+     * @throws AssertionFailedError In case the timeout expired
+     */
+    public static void waitUntilConditionOrFail(final BooleanSupplier condition, final Duration timeout, final Duration delay, final Supplier<String> timeoutMessageSupplier) {
+
+        Objects.requireNonNull(timeoutMessageSupplier);
+
+        waitUntilConditionOrThrow(condition, timeout, delay, () -> new AssertionFailedError(timeoutMessageSupplier.get()));
+
+    }
+
+    /**
+     * Wait for a condition, throw exception otherwise.
+     *
+     * @param condition The condition to check, returning {@code true} means success.
+     * @param budget The remaining time budget for this step.
+     * @param delay The delay between checks.
+     * @param exceptionSupplier The supplier of the exception to throw.
+     * @throws AssertionFailedError In case the timeout expired
+     */
+    public static <X extends Throwable> void waitUntilConditionOrThrow(final BooleanSupplier condition, final Duration timeout, final Duration delay, final Supplier<X> exceptionSupplier) throws X {
+
+        Objects.requireNonNull(exceptionSupplier);
+
+        waitUntilCondition(condition, timeout, delay, () -> {
+            throw exceptionSupplier.get();
+        });
+
+    }
+
+
+    /**
+     * Wait for a condition, call handler otherwise.
+     *
+     * @param condition The condition to check, returning {@code true} means success.
+     * @param budget The remaining time budget for this step.
+     * @param delay The delay between checks.
+     * @param timeoutHandler The handler to call in case of the timeout.
+     * @param <X> The type of exception thrown by the timeout handler.
+     * @throws AssertionFailedError In case the timeout expired
+     */
+    public static <X extends Throwable> void waitUntilCondition(final BooleanSupplier condition, final Duration timeout, final Duration delay, final TimeoutHandler<X> timeoutHandler) throws X {
+
+        Objects.requireNonNull(timeoutHandler);
+
+        if (!waitUntilCondition(condition, timeout, delay)) {
+            timeoutHandler.timeout();
         }
 
-        if (condition.test(WaitPhase.LAST_TRY)) {
-            log.info("Successfully wait for: {} , it passed on last try", forWhat);
-            return;
+    }
+
+    /**
+     * Wait for condition, return result.
+     * <p>
+     * This will check will put a priority on checking the condition, and only wait, when there is remaining time budget left.
+     * @param condition The condition to check, returning {@code true} means success.
+     * @param budget The remaining time budget for this step.
+     * @param delay The delay between checks.
+     * @return {@code true} if the condition was met, {@code false otherwise}.
+     */
+    public static boolean waitUntilCondition(final BooleanSupplier condition, final Duration timeout, final Duration delay) {
+
+        Objects.requireNonNull(condition);
+        Objects.requireNonNull(timeout);
+        Objects.requireNonNull(delay);
+
+        final Instant deadline = Instant.now().plus(timeout);
+
+        while ( true) {
+
+            // test first
+
+            if (condition.getAsBoolean()) {
+                return true;
+            }
+
+            // if the timeout has expired ... stop
+
+            final Duration remaining = Duration.between(deadline, Instant.now());
+            if ( !remaining.isNegative() ) {
+                return false;
+            }
+
+            // otherwise sleep
+
+            log.debug("next iteration, remaining time: {}", remaining);
+            try {
+                Thread.sleep(delay.toMillis());
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
         }
 
-        throw new IllegalStateException("Failed to wait for: " + forWhat);
     }
 
     public static void waitForChangedResourceVersion(final TimeoutBudget budget, final String namespace, final String name, final String currentResourceVersion) throws Exception {
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/bases/IoTTestBase.java b/systemtests/src/test/java/io/enmasse/systemtest/bases/IoTTestBase.java
index 0b60def77..836e6c5da 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/bases/IoTTestBase.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/bases/IoTTestBase.java
@@ -40,6 +40,13 @@ public abstract class IoTTestBase extends TestBase implements ITestBaseIsolated
     protected static final String IOT_ADDRESS_CONTROL = "control";
     protected static final String IOT_ADDRESS_COMMAND = "command";
     protected static final String IOT_ADDRESS_COMMAND_RESPONSE = "command_response";
+    protected static final String[] IOT_ADDRESSES = new String[] {
+                    IOT_ADDRESS_TELEMETRY,
+                    IOT_ADDRESS_EVENT,
+                    IOT_ADDRESS_CONTROL,
+                    IOT_ADDRESS_COMMAND,
+                    IOT_ADDRESS_COMMAND_RESPONSE,
+    };
 
     private static Logger log = CustomLogger.getLogger();
     protected String iotProjectNamespace = "iot-project-ns";
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/iot/IoTProjectManagedTest.java b/systemtests/src/test/java/io/enmasse/systemtest/iot/IoTProjectManagedTest.java
index 2adab8bc5..d239bbd90 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/iot/IoTProjectManagedTest.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/iot/IoTProjectManagedTest.java
@@ -138,7 +138,7 @@ class IoTProjectManagedTest extends IoTTestBase implements ITestIsolatedStandard
 
         //username "adapter"
         //name "project-address-space"+".adapter"
-        User user = CommonResourcesManager.getInstance().getUser(addressSpace, "adapter");
+        User user = CommonResourcesManager.getInstance().getUser(addressSpace, "adapter-" + project.getMetadata().getUid());
         assertNotNull(user);
         assertEquals(1, user.getMetadata().getOwnerReferences().size());
         assertTrue(isOwner(project, user.getMetadata().getOwnerReferences().get(0)));
@@ -150,7 +150,7 @@ class IoTProjectManagedTest extends IoTTestBase implements ITestIsolatedStandard
         assertThat(authorizations, containsInAnyOrder(
                 asList(
                         assertAdapterAuthorization( of(send), expandAddresses(addressSuffix, IOT_ADDRESS_TELEMETRY, IOT_ADDRESS_EVENT, IOT_ADDRESS_COMMAND_RESPONSE)),
-                        assertAdapterAuthorization(of(recv), expandAddresses(addressSuffix, IOT_ADDRESS_COMMAND)),
+                        assertAdapterAuthorization( of(recv), expandAddresses(addressSuffix, IOT_ADDRESS_COMMAND)),
                         assertAdapterAuthorization( of(recv, send), expandAddresses(addressSuffix, IOT_ADDRESS_CONTROL)))));
     }
 
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/iot/project/AbstractIoTProjectTestBase.java b/systemtests/src/test/java/io/enmasse/systemtest/iot/project/AbstractIoTProjectTestBase.java
new file mode 100644
index 000000000..96c9744bd
--- /dev/null
+++ b/systemtests/src/test/java/io/enmasse/systemtest/iot/project/AbstractIoTProjectTestBase.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package io.enmasse.systemtest.iot.project;
+
+
+
+import java.nio.ByteBuffer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import io.enmasse.iot.model.v1.IoTConfig;
+import io.enmasse.iot.model.v1.IoTConfigBuilder;
+import io.enmasse.systemtest.Endpoint;
+import io.enmasse.systemtest.bases.DefaultDeviceRegistry;
+import io.enmasse.systemtest.bases.IoTTestBase;
+import io.enmasse.systemtest.bases.isolated.ITestIsolatedStandard;
+import io.enmasse.systemtest.certs.CertBundle;
+import io.enmasse.systemtest.iot.CredentialsRegistryClient;
+import io.enmasse.systemtest.iot.DeviceRegistryClient;
+import io.enmasse.systemtest.platform.apps.SystemtestsKubernetesApps;
+import io.enmasse.systemtest.utils.CertificateUtils;
+
+public abstract class AbstractIoTProjectTestBase extends IoTTestBase implements ITestIsolatedStandard {
+
+    protected DeviceRegistryClient registryClient;
+    protected CredentialsRegistryClient credentialsClient;
+
+    @BeforeEach
+    void initEnv() throws Exception {
+        CertBundle certBundle = CertificateUtils.createCertBundle();
+        IoTConfig iotConfig = new IoTConfigBuilder()
+                .withNewMetadata()
+                .withName("default")
+                .endMetadata()
+                .withNewSpec()
+                .withNewServices()
+
+                .withDeviceRegistry(DefaultDeviceRegistry.newInfinispanBased())
+
+                .endServices()
+                .withNewAdapters()
+                .withNewMqtt()
+                .withNewEndpoint()
+                .withNewKeyCertificateStrategy()
+                .withCertificate(ByteBuffer.wrap(certBundle.getCert().getBytes()))
+                .withKey(ByteBuffer.wrap(certBundle.getKey().getBytes()))
+                .endKeyCertificateStrategy()
+                .endEndpoint()
+                .endMqtt()
+                .endAdapters()
+                .endSpec()
+                .build();
+
+        createIoTConfig(iotConfig);
+
+        final Endpoint deviceRegistryEndpoint = kubernetes.getExternalEndpoint("device-registry");
+        registryClient = new DeviceRegistryClient(kubernetes, deviceRegistryEndpoint);
+        credentialsClient = new CredentialsRegistryClient(kubernetes, deviceRegistryEndpoint);
+    }
+
+    @AfterEach
+    void cleanEnv(ExtensionContext context) throws Exception {
+        if (context.getExecutionException().isPresent()) { //test failed
+            logCollector.collectHttpAdapterQdrProxyState();
+        }
+
+        SystemtestsKubernetesApps.deleteInfinispanServer(kubernetes.getInfraNamespace());
+    }
+
+}
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/iot/project/ManagedTest.java b/systemtests/src/test/java/io/enmasse/systemtest/iot/project/ManagedTest.java
new file mode 100644
index 000000000..583d3cc6f
--- /dev/null
+++ b/systemtests/src/test/java/io/enmasse/systemtest/iot/project/ManagedTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.systemtest.iot.project;
+
+import static io.enmasse.systemtest.utils.AddressSpaceUtils.addressSpaceExists;
+import static io.enmasse.systemtest.utils.TestUtils.waitUntilConditionOrFail;
+import static java.time.Duration.ofMinutes;
+import static java.time.Duration.ofSeconds;
+
+import java.util.function.BiConsumer;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+
+import io.enmasse.address.model.Address;
+import io.enmasse.address.model.AddressList;
+import io.enmasse.address.model.AddressSpace;
+import io.enmasse.address.model.AddressSpaceList;
+import io.enmasse.address.model.DoneableAddress;
+import io.enmasse.address.model.DoneableAddressSpace;
+import io.enmasse.address.model.KubeUtil;
+import io.enmasse.iot.model.v1.DoneableIoTProject;
+import io.enmasse.iot.model.v1.IoTProject;
+import io.enmasse.iot.model.v1.IoTProjectBuilder;
+import io.enmasse.iot.model.v1.IoTProjectList;
+import io.enmasse.systemtest.TestTag;
+import io.enmasse.systemtest.logs.CustomLogger;
+import io.enmasse.systemtest.utils.IoTUtils;
+import io.enmasse.user.model.v1.DoneableUser;
+import io.enmasse.user.model.v1.User;
+import io.enmasse.user.model.v1.UserList;
+import io.fabric8.kubernetes.client.dsl.MixedOperation;
+import io.fabric8.kubernetes.client.dsl.Resource;
+
+@Tag(TestTag.SHARED_IOT)
+public class ManagedTest extends AbstractIoTProjectTestBase  {
+
+    private static final Logger log = CustomLogger.getLogger();
+
+    private MixedOperation<IoTProject, IoTProjectList, DoneableIoTProject, Resource<IoTProject, DoneableIoTProject>> client;
+    private MixedOperation<Address, AddressList, DoneableAddress, Resource<Address, DoneableAddress>> addressClient;
+    private MixedOperation<AddressSpace, AddressSpaceList, DoneableAddressSpace, Resource<AddressSpace, DoneableAddressSpace>> addressSpaceClient;
+    private MixedOperation<User, UserList, DoneableUser, Resource<User, DoneableUser>> userClient;
+
+    @BeforeEach
+    public void initClients () {
+        this.client = kubernetes.getIoTProjectClient(this.iotProjectNamespace);
+        this.addressClient = kubernetes.getAddressClient(this.iotProjectNamespace);
+        this.addressSpaceClient = kubernetes.getAddressSpaceClient(this.iotProjectNamespace);
+        this.userClient = kubernetes.getUserClient(this.iotProjectNamespace);
+    }
+
+    @Test
+    public void testChangeAddressSpace() throws Exception {
+
+        var project = IoTUtils.getBasicIoTProjectObject("iot1", "as1", this.iotProjectNamespace);
+        createIoTProject(project);
+
+        assertManagedResources(Assertions::assertNotNull, project, "as1");
+
+        project = new IoTProjectBuilder(project)
+                .editSpec()
+                .editDownstreamStrategy()
+                .editManagedStrategy()
+                .editAddressSpace()
+
+                .withName("as1a")
+
+                .endAddressSpace()
+                .endManagedStrategy()
+                .endDownstreamStrategy()
+                .endSpec()
+                .build();
+
+        // update the project
+
+        log.info("Update project namespace");
+        client.createOrReplace(project);
+
+        // immediately after the change, the project is still ready but the new
+        // address space is still missing, so we need to wait for it to be created
+        // otherwise io.enmasse.systemtest.utils.IoTUtils.waitForIoTProjectReady(Kubernetes, IoTProject) will fail
+
+        waitUntilConditionOrFail(
+                addressSpaceExists(project.getMetadata().getNamespace(), project.getSpec().getDownstreamStrategy().getManagedStrategy().getAddressSpace().getName()),
+                ofMinutes(5), ofSeconds(10),
+                () -> String.format("Expected address space to be created"));
+
+        // wait until the project and address space become ready
+
+        log.info("For for project to become ready again");
+        IoTUtils.waitForIoTProjectReady(kubernetes, project);
+
+        // assert existence
+
+        assertManagedResources(Assertions::assertNotNull, project, "as1a");
+        assertManagedResources(Assertions::assertNull, project, "as1");
+
+    }
+
+    private void assertManagedResources(final BiConsumer<Object,String> assertor, final IoTProject project, final String addressSpaceName) {
+        assertObject(assertor, "Address space", this.addressSpaceClient, addressSpaceName);
+        assertObject(assertor, "Adapter user", this.userClient, addressSpaceName + ".adapter-" + project.getMetadata().getUid());
+        for ( final String address : IOT_ADDRESSES) {
+            var addressName = address + "/" + this.iotProjectNamespace + "." + project.getMetadata().getName();
+            var metaName = KubeUtil.sanitizeForGo(addressSpaceName, addressName);
+            assertObject(assertor, "Address: " + addressName + " / " + metaName, this.addressClient, metaName);
+        }
+    }
+
+    private static void assertObject(final BiConsumer<Object,String> assertor, final String message, final MixedOperation<? extends Object, ?, ?, ?> client, final String name) {
+        var object = client.withName(name).get();
+        assertor.accept(object, message);
+    }
+
+}
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/shared/standard/api/UserApiTest.java b/systemtests/src/test/java/io/enmasse/systemtest/shared/standard/api/UserApiTest.java
index fa79ef699..781fea166 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/shared/standard/api/UserApiTest.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/shared/standard/api/UserApiTest.java
@@ -15,12 +15,17 @@ import io.enmasse.systemtest.bases.shared.ITestSharedStandard;
 import io.enmasse.systemtest.cmdclients.KubeCMDClient;
 import io.enmasse.systemtest.logs.CustomLogger;
 import io.enmasse.systemtest.model.addressplan.DestinationPlan;
+import io.enmasse.systemtest.platform.Kubernetes;
 import io.enmasse.systemtest.utils.AddressUtils;
 import io.enmasse.systemtest.utils.UserUtils;
 import io.enmasse.user.model.v1.DoneableUser;
 import io.enmasse.user.model.v1.Operation;
 import io.enmasse.user.model.v1.User;
 import io.enmasse.user.model.v1.UserAuthorizationBuilder;
+import io.enmasse.user.model.v1.UserBuilder;
+import io.enmasse.user.model.v1.UserCrd;
+import io.enmasse.user.model.v1.UserList;
+
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
 import org.slf4j.Logger;
@@ -34,6 +39,8 @@ import java.util.concurrent.TimeUnit;
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -134,4 +141,50 @@ public class UserApiTest extends TestBase implements ITestSharedStandard {
             kubernetes.deleteServiceAccount("test-service-account", environment.namespace());
         }
     }
+
+    @Test
+    void testSetAnnotations() {
+
+        var client = Kubernetes.getInstance().getClient();
+        var userCrd = client.customResources(UserCrd.messagingUser()   , User.class, UserList.class, DoneableUser.class);
+
+        var name = getSharedAddressSpace().getMetadata().getName()+ ".foo";
+        var user = new UserBuilder()
+                .withNewMetadata()
+                .withNamespace(getSharedAddressSpace().getMetadata().getNamespace())
+                .withName(name)
+                .addToAnnotations("annotation1", "value1")
+                .addToAnnotations("annotation2", "value2")
+                .endMetadata()
+                .build();
+
+        // create
+        userCrd.create(user);
+
+        // read again
+        var actualUser = userCrd.inNamespace(getSharedAddressSpace().getMetadata().getNamespace()).withName(name).get();
+
+        assertNotNull(actualUser);
+        assertThat(actualUser.getAnnotation("annotation1"), is("value1"));
+        assertThat(actualUser.getAnnotation("annotation2"), is("value2"));
+
+        var updateUser = new UserBuilder(actualUser)
+                .editOrNewMetadata()
+                .addToAnnotations("annotation1", "value1a")
+                .removeFromAnnotations("annotation2")
+                .addToAnnotations("annotation3", "value3")
+                .endMetadata()
+                .build();
+
+        userCrd.createOrReplace(updateUser);
+
+        // read again
+        actualUser = userCrd.inNamespace(getSharedAddressSpace().getMetadata().getNamespace()).withName(name).get();
+
+        assertNotNull(actualUser);
+        assertThat(actualUser.getAnnotation("annotation1"), is("value1a"));
+        assertThat(actualUser.getAnnotation("annotation2"), nullValue());
+        assertThat(actualUser.getAnnotation("annotation3"), is("value3"));
+
+    }
 }