Repository: EnMasseProject/enmasse
Pull request #2410 (2019-03-06 10:46:47)

Upgrade to Hono 0.9


Patch:
diff --git a/Makefile.env.mk b/Makefile.env.mk
index 70fcbb470..dbcd1c5cc 100644
--- a/Makefile.env.mk
+++ b/Makefile.env.mk
@@ -54,7 +54,7 @@ QDR_PROXY_CONFIGURATOR_IMAGE       ?= "$(DOCKER_REGISTRY_PREFIX)$(DOCKER_ORG)/qd
 
 
 DEFAULT_PROJECT ?= "enmasse-infra"
-HONO_VERSION ?= "0.8"
+HONO_VERSION ?= "0.9"
 IMAGE_PULL_POLICY ?= "Always"
 
 IMAGE_ENV=ADDRESS_SPACE_CONTROLLER_IMAGE=$(ADDRESS_SPACE_CONTROLLER_IMAGE) \
diff --git a/agent/lib/auth_utils.js b/agent/lib/auth_utils.js
index 2787aa88d..ffc1e474d 100644
--- a/agent/lib/auth_utils.js
+++ b/agent/lib/auth_utils.js
@@ -144,6 +144,10 @@ function patch(sessions, kc_idp_hint) {
                 log.info("Modified redirect: %s", target);
             }
 
+            if (!target) {
+                target = "/";
+            }
+            log.info("Sending redirect: %s", target);
             response.statusCode = 302;
             response.setHeader('Location', target);
             response.end();
diff --git a/agent/npm-shrinkwrap.json b/agent/npm-shrinkwrap.json
index b1ce0ff7b..37d1a9863 100644
--- a/agent/npm-shrinkwrap.json
+++ b/agent/npm-shrinkwrap.json
@@ -769,9 +769,9 @@
             "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
         },
         "jwk-to-pem": {
-            "version": "1.2.6",
-            "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-1.2.6.tgz",
-            "integrity": "sha1-1QfOzkAInFJI4J7GgmaiAwqcYyU=",
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.1.tgz",
+            "integrity": "sha512-KKu0WuDDjqw2FlRFp9/vk9TMO/KvgpZVKzdhhYcNyy5OwE8dw9lOK5OQTQHIJ7m+HioI/4P44sAtVuDrQ8KQfw==",
             "requires": {
                 "asn1.js": "4.10.1",
                 "elliptic": "6.4.1",
@@ -779,11 +779,11 @@
             }
         },
         "keycloak-connect": {
-            "version": "3.4.3",
-            "resolved": "https://registry.npmjs.org/keycloak-connect/-/keycloak-connect-3.4.3.tgz",
-            "integrity": "sha1-Z3W1mRJdJNUk9uZKXlEv2gBpLE4=",
+            "version": "4.8.3",
+            "resolved": "https://registry.npmjs.org/keycloak-connect/-/keycloak-connect-4.8.3.tgz",
+            "integrity": "sha512-4MRRDmztHl2c1JiyxoZO3aAkqeYQ06gKUXJQ5BjL1RGZuqDs2I8vmSUL+ou4Gm0NamoJa87daT1NBg98ZW1mhw==",
             "requires": {
-                "jwk-to-pem": "1.2.6"
+                "jwk-to-pem": "2.0.1"
             }
         },
         "levn": {
diff --git a/agent/package.json b/agent/package.json
index 8e21ec848..2aebcf293 100644
--- a/agent/package.json
+++ b/agent/package.json
@@ -11,7 +11,7 @@
     "dependencies": {
         "debug": "^3.1.*",
         "rhea": ">=0.2.11",
-        "keycloak-connect": "3.4.3",
+        "keycloak-connect": "4.8.3",
         "ws": "*"
     },
     "devDependencies": {
diff --git a/cmd/enmasse-controller-manager/main.go b/cmd/enmasse-controller-manager/main.go
index ee01da8f6..3718745b7 100644
--- a/cmd/enmasse-controller-manager/main.go
+++ b/cmd/enmasse-controller-manager/main.go
@@ -11,6 +11,8 @@ import (
 	"os"
 	"runtime"
 
+	"github.com/enmasseproject/enmasse/version"
+
 	enmassescheme "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned/scheme"
 	"k8s.io/client-go/kubernetes/scheme"
 
@@ -29,6 +31,7 @@ func printVersion() {
 	log.Info(fmt.Sprintf("Go Version: %s", runtime.Version()))
 	log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH))
 	log.Info(fmt.Sprintf("operator-sdk Version: %v", sdkVersion.Version))
+	log.Info(fmt.Sprintf("EnMasse Version: %v", version.Version))
 }
 
 func main() {
diff --git a/iot/examples/iot-config.yaml b/iot/examples/iot-config.yaml
new file mode 100644
index 000000000..ee582ff00
--- /dev/null
+++ b/iot/examples/iot-config.yaml
@@ -0,0 +1,5 @@
+kind: IoTConfig
+apiVersion: iot.enmasse.io/v1alpha1
+metadata:
+  name: default
+spec:
diff --git a/iot/fabric8-s2i-java-custom/Dockerfile b/iot/fabric8-s2i-java-custom/Dockerfile
index e1c88edd1..3d384d302 100644
--- a/iot/fabric8-s2i-java-custom/Dockerfile
+++ b/iot/fabric8-s2i-java-custom/Dockerfile
@@ -4,7 +4,7 @@
 FROM fabric8/s2i-java:3.0-java11
 
 ENV \
-    NETTY_TCNATIVE_VERSION=2.0.10.Final
+    NETTY_TCNATIVE_VERSION=2.0.17.Final
 
 USER root
 
diff --git a/iot/iot-application-base/pom.xml b/iot/iot-application-base/pom.xml
new file mode 100644
index 000000000..e08904e68
--- /dev/null
+++ b/iot/iot-application-base/pom.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+  xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>io.enmasse</groupId>
+    <artifactId>iot</artifactId>
+    <version>0.27-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>iot-application-base</artifactId>
+  <packaging>pom</packaging>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-transport-native-epoll</artifactId>
+      <scope>runtime</scope>
+      <classifier>linux-x86_64</classifier>
+    </dependency>
+
+    <dependency>
+      <groupId>io.netty</groupId>
+      <artifactId>netty-tcnative</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>io.micrometer</groupId>
+      <artifactId>micrometer-registry-prometheus</artifactId>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/iot/iot-auth-service/pom.xml b/iot/iot-auth-service/pom.xml
index cb58ae250..e33ee921b 100644
--- a/iot/iot-auth-service/pom.xml
+++ b/iot/iot-auth-service/pom.xml
@@ -7,8 +7,9 @@
 
   <parent>
     <groupId>io.enmasse</groupId>
-    <artifactId>iot</artifactId>
+    <artifactId>iot-application-base</artifactId>
     <version>0.27-SNAPSHOT</version>
+    <relativePath>../iot-application-base</relativePath>
   </parent>
 
   <artifactId>iot-auth-service</artifactId>
diff --git a/iot/iot-device-registry/pom.xml b/iot/iot-device-registry/pom.xml
index 0befd7ab4..aacccc3a4 100644
--- a/iot/iot-device-registry/pom.xml
+++ b/iot/iot-device-registry/pom.xml
@@ -7,8 +7,9 @@
 
   <parent>
     <groupId>io.enmasse</groupId>
-    <artifactId>iot</artifactId>
+    <artifactId>iot-application-base</artifactId>
     <version>0.27-SNAPSHOT</version>
+    <relativePath>../iot-application-base</relativePath>
   </parent>
 
   <artifactId>iot-device-registry</artifactId>
diff --git a/iot/iot-http-adapter/pom.xml b/iot/iot-http-adapter/pom.xml
index 4936c6386..d96f9f5ac 100644
--- a/iot/iot-http-adapter/pom.xml
+++ b/iot/iot-http-adapter/pom.xml
@@ -7,8 +7,9 @@
 
   <parent>
     <groupId>io.enmasse</groupId>
-    <artifactId>iot</artifactId>
+    <artifactId>iot-application-base</artifactId>
     <version>0.27-SNAPSHOT</version>
+    <relativePath>../iot-application-base</relativePath>
   </parent>
 
   <artifactId>iot-http-adapter</artifactId>
diff --git a/iot/iot-mqtt-adapter/pom.xml b/iot/iot-mqtt-adapter/pom.xml
index 3505177ff..4ba52f2a0 100644
--- a/iot/iot-mqtt-adapter/pom.xml
+++ b/iot/iot-mqtt-adapter/pom.xml
@@ -7,8 +7,9 @@
 
   <parent>
     <groupId>io.enmasse</groupId>
-    <artifactId>iot</artifactId>
+    <artifactId>iot-application-base</artifactId>
     <version>0.27-SNAPSHOT</version>
+    <relativePath>../iot-application-base</relativePath>
   </parent>
 
   <artifactId>iot-mqtt-adapter</artifactId>
diff --git a/iot/iot-tenant-service/pom.xml b/iot/iot-tenant-service/pom.xml
index 892f70dc8..aa3a18f6f 100644
--- a/iot/iot-tenant-service/pom.xml
+++ b/iot/iot-tenant-service/pom.xml
@@ -7,8 +7,9 @@
 
   <parent>
     <groupId>io.enmasse</groupId>
-    <artifactId>iot</artifactId>
+    <artifactId>iot-application-base</artifactId>
     <version>0.27-SNAPSHOT</version>
+    <relativePath>../iot-application-base</relativePath>
   </parent>
 
   <artifactId>iot-tenant-service</artifactId>
@@ -39,41 +40,6 @@
       <artifactId>kubernetes-client</artifactId>
     </dependency>
 
-    <dependency>
-      <groupId>io.netty</groupId>
-      <artifactId>netty-tcnative</artifactId>
-      <version>${netty-tcnative.version}</version>
-    </dependency>
-
-    <dependency>
-      <groupId>io.netty</groupId>
-      <artifactId>netty-transport-native-epoll</artifactId>
-      <scope>runtime</scope>
-      <classifier>linux-x86_64</classifier>
-    </dependency>
-
-    <dependency>
-      <groupId>io.micrometer</groupId>
-      <artifactId>micrometer-registry-prometheus</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework</groupId>
-      <artifactId>spring-web</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework</groupId>
-      <artifactId>spring-webmvc</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework.boot</groupId>
-      <artifactId>spring-boot-starter-actuator</artifactId>
-      <version>${spring.boot.version}</version>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework.boot</groupId>
-      <artifactId>spring-boot-starter-jetty</artifactId>
-      <version>${spring.boot.version}</version>
-    </dependency>
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-validation</artifactId>
diff --git a/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/config/HealthConfiguration.java b/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/config/HealthConfiguration.java
new file mode 100644
index 000000000..ff84d6723
--- /dev/null
+++ b/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/config/HealthConfiguration.java
@@ -0,0 +1,23 @@
+/*
+ * 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.tenant.config;
+
+import org.eclipse.hono.config.ApplicationConfigProperties;
+import org.eclipse.hono.service.VertxBasedHealthCheckServer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import io.vertx.core.Vertx;
+
+@Configuration
+public class HealthConfiguration {
+
+    @Bean
+    public VertxBasedHealthCheckServer healthCheckServer (final Vertx vertx, final ApplicationConfigProperties config) {
+        return new VertxBasedHealthCheckServer(vertx, config);
+    }
+
+}
diff --git a/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/config/MetricsConfiguration.java b/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/config/MetricsConfiguration.java
index 69590d996..d552cfd70 100644
--- a/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/config/MetricsConfiguration.java
+++ b/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/config/MetricsConfiguration.java
@@ -19,7 +19,7 @@ public class MetricsConfiguration {
     public MeterRegistryCustomizer<MeterRegistry> commonTags() {
 
         return r -> r.config().commonTags(
-                MetricsTags.forService(MetricsTags.VALUE_SERVICE_REGISTRY));
+                MetricsTags.forService("tenant-service"));
 
     }
 
diff --git a/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/impl/TenantServiceImpl.java b/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/impl/TenantServiceImpl.java
index fe46be8d9..da01ad053 100644
--- a/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/impl/TenantServiceImpl.java
+++ b/iot/iot-tenant-service/src/main/java/io/enmasse/iot/tenant/impl/TenantServiceImpl.java
@@ -25,6 +25,7 @@ import io.enmasse.iot.model.IoTProjects;
 import io.enmasse.iot.model.IoTProjects.Client;
 import io.enmasse.iot.model.v1.IoTProject;
 import io.fabric8.kubernetes.client.KubernetesClient;
+import io.opentracing.Span;
 import io.vertx.core.AsyncResult;
 import io.vertx.core.Future;
 import io.vertx.core.Handler;
@@ -73,7 +74,7 @@ public class TenantServiceImpl extends AbstractKubernetesTenantService {
 
     @Override
     public void get(
-            final String tenantId,
+            final String tenantId, final Span span,
             final Handler<AsyncResult<TenantResult<JsonObject>>> resultHandler) {
 
         logger.trace("Get - Tenant Id: {}", tenantId);
@@ -88,10 +89,10 @@ public class TenantServiceImpl extends AbstractKubernetesTenantService {
     }
 
     @Override
-    public void get(final X500Principal subjectDn,
+    public void get(final X500Principal subjectDn, final Span span,
             final Handler<AsyncResult<TenantResult<JsonObject>>> resultHandler) {
 
-        super.get(subjectDn, resultHandler);
+        super.get(subjectDn, span, resultHandler);
 
     }
 
diff --git a/iot/iot-tenant-service/src/main/resources/config/application.yml b/iot/iot-tenant-service/src/main/resources/config/application.yml
index 2bc934d2b..01ea07938 100644
--- a/iot/iot-tenant-service/src/main/resources/config/application.yml
+++ b/iot/iot-tenant-service/src/main/resources/config/application.yml
@@ -11,22 +11,3 @@ enmasse:
         maxInstances: 1
         healthCheckBindAddress: 0.0.0.0
         healthCheckPort: 8088
-
-server:
-  port: -1
-## Enable management server
-management:
-  port: 8081
-  security:
-    enabled: false
-## Disable all but the promtheus endpoint
-endpoints:
-  enabled: false
-  prometheus:
-    enabled: true
-
-management:
-  metrics:
-    export:
-      graphite:
-        enabled: false
diff --git a/iot/pom.xml b/iot/pom.xml
index 260e545be..d81e5d753 100644
--- a/iot/pom.xml
+++ b/iot/pom.xml
@@ -13,16 +13,15 @@
   <packaging>pom</packaging>
 
   <properties>
-    <hono.version>0.8</hono.version>
+    <hono.version>0.9</hono.version>
 
     <!-- align with Hono compatible versions -->
     <logback.version>1.2.3</logback.version>
-    <spring.boot.version>1.5.18.RELEASE</spring.boot.version>
-    <spring.version>4.3.19.RELEASE</spring.version>
+    <spring.boot.version>1.5.19.RELEASE</spring.boot.version>
     <micrometer.version>1.0.9</micrometer.version>
-    <vertx.version>3.5.4</vertx.version>
-    <netty-tcnative.version>2.0.10.Final</netty-tcnative.version>
-    <netty.version>4.1.19.Final</netty.version>
+    <vertx.version>3.6.3</vertx.version>
+    <netty-tcnative.version>2.0.17.Final</netty-tcnative.version>
+    <netty.version>4.1.30.Final</netty.version>
   </properties>
 
   <modules>
@@ -53,21 +52,17 @@
         <version>${spring.boot.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>io.netty</groupId>
+        <artifactId>netty-tcnative</artifactId>
+        <version>${netty-tcnative.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>io.micrometer</groupId>
         <artifactId>micrometer-registry-prometheus</artifactId>
         <version>${micrometer.version}</version>
       </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-web</artifactId>
-        <version>${spring.version}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.springframework</groupId>
-        <artifactId>spring-webmvc</artifactId>
-        <version>${spring.version}</version>
-      </dependency>
 
     </dependencies>
   </dependencyManagement>
diff --git a/pkg/apis/iot/v1alpha1/register.go b/pkg/apis/iot/v1alpha1/register.go
index f7de2a5fb..dd214a622 100644
--- a/pkg/apis/iot/v1alpha1/register.go
+++ b/pkg/apis/iot/v1alpha1/register.go
@@ -33,6 +33,9 @@ func addKnownTypes(scheme *runtime.Scheme) error {
 	scheme.AddKnownTypes(SchemeGroupVersion,
 		&IoTProject{},
 		&IoTProjectList{},
+
+		&IoTConfig{},
+		&IoTConfigList{},
 	)
 
 	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
diff --git a/pkg/apis/iot/v1alpha1/types.go b/pkg/apis/iot/v1alpha1/types.go
index be21e59d6..de5d60226 100644
--- a/pkg/apis/iot/v1alpha1/types.go
+++ b/pkg/apis/iot/v1alpha1/types.go
@@ -7,6 +7,7 @@ package v1alpha1
 
 import (
 	"github.com/enmasseproject/enmasse/pkg/util"
+	"k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -83,3 +84,51 @@ type IoTProjectList struct {
 
 	Items []IoTProject `json:"items"`
 }
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+type IoTConfig struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   IoTConfigSpec   `json:"spec,omitempty"`
+	Status IoTConfigStatus `json:"status,omitempty"`
+}
+
+type IoTConfigSpec struct {
+	Adapters               []AdapterSpec   `json:"adapters,omitempty"`
+	DefaultImageProperties ImageProperties `json:"defaultImageProperties,omitempty"`
+}
+
+type ImageProperties struct {
+	Repository     *string `json:"repository,omitempty"`
+	UseImageStream *bool   `json:"useImageStream,omitempty"`
+
+	Tag        string         `json:"tag,omitempty"`
+	PullPolicy *v1.PullPolicy `json:"PullPolicy,omitempty"`
+}
+
+type AdapterSpec struct {
+	AdapterType string `json:"type"`
+	Replicas    int    `json:"replicas"`
+}
+
+type IoTConfigStatus struct {
+	Initialized bool   `json:"initialized"`
+	State       string `json:"state"`
+}
+
+const (
+	ConfigStateWrongName = "WrongName"
+	ConfigStateRunning   = "Running"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+type IoTConfigList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+
+	Items []IoTConfig `json:"items"`
+}
diff --git a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
index 9f888043c..a8ec222a7 100644
--- a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
@@ -10,9 +10,26 @@
 package v1alpha1
 
 import (
+	v1 "k8s.io/api/core/v1"
 	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 *AdapterSpec) DeepCopyInto(out *AdapterSpec) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdapterSpec.
+func (in *AdapterSpec) DeepCopy() *AdapterSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(AdapterSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Credentials) DeepCopyInto(out *Credentials) {
 	*out = *in
@@ -82,6 +99,136 @@ func (in *ExternalDownstreamStrategy) DeepCopy() *ExternalDownstreamStrategy {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ImageProperties) DeepCopyInto(out *ImageProperties) {
+	*out = *in
+	if in.Repository != nil {
+		in, out := &in.Repository, &out.Repository
+		*out = new(string)
+		**out = **in
+	}
+	if in.UseImageStream != nil {
+		in, out := &in.UseImageStream, &out.UseImageStream
+		*out = new(bool)
+		**out = **in
+	}
+	if in.PullPolicy != nil {
+		in, out := &in.PullPolicy, &out.PullPolicy
+		*out = new(v1.PullPolicy)
+		**out = **in
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageProperties.
+func (in *ImageProperties) DeepCopy() *ImageProperties {
+	if in == nil {
+		return nil
+	}
+	out := new(ImageProperties)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IoTConfig) DeepCopyInto(out *IoTConfig) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IoTConfig.
+func (in *IoTConfig) DeepCopy() *IoTConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(IoTConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *IoTConfig) 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 *IoTConfigList) DeepCopyInto(out *IoTConfigList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	out.ListMeta = in.ListMeta
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]IoTConfig, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IoTConfigList.
+func (in *IoTConfigList) DeepCopy() *IoTConfigList {
+	if in == nil {
+		return nil
+	}
+	out := new(IoTConfigList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *IoTConfigList) 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 *IoTConfigSpec) DeepCopyInto(out *IoTConfigSpec) {
+	*out = *in
+	if in.Adapters != nil {
+		in, out := &in.Adapters, &out.Adapters
+		*out = make([]AdapterSpec, len(*in))
+		copy(*out, *in)
+	}
+	in.DefaultImageProperties.DeepCopyInto(&out.DefaultImageProperties)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IoTConfigSpec.
+func (in *IoTConfigSpec) DeepCopy() *IoTConfigSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(IoTConfigSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *IoTConfigStatus) DeepCopyInto(out *IoTConfigStatus) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IoTConfigStatus.
+func (in *IoTConfigStatus) DeepCopy() *IoTConfigStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(IoTConfigStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *IoTProject) DeepCopyInto(out *IoTProject) {
 	*out = *in
diff --git a/pkg/apis/user/v1beta1/zz_generated.deepcopy.go b/pkg/apis/user/v1beta1/zz_generated.deepcopy.go
index e2e4679c8..c71b00d71 100644
--- a/pkg/apis/user/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/user/v1beta1/zz_generated.deepcopy.go
@@ -16,6 +16,11 @@ import (
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AuthenticationSpec) DeepCopyInto(out *AuthenticationSpec) {
 	*out = *in
+	if in.Password != nil {
+		in, out := &in.Password, &out.Password
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
@@ -118,7 +123,7 @@ func (in *MessagingUserList) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *MessagingUserSpec) DeepCopyInto(out *MessagingUserSpec) {
 	*out = *in
-	out.Authentication = in.Authentication
+	in.Authentication.DeepCopyInto(&out.Authentication)
 	if in.Authorization != nil {
 		in, out := &in.Authorization, &out.Authorization
 		*out = make([]AuthorizationSpec, len(*in))
diff --git a/pkg/client/clientset/versioned/typed/iot/v1alpha1/fake/fake_iot_client.go b/pkg/client/clientset/versioned/typed/iot/v1alpha1/fake/fake_iot_client.go
index 09057b2e1..435cd497a 100644
--- a/pkg/client/clientset/versioned/typed/iot/v1alpha1/fake/fake_iot_client.go
+++ b/pkg/client/clientset/versioned/typed/iot/v1alpha1/fake/fake_iot_client.go
@@ -17,6 +17,10 @@ type FakeIotV1alpha1 struct {
 	*testing.Fake
 }
 
+func (c *FakeIotV1alpha1) IoTConfigs(namespace string) v1alpha1.IoTConfigInterface {
+	return &FakeIoTConfigs{c, namespace}
+}
+
 func (c *FakeIotV1alpha1) IoTProjects(namespace string) v1alpha1.IoTProjectInterface {
 	return &FakeIoTProjects{c, namespace}
 }
diff --git a/pkg/client/clientset/versioned/typed/iot/v1alpha1/fake/fake_iotconfig.go b/pkg/client/clientset/versioned/typed/iot/v1alpha1/fake/fake_iotconfig.go
new file mode 100644
index 000000000..8db3bfbf6
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/iot/v1alpha1/fake/fake_iotconfig.go
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2018, 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 (
+	v1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	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"
+)
+
+// FakeIoTConfigs implements IoTConfigInterface
+type FakeIoTConfigs struct {
+	Fake *FakeIotV1alpha1
+	ns   string
+}
+
+var iotconfigsResource = schema.GroupVersionResource{Group: "iot.enmasse.io", Version: "v1alpha1", Resource: "iotconfigs"}
+
+var iotconfigsKind = schema.GroupVersionKind{Group: "iot.enmasse.io", Version: "v1alpha1", Kind: "IoTConfig"}
+
+// Get takes name of the ioTConfig, and returns the corresponding ioTConfig object, and an error if there is any.
+func (c *FakeIoTConfigs) Get(name string, options v1.GetOptions) (result *v1alpha1.IoTConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(iotconfigsResource, c.ns, name), &v1alpha1.IoTConfig{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.IoTConfig), err
+}
+
+// List takes label and field selectors, and returns the list of IoTConfigs that match those selectors.
+func (c *FakeIoTConfigs) List(opts v1.ListOptions) (result *v1alpha1.IoTConfigList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(iotconfigsResource, iotconfigsKind, c.ns, opts), &v1alpha1.IoTConfigList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.IoTConfigList{ListMeta: obj.(*v1alpha1.IoTConfigList).ListMeta}
+	for _, item := range obj.(*v1alpha1.IoTConfigList).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 ioTConfigs.
+func (c *FakeIoTConfigs) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(iotconfigsResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a ioTConfig and creates it.  Returns the server's representation of the ioTConfig, and an error, if there is any.
+func (c *FakeIoTConfigs) Create(ioTConfig *v1alpha1.IoTConfig) (result *v1alpha1.IoTConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(iotconfigsResource, c.ns, ioTConfig), &v1alpha1.IoTConfig{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.IoTConfig), err
+}
+
+// Update takes the representation of a ioTConfig and updates it. Returns the server's representation of the ioTConfig, and an error, if there is any.
+func (c *FakeIoTConfigs) Update(ioTConfig *v1alpha1.IoTConfig) (result *v1alpha1.IoTConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(iotconfigsResource, c.ns, ioTConfig), &v1alpha1.IoTConfig{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.IoTConfig), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakeIoTConfigs) UpdateStatus(ioTConfig *v1alpha1.IoTConfig) (*v1alpha1.IoTConfig, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(iotconfigsResource, "status", c.ns, ioTConfig), &v1alpha1.IoTConfig{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.IoTConfig), err
+}
+
+// Delete takes name of the ioTConfig and deletes it. Returns an error if one occurs.
+func (c *FakeIoTConfigs) Delete(name string, options *v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteAction(iotconfigsResource, c.ns, name), &v1alpha1.IoTConfig{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakeIoTConfigs) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(iotconfigsResource, c.ns, listOptions)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.IoTConfigList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched ioTConfig.
+func (c *FakeIoTConfigs) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.IoTConfig, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(iotconfigsResource, c.ns, name, data, subresources...), &v1alpha1.IoTConfig{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.IoTConfig), err
+}
diff --git a/pkg/client/clientset/versioned/typed/iot/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/iot/v1alpha1/generated_expansion.go
index d66d32aab..1a46a9d31 100644
--- a/pkg/client/clientset/versioned/typed/iot/v1alpha1/generated_expansion.go
+++ b/pkg/client/clientset/versioned/typed/iot/v1alpha1/generated_expansion.go
@@ -7,4 +7,6 @@
 
 package v1alpha1
 
+type IoTConfigExpansion interface{}
+
 type IoTProjectExpansion interface{}
diff --git a/pkg/client/clientset/versioned/typed/iot/v1alpha1/iot_client.go b/pkg/client/clientset/versioned/typed/iot/v1alpha1/iot_client.go
index 3e68610d1..a8ccd09ac 100644
--- a/pkg/client/clientset/versioned/typed/iot/v1alpha1/iot_client.go
+++ b/pkg/client/clientset/versioned/typed/iot/v1alpha1/iot_client.go
@@ -16,6 +16,7 @@ import (
 
 type IotV1alpha1Interface interface {
 	RESTClient() rest.Interface
+	IoTConfigsGetter
 	IoTProjectsGetter
 }
 
@@ -24,6 +25,10 @@ type IotV1alpha1Client struct {
 	restClient rest.Interface
 }
 
+func (c *IotV1alpha1Client) IoTConfigs(namespace string) IoTConfigInterface {
+	return newIoTConfigs(c, namespace)
+}
+
 func (c *IotV1alpha1Client) IoTProjects(namespace string) IoTProjectInterface {
 	return newIoTProjects(c, namespace)
 }
diff --git a/pkg/client/clientset/versioned/typed/iot/v1alpha1/iotconfig.go b/pkg/client/clientset/versioned/typed/iot/v1alpha1/iotconfig.go
new file mode 100644
index 000000000..6f08c4ecb
--- /dev/null
+++ b/pkg/client/clientset/versioned/typed/iot/v1alpha1/iotconfig.go
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2018, 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 v1alpha1
+
+import (
+	v1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	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"
+)
+
+// IoTConfigsGetter has a method to return a IoTConfigInterface.
+// A group's client should implement this interface.
+type IoTConfigsGetter interface {
+	IoTConfigs(namespace string) IoTConfigInterface
+}
+
+// IoTConfigInterface has methods to work with IoTConfig resources.
+type IoTConfigInterface interface {
+	Create(*v1alpha1.IoTConfig) (*v1alpha1.IoTConfig, error)
+	Update(*v1alpha1.IoTConfig) (*v1alpha1.IoTConfig, error)
+	UpdateStatus(*v1alpha1.IoTConfig) (*v1alpha1.IoTConfig, error)
+	Delete(name string, options *v1.DeleteOptions) error
+	DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
+	Get(name string, options v1.GetOptions) (*v1alpha1.IoTConfig, error)
+	List(opts v1.ListOptions) (*v1alpha1.IoTConfigList, error)
+	Watch(opts v1.ListOptions) (watch.Interface, error)
+	Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.IoTConfig, err error)
+	IoTConfigExpansion
+}
+
+// ioTConfigs implements IoTConfigInterface
+type ioTConfigs struct {
+	client rest.Interface
+	ns     string
+}
+
+// newIoTConfigs returns a IoTConfigs
+func newIoTConfigs(c *IotV1alpha1Client, namespace string) *ioTConfigs {
+	return &ioTConfigs{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the ioTConfig, and returns the corresponding ioTConfig object, and an error if there is any.
+func (c *ioTConfigs) Get(name string, options v1.GetOptions) (result *v1alpha1.IoTConfig, err error) {
+	result = &v1alpha1.IoTConfig{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of IoTConfigs that match those selectors.
+func (c *ioTConfigs) List(opts v1.ListOptions) (result *v1alpha1.IoTConfigList, err error) {
+	result = &v1alpha1.IoTConfigList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Do().
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested ioTConfigs.
+func (c *ioTConfigs) Watch(opts v1.ListOptions) (watch.Interface, error) {
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Watch()
+}
+
+// Create takes the representation of a ioTConfig and creates it.  Returns the server's representation of the ioTConfig, and an error, if there is any.
+func (c *ioTConfigs) Create(ioTConfig *v1alpha1.IoTConfig) (result *v1alpha1.IoTConfig, err error) {
+	result = &v1alpha1.IoTConfig{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		Body(ioTConfig).
+		Do().
+		Into(result)
+	return
+}
+
+// Update takes the representation of a ioTConfig and updates it. Returns the server's representation of the ioTConfig, and an error, if there is any.
+func (c *ioTConfigs) Update(ioTConfig *v1alpha1.IoTConfig) (result *v1alpha1.IoTConfig, err error) {
+	result = &v1alpha1.IoTConfig{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		Name(ioTConfig.Name).
+		Body(ioTConfig).
+		Do().
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+
+func (c *ioTConfigs) UpdateStatus(ioTConfig *v1alpha1.IoTConfig) (result *v1alpha1.IoTConfig, err error) {
+	result = &v1alpha1.IoTConfig{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		Name(ioTConfig.Name).
+		SubResource("status").
+		Body(ioTConfig).
+		Do().
+		Into(result)
+	return
+}
+
+// Delete takes name of the ioTConfig and deletes it. Returns an error if one occurs.
+func (c *ioTConfigs) Delete(name string, options *v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		Name(name).
+		Body(options).
+		Do().
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *ioTConfigs) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		VersionedParams(&listOptions, scheme.ParameterCodec).
+		Body(options).
+		Do().
+		Error()
+}
+
+// Patch applies the patch and returns the patched ioTConfig.
+func (c *ioTConfigs) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.IoTConfig, err error) {
+	result = &v1alpha1.IoTConfig{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("iotconfigs").
+		SubResource(subresources...).
+		Name(name).
+		Body(data).
+		Do().
+		Into(result)
+	return
+}
diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go
index f88d96105..c2eb6b927 100644
--- a/pkg/client/informers/externalversions/generic.go
+++ b/pkg/client/informers/externalversions/generic.go
@@ -50,6 +50,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Enmasse().V1beta1().AddressSpaces().Informer()}, nil
 
 		// Group=iot.enmasse.io, Version=v1alpha1
+	case v1alpha1.SchemeGroupVersion.WithResource("iotconfigs"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Iot().V1alpha1().IoTConfigs().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("iotprojects"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Iot().V1alpha1().IoTProjects().Informer()}, nil
 
diff --git a/pkg/client/informers/externalversions/iot/v1alpha1/interface.go b/pkg/client/informers/externalversions/iot/v1alpha1/interface.go
index 7e9b9b0bd..3b4d6ddbb 100644
--- a/pkg/client/informers/externalversions/iot/v1alpha1/interface.go
+++ b/pkg/client/informers/externalversions/iot/v1alpha1/interface.go
@@ -13,6 +13,8 @@ import (
 
 // Interface provides access to all the informers in this group version.
 type Interface interface {
+	// IoTConfigs returns a IoTConfigInformer.
+	IoTConfigs() IoTConfigInformer
 	// IoTProjects returns a IoTProjectInformer.
 	IoTProjects() IoTProjectInformer
 }
@@ -28,6 +30,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList
 	return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
 }
 
+// IoTConfigs returns a IoTConfigInformer.
+func (v *version) IoTConfigs() IoTConfigInformer {
+	return &ioTConfigInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
 // IoTProjects returns a IoTProjectInformer.
 func (v *version) IoTProjects() IoTProjectInformer {
 	return &ioTProjectInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/client/informers/externalversions/iot/v1alpha1/iotconfig.go b/pkg/client/informers/externalversions/iot/v1alpha1/iotconfig.go
new file mode 100644
index 000000000..208e804b9
--- /dev/null
+++ b/pkg/client/informers/externalversions/iot/v1alpha1/iotconfig.go
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018, 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 v1alpha1
+
+import (
+	time "time"
+
+	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	versioned "github.com/enmasseproject/enmasse/pkg/client/clientset/versioned"
+	internalinterfaces "github.com/enmasseproject/enmasse/pkg/client/informers/externalversions/internalinterfaces"
+	v1alpha1 "github.com/enmasseproject/enmasse/pkg/client/listers/iot/v1alpha1"
+	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"
+)
+
+// IoTConfigInformer provides access to a shared informer and lister for
+// IoTConfigs.
+type IoTConfigInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.IoTConfigLister
+}
+
+type ioTConfigInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewIoTConfigInformer constructs a new informer for IoTConfig 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 NewIoTConfigInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredIoTConfigInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredIoTConfigInformer constructs a new informer for IoTConfig 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 NewFilteredIoTConfigInformer(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.IotV1alpha1().IoTConfigs(namespace).List(options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.IotV1alpha1().IoTConfigs(namespace).Watch(options)
+			},
+		},
+		&iotv1alpha1.IoTConfig{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *ioTConfigInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredIoTConfigInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *ioTConfigInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&iotv1alpha1.IoTConfig{}, f.defaultInformer)
+}
+
+func (f *ioTConfigInformer) Lister() v1alpha1.IoTConfigLister {
+	return v1alpha1.NewIoTConfigLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/client/listers/iot/v1alpha1/expansion_generated.go b/pkg/client/listers/iot/v1alpha1/expansion_generated.go
index 846433e9a..5f396f71a 100644
--- a/pkg/client/listers/iot/v1alpha1/expansion_generated.go
+++ b/pkg/client/listers/iot/v1alpha1/expansion_generated.go
@@ -7,6 +7,14 @@
 
 package v1alpha1
 
+// IoTConfigListerExpansion allows custom methods to be added to
+// IoTConfigLister.
+type IoTConfigListerExpansion interface{}
+
+// IoTConfigNamespaceListerExpansion allows custom methods to be added to
+// IoTConfigNamespaceLister.
+type IoTConfigNamespaceListerExpansion interface{}
+
 // IoTProjectListerExpansion allows custom methods to be added to
 // IoTProjectLister.
 type IoTProjectListerExpansion interface{}
diff --git a/pkg/client/listers/iot/v1alpha1/iotconfig.go b/pkg/client/listers/iot/v1alpha1/iotconfig.go
new file mode 100644
index 000000000..d2a396bc8
--- /dev/null
+++ b/pkg/client/listers/iot/v1alpha1/iotconfig.go
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2018, 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 v1alpha1
+
+import (
+	v1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// IoTConfigLister helps list IoTConfigs.
+type IoTConfigLister interface {
+	// List lists all IoTConfigs in the indexer.
+	List(selector labels.Selector) (ret []*v1alpha1.IoTConfig, err error)
+	// IoTConfigs returns an object that can list and get IoTConfigs.
+	IoTConfigs(namespace string) IoTConfigNamespaceLister
+	IoTConfigListerExpansion
+}
+
+// ioTConfigLister implements the IoTConfigLister interface.
+type ioTConfigLister struct {
+	indexer cache.Indexer
+}
+
+// NewIoTConfigLister returns a new IoTConfigLister.
+func NewIoTConfigLister(indexer cache.Indexer) IoTConfigLister {
+	return &ioTConfigLister{indexer: indexer}
+}
+
+// List lists all IoTConfigs in the indexer.
+func (s *ioTConfigLister) List(selector labels.Selector) (ret []*v1alpha1.IoTConfig, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.IoTConfig))
+	})
+	return ret, err
+}
+
+// IoTConfigs returns an object that can list and get IoTConfigs.
+func (s *ioTConfigLister) IoTConfigs(namespace string) IoTConfigNamespaceLister {
+	return ioTConfigNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// IoTConfigNamespaceLister helps list and get IoTConfigs.
+type IoTConfigNamespaceLister interface {
+	// List lists all IoTConfigs in the indexer for a given namespace.
+	List(selector labels.Selector) (ret []*v1alpha1.IoTConfig, err error)
+	// Get retrieves the IoTConfig from the indexer for a given namespace and name.
+	Get(name string) (*v1alpha1.IoTConfig, error)
+	IoTConfigNamespaceListerExpansion
+}
+
+// ioTConfigNamespaceLister implements the IoTConfigNamespaceLister
+// interface.
+type ioTConfigNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all IoTConfigs in the indexer for a given namespace.
+func (s ioTConfigNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.IoTConfig, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.IoTConfig))
+	})
+	return ret, err
+}
+
+// Get retrieves the IoTConfig from the indexer for a given namespace and name.
+func (s ioTConfigNamespaceLister) Get(name string) (*v1alpha1.IoTConfig, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("iotconfig"), name)
+	}
+	return obj.(*v1alpha1.IoTConfig), nil
+}
diff --git a/pkg/controller/add_iotconfig.go b/pkg/controller/add_iotconfig.go
new file mode 100644
index 000000000..f28e95fe5
--- /dev/null
+++ b/pkg/controller/add_iotconfig.go
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package controller
+
+import (
+	"github.com/enmasseproject/enmasse/pkg/controller/iotconfig"
+	"github.com/enmasseproject/enmasse/pkg/util"
+)
+
+func init() {
+	if util.IsModuleEnabled("IOT_CONFIG") {
+		AddToManagerFuncs = append(AddToManagerFuncs, iotconfig.Add)
+	}
+}
diff --git a/pkg/controller/add_iotproject.go b/pkg/controller/add_iotproject.go
index 2ecbf78f3..a7bb1ce79 100644
--- a/pkg/controller/add_iotproject.go
+++ b/pkg/controller/add_iotproject.go
@@ -14,7 +14,7 @@ func init() {
 
 	// add ourselves to the list of controllers
 
-	if util.IsModuleEnabled("iot") {
+	if util.IsModuleEnabled("IOT_PROJECT") {
 		AddToManagerFuncs = append(AddToManagerFuncs, iotproject.Add)
 	}
 }
diff --git a/pkg/controller/iotconfig/collector.go b/pkg/controller/iotconfig/collector.go
new file mode 100644
index 000000000..1832fe697
--- /dev/null
+++ b/pkg/controller/iotconfig/collector.go
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package iotconfig
+
+import (
+	"context"
+
+	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	"github.com/enmasseproject/enmasse/pkg/util/install"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+)
+
+func (r *ReconcileIoTConfig) processCollector(ctx context.Context, config *iotv1alpha1.IoTConfig) error {
+	return r.processDeployment(ctx, "iot-gc", config, r.reconcileCollectorDeployment)
+}
+
+func (r *ReconcileIoTConfig) reconcileCollectorDeployment(config *iotv1alpha1.IoTConfig, deployment *appsv1.Deployment) error {
+
+	install.ApplyDeploymentDefaults(deployment, "iot", "iot-core", "iot-gc")
+
+	deployment.Spec.Replicas = nil
+
+	err := install.ApplyContainerWithError(deployment, "collector", func(container *corev1.Container) error {
+		if err := SetContainerImage(container, "iot-gc", MakeImageProperties(config)); err != nil {
+			return err
+		}
+
+		container.Resources = corev1.ResourceRequirements{
+			Limits: corev1.ResourceList{
+				corev1.ResourceMemory: *resource.NewQuantity(128*1024*1024 /* 128Mi */, resource.BinarySI),
+			},
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/controller/iotconfig/iotconfig_controller.go b/pkg/controller/iotconfig/iotconfig_controller.go
new file mode 100644
index 000000000..40f93d7a3
--- /dev/null
+++ b/pkg/controller/iotconfig/iotconfig_controller.go
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package iotconfig
+
+import (
+	"context"
+
+	"github.com/enmasseproject/enmasse/pkg/util/recon"
+
+	"github.com/enmasseproject/enmasse/pkg/util"
+
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
+	appsv1 "k8s.io/api/apps/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/manager"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
+	"sigs.k8s.io/controller-runtime/pkg/source"
+)
+
+var log = logf.Log.WithName("controller_iotconfig")
+
+// Gets called by parent "init", adding as to the manager
+func Add(mgr manager.Manager) error {
+	return add(mgr, newReconciler(
+		mgr,
+		util.GetEnvOrDefault("ENMASSE_IOT_CONFIG_NAME", "default"),
+		util.GetEnvOrDefault("ENMASSE_IOT_CONFIG_NAMESPACE", "enmasse-infra"),
+	))
+}
+
+func newReconciler(mgr manager.Manager, configName string, configNamespace string) *ReconcileIoTConfig {
+	return &ReconcileIoTConfig{
+		client:          mgr.GetClient(),
+		scheme:          mgr.GetScheme(),
+		configName:      configName,
+		configNamespace: configNamespace,
+	}
+}
+
+func add(mgr manager.Manager, r *ReconcileIoTConfig) error {
+
+	// Create a new controller
+	c, err := controller.New("iotconfig-controller", mgr, controller.Options{Reconciler: r})
+	if err != nil {
+		return err
+	}
+
+	// Watch for changes to primary resource IoTConfig
+	err = c.Watch(&source.Kind{Type: &iotv1alpha1.IoTConfig{}}, &handler.EnqueueRequestForObject{})
+	if err != nil {
+		return err
+	}
+
+	// watch for generated Deployment resources
+	err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{
+		OwnerType:    &iotv1alpha1.IoTConfig{},
+		IsController: true,
+	})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ensure we are implementing the interface
+var _ reconcile.Reconciler = &ReconcileIoTConfig{}
+
+type ReconcileIoTConfig struct {
+	// This client, initialized using mgr.Client() above, is a split client
+	// that reads objects from the cache and writes to the apiserver
+	client client.Client
+	scheme *runtime.Scheme
+
+	// The name of the configuration we are watching
+	// we are watching only one config, in one namespace
+	configName      string
+	configNamespace string
+}
+
+// Reconcile
+//
+// returning an error will get the request re-queued
+func (r *ReconcileIoTConfig) Reconcile(request reconcile.Request) (reconcile.Result, error) {
+	reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
+	reqLogger.Info("Reconciling IoTConfig")
+
+	// Get config
+	config := &iotv1alpha1.IoTConfig{}
+	err := r.client.Get(context.TODO(), request.NamespacedName, config)
+
+	ctx := context.TODO()
+
+	if err != nil {
+
+		if errors.IsNotFound(err) {
+
+			reqLogger.Info("Config was not found")
+
+			// Request object not found, could have been deleted after reconcile request.
+			// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
+			// Return and don't requeue
+			return reconcile.Result{}, nil
+		}
+
+		// Error reading the object - requeue the request.
+		return reconcile.Result{}, err
+	}
+
+	if config.Name != r.configName || config.Namespace != r.configNamespace {
+		return r.failWrongConfigName(ctx, config)
+	}
+
+	rc := &recon.ReconcileContext{}
+
+	rc.ProcessSimple(func() error {
+		return r.processCollector(ctx, config)
+	})
+
+	return rc.Result()
+}
+
+func (r *ReconcileIoTConfig) failWrongConfigName(ctx context.Context, config *iotv1alpha1.IoTConfig) (reconcile.Result, error) {
+
+	config.Status.Initialized = false
+	config.Status.State = iotv1alpha1.ConfigStateWrongName
+
+	if err := r.client.Update(ctx, config); err != nil {
+		return reconcile.Result{}, err
+	}
+
+	return reconcile.Result{}, nil
+}
+
+func (r *ReconcileIoTConfig) processDeployment(ctx context.Context, name string, config *iotv1alpha1.IoTConfig, manipulator func(config *iotv1alpha1.IoTConfig, deployment *appsv1.Deployment) error) error {
+
+	deployment := appsv1.Deployment{
+		ObjectMeta: v1.ObjectMeta{Namespace: config.Namespace, Name: name},
+	}
+
+	_, err := controllerutil.CreateOrUpdate(ctx, r.client, &deployment, func(existing runtime.Object) error {
+		existingDeployment := existing.(*appsv1.Deployment)
+
+		if err := controllerutil.SetControllerReference(config, existingDeployment, r.scheme); err != nil {
+			return err
+		}
+
+		return manipulator(config, existingDeployment)
+	})
+
+	if err != nil {
+		log.Error(err, "Failed calling CreateOrUpdate")
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/controller/iotconfig/utils.go b/pkg/controller/iotconfig/utils.go
new file mode 100644
index 000000000..ea57da73d
--- /dev/null
+++ b/pkg/controller/iotconfig/utils.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 iotconfig
+
+import (
+	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	"github.com/enmasseproject/enmasse/pkg/util/install"
+	corev1 "k8s.io/api/core/v1"
+)
+
+func FindAdapterSpec(config *iotv1alpha1.IoTConfig, adapterType string) *iotv1alpha1.AdapterSpec {
+
+	for _, a := range config.Spec.Adapters {
+		if a.AdapterType == adapterType {
+			return &a
+		}
+	}
+
+	return nil
+}
+
+func MakeImageProperties(config *iotv1alpha1.IoTConfig) iotv1alpha1.ImageProperties {
+	return install.FlattenImageProperties([]*iotv1alpha1.ImageProperties{
+		&config.Spec.DefaultImageProperties,
+	})
+}
+
+func SetContainerImage(container *corev1.Container, imageName string, properties iotv1alpha1.ImageProperties) error {
+
+	image, err := install.MakeImage(imageName, properties)
+	if err != nil {
+		return err
+	}
+
+	container.Image = image
+	container.ImagePullPolicy = *properties.PullPolicy
+
+	return nil
+}
diff --git a/pkg/util/env.go b/pkg/util/env.go
new file mode 100644
index 000000000..133d93782
--- /dev/null
+++ b/pkg/util/env.go
@@ -0,0 +1,19 @@
+/*
+ * 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 (
+	"os"
+)
+
+func GetEnvOrDefault(key string, defaultValue string) string {
+	value, ok := os.LookupEnv(key)
+	if ok {
+		return value
+	} else {
+		return defaultValue
+	}
+}
diff --git a/pkg/util/install/utils.go b/pkg/util/install/utils.go
new file mode 100644
index 000000000..17884d5aa
--- /dev/null
+++ b/pkg/util/install/utils.go
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package install
+
+import (
+	"fmt"
+	"strings"
+
+	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// Apply some default deployment values
+func ApplyDeploymentDefaults(deployment *appsv1.Deployment, component string, app string, name string) {
+	if deployment.Labels == nil {
+		deployment.Labels = make(map[string]string)
+	}
+
+	deployment.Labels["component"] = component
+	deployment.Labels["app"] = app
+	deployment.Labels["name"] = name
+
+	deployment.Spec.Selector = &v1.LabelSelector{
+		MatchLabels: map[string]string{
+			"component": component,
+			"app":       app,
+			"name":      name,
+		},
+	}
+
+	if deployment.Spec.Template.ObjectMeta.Labels == nil {
+		deployment.Spec.Template.ObjectMeta.Labels = make(map[string]string)
+	}
+
+	deployment.Spec.Template.ObjectMeta.Labels["component"] = component
+	deployment.Spec.Template.ObjectMeta.Labels["app"] = app
+	deployment.Spec.Template.ObjectMeta.Labels["name"] = name
+
+}
+
+func ApplyContainer(deployment *appsv1.Deployment, name string, mutator func(*corev1.Container)) {
+	// call "with error", and eat up the error
+	_ = ApplyContainerWithError(deployment, name, func(container *corev1.Container) error {
+		mutator(container)
+		return nil
+	})
+}
+
+func ApplyContainerWithError(deployment *appsv1.Deployment, name string, mutator func(*corev1.Container) error) error {
+
+	if deployment.Spec.Template.Spec.Containers == nil {
+		deployment.Spec.Template.Spec.Containers = make([]corev1.Container, 0)
+	}
+
+	for i, c := range deployment.Spec.Template.Spec.Containers {
+		if c.Name == name {
+			return mutator(&deployment.Spec.Template.Spec.Containers[i])
+		}
+	}
+
+	c := &corev1.Container{
+		Name: name,
+	}
+
+	err := mutator(c)
+	if err == nil {
+		deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, *c)
+	}
+	return err
+}
+
+// Extracts the default pull policy from a tag, unless provided
+// The result of this function is guaranteed to be non-nil.
+func PullPolicyFromTag(tag string, pullPolicy *corev1.PullPolicy) *corev1.PullPolicy {
+
+	if pullPolicy != nil && *pullPolicy != "" {
+		return pullPolicy
+	}
+
+	// eval
+
+	if strings.HasSuffix(tag, "-SNAPSHOT") {
+		s := corev1.PullAlways
+		return &s
+	} else {
+		s := corev1.PullIfNotPresent
+		return &s
+	}
+
+}
+
+// Provide a default set of image properties.
+func MakeDefaultImageProperties() iotv1alpha1.ImageProperties {
+
+	useImageStream := defaultUseImageStreams
+	repository := defaultRepository
+	pullPolicy := defaultPullPolicy
+
+	return iotv1alpha1.ImageProperties{
+		Repository:     &repository,
+		UseImageStream: &useImageStream,
+		PullPolicy:     &pullPolicy,
+		Tag:            defaultTag,
+	}
+}
+
+func applyImageProperties(result *iotv1alpha1.ImageProperties, provided iotv1alpha1.ImageProperties) {
+
+	if result == nil {
+		return
+	}
+
+	if provided.Repository != nil {
+		result.Repository = provided.Repository
+	}
+	if provided.UseImageStream != nil {
+		result.UseImageStream = provided.UseImageStream
+	}
+	if provided.Tag != "" {
+		result.Tag = provided.Tag
+	}
+	if provided.PullPolicy != nil {
+		result.PullPolicy = provided.PullPolicy
+	}
+
+}
+
+func FlattenImageProperties(properties []*iotv1alpha1.ImageProperties) iotv1alpha1.ImageProperties {
+
+	// start with default
+
+	result := MakeDefaultImageProperties()
+
+	// Flatten
+
+	for _, p := range properties {
+		if p != nil {
+			applyImageProperties(&result, *p)
+		}
+	}
+
+	// eval pull policy, if not set
+
+	result.PullPolicy = PullPolicyFromTag(result.Tag, result.PullPolicy)
+
+	// return
+
+	return result
+}
+
+func MakeImage(baseName string, properties iotv1alpha1.ImageProperties) (string, error) {
+	if properties.Tag == "" {
+		return "", fmt.Errorf("missing tag in image properties")
+	}
+
+	name := baseName + ":" + properties.Tag
+
+	if properties.UseImageStream != nil && *properties.UseImageStream {
+		return name, nil
+	}
+
+	if properties.Repository == nil || *properties.Repository == "" {
+		return "", fmt.Errorf("missing repository in image properties")
+	}
+
+	name = *properties.Repository + "/" + name
+
+	return name, nil
+}
diff --git a/pkg/util/install/vars.go b/pkg/util/install/vars.go
new file mode 100644
index 000000000..9556dbc50
--- /dev/null
+++ b/pkg/util/install/vars.go
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package install
+
+import (
+	"os"
+
+	corev1 "k8s.io/api/core/v1"
+
+	"github.com/enmasseproject/enmasse/version"
+)
+
+var (
+	// The default tag used for images
+	defaultTag string
+	// The default repository used for images
+	defaultRepository string
+	// The default setting of using image streams
+	defaultUseImageStreams bool
+	// The default pull policy, might be empty
+	defaultPullPolicy corev1.PullPolicy
+)
+
+func init() {
+	defaultTag = os.Getenv("ENMASSE_DEFAULT_TAG")
+	if defaultTag == "" {
+		defaultTag = version.Version
+	}
+
+	defaultUseImageStreams = os.Getenv("ENMASSE_DEFAULT_USE_IMAGE_STREAMS") == "true"
+
+	defaultRepository = os.Getenv("ENMASSE_DEFAULT_REPOSITORY")
+	if defaultRepository == "" {
+		defaultRepository = "unset.container.repository"
+	}
+
+	defaultPullPolicy = corev1.PullPolicy(os.Getenv("ENMASSE_DEFAULT_PULL_POLICY"))
+}
diff --git a/pkg/util/math.go b/pkg/util/math.go
new file mode 100644
index 000000000..c1fb1bdd0
--- /dev/null
+++ b/pkg/util/math.go
@@ -0,0 +1,21 @@
+/*
+ * 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 (
+	"time"
+)
+
+func Max(x, y int64) int64 {
+	if x < y {
+		return y
+	}
+	return x
+}
+
+func MaxDuration(x, y time.Duration) time.Duration {
+	return time.Duration(Max(int64(x), int64(y)))
+}
diff --git a/pkg/util/modules.go b/pkg/util/modules.go
index 4e32a08f3..41950c7ef 100644
--- a/pkg/util/modules.go
+++ b/pkg/util/modules.go
@@ -66,16 +66,18 @@ func IsModuleEnabled(module string) bool {
 
 func isModuleEnabled(environment environmentProvider, module string) bool {
 
+	module = strings.ToUpper(module)
+
 	// check is "everything" is enabled ...
 	if isBooleanEnv(environment, "CONTROLLER_ENABLE_ALL", false) {
 		// ... it is
 		return true
 	}
 
-	val := getBooleanEnv(environment, "CONTROLLER_ENABLE_"+strings.ToUpper(module))
+	val := getBooleanEnv(environment, "CONTROLLER_ENABLE_"+module)
 	// if val is set ...
 	if val != nil {
-		// ... it is authorative
+		// ... it is authoritative
 		return *val
 	}
 
diff --git a/pkg/util/recon/reconcile.go b/pkg/util/recon/reconcile.go
new file mode 100644
index 000000000..480417385
--- /dev/null
+++ b/pkg/util/recon/reconcile.go
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package recon
+
+import (
+	"time"
+
+	"github.com/enmasseproject/enmasse/pkg/util"
+	"go.uber.org/multierr"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+type ReconcileContext struct {
+	requeue      bool
+	requeueAfter time.Duration
+	error        error
+}
+
+func (r *ReconcileContext) Process(processor func() (reconcile.Result, error)) {
+	result, err := processor()
+
+	if err != nil {
+		r.error = multierr.Append(r.error, err)
+	} else {
+		if result.Requeue {
+			r.requeue = true
+		}
+		r.requeueAfter = util.MaxDuration(r.requeueAfter, result.RequeueAfter)
+	}
+}
+
+func (r *ReconcileContext) ProcessSimple(processor func() error) {
+	r.Process(func() (reconcile.Result, error) {
+		err := processor()
+		return reconcile.Result{}, err
+	})
+}
+
+func (r *ReconcileContext) NeedRequeue() bool {
+	return r.requeue
+}
+
+func (r *ReconcileContext) Error() error {
+	return r.error
+}
+
+func (r *ReconcileContext) Result() (reconcile.Result, error) {
+	return reconcile.Result{
+		Requeue:      r.requeue,
+		RequeueAfter: r.requeueAfter,
+	}, r.error
+}
diff --git a/pom.xml b/pom.xml
index ffc962410..64d516e9f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
         <hamcrest.version>1.3</hamcrest.version>
         <junit.jupiter.version>5.4.0</junit.jupiter.version>
         <junit.platform.runner.version>1.4.0</junit.platform.runner.version>
+        <junit.platform.surefire.provider.version>1.3.2</junit.platform.surefire.provider.version>
         <junit.platform.launcher.version>1.4.0</junit.platform.launcher.version>
         <artemis.version>2.6.4</artemis.version>
         <ngwebdriver.version>1.1.4</ngwebdriver.version>
@@ -843,6 +844,13 @@
                       <vertx.logger-delegate-factory-class-name>io.vertx.core.logging.SLF4JLogDelegateFactory</vertx.logger-delegate-factory-class-name>
                     </systemPropertyVariables>
                 </configuration>
+                <dependencies>
+                  <dependency>
+                    <groupId>org.junit.platform</groupId>
+                    <artifactId>junit-platform-surefire-provider</artifactId>
+                    <version>${junit.platform.surefire.provider.version}</version>
+                  </dependency>
+                </dependencies>
             </plugin>
             <plugin>
                 <groupId>com.mycila</groupId>
diff --git a/templates/enmasse-controller-manager/010-ServiceAccount-enmasse-controller-manager b/templates/enmasse-controller-manager/010-ServiceAccount-enmasse-controller-manager
new file mode 100644
index 000000000..8042ab7f6
--- /dev/null
+++ b/templates/enmasse-controller-manager/010-ServiceAccount-enmasse-controller-manager
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: enmasse-controller-manager
+  labels:
+    app: enmasse
diff --git a/templates/enmasse-controller-manager/020-Role-enmasse-controller-manager.yaml b/templates/enmasse-controller-manager/020-Role-enmasse-controller-manager.yaml
new file mode 100644
index 000000000..190dbc8ee
--- /dev/null
+++ b/templates/enmasse-controller-manager/020-Role-enmasse-controller-manager.yaml
@@ -0,0 +1,13 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app: enmasse
+  name: enmasse.io:enmasse-controller-manager
+rules:
+  - apiGroups: [ "admin.enmasse.io" ]
+    resources: [ "authenticationservice" ]
+    verbs: [ "get", "list", "watch", "update", "create", "patch" ]
+  - apiGroups: [ "iot.enmasse.io" ]
+    resources: [ "iotconfig" ]
+    verbs: [ "get", "list", "watch", "update", "create", "patch" ]
diff --git a/templates/enmasse-controller-manager/030-Deployment-enmasse-controller-manager.yaml b/templates/enmasse-controller-manager/030-Deployment-enmasse-controller-manager.yaml
index 422214b91..e8b8a9da7 100644
--- a/templates/enmasse-controller-manager/030-Deployment-enmasse-controller-manager.yaml
+++ b/templates/enmasse-controller-manager/030-Deployment-enmasse-controller-manager.yaml
@@ -17,6 +17,7 @@ spec:
         app: enmasse
         name: enmasse-controller-manager
     spec:
+      serviceAccountName: enmasse-controller-manager
       containers:
       - name: controller
         image: ${ENMASSE_CONTROLLER_MANAGER_IMAGE}
diff --git a/templates/enmasse-controller-manager/030-RoleBinding-enmasse-controller-manager.yaml b/templates/enmasse-controller-manager/030-RoleBinding-enmasse-controller-manager.yaml
new file mode 100644
index 000000000..26638582b
--- /dev/null
+++ b/templates/enmasse-controller-manager/030-RoleBinding-enmasse-controller-manager.yaml
@@ -0,0 +1,14 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: enmasse-controller-manager
+  labels:
+    app: enmasse
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: enmasse.io:enmasse-controller-manager
+subjects:
+- kind: ServiceAccount
+  name: enmasse-controller-manager
+  namespace: ${NAMESPACE}
diff --git a/templates/iot/api/010-CustomResourceDefinition-iot-config.yaml b/templates/iot/api/010-CustomResourceDefinition-iot-config.yaml
new file mode 100644
index 000000000..b347f66bf
--- /dev/null
+++ b/templates/iot/api/010-CustomResourceDefinition-iot-config.yaml
@@ -0,0 +1,16 @@
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: iotconfigs.iot.enmasse.io
+  labels:
+    enmasse-component: iot
+spec:
+  group: iot.enmasse.io
+  version: v1alpha1
+  scope: Namespaced
+  names:
+    kind: IoTConfig
+    plural: iotconfigs
+    singular: iotconfig
+    shortNames:
+    - icfg
diff --git a/templates/iot/auth/020-ConfigMap-service-auth.yaml b/templates/iot/auth/020-ConfigMap-service-auth.yaml
index bd91aa1f5..9ec3b4298 100644
--- a/templates/iot/auth/020-ConfigMap-service-auth.yaml
+++ b/templates/iot/auth/020-ConfigMap-service-auth.yaml
@@ -12,6 +12,8 @@ data:
     hono:
       app:
         maxInstances: 1
+        healthCheckBindAddress: 0.0.0.0
+        healthCheckPort: 8088
       auth:
         amqp:
           bindAddress: 0.0.0.0
@@ -22,14 +24,6 @@ data:
           trustStoreFormat: PEM
         svc:
           permissionsPath: file:///etc/config/permissions.json
-      metrics:
-        legacy: false
-    management:
-      metrics:
-        enabled: false
-        export:
-          graphite:
-            enabled: false
 
   permissions.json: |+
     {
diff --git a/templates/iot/auth/050-Deployment-service-auth.yaml b/templates/iot/auth/050-Deployment-service-auth.yaml
index 8db8046d6..f7cb153f1 100644
--- a/templates/iot/auth/050-Deployment-service-auth.yaml
+++ b/templates/iot/auth/050-Deployment-service-auth.yaml
@@ -35,20 +35,18 @@ spec:
           valueFrom:
             fieldRef:
               fieldPath: metadata.namespace
-        livenessProbe:
-          # change for Hono 0.9
-          initialDelaySeconds: 25
-          periodSeconds: 9
-          tcpSocket:
-            port: 5671
-          timeoutSeconds: 1
         readinessProbe:
-          # change for Hono 0.9
-          initialDelaySeconds: 15
-          periodSeconds: 5
-          tcpSocket:
-            port: 5671
-          timeoutSeconds: 1
+          httpGet:
+            path: /readiness
+            port: 8088
+            scheme: HTTP
+          initialDelaySeconds: 10
+        livenessProbe:
+          httpGet:
+            path: /liveness
+            port: 8088
+            scheme: HTTP
+          initialDelaySeconds: 180
         resources:
           limits:
             memory: 512Mi
diff --git a/templates/iot/device-registry/020-ConfigMap-service-device-registry.yaml b/templates/iot/device-registry/020-ConfigMap-service-device-registry.yaml
index a70d9306b..e4b412e3a 100644
--- a/templates/iot/device-registry/020-ConfigMap-service-device-registry.yaml
+++ b/templates/iot/device-registry/020-ConfigMap-service-device-registry.yaml
@@ -46,14 +46,6 @@ data:
         svc:
           filename: /var/lib/hono/device-registry/tenants.json
           saveToFile: true
-      metrics:
-        legacy: false
-    management:
-      metrics:
-        enabled: false
-        export:
-          graphite:
-            enabled: false
 
   logback-spring.xml: |+
       <?xml version="1.0" encoding="UTF-8"?>
diff --git a/templates/iot/iot-gc/030-Deployment-iot-gc.yaml b/templates/iot/iot-gc/030-Deployment-iot-gc.yaml
deleted file mode 100644
index c5623d9ca..000000000
--- a/templates/iot/iot-gc/030-Deployment-iot-gc.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-kind: Deployment
-apiVersion: apps/v1
-metadata:
-  name: iot-gc
-  labels:
-    app: iot-core
-    name: iot-gc
-    enmasse-component: iot
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: iot-core
-      name: iot-gc
-  template:
-    metadata:
-      labels:
-        app: iot-core
-        name: iot-gc
-        enmasse-component: iot
-    spec:
-      containers:
-      - name: collector
-        image: ${IOT_GC_IMAGE}
-        imagePullPolicy: ${IMAGE_PULL_POLICY}
-        resources:
-          limits:
-            memory: 128Mi
diff --git a/version/version.go b/version/version.go
index 23c40949b..bc569fee4 100644
--- a/version/version.go
+++ b/version/version.go
@@ -5,6 +5,17 @@
 
 package version
 
+import (
+	"os"
+)
+
 var (
-	Version = "0.0.1"
+	Version string
 )
+
+func init() {
+	Version = os.Getenv("ENMASSE_VERSION")
+	if Version == "" {
+		Version = "latest"
+	}
+}


Test patch:
diff --git a/pkg/util/install/utils_test.go b/pkg/util/install/utils_test.go
new file mode 100644
index 000000000..2c0bc7311
--- /dev/null
+++ b/pkg/util/install/utils_test.go
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package install
+
+import (
+	"testing"
+
+	appv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+)
+
+func TestAddContainer(t *testing.T) {
+	d := appv1.Deployment{}
+
+	ApplyContainer(&d, "foo", func(container *corev1.Container) {
+		container.Image = "bar"
+	})
+
+	if d.Spec.Template.Spec.Containers == nil {
+		t.Fatalf("Nil container array")
+	}
+	if l := len(d.Spec.Template.Spec.Containers); l != 1 {
+		t.Fatalf("Expected exactly one entry, got: %d", l)
+	}
+	if d.Spec.Template.Spec.Containers[0].Name != "foo" {
+		t.Errorf("Container name must be 'foo', was: '%s'", d.Spec.Template.Spec.Containers[0].Name)
+	}
+	if d.Spec.Template.Spec.Containers[0].Image != "bar" {
+		t.Errorf("Container image must be 'bar', was: '%s'", d.Spec.Template.Spec.Containers[0].Image)
+	}
+}
+
+func TestReplaceContainer(t *testing.T) {
+	d := appv1.Deployment{}
+
+	ApplyContainer(&d, "foo", func(container *corev1.Container) {
+		container.Image = "bar"
+	})
+	ApplyContainer(&d, "foo", func(container *corev1.Container) {
+		container.Image = "baz"
+	})
+
+	if d.Spec.Template.Spec.Containers == nil {
+		t.Fatalf("Nil container array")
+	}
+	if l := len(d.Spec.Template.Spec.Containers); l != 1 {
+		t.Fatalf("Expected exactly one entry, got: %d", l)
+	}
+	if d.Spec.Template.Spec.Containers[0].Name != "foo" {
+		t.Errorf("Container name must be 'foo', was: '%s'", d.Spec.Template.Spec.Containers[0].Name)
+	}
+	if d.Spec.Template.Spec.Containers[0].Image != "baz" {
+		t.Errorf("Container image must be 'baz', was: '%s'", d.Spec.Template.Spec.Containers[0].Image)
+	}
+}
+
+func TestTwoContainers(t *testing.T) {
+	d := appv1.Deployment{}
+
+	ApplyContainer(&d, "foo", func(container *corev1.Container) {
+		container.Image = "bar"
+	})
+	ApplyContainer(&d, "foo2", func(container *corev1.Container) {
+		container.Image = "baz"
+	})
+	ApplyContainer(&d, "foo", func(container *corev1.Container) {
+		container.Image = "baz2"
+	})
+
+	if d.Spec.Template.Spec.Containers == nil {
+		t.Fatalf("Nil container array")
+	}
+	if l := len(d.Spec.Template.Spec.Containers); l != 2 {
+		t.Fatalf("Expected exactly one entry, got: %d", l)
+	}
+
+	if d.Spec.Template.Spec.Containers[0].Name != "foo" {
+		t.Errorf("Container name must be 'foo', was: '%s'", d.Spec.Template.Spec.Containers[0].Name)
+	}
+	if d.Spec.Template.Spec.Containers[0].Image != "baz2" {
+		t.Errorf("Container image must be 'baz2', was: '%s'", d.Spec.Template.Spec.Containers[0].Image)
+	}
+
+	if d.Spec.Template.Spec.Containers[1].Name != "foo2" {
+		t.Errorf("Container name must be 'foo2', was: '%s'", d.Spec.Template.Spec.Containers[0].Name)
+	}
+	if d.Spec.Template.Spec.Containers[1].Image != "baz" {
+		t.Errorf("Container image must be 'baz', was: '%s'", d.Spec.Template.Spec.Containers[0].Image)
+	}
+}
diff --git a/systemtests/pom.xml b/systemtests/pom.xml
index 2ca34b816..24b0d0b8e 100644
--- a/systemtests/pom.xml
+++ b/systemtests/pom.xml
@@ -124,6 +124,13 @@
                         <enmasse.version>${release.version}</enmasse.version>
                     </systemProperties>
                 </configuration>
+                <dependencies>
+                  <dependency>
+                    <groupId>org.junit.platform</groupId>
+                    <artifactId>junit-platform-surefire-provider</artifactId>
+                    <version>${junit.platform.surefire.provider.version}</version>
+                  </dependency>
+                </dependencies>
             </plugin>
             <plugin>
                 <groupId>org.owasp</groupId>
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/selenium/page/OpenshiftLoginWebPage.java b/systemtests/src/main/java/io/enmasse/systemtest/selenium/page/OpenshiftLoginWebPage.java
index c6b79420c..072349cb8 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/selenium/page/OpenshiftLoginWebPage.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/selenium/page/OpenshiftLoginWebPage.java
@@ -24,11 +24,11 @@ public class OpenshiftLoginWebPage implements IWebPage {
     }
 
     private WebElement getUsernameTextInput() {
-        return selenium.getDriver().findElement(By.id("inputUsername"));
+        return selenium.getDriver().findElement(By.id("username"));
     }
 
     private WebElement getPasswordTextInput() {
-        return selenium.getDriver().findElement(By.id("inputPassword"));
+        return selenium.getDriver().findElement(By.id("password"));
     }
 
     private WebElement getLoginButton() {