Repository: EnMasseProject/enmasse
Pull request #2443 (2019-03-13 14:53:01)

Deploy IoT operator on Travis test
This PR deploys the enmasse-controller-manager and the iot-operator in an additional travis job. This can be the basis for further running IoT system tests on each PR, without make the Travis build run longer.

~~**Note:** This PR builds on top of #2439 and thus already contains its content. So this PR must be rebased before it can be merged.~~

ctron: @famartinrh You might want to have a look at this as well, this is the initial system test for IoT, it simply checks of the components got deployed.
kornys: @enmasse-ci run tests testcase=**.SmokeTest
ctron: Rebased with master. This one should be good to go now.
ctron: Currently most Travis builds run around 42 minutes. There is a limit of 50 minutes per job. Adding a minute of reserve, this would allow us to have 7 minutes for the IoT part, including the deployment of the extra components. Assuming that nothing more gets added by other components in the future.

Also, at the moment it takes ~42 minutes to validate a PR. Assuming that we have a second, or maybe a third job, we could parallelize more tests, and be quicker in validating PRs.

Altogether I think this is the right time to split off jobs and run them in parallel.
lulf: I agree, the remaining time is a bit little. Hopefully we can improve the speed a bit in the future to allow running it in a single job so that we can avoid PR builds being blocked.

In my view then what remains for this  PR is my in-code comments and its good to go.
ctron: I added an env-var to the script to explicitly deploy IoT components. But I will double check it that works in the travis build.

Patch:
diff --git a/.travis.yml b/.travis.yml
index fd9c1684c..1142a15d4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -59,12 +59,20 @@ jobs:
           on:
             tags: true
             repo: EnMasseProject/enmasse
-    - before_install:
-        - curl -L https://github.com/openshift/source-to-image/releases/download/v1.1.13/source-to-image-v1.1.13-b54d75d3-linux-amd64.tar.gz --output s2i.tar.gz
-        - tar xzf s2i.tar.gz
+    # we keep the "build" stage name in order to run in parallel
+    - dist: xenial
       install: true
+      before_install:
+        - sudo pip install requests[security] ansible
+        - gem install asciidoctor
+        - gimme 1.10.5
+        - source ~/.gimme/envs/go1.10.5.env
+        - go version
       script:
-        - ./s2i build . fabric8/s2i-java:3.0-java11 iot-tenant-service -e MAVEN_ARGS_APPEND="-pl io.enmasse:iot-tenant-service --also-make" -e ARTIFACT_DIR="iot/iot-tenant-service/target"
+        - "./.travis/setup-kubernetes.sh 'systemtests' ${OPENSHIFT} ${MINIKUBE} ${KUBECTL}"
+        - "./.travis/build-only.sh"
+        - "./.travis/test-iot.sh"
+
 env:
   global:
     - BRANCH=${TRAVIS_BRANCH}
diff --git a/.travis/build-only.sh b/.travis/build-only.sh
new file mode 100755
index 000000000..29010ba9c
--- /dev/null
+++ b/.travis/build-only.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+set -e
+CURDIR=`readlink -f \`dirname $0\``
+source ${CURDIR}/common.sh
+
+export TAG=${TAG:-latest}
+export DOCKER_ORG=${DOCKER_ORG:-$USER}
+export DOCKER_REGISTRY="localhost:5000"
+
+# We do a slim build and drop all output to shrink the console output
+
+make SKIP_TESTS=true > /dev/null
+make -j 4 docker_tag docker_push > /dev/null
+make -C templates > /dev/null
diff --git a/Makefile b/Makefile
index 08e50e528..c3f6ffe04 100644
--- a/Makefile
+++ b/Makefile
@@ -19,7 +19,7 @@ ASCIIDOCTOR_EXTRA_FLAGS = --failure-level WARN
 ASCIIDOCTOR_FLAGS       = -v -a EnMasseVersion=$(VERSION) -t -dbook $(ASCIIDOCTOR_EXTRA_FLAGS)
 
 ifeq ($(SKIP_TESTS),true)
-	MAVEN_ARGS="-DskipTests"
+	MAVEN_ARGS=-DskipTests -Dmaven.test.skip=true
 endif
 ifneq ($(strip $(PROJECT_DISPLAY_NAME)),)
 	MAVEN_ARGS+="-Dapplication.display.name=$(PROJECT_DISPLAY_NAME)"
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java
index 01ed27160..3f20716ae 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java
@@ -4,13 +4,27 @@
  */
 package io.enmasse.iot.model.v1;
 
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
 
-//FIXME: implement missing fields and remove ignore annotation
-@JsonIgnoreProperties(ignoreUnknown = true)
+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 DownstreamStrategy {
 
     private ExternalDownstreamStrategy externalStrategy;
+    private ManagedDownstreamStrategy managedStrategy;
 
     public ExternalDownstreamStrategy getExternalStrategy() {
         return this.externalStrategy;
@@ -20,4 +34,12 @@ public class DownstreamStrategy {
         this.externalStrategy = externalStrategy;
     }
 
+    public ManagedDownstreamStrategy getManagedStrategy() {
+        return managedStrategy;
+    }
+
+    public void setManagedStrategy(ManagedDownstreamStrategy managedStrategy) {
+        this.managedStrategy = managedStrategy;
+    }
+
 }
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java
index 21617167e..3a3535623 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java
@@ -7,9 +7,25 @@ package io.enmasse.iot.model.v1;
 
 import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
 
+import java.nio.ByteBuffer;
+
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 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"
+                )
+        )
 //FIXME: implement missing fields and remove ignore annotation
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonInclude(NON_NULL)
@@ -25,7 +41,7 @@ public class ExternalDownstreamStrategy {
 
     private boolean tls;
 
-    private byte[] certificate;
+    private ByteBuffer certificate;
 
     public String getHost() {
         return this.host;
@@ -67,11 +83,11 @@ public class ExternalDownstreamStrategy {
         this.tls = tls;
     }
 
-    public byte[] getCertificate() {
+    public ByteBuffer getCertificate() {
         return this.certificate;
     }
 
-    public void setCertificate(final byte[] certificate) {
+    public void setCertificate(final ByteBuffer certificate) {
         this.certificate = certificate;
     }
 
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java
index 89858f0a0..337806c7a 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java
@@ -31,6 +31,7 @@ public class IoTProject extends AbstractHasMetadata<IoTProject> {
     public static final String KIND = "IoTProject";
 
     private IoTProjectSpec spec;
+    private IoTProjectStatus status;
 
     public IoTProject() {
         super(KIND, IoTCrd.API_VERSION);
@@ -44,4 +45,12 @@ public class IoTProject extends AbstractHasMetadata<IoTProject> {
         return this.spec;
     }
 
+    public IoTProjectStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(IoTProjectStatus status) {
+        this.status = status;
+    }
+
 }
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java
index 4093a29c2..7f9821708 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java
@@ -6,6 +6,20 @@ package io.enmasse.iot.model.v1;
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
 
+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"
+                )
+        )
 public class IoTProjectSpec {
 
     private DownstreamStrategy downstreamStrategy;
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectStatus.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectStatus.java
new file mode 100644
index 000000000..9b24f8007
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectStatus.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+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"
+                )
+        )
+//FIXME: implement missing fields and remove ignore annotation
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class IoTProjectStatus {
+
+    @JsonProperty("isReady")
+    private boolean ready = false;
+
+    public boolean isReady() {
+        return ready;
+    }
+
+    public void setReady(boolean ready) {
+        this.ready = ready;
+    }
+
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/ManagedDownstreamStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/ManagedDownstreamStrategy.java
new file mode 100644
index 000000000..ec418abce
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/ManagedDownstreamStrategy.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018, 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 static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+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"
+                )
+        )
+//FIXME: implement missing fields and remove ignore annotation
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(NON_NULL)
+public class ManagedDownstreamStrategy {
+
+    private String addressSpaceName;
+
+    public String getAddressSpaceName() {
+        return addressSpaceName;
+    }
+
+    public void setAddressSpaceName(String addressSpaceName) {
+        this.addressSpaceName = addressSpaceName;
+    }
+
+}
diff --git a/api-server/src/main/java/io/enmasse/api/server/ApiServer.java b/api-server/src/main/java/io/enmasse/api/server/ApiServer.java
index c35816580..fd25e62dc 100644
--- a/api-server/src/main/java/io/enmasse/api/server/ApiServer.java
+++ b/api-server/src/main/java/io/enmasse/api/server/ApiServer.java
@@ -37,7 +37,6 @@ import java.io.UncheckedIOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
-import java.security.Security;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.time.Clock;
diff --git a/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java b/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java
index affe7ebed..ff814e773 100644
--- a/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java
+++ b/api-server/src/main/java/io/enmasse/api/server/HTTPServer.java
@@ -24,7 +24,6 @@ import io.vertx.core.http.HttpServer;
 import io.vertx.core.http.HttpServerOptions;
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.JksOptions;
-import io.vertx.core.net.PemKeyCertOptions;
 import io.vertx.core.net.PemTrustOptions;
 import org.jboss.resteasy.plugins.server.vertx.VertxRequestHandler;
 import org.jboss.resteasy.plugins.server.vertx.VertxResteasyDeployment;
diff --git a/pkg/controller/iotconfig/adapter.go b/pkg/controller/iotconfig/adapter.go
index e0fbcca1d..4e1086ab5 100644
--- a/pkg/controller/iotconfig/adapter.go
+++ b/pkg/controller/iotconfig/adapter.go
@@ -108,6 +108,11 @@ func (r *ReconcileIoTConfig) processQdrProxyConfig(ctx context.Context, config *
 
 	rc.ProcessSimple(func() error {
 		return r.processConfigMap(ctx, "qdr-proxy-configurator", config, func(config *iotv1alpha1.IoTConfig, configMap *corev1.ConfigMap) error {
+
+			if configMap.Data == nil {
+				configMap.Data = make(map[string]string)
+			}
+
 			configMap.Data["qdrouterd.conf"] = `
 router {
   mode: standalone


Test patch:
diff --git a/.travis/test-iot.sh b/.travis/test-iot.sh
new file mode 100755
index 000000000..db66463cb
--- /dev/null
+++ b/.travis/test-iot.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+CURDIR=`readlink -f \`dirname $0\``
+source ${CURDIR}/common.sh
+
+TAG=${TAG:-latest}
+
+DEPLOY_IOT=true time ./systemtests/scripts/run_test_kubernetes.sh templates/build/enmasse-${TAG} "smoke-iot"
diff --git a/api-model/src/test/java/io/enmasse/address/model/v1/address/SerializationTest.java b/api-model/src/test/java/io/enmasse/address/model/v1/address/SerializationTest.java
index d129266c4..0906ffd5f 100644
--- a/api-model/src/test/java/io/enmasse/address/model/v1/address/SerializationTest.java
+++ b/api-model/src/test/java/io/enmasse/address/model/v1/address/SerializationTest.java
@@ -11,9 +11,7 @@ import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
diff --git a/api-model/src/test/java/io/enmasse/iot/model/v1/IoTProjectTest.java b/api-model/src/test/java/io/enmasse/iot/model/v1/IoTProjectTest.java
new file mode 100644
index 000000000..9a3138b2b
--- /dev/null
+++ b/api-model/src/test/java/io/enmasse/iot/model/v1/IoTProjectTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2018, 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 static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.ByteBuffer;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class IoTProjectTest {
+
+    @Test
+    void testCreateExternal() {
+        IoTProject project = new IoTProjectBuilder()
+                .withNewMetadata()
+                .withName("proj")
+                .endMetadata()
+                .withNewSpec()
+                .withNewDownstreamStrategy()
+                .withNewExternalStrategy()
+                .withCertificate(ByteBuffer.wrap("a".getBytes()))
+                .withHost("host")
+                .withPassword("pwd")
+                .withUsername("username")
+                .withPort(1234)
+                .withTls(true)
+                .endExternalStrategy()
+                .endDownstreamStrategy()
+                .endSpec()
+                .build();
+
+        assertEquals("proj", project.getMetadata().getName());
+        assertArrayEquals("a".getBytes(), project.getSpec().getDownstreamStrategy().getExternalStrategy().getCertificate().array());
+        assertEquals("host", project.getSpec().getDownstreamStrategy().getExternalStrategy().getHost());
+        assertEquals("pwd", project.getSpec().getDownstreamStrategy().getExternalStrategy().getPassword());
+        assertEquals("username", project.getSpec().getDownstreamStrategy().getExternalStrategy().getUsername());
+        assertEquals(1234, project.getSpec().getDownstreamStrategy().getExternalStrategy().getPort());
+        assertEquals(true, project.getSpec().getDownstreamStrategy().getExternalStrategy().isTls());
+        assertNull(project.getSpec().getDownstreamStrategy().getManagedStrategy());
+    }
+
+    @Test
+    void testCreateManaged() {
+        IoTProject project = new IoTProjectBuilder()
+                .withNewMetadata()
+                .withName("proj")
+                .endMetadata()
+                .withNewSpec()
+                .withNewDownstreamStrategy()
+                .withNewManagedStrategy()
+                .withAddressSpaceName("managed")
+                .endManagedStrategy()
+                .endDownstreamStrategy()
+                .endSpec()
+                .build();
+
+        assertEquals("proj", project.getMetadata().getName());
+        assertEquals("managed", project.getSpec().getDownstreamStrategy().getManagedStrategy().getAddressSpaceName());
+        assertNull(project.getSpec().getDownstreamStrategy().getExternalStrategy());
+    }
+
+    @Test
+    public void testParse() throws IOException {
+        final ObjectMapper mapper = new ObjectMapper();
+
+        final URL url = IoTProjectTest.class.getResource("resources/external-project.json");
+        final IoTProject value = mapper.readValue(url, IoTProject.class);
+
+        assertThat(value, notNullValue());
+
+        assertThat(value.getMetadata(), notNullValue());
+        assertThat(value.getMetadata().getName(), is("iot"));
+
+        assertThat(value.getSpec(), notNullValue());
+
+        assertThat(value.getSpec().getDownstreamStrategy(), notNullValue());
+        assertThat(value.getSpec().getDownstreamStrategy().getExternalStrategy(), notNullValue());
+        assertThat(value.getSpec().getDownstreamStrategy().getExternalStrategy().getCertificate(), notNullValue());
+        assertThat(value.getSpec().getDownstreamStrategy().getExternalStrategy().getCertificate().remaining(), is(1220));
+    }
+
+}
diff --git a/api-model/src/test/resources/io/enmasse/iot/model/v1/resources/external-project.json b/api-model/src/test/resources/io/enmasse/iot/model/v1/resources/external-project.json
new file mode 100644
index 000000000..d5ed6f303
--- /dev/null
+++ b/api-model/src/test/resources/io/enmasse/iot/model/v1/resources/external-project.json
@@ -0,0 +1,14 @@
+{
+    "apiVersion": "iot.enmasse.io/v1alpah1",
+    "kind": "IoTProject",
+    "metadata": {
+        "name": "iot"
+    },
+    "spec": {
+        "downstreamStrategy": {
+            "externalStrategy": {
+                "certificate": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURWekNDQWorZ0F3SUJBZ0lKQU9Pb244R1pQUWVyTUEwR0NTcUdTSWIzRFFFQkN3VUFNRUl4Q3pBSkJnTlYKQkFZVEFsaFlNUlV3RXdZRFZRUUhEQXhFWldaaGRXeDBJRU5wZEhreEhEQWFCZ05WQkFvTUUwUmxabUYxYkhRZwpRMjl0Y0dGdWVTQk1kR1F3SGhjTk1Ua3dNekEwTVRVME56RTJXaGNOTkRrd05ERTFNVFUwTnpFMldqQkNNUXN3CkNRWURWUVFHRXdKWVdERVZNQk1HQTFVRUJ3d01SR1ZtWVhWc2RDQkRhWFI1TVJ3d0dnWURWUVFLREJORVpXWmgKZFd4MElFTnZiWEJoYm5rZ1RIUmtNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQQoxeDhSOWx4SkNCako0dG1wUi9hT1hzRE82bGtWNHZnL3RFUVFyajV2VWpuM0xqNW5kaXJVM3ZPOS9INGp2c09oClBoOUFYSy9WYWhzOFVGWEFuVnYyN29XSC95Q2xhMndwVCtlVjAwajZhcVluMmZaQXpzb0lMeDc4Rk9HTmlNQ0gKczMrSklvK3pDU3p6QnE2dUhNQ0w3c3hGbGowMStNY21iTWYvNjEzZHdmcGl1cjV0ZmtjbWthbkhyUlFLOUtkTApMZFNYQ2FLRGRPeldvK2FSRWRIMHFMMG9qekJ2Q2lNd0hzSWo2d25SeFo3WS9pa2M4WG5JYlNka1Backl1c1M3CmxVM0xFR2U5L3dYRmlzcVJLd01TdUUySVpDMXlISi9zWDlVUHRMSjM0U29wcTZNR2R6SEdsU0RheE5nNWxYWmQKOTY4MHNqb2F0SWJiU1FqMVJOSzJId0lEQVFBQm8xQXdUakFkQmdOVkhRNEVGZ1FVS1JkOHJUR0kwWVFHeW8vbwo0eDE1cUlPSE5sTXdId1lEVlIwakJCZ3dGb0FVS1JkOHJUR0kwWVFHeW8vbzR4MTVxSU9ITmxNd0RBWURWUjBUCkJBVXdBd0VCL3pBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQUp3STVSQnU3SnBQNHRlb25yMktiTXpBWk9USmEKU0ZYS29Hc0RJNlJRbC94aVZwY05CVVNYS1gyU2JsVjRoeVJtZS9KSzB6TFpabnZTWXRxQmk3NXhqbFJQTjdGQQpjRTJTdUo1eEFaUHFVUW9Yc1BhbFAvM2pYd0pUUGJJd0U1N2wzdzVrVGxPS3RYR2NCZGNTUHlteU00aldnTVlSCmZWaGQ0b09GcUNOek1SMWp1WFVqTFJBcUtONHF2NklqbGo2NGQ2SmFYRGxCVm4wTUZWZkRaTUZPdmt5SytoVjgKYkVHSUtNaUlZNjNjK3JxMzJUeGo5TG9EOEZNc2xhd0FiUHFjcXIrUVBRckluSDJFZFJGOXhuUE8rUElPdmJ6dgpYK01uSkU1QVlwWitLNHA5VDZoL2VGM2E1cXAwUXUycmJPYnRldlZ0Tk01NTkyV2FtUUhaTGdDdXBnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="
+            }
+        }
+    }
+}
diff --git a/api-server/src/test/java/io/enmasse/api/v1/http/HttpAddressServiceTest.java b/api-server/src/test/java/io/enmasse/api/v1/http/HttpAddressServiceTest.java
index 5a47f53ed..0fbb1b4b0 100644
--- a/api-server/src/test/java/io/enmasse/api/v1/http/HttpAddressServiceTest.java
+++ b/api-server/src/test/java/io/enmasse/api/v1/http/HttpAddressServiceTest.java
@@ -9,7 +9,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.fge.jsonpatch.JsonPatch;
 import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
 import io.enmasse.address.model.*;
-import io.enmasse.api.common.DefaultExceptionMapper;
 import io.enmasse.api.server.TestSchemaProvider;
 import io.enmasse.k8s.api.AddressApi;
 import io.enmasse.k8s.api.AddressSpaceApi;
@@ -40,7 +39,6 @@ public class HttpAddressServiceTest {
     private final AddressSpaceApi addressSpaceApi = new TestAddressSpaceApi();
     private final HttpAddressService addressService = new HttpAddressService(addressSpaceApi, new TestSchemaProvider(), Clock.systemUTC());
     private final SecurityContext securityContext = mock(SecurityContext.class);
-    private DefaultExceptionMapper exceptionMapper = new DefaultExceptionMapper();
     private AddressSpace addressSpace;
     private AddressApi addressApi;
     private Address address1;
diff --git a/api-server/src/test/java/io/enmasse/api/v1/http/HttpUserServiceTest.java b/api-server/src/test/java/io/enmasse/api/v1/http/HttpUserServiceTest.java
index addd8a06c..462af7ad6 100644
--- a/api-server/src/test/java/io/enmasse/api/v1/http/HttpUserServiceTest.java
+++ b/api-server/src/test/java/io/enmasse/api/v1/http/HttpUserServiceTest.java
@@ -22,10 +22,7 @@ import javax.ws.rs.core.SecurityContext;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.fge.jsonpatch.JsonPatch;
-import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
-import io.enmasse.address.model.AddressSpace;
 import org.jboss.resteasy.spi.ResteasyUriInfo;
-import org.junit.Ignore;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
diff --git a/systemtests/pom.xml b/systemtests/pom.xml
index 24b0d0b8e..c3acb8784 100644
--- a/systemtests/pom.xml
+++ b/systemtests/pom.xml
@@ -212,6 +212,22 @@
                 <skip.tags>marathon</skip.tags>
             </properties>
         </profile>
+        <profile>
+            <id>systemtests-shared-iot</id>
+            <properties>
+                <tags>shared-iot</tags>
+                <skip.tests>false</skip.tests>
+                <skip.tags>marathon</skip.tags>
+            </properties>
+        </profile>
+        <profile>
+            <id>systemtests-smoke-iot</id>
+            <properties>
+                <tags>shared-iot &amp; smoke</tags>
+                <skip.tests>false</skip.tests>
+                <skip.tags>marathon</skip.tags>
+            </properties>
+        </profile>
         <profile>
             <id>systemtests-marathon</id>
             <properties>
diff --git a/systemtests/scripts/run_test_kubernetes.sh b/systemtests/scripts/run_test_kubernetes.sh
index d4bc2fff6..efdb81ea4 100755
--- a/systemtests/scripts/run_test_kubernetes.sh
+++ b/systemtests/scripts/run_test_kubernetes.sh
@@ -8,6 +8,7 @@ TEST_PROFILE=$2
 TESTCASE=$3
 failure=0
 
+BASE_DIR="${CURDIR}/../../"
 API_URL=$(kubectl config view --minify | grep server | cut -f 2- -d ":" | tr -d " ")
 API_TOKEN=$(kubectl describe secret $(kubectl get serviceaccount default -o jsonpath='{.secrets[0].name}') | grep -E '^token' | cut -f2 -d':' | tr -d " ")
 
@@ -27,7 +28,6 @@ export KUBERNETES_NAMESPACE=${SANITIZED_NAMESPACE}
 kubectl create namespace ${KUBERNETES_NAMESPACE}
 kubectl config set-context $(kubectl config current-context) --namespace=${KUBERNETES_NAMESPACE}
 
-
 mkdir -p api-server-cert/
 openssl req -new -x509 -batch -nodes -days 11000 -subj "/O=io.enmasse/CN=api-server.${KUBERNETES_NAMESPACE}.svc.cluster.local" -out api-server-cert/tls.crt -keyout api-server-cert/tls.key
 kubectl create secret tls api-server-cert --cert=api-server-cert/tls.crt --key=api-server-cert/tls.key
@@ -47,6 +47,21 @@ kubectl create -f ${ENMASSE_DIR}/install/components/standard-authservice
 kubectl create -f ${ENMASSE_DIR}/install/components/example-plans
 kubectl create -f ${ENMASSE_DIR}/install/components/example-roles
 
+
+if [ "$DEPLOY_IOT" = "true" ]; then
+    echo "Deploying IoT components"
+
+    "${BASE_DIR}/iot/examples/k8s-tls/create"
+    NAMESPACE="${KUBERNETES_NAMESPACE}" PREFIX="systemtests-" "${BASE_DIR}/iot/examples/k8s-tls/deploy"
+
+    kubectl create -f ${ENMASSE_DIR}/install/components/iot/api
+    kubectl create -f ${ENMASSE_DIR}/install/components/enmasse-controller-manager
+    kubectl create -f ${ENMASSE_DIR}/install/components/iot/common
+    kubectl create -f ${ENMASSE_DIR}/install/components/iot/operator
+else
+    echo "Not deploying IoT components"
+fi
+
 #environment info
 LOG_DIR="${ARTIFACTS_DIR}/kubernetes-info/"
 mkdir -p ${LOG_DIR}
@@ -63,13 +78,20 @@ fi
 
 wait_until_enmasse_up 'kubernetes' ${KUBERNETES_NAMESPACE}
 
+echo "Running test profile: ${TEST_PROFILE}"
 #execute test
-if [[ "${TEST_PROFILE}" = "smoke" ]]; then
+case "${TEST_PROFILE}" in
+"smoke")
     run_test "**.SmokeTest" systemtests-shared-brokered || failure=$(($failure + 1))
     run_test "**.SmokeTest" systemtests-shared-standard || failure=$(($failure + 1))
-else
+    ;;
+"smoke-iot")
+    run_test systemtests-smoke-iot || failure=$(($failure + 1))
+    ;;
+*)
     run_test ${TESTCASE} systemtests || failure=$(($failure + 1))
-fi
+    ;;
+esac
 
 kubectl get events --all-namespaces
 
diff --git a/systemtests/scripts/test_func.sh b/systemtests/scripts/test_func.sh
index 1af1b2eb1..da8e7c3c5 100755
--- a/systemtests/scripts/test_func.sh
+++ b/systemtests/scripts/test_func.sh
@@ -61,8 +61,11 @@ function wait_until_enmasse_up() {
     UPGRADE=${3:-false}
 
     expected_pods=6
+    if [ "$DEPLOY_IOT" = "true" ]; then
+        expected_pods=$(($expected_pods + 2))
+    fi
     if [[ "$CLUSTER_TYPE" == "kubernetes" ]]; then
-        expected_pods=5
+        expected_pods=$(($expected_pods - 1))
     fi
 
     wait_until_up ${expected_pods} ${NAMESPACE} ${UPGRADE}
diff --git a/systemtests/scripts/wait_until_up.sh b/systemtests/scripts/wait_until_up.sh
index abaa683fa..914e8d17b 100755
--- a/systemtests/scripts/wait_until_up.sh
+++ b/systemtests/scripts/wait_until_up.sh
@@ -62,7 +62,7 @@ do
     NOW=$(date +%s)
     if [[ ${NOW} -gt ${END} ]]; then
         err "Timed out waiting for nodes to come up!"
-        pods=`${CMD} get pods -n ${NAMESPACE}`
+        pods=$(${CMD} describe pods -n ${NAMESPACE} ; ${CMD} get pods -n ${NAMESPACE})
         err_and_exit "PODS: ${pods}"
     fi
     num_running=`${CMD} get pods -n ${NAMESPACE}| grep -v deploy | grep -c Running`
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/TestTag.java b/systemtests/src/main/java/io/enmasse/systemtest/TestTag.java
index d6e9c44f0..ca4d05130 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/TestTag.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/TestTag.java
@@ -8,6 +8,7 @@ public class TestTag {
     public static final String isolated = "isolated";
     public static final String sharedBrokered = "shared-brokered";
     public static final String sharedStandard = "shared-standard";
+    public static final String sharedIot = "shared-iot";
     public static final String marathon = "marathon";
     public static final String nonPR = "nonPR";
     public static final String upgrade = "upgrade";
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java b/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java
index 1938db73b..443d4e04e 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/TestUtils.java
@@ -5,6 +5,8 @@
 
 package io.enmasse.systemtest;
 
+import static java.lang.Boolean.TRUE;
+
 import java.net.HttpURLConnection;
 import java.net.Inet4Address;
 import java.net.InetAddress;
@@ -163,15 +165,30 @@ public class TestUtils {
     private static int numReady(List<Pod> pods) {
         int numReady = 0;
         for (Pod pod : pods) {
-            if ("Running".equals(pod.getStatus().getPhase())) {
+            if (podReady(pod)) {
                 numReady++;
             } else {
-                log.info("POD " + pod.getMetadata().getName() + " in status : " + pod.getStatus().getPhase());
+                final int c = pod.getStatus().getContainerStatuses().size();
+                final long r = pod.getStatus().getContainerStatuses().stream().map(ContainerStatus::getReady).filter(TRUE::equals).count();
+                log.info("POD {} in status : {} {}/{}", pod.getMetadata().getName(), pod.getStatus().getPhase(), r, c);
             }
         }
         return numReady;
     }
 
+    private static boolean podReady(final Pod pod) {
+        if (!pod.getStatus().getPhase().equals("Running")) {
+            return false;
+        }
+
+        for ( final ContainerStatus c : pod.getStatus().getContainerStatuses() ) {
+            if (!Boolean.TRUE.equals(c.getReady())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Wait for expected count of pods within AddressSpace
      *
@@ -793,8 +810,6 @@ public class TestUtils {
      */
     private static User convertJsonToUser(JsonObject userJson) {
         log.info("Got User object: {}", userJson.toString());
-        String name = userJson.getJsonObject("metadata").getString("name");
-        String namespace = userJson.getJsonObject("metadata").getString("namespace");
         JsonObject spec = userJson.getJsonObject("spec");
 
         String username = spec.getString("username");
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java b/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java
index 7a54abc71..2495e2d4d 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java
@@ -12,6 +12,7 @@ import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.*;
 
@@ -245,4 +246,15 @@ public class KubeCMDClient extends CmdClient {
         List<String> command = Arrays.asList(CMD, "get", "events", "-n", namespace);
         return execute(command, ONE_MINUTE_TIMEOUT, false);
     }
+
+    public static ExecutionResultData deleteIoTConfig(String namespace, String name) {
+        List<String> ressourcesCmd = getRessourcesCmd("delete", "iotconfig", namespace, name, Optional.empty());
+        return execute(ressourcesCmd, DEFAULT_SYNC_TIMEOUT, true);
+    }
+
+    public static ExecutionResultData createFromFile(String namespace, Path path) {
+        Objects.requireNonNull(namespace);
+        Objects.requireNonNull(path);
+        return execute(Arrays.asList("kubectl", "-n", namespace, "create", "--validate=false", "-f", path.toString()), DEFAULT_SYNC_TIMEOUT, true);
+    }
 }
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/iot/SimpleK8sDeployTest.java b/systemtests/src/test/java/io/enmasse/systemtest/iot/SimpleK8sDeployTest.java
new file mode 100644
index 000000000..260b46eb8
--- /dev/null
+++ b/systemtests/src/test/java/io/enmasse/systemtest/iot/SimpleK8sDeployTest.java
@@ -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 io.enmasse.systemtest.iot;
+
+import static io.enmasse.systemtest.TestTag.sharedIot;
+import static io.enmasse.systemtest.TestTag.smoke;
+
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import io.enmasse.systemtest.Environment;
+import io.enmasse.systemtest.Kubernetes;
+import io.enmasse.systemtest.TestUtils;
+import io.enmasse.systemtest.TimeoutBudget;
+import io.enmasse.systemtest.cmdclients.KubeCMDClient;
+
+@Tag(sharedIot)
+@Tag(smoke)
+public class SimpleK8sDeployTest {
+
+    private static final String NAMESPACE = Environment.getInstance().namespace();
+
+    private static final String[] EXPECTED_DEPLOYMENTS = new String[] {
+                    "iot-auth-service",
+                    "iot-device-registry",
+                    "iot-gc",
+                    "iot-http-adapter",
+                    "iot-mqtt-adapter",
+                    "iot-tenant-service",
+    };
+
+    private static final Map<String,String> IOT_LABELS;
+
+    static {
+        IOT_LABELS= new HashMap<> ();
+        IOT_LABELS.put("component", "iot");
+    }
+
+    private Kubernetes client = Kubernetes.getInstance();
+
+    @BeforeAll
+    protected static void setup () throws Exception {
+        KubeCMDClient.createFromFile(NAMESPACE, Paths.get("templates/iot/k8s-systemtests-config.json"));
+    }
+
+    @AfterAll
+    protected static void cleanup() throws Exception {
+        KubeCMDClient.deleteIoTConfig(NAMESPACE, "default");
+    }
+
+    @Test
+    public void testDeploy() throws Exception {
+
+        final TimeoutBudget budget = TimeoutBudget.ofDuration(Duration.ofMinutes(5));
+
+        TestUtils.waitUntilCondition("IoT Config to deploy", () -> allDeploymentsPresent(), budget);
+        TestUtils.waitForNReplicas(this.client , EXPECTED_DEPLOYMENTS.length, IOT_LABELS, budget);
+    }
+
+    private boolean allDeploymentsPresent () {
+        final String[] deployments = this.client.listDeployments(IOT_LABELS).stream()
+                .map(deployment -> deployment.getMetadata().getName())
+                .toArray(String[]::new);
+        Arrays.sort(deployments);
+        return Arrays.equals(deployments, EXPECTED_DEPLOYMENTS);
+    }
+
+
+}
diff --git a/systemtests/templates/iot/k8s-systemtests-config.json b/systemtests/templates/iot/k8s-systemtests-config.json
new file mode 100644
index 000000000..10bcbee87
--- /dev/null
+++ b/systemtests/templates/iot/k8s-systemtests-config.json
@@ -0,0 +1,21 @@
+{
+  "apiVersion": "iot.enmasse.io/v1alpha1",
+  "kind": "IoTConfig",
+  "metadata": {
+    "name": "default"
+  },
+  "spec": {
+    "interServiceCertificates": {
+      "secretCertificatesStrategy": {
+        "caSecretName": "systemtests-iot-service-ca",
+        "serviceSecretNames": {
+          "iot-auth-service-tls": "systemtests-iot-auth-service-tls",
+          "iot-tenant-service-tls": "systemtests-iot-tenant-service-tls",
+          "iot-device-registry-tls": "systemtests-iot-device-registry-tls",
+          "iot-http-adapter-tls": "systemtests-iot-http-adapter-tls",
+          "iot-mqtt-adapter-tls": "systemtests-iot-mqtt-adapter-tls"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file