Repository: EnMasseProject/enmasse
Pull request #2447 (2019-03-14 12:15:19)

IoTProject in api-model
This PR adds more classes and builder annotations to IoTProject. The classes are inspired from go code of iot-operator. 

famartinrh: I think we can implement pojo classes for IoTConfig in a future PR, do you agree @ctron ??

Patch:
diff --git a/.travis.yml b/.travis.yml
index 4fa91d379..fd9c1684c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -27,11 +27,7 @@ jobs:
   include:
     - stage: build
       before_install:
-        - sudo pip install requests[security] ansible
-        - gem install asciidoctor
-        - sudo pip install requests[security] ansible
-        - sudo add-apt-repository -y ppa:masterminds/glide && sudo apt-get update
-        - sudo apt-get install -y glide
+        - pip install --user requests[security] ansible
         - gem install asciidoctor
         - gimme 1.10.5
         - source ~/.gimme/envs/go1.10.5.env
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/iot/examples/k8s-tls/README.md b/iot/examples/k8s-tls/README.md
new file mode 100644
index 000000000..a2b835ae1
--- /dev/null
+++ b/iot/examples/k8s-tls/README.md
@@ -0,0 +1,82 @@
+# Inter service certificates example
+
+This example helps manually creating inter service certificates for the
+EnMasse IoT components.
+
+**Note:** When running on OpenShift the service CA is being used for this
+          by default. However you can still override this in the way described
+          here.
+
+**Note:** The content in this directory serves as example only! If you want to run
+          this in production, you need to create your own key & certificate setup,
+          based on your own requirements.
+
+## Creating keys & certificates
+
+In order to create a new a new service CA, including keys and certificates for the
+different services execute the following command from inside this directory:
+
+    ./create
+
+This will create a new set of key material in the directory `./build`.
+
+## Deploy keys & certificates
+
+Once you created the new key material, you can deploy it to the Kubernetes cluster.
+For this to work you need to:
+
+  * Have `kubectl` in your `$PATH`
+  * Be logged in to the cluster you want to deploy to
+  * Have the permissions to create new *secrets*
+
+Run the following command to deploy:
+
+    ./deploy
+
+By default this will create new *secrets* in the `enmasse-infra` namespace. You
+can use the following environment variables to change the deployment process:
+
+  * `NAMESPACE` –  Change the namespace to deploy the secrets to. Defaults to `enmasse-infra`.
+  * `PREFIX` – Add a prefix to all secrets. If you want to have a delimiter between the prefix
+                and the actual name (like a dash), then you need to provide that as well.
+                Defaults to an empty prefix. 
+
+## Undeploy keys & certificates
+
+You can undeploy the secrets using the following command:
+
+    ./undeploy
+
+For this to work you need to:
+
+  * Have `kubectl` in your `$PATH`
+  * Be logged in to the cluster you want to undeploy from
+  * Have the permissions to delete *secrets*
+
+You can use the same environment variables as described above.
+
+## Configuring IoT infrastructure
+
+You will need to configure the IoT infrastructure to let it know which
+TLS key and certificates to use for each service.
+
+The following example assumed that you deployed the keys & certificates to
+the namespace `enmasse-infra` with the prefix `foo-`:
+
+~~~yaml
+apiVersion: iot.enmasse.io/v1alpha1
+kind: IoTConfig
+metadata:
+  name: default
+  namespace: enmasse-infra
+spec:
+  interServiceCertificates:
+    secretCertificatesStrategy:
+      caSecretName: foo-iot-service-ca
+      serviceSecretNames:
+        iot-auth-service-tls: foo-iot-auth-service-tls
+        iot-device-registry-tls: foo-iot-device-registry-tls
+        iot-http-adapter-tls: foo-iot-http-adapter-tls
+        iot-mqtt-adapter-tls: foo-iot-mqtt-adapter-tls
+        iot-tenant-service-tls: foo-iot-tenant-service-tls
+~~~
diff --git a/iot/examples/k8s-tls/ca.cnf b/iot/examples/k8s-tls/ca.cnf
new file mode 100644
index 000000000..80307120d
--- /dev/null
+++ b/iot/examples/k8s-tls/ca.cnf
@@ -0,0 +1,30 @@
+[req]
+distinguished_name = subject
+
+[ subject ]
+countryName_default = US
+stateOrProvinceName_default = 
+localityName_default = 
+organizationName_default = EnMasse
+commonName_default = enmasse-iot
+
+
+[x509_ext]
+
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer:always
+basicConstraints = critical,CA:true
+keyUsage = keyCertSign,cRLSign
+
+[intermediate_ext]
+
+subjectKeyIdentifier = hash
+basicConstraints = critical,CA:true
+keyUsage = keyCertSign,cRLSign
+
+[san_ext]
+
+subjectAltName = ${ENV::SAN}
+subjectKeyIdentifier = hash
+keyUsage = keyAgreement,keyEncipherment,digitalSignature
+extendedKeyUsage = serverAuth, clientAuth
\ No newline at end of file
diff --git a/iot/examples/k8s-tls/common b/iot/examples/k8s-tls/common
new file mode 100644
index 000000000..920aa298a
--- /dev/null
+++ b/iot/examples/k8s-tls/common
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+die() { echo "$*" 1>&2 ; exit 1; }
+
+IOT_SERVICES="iot-auth-service iot-tenant-service iot-device-registry iot-http-adapter iot-mqtt-adapter"
diff --git a/iot/examples/k8s-tls/create b/iot/examples/k8s-tls/create
new file mode 100755
index 000000000..babfc7649
--- /dev/null
+++ b/iot/examples/k8s-tls/create
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+set -e
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+source "$SCRIPTDIR/common"
+
+OUTDIR="$SCRIPTDIR/build"
+
+: ${NAMESPACE:=enmasse-infra}
+
+rm -Rf "$OUTDIR"
+mkdir -p "$OUTDIR"
+
+export SAN=""
+
+# Create root
+
+openssl req -x509 -config "$SCRIPTDIR/ca.cnf" -nodes -newkey rsa:4096 -keyout "$OUTDIR/root-key.pem" -out "$OUTDIR/root-cert.pem" -days 365 -subj "/O=EnMasse/OU=IoT/CN=root"
+
+# Create CA
+
+openssl req -config "$SCRIPTDIR/ca.cnf" -reqexts intermediate_ext -nodes -newkey rsa:4096 -keyout "$OUTDIR/ca-key.pem" -days 365 -subj "/O=EnMasse/OU=IoT/CN=ca" | \
+openssl x509 -req -extfile "$SCRIPTDIR/ca.cnf" -extensions intermediate_ext -out "$OUTDIR/ca-cert.pem" -days 365 -CA "$OUTDIR/root-cert.pem" -CAkey "$OUTDIR/root-key.pem" -CAcreateserial
+
+# Create trust base
+
+cat "$OUTDIR/ca-cert.pem" "$OUTDIR/root-cert.pem" > "$OUTDIR/trusted-certs.pem"
+
+# Export information
+
+CA_SUBJECT=$(openssl x509 -in "$OUTDIR/ca-cert.pem" -noout -subject -nameopt RFC2253 | sed s/^subject=//)
+PK=$(openssl x509 -in "$OUTDIR/ca-cert.pem" -noout -pubkey | sed /^---/d | sed -z 's/\n//g')
+
+echo $CA_SUBJECT
+echo $PK
+
+function createServiceKeyAndCert() {
+  openssl req -config "$SCRIPTDIR/ca.cnf" -nodes -newkey rsa:4096 -keyout "$OUTDIR/$1-key.pem" -days 365 -subj "/O=EnMasse/OU=IoT/CN=$1" | \
+  SAN="DNS.1:${1}.svc,DNS.2:${1}.${NAMESPACE}.svc,DNS.3:localhost" openssl x509 -req -extfile "$SCRIPTDIR/ca.cnf" -extensions "san_ext" -out "$OUTDIR/$1.pem" -days 365 -CA "$OUTDIR/ca-cert.pem" -CAkey "$OUTDIR/ca-key.pem" -CAcreateserial
+
+  cat "$OUTDIR/$1.pem" "$OUTDIR/ca-cert.pem" > "$OUTDIR/$1-fullchain.pem"
+
+  #openssl x509 -noout -text -in "$OUTDIR/$1.pem"
+}
+
+for i in ${IOT_SERVICES}; do
+    createServiceKeyAndCert "$i"
+done
diff --git a/iot/examples/k8s-tls/deploy b/iot/examples/k8s-tls/deploy
new file mode 100755
index 000000000..caef4983c
--- /dev/null
+++ b/iot/examples/k8s-tls/deploy
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+set -e
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+source "$SCRIPTDIR/common"
+
+OUTDIR="$SCRIPTDIR/build"
+
+: ${NAMESPACE:=enmasse-infra}
+: ${PREFIX:=}
+
+$SCRIPTDIR/undeploy
+
+for i in ${IOT_SERVICES}; do
+  kubectl -n "$NAMESPACE" create secret tls "${PREFIX}${i}-tls" --key="$OUTDIR/${i}-key.pem" --cert="$OUTDIR/${i}-fullchain.pem"
+done
+
+kubectl -n "$NAMESPACE" create secret generic "${PREFIX}iot-service-ca" --from-file=service-ca.crt="$OUTDIR/trusted-certs.pem"
diff --git a/iot/examples/k8s-tls/undeploy b/iot/examples/k8s-tls/undeploy
new file mode 100755
index 000000000..4a868704c
--- /dev/null
+++ b/iot/examples/k8s-tls/undeploy
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+set -e
+
+SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
+source "$SCRIPTDIR/common"
+
+OUTDIR="$SCRIPTDIR/build"
+
+: ${NAMESPACE:=enmasse-infra}
+: ${PREFIX:=}
+
+function deleteSecret () {
+  kubectl -n "$NAMESPACE" delete secret "$1" --ignore-not-found=true
+}
+
+for i in ${IOT_SERVICES}; do
+  deleteSecret "${PREFIX}${i}-tls"
+done
+
+deleteSecret "${PREFIX}iot-service-ca"
diff --git a/iot/iot-auth-service/Dockerfile b/iot/iot-auth-service/Dockerfile
index be017ac77..aab70a287 100644
--- a/iot/iot-auth-service/Dockerfile
+++ b/iot/iot-auth-service/Dockerfile
@@ -1,4 +1,4 @@
-FROM enmasseproject/java-base:8-11
+FROM enmasseproject/java-base:11-0
 
 ARG version
 ARG maven_version
diff --git a/iot/iot-device-registry/Dockerfile b/iot/iot-device-registry/Dockerfile
index c1eb9830d..572be0822 100644
--- a/iot/iot-device-registry/Dockerfile
+++ b/iot/iot-device-registry/Dockerfile
@@ -1,4 +1,4 @@
-FROM enmasseproject/java-base:8-11
+FROM enmasseproject/java-base:11-0
 
 ARG version
 ARG maven_version
diff --git a/iot/iot-http-adapter/Dockerfile b/iot/iot-http-adapter/Dockerfile
index cc28da42d..0d92cff53 100644
--- a/iot/iot-http-adapter/Dockerfile
+++ b/iot/iot-http-adapter/Dockerfile
@@ -1,4 +1,4 @@
-FROM enmasseproject/java-base:8-11
+FROM enmasseproject/java-base:11-0
 
 ARG version
 ARG maven_version
diff --git a/iot/iot-mqtt-adapter/Dockerfile b/iot/iot-mqtt-adapter/Dockerfile
index c5dc4b313..79213cf33 100644
--- a/iot/iot-mqtt-adapter/Dockerfile
+++ b/iot/iot-mqtt-adapter/Dockerfile
@@ -1,4 +1,4 @@
-FROM enmasseproject/java-base:8-11
+FROM enmasseproject/java-base:11-0
 
 ARG version
 ARG maven_version
diff --git a/iot/iot-tenant-service/Dockerfile b/iot/iot-tenant-service/Dockerfile
index 55827e4ef..4440c0cf7 100644
--- a/iot/iot-tenant-service/Dockerfile
+++ b/iot/iot-tenant-service/Dockerfile
@@ -1,4 +1,4 @@
-FROM enmasseproject/java-base:8-11
+FROM enmasseproject/java-base:11-0
 
 ARG version
 ARG maven_version
diff --git a/pkg/apis/iot/v1alpha1/types.go b/pkg/apis/iot/v1alpha1/types.go
index 2b026ff07..9b5ff33f9 100644
--- a/pkg/apis/iot/v1alpha1/types.go
+++ b/pkg/apis/iot/v1alpha1/types.go
@@ -6,6 +6,8 @@
 package v1alpha1
 
 import (
+	"encoding/json"
+
 	"github.com/enmasseproject/enmasse/pkg/apis/enmasse/v1beta1"
 	"github.com/enmasseproject/enmasse/pkg/util"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -30,6 +32,8 @@ func (project *IoTProject) TenantName() string {
 
 type IoTProjectSpec struct {
 	DownstreamStrategy DownstreamStrategy `json:"downstreamStrategy"`
+
+	Configuration json.RawMessage `json:"configuration,omitempty"`
 }
 
 type IoTProjectStatus struct {
@@ -113,7 +117,9 @@ type ServiceCAStrategy struct {
 }
 
 type SecretCertificatesStrategy struct {
-	SecretName string `json:"secretName"`
+	CASecretName string `json:"caSecretName"`
+
+	ServiceSecretNames map[string]string `json:"serviceSecretNames"`
 }
 
 type AdapterSpec struct {
diff --git a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
index 7251dcac1..7c2b7bc0b 100644
--- a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
@@ -105,7 +105,7 @@ func (in *InterServiceCertificates) DeepCopyInto(out *InterServiceCertificates)
 	if in.SecretCertificatesStrategy != nil {
 		in, out := &in.SecretCertificatesStrategy, &out.SecretCertificatesStrategy
 		*out = new(SecretCertificatesStrategy)
-		**out = **in
+		(*in).DeepCopyInto(*out)
 	}
 	if in.ServiceCAStrategy != nil {
 		in, out := &in.ServiceCAStrategy, &out.ServiceCAStrategy
@@ -385,6 +385,13 @@ func (in *ProvidedDownstreamStrategy) DeepCopy() *ProvidedDownstreamStrategy {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SecretCertificatesStrategy) DeepCopyInto(out *SecretCertificatesStrategy) {
 	*out = *in
+	if in.ServiceSecretNames != nil {
+		in, out := &in.ServiceSecretNames, &out.ServiceSecretNames
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	return
 }
 
diff --git a/pkg/controller/iotconfig/auth_service.go b/pkg/controller/iotconfig/auth_service.go
index 69a131fcc..096e747ae 100644
--- a/pkg/controller/iotconfig/auth_service.go
+++ b/pkg/controller/iotconfig/auth_service.go
@@ -80,17 +80,8 @@ func (r *ReconcileIoTConfig) reconcileAuthServiceDeployment(config *iotv1alpha1.
 
 		// volume mounts
 
-		if len(container.VolumeMounts) != 2 {
-			container.VolumeMounts = make([]corev1.VolumeMount, 2)
-		}
-
-		container.VolumeMounts[0].Name = "config"
-		container.VolumeMounts[0].MountPath = "/etc/config"
-		container.VolumeMounts[0].ReadOnly = true
-
-		container.VolumeMounts[1].Name = "tls"
-		container.VolumeMounts[1].MountPath = "/etc/tls"
-		container.VolumeMounts[1].ReadOnly = true
+		install.AppendVolumeMountSimple(container, "config", "/etc/config", true)
+		install.AppendVolumeMountSimple(container, "tls", "/etc/tls", true)
 
 		// return
 
diff --git a/pkg/controller/iotconfig/device_registry.go b/pkg/controller/iotconfig/device_registry.go
index 4476eaafd..793eefd92 100644
--- a/pkg/controller/iotconfig/device_registry.go
+++ b/pkg/controller/iotconfig/device_registry.go
@@ -106,21 +106,9 @@ func (r *ReconcileIoTConfig) reconcileDeviceRegistryDeployment(config *iotv1alph
 
 		// volume mounts
 
-		if len(container.VolumeMounts) != 3 {
-			container.VolumeMounts = make([]corev1.VolumeMount, 3)
-		}
-
-		container.VolumeMounts[0].Name = "config"
-		container.VolumeMounts[0].MountPath = "/etc/config"
-		container.VolumeMounts[0].ReadOnly = true
-
-		container.VolumeMounts[1].Name = "tls"
-		container.VolumeMounts[1].MountPath = "/etc/tls"
-		container.VolumeMounts[1].ReadOnly = true
-
-		container.VolumeMounts[2].Name = "registry"
-		container.VolumeMounts[2].MountPath = "/var/lib/hono/device-registry"
-		container.VolumeMounts[2].ReadOnly = false
+		install.AppendVolumeMountSimple(container, "config", "/etc/config", true)
+		install.AppendVolumeMountSimple(container, "tls", "/etc/tls", true)
+		install.AppendVolumeMountSimple(container, "registry", "/var/lib/hono/device-registry", false)
 
 		// return
 
diff --git a/pkg/controller/iotconfig/http_adapter.go b/pkg/controller/iotconfig/http_adapter.go
index 4e2806c73..77fc3266f 100644
--- a/pkg/controller/iotconfig/http_adapter.go
+++ b/pkg/controller/iotconfig/http_adapter.go
@@ -103,17 +103,8 @@ func (r *ReconcileIoTConfig) reconcileHttpAdapterDeployment(config *iotv1alpha1.
 
 		// volume mounts
 
-		if len(container.VolumeMounts) != 2 {
-			container.VolumeMounts = make([]corev1.VolumeMount, 2)
-		}
-
-		container.VolumeMounts[0].Name = "config"
-		container.VolumeMounts[0].MountPath = "/etc/config"
-		container.VolumeMounts[0].ReadOnly = true
-
-		container.VolumeMounts[1].Name = "tls"
-		container.VolumeMounts[1].MountPath = "/etc/tls"
-		container.VolumeMounts[1].ReadOnly = true
+		install.AppendVolumeMountSimple(container, "config", "/etc/config", true)
+		install.AppendVolumeMountSimple(container, "tls", "/etc/tls", true)
 
 		// return
 
diff --git a/pkg/controller/iotconfig/inter_service.go b/pkg/controller/iotconfig/inter_service.go
index f6f65575b..088bc5df9 100644
--- a/pkg/controller/iotconfig/inter_service.go
+++ b/pkg/controller/iotconfig/inter_service.go
@@ -34,7 +34,7 @@ func ApplyInterServiceForDeployment(config *iotv1alpha1.IoTConfig, deployment *a
 	if cfg.ServiceCAStrategy != nil {
 		return applyInterServiceForDeploymentServiceCa(deployment, secretName)
 	} else if cfg.SecretCertificatesStrategy != nil {
-		return applyInterServiceForDeploymentSecretCertificates(deployment, cfg.SecretCertificatesStrategy)
+		return applyInterServiceForDeploymentSecretCertificates(deployment, cfg.SecretCertificatesStrategy, secretName)
 	}
 
 	return fmt.Errorf("unknown inter service certificates configuration")
@@ -100,6 +100,7 @@ func applyInterServiceForServiceServiceCa(service *corev1.Service, secretName st
 
 func applyInterServiceForDeploymentServiceCa(deployment *appsv1.Deployment, secretName string) error {
 	install.ApplySecretVolume(deployment, "tls", secretName)
+	install.DropVolume(deployment, "tls-service-ca")
 	return nil
 }
 
@@ -109,6 +110,8 @@ func appendTrustStoresForServiceCa(container *corev1.Container, env []string) er
 		container.Env = append(container.Env, corev1.EnvVar{Name: e, Value: "/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt"})
 	}
 
+	install.DropVolumeMount(container, "tls-service-ca")
+
 	return nil
 }
 
@@ -123,19 +126,29 @@ func applyInterServiceForServiceSecretCertificates(service *corev1.Service, _ *i
 	return nil
 }
 
-func applyInterServiceForDeploymentSecretCertificates(deployment *appsv1.Deployment, cfg *iotv1alpha1.SecretCertificatesStrategy) error {
-	if cfg.SecretName == "" {
-		return fmt.Errorf("inter service secret name must not be empty")
+func applyInterServiceForDeploymentSecretCertificates(deployment *appsv1.Deployment, cfg *iotv1alpha1.SecretCertificatesStrategy, secretName string) error {
+	if cfg.CASecretName == "" {
+		return fmt.Errorf("inter service secret CA name must not be empty")
+	}
+	install.ApplySecretVolume(deployment, "tls-service-ca", cfg.CASecretName)
+
+	mappedSecretName := cfg.ServiceSecretNames[secretName]
+	if mappedSecretName == "" {
+		return fmt.Errorf("secret name %s mapped to an empty secret name", secretName)
 	}
-	install.ApplySecretVolume(deployment, "tls", cfg.SecretName)
+
+	install.ApplySecretVolume(deployment, "tls", mappedSecretName)
+
 	return nil
 }
 
 func appendTrustStoresForSecretCertificates(container *corev1.Container, env []string) error {
 
 	for _, e := range env {
-		container.Env = append(container.Env, corev1.EnvVar{Name: e, Value: "/etc/tls/tls.crt"})
+		container.Env = append(container.Env, corev1.EnvVar{Name: e, Value: "/etc/tls-service-ca/service-ca.crt"})
 	}
 
+	install.AppendVolumeMountSimple(container, "tls-service-ca", "/etc/tls-service-ca", true)
+
 	return nil
 }
diff --git a/pkg/controller/iotconfig/mqtt_adapter.go b/pkg/controller/iotconfig/mqtt_adapter.go
index 328e0b85a..25a722431 100644
--- a/pkg/controller/iotconfig/mqtt_adapter.go
+++ b/pkg/controller/iotconfig/mqtt_adapter.go
@@ -103,17 +103,8 @@ func (r *ReconcileIoTConfig) reconcileMqttAdapterDeployment(config *iotv1alpha1.
 
 		// volume mounts
 
-		if len(container.VolumeMounts) != 2 {
-			container.VolumeMounts = make([]corev1.VolumeMount, 2)
-		}
-
-		container.VolumeMounts[0].Name = "config"
-		container.VolumeMounts[0].MountPath = "/etc/config"
-		container.VolumeMounts[0].ReadOnly = true
-
-		container.VolumeMounts[1].Name = "tls"
-		container.VolumeMounts[1].MountPath = "/etc/tls"
-		container.VolumeMounts[1].ReadOnly = true
+		install.AppendVolumeMountSimple(container, "config", "/etc/config", true)
+		install.AppendVolumeMountSimple(container, "tls", "/etc/tls", true)
 
 		// return
 
diff --git a/pkg/controller/iotconfig/tenant_service.go b/pkg/controller/iotconfig/tenant_service.go
index 22aa4799a..e0ec5cf5d 100644
--- a/pkg/controller/iotconfig/tenant_service.go
+++ b/pkg/controller/iotconfig/tenant_service.go
@@ -77,23 +77,14 @@ func (r *ReconcileIoTConfig) reconcileTenantServiceDeployment(config *iotv1alpha
 			{Name: "ENMASSE_IOT_TENANT_ENDPOINT_AMQP_NATIVE_TLS_REQUIRED", Value: "false"},
 		}
 
-		if err := AppendTrustStores(config, container, []string{"ENMASSE_IOT_TENANT_ENDPOINT_AMQP_TRUST_STORE_PATH"}); err != nil {
+		if err := AppendTrustStores(config, container, []string{"ENMASSE_IOT_AUTH_TRUST_STORE_PATH"}); err != nil {
 			return err
 		}
 
 		// volume mounts
 
-		if len(container.VolumeMounts) != 2 {
-			container.VolumeMounts = make([]corev1.VolumeMount, 2)
-		}
-
-		container.VolumeMounts[0].Name = "config"
-		container.VolumeMounts[0].MountPath = "/etc/config"
-		container.VolumeMounts[0].ReadOnly = true
-
-		container.VolumeMounts[1].Name = "tls"
-		container.VolumeMounts[1].MountPath = "/etc/tls"
-		container.VolumeMounts[1].ReadOnly = true
+		install.AppendVolumeMountSimple(container, "config", "/etc/config", true)
+		install.AppendVolumeMountSimple(container, "tls", "/etc/tls", true)
 
 		// return
 
diff --git a/pkg/util/install/utils.go b/pkg/util/install/utils.go
index 56180bf01..ef0dc6535 100644
--- a/pkg/util/install/utils.go
+++ b/pkg/util/install/utils.go
@@ -153,6 +153,24 @@ func ApplyVolumeWithError(deployment *appsv1.Deployment, name string, mutator fu
 	return err
 }
 
+func DropVolume(deployment *appsv1.Deployment, name string) {
+	if deployment.Spec.Template.Spec.Volumes == nil {
+		return
+	}
+
+	// removing an entry from an array in go is tricky ...
+
+	for i := len(deployment.Spec.Template.Spec.Volumes) - 1; i >= 0; i-- {
+		v := deployment.Spec.Template.Spec.Volumes[i]
+		if v.Name == name {
+			deployment.Spec.Template.Spec.Volumes = append(
+				deployment.Spec.Template.Spec.Volumes[:i],
+				deployment.Spec.Template.Spec.Volumes[i+1:]...,
+			)
+		}
+	}
+}
+
 func SetContainerImage(container *corev1.Container, imageName string, overrides v1beta1.ImageOverridesProvider) error {
 
 	resolved, err := images.GetImage(imageName)
@@ -241,3 +259,20 @@ func AppendVolumeMountSimple(container *corev1.Container, name string, path stri
 		mount.ReadOnly = readOnly
 	})
 }
+
+func DropVolumeMount(container *corev1.Container, name string) {
+	if container.VolumeMounts == nil {
+		return
+	}
+
+	// removing an entry from an array in go is tricky ...
+
+	for i := len(container.VolumeMounts) - 1; i >= 0; i-- {
+		v := container.VolumeMounts[i]
+		if v.Name == name {
+			container.VolumeMounts = append(
+				container.VolumeMounts[:i], container.VolumeMounts[i+1:]...,
+			)
+		}
+	}
+}
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
index 8b24f4655..239a1deb7 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
@@ -95,7 +95,8 @@ public class AddressController implements Watcher<Address> {
                 .collect(Collectors.toMap(a -> a.getSpec().getAddress(),
                                           a -> new ProvisionState(a.getStatus(),
                                                                   a.getAnnotation(AnnotationKeys.BROKER_ID),
-                                                                  a.getAnnotation(AnnotationKeys.CLUSTER_ID))));
+                                                                  a.getAnnotation(AnnotationKeys.CLUSTER_ID),
+                                                                  a.getAnnotation(AnnotationKeys.APPLIED_PLAN))));
 
         AddressSpacePlan addressSpacePlan = addressSpaceType.findAddressSpacePlan(options.getAddressSpacePlanName()).orElseThrow(() -> new RuntimeException("Unable to handle updates: address space plan " + options.getAddressSpacePlanName() + " not found!"));
 
@@ -161,7 +162,8 @@ public class AddressController implements Watcher<Address> {
             ProvisionState previous = previousStatus.get(address.getSpec().getAddress());
             ProvisionState current = new ProvisionState(address.getStatus(),
                     address.getAnnotation(AnnotationKeys.BROKER_ID),
-                    address.getAnnotation(AnnotationKeys.CLUSTER_ID));
+                    address.getAnnotation(AnnotationKeys.CLUSTER_ID),
+                    address.getAnnotation(AnnotationKeys.APPLIED_PLAN));
             if (!current.equals(previous)) {
                 try {
                     addressApi.replaceAddress(address);
@@ -463,11 +465,13 @@ public class AddressController implements Watcher<Address> {
         private final Status status;
         private final String brokerId;
         private final String clusterId;
+        private final String plan;
 
-        public ProvisionState(Status status, String brokerId, String clusterId) {
+        public ProvisionState(Status status, String brokerId, String clusterId, String plan) {
             this.status = new Status(status);
             this.brokerId = brokerId;
             this.clusterId = clusterId;
+            this.plan = plan;
         }
 
         @Override
@@ -477,12 +481,13 @@ public class AddressController implements Watcher<Address> {
             ProvisionState that = (ProvisionState) o;
             return Objects.equals(status, that.status) &&
                     Objects.equals(brokerId, that.brokerId) &&
-                    Objects.equals(clusterId, that.clusterId);
+                    Objects.equals(clusterId, that.clusterId) &&
+                    Objects.equals(plan, that.plan);
         }
 
         @Override
         public int hashCode() {
-            return Objects.hash(status, brokerId, clusterId);
+            return Objects.hash(status, brokerId, clusterId, plan);
         }
 
         @Override
@@ -491,6 +496,7 @@ public class AddressController implements Watcher<Address> {
                     "status=" + status +
                     ", brokerId='" + brokerId + '\'' +
                     ", clusterId='" + clusterId + '\'' +
+                    ", plan='" + plan + '\'' +
                     '}';
         }
     }


Test patch:
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/pkg/apis/iot/v1alpha1/types_test.go b/pkg/apis/iot/v1alpha1/types_test.go
new file mode 100644
index 000000000..e431cdb77
--- /dev/null
+++ b/pkg/apis/iot/v1alpha1/types_test.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 v1alpha1
+
+import (
+	"encoding/json"
+	"testing"
+)
+
+const testExpected1 = `{"downstreamStrategy":{},"configuration":{"enabled":true,"foo":{"bar":"baz","bar2":2}}}`
+
+// Test to decode and re-encode the generic config section
+func TestSerializeConfiguration(t *testing.T) {
+	spec := &IoTProjectSpec{}
+	if err := json.Unmarshal([]byte(`
+{
+	"configuration": {
+		"enabled": true,
+		"foo": {
+			"bar": "baz",
+			"bar2": 2
+        }
+	}
+}
+`), spec); err != nil {
+		t.Fatalf("Failed to decode: %v", err)
+		return
+	}
+
+	data, err := json.Marshal(spec)
+	if err != nil {
+		t.Fatalf("Failed to encode: %v", err)
+		return
+	}
+
+	if string(data) != testExpected1 {
+		t.Error("Expected JSON to be: ", testExpected1, " but was ", string(data))
+	}
+}
diff --git a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
index 1be8c5348..4c4eca3f5 100644
--- a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
+++ b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
@@ -23,6 +23,7 @@ import io.fabric8.openshift.client.OpenShiftClient;
 import io.vertx.core.Vertx;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 
 import java.io.IOException;
 import java.time.Duration;
@@ -195,6 +196,37 @@ public class AddressControllerTest {
         assertFalse(pending.getStatus().getMessages().isEmpty());
     }
 
+    @Test
+    public void testChangedPlanIsReplaced() throws Exception {
+        Address a = new AddressBuilder()
+                .withNewMetadata()
+                .withName("myspace.a1")
+                .withNamespace("ns")
+                .endMetadata()
+
+                .withNewSpec()
+                .withAddress("a1")
+                .withAddressSpace("myspace")
+                .withType("anycast")
+                .withPlan("small-anycast")
+                .endSpec()
+                .withNewStatus()
+                .withReady(true)
+                .withPhase(Phase.Active)
+                .endStatus()
+                .build();
+
+
+        assertNotEquals(a.getSpec().getPlan(), a.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+        controller.onUpdate(Arrays.asList(a));
+        ArgumentCaptor<Address> captor = ArgumentCaptor.forClass(Address.class);
+        verify(mockApi).replaceAddress(captor.capture());
+        Address captured = captor.getValue();
+        assertEquals(captured.getSpec().getPlan(), captured.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+        assertEquals(a.getSpec().getPlan(), a.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+
+    }
+
     @Test
     public void testMovesBrokersToDrained() throws Exception {
         Address alive = new AddressBuilder()
diff --git a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
index 5df7bfaad..583cbecd9 100644
--- a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
+++ b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
@@ -391,14 +391,18 @@ public class AddressProvisionerTest {
 
         Set<Address> addresses = new HashSet<>();
         Address q1 = createQueue("q1", "xlarge-queue");
+        Address a1 = createAddress("a1", "anycast", "small-anycast");
         addresses.add(q1);
+        addresses.add(a1);
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(addresses);
 
         assertNotEquals(q1.getSpec().getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+        assertNotEquals(a1.getSpec().getPlan(), a1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
         @SuppressWarnings("unused")
-        Map<String, Map<String, UsageInfo>> neededMap = provisioner.checkQuota(usageMap, Sets.newSet(q1), Sets.newSet(q1));
+        Map<String, Map<String, UsageInfo>> neededMap = provisioner.checkQuota(usageMap, Sets.newSet(a1, q1), Sets.newSet(a1, q1));
 
         assertEquals(q1.getSpec().getPlan(), q1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+        assertEquals(a1.getSpec().getPlan(), a1.getAnnotation(AnnotationKeys.APPLIED_PLAN));
     }
 
     @Test