Merge v8.0 Into Master

This is a combination of all the commits.

=======================================================================

Enable Robot Tests

Enabling robot tests in CI/CD pipeline
Propagate the parameter DO_ROBOT to stage_3-merge
Setting OSM_HOSTNAME for robot test execution
Adding prometheus_hostname env variable for ci/cd tests
Archive containers logs in robot test execution

Signed-off-by: beierlm <mark.beierl@canonical.com>
Author: Felipe Vicens <felipe.vicens@atos.net>

8716 - K8s Proxy Charms

This commit contains the following changes:

- Changes to LCM Charm to configure vca k8s cloud
- K8s installation:
  - Add storageclass to K8s
  - Add k8s to VCA
  - Configure lcm.env to pass k8s cloud name
- Pass k8s cloud name to LCM Charm in charmed
installation
- Pin juju version tu 2.8/stable
- Instal juju after Microk8s in charmed
installation

Signed-off-by: Dominik Fleischmann <dominik.fleischmann@canonical.com>

Add PLA charm

Signed-off-by: David Garcia <david.garcia@canonical.com>

Add NG-UI Charm

Signed-off-by: David Garcia <david.garcia@canonical.com>

Fix bugs in NG-UI charm

Signed-off-by: David Garcia <david.garcia@canonical.com>

Updated Dockerfiles to use ReleaseEIGHT-daily repo by default

Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

Fix bug 1124: K8s added to juju after k8s has been initialized

Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

Helm and k8s storage initialization for k8s-based installation

Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

Fix bug 1125: iptables must be installed and configured if not present

Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

Fix bug 1129: juju_addk8s requires the storage class

Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

Fix issue in UI charm

Scale policy is only available in stateful pods

Signed-off-by: David Garcia <david.garcia@canonical.com>

Adds image overlay to bundle

When installing using a bundle, the image overlay was not being
generated, resulting in the --tag option being ignored.

Bug 1130

Signed-off-by: beierlm <mark.beierl@canonical.com>

Fix uninstall process, will fix issues is daily-stage4

Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

Fix PLA relations, add missing ENVs, and include it in the bundle

Signed-off-by: David Garcia <david.garcia@canonical.com>

Fix 1127 bug: Use config instead of resources for SSL certificates

Signed-off-by: David Garcia <david.garcia@canonical.com>

Remove unused config from PLA overlayx

Signed-off-by: David Garcia <david.garcia@canonical.com>

Installer: refactor file generation, improved ng-ui and light-ui separation, fixes in PLA

Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>

Pin libjuju version to 2.8.2

This fixes bug 1144

Signed-off-by: David Garcia <david.garcia@canonical.com>

Pin libjuju version to 2.8.2

Fixes bug 1144

Signed-off-by: Mark Beierl <mark.beierl@canonical.com>

Remove api_proxy variable in charmed installer

- Fixes bug 1154

Signed-off-by: David Garcia <david.garcia@canonical.com>

Add waiting period to Storage Class creation

This commit adds a waiting period until the
storage class is available.

It also fixes a minor bug in the juju add-k8s
command.

This fixes Bug 1155

Signed-off-by: Dominik Fleischmann <dominik.fleischmann@canonical.com>

Remove api_proxy from osm bundles

Signed-off-by: David Garcia <david.garcia@canonical.com>

Make Charmed Installer Idempotent

By default, the charmed installer will create a controller called
'controller', unless an external VCA is specified using the --vca
option.  Upon uninstall, it is not known or safe to remove this
Juju controller as there might be other applications using it.

This change shows that message on uninstall, giving the user the
opportunity to remove it with a command.

Also, as part of the installation, this change checks for the
controller and if it exists, presents the user with options for
how to resolve the situation.

Fixes bug 1126

Signed-off-by: beierlm <mark.beierl@canonical.com>

Remove unneeded mysql interface in PLA, fix osm-ha bundle, and add README.md to bundles

Signed-off-by: David Garcia <david.garcia@canonical.com>

Expose NBI and NG-UI

Adds ingress rules for K8s installation to expose the NBI, the
NG-UI and allow for unlimited content size.

Fixes bug 1171

Signed-off-by: beierlm <mark.beierl@canonical.com>

Installer Quits Too Soon

Increases the timeout and provides more feedback on progress.

Signed-off-by: beierlm <mark.beierl@canonical.com>

Change in returned storage status format from microk8s

Signed-off-by: calvinosanc1 <guillermo.calvino@canonical.com>

========================================================================================

Change-Id: I9223675bb9ad25a0da1f20215e79ef392e544eb5
Signed-off-by: beierlm <mark.beierl@canonical.com>
diff --git a/installers/charm/README.md b/installers/charm/README.md
index 0fb31db..a8467b0 100644
--- a/installers/charm/README.md
+++ b/installers/charm/README.md
@@ -43,7 +43,8 @@
 ├── nbi-k8s
 ├── pol-k8s
 ├── ro-k8s
-└── ui-k8s
+├── ui-k8s
+└── ng-ui --> new operator framework
 
 ```
 
diff --git a/installers/charm/bundles/osm-ha/README.md b/installers/charm/bundles/osm-ha/README.md
new file mode 100644
index 0000000..84dbde4
--- /dev/null
+++ b/installers/charm/bundles/osm-ha/README.md
@@ -0,0 +1,32 @@
+<!--
+ Copyright 2020 Canonical Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+# Charmed OSM
+
+
+Charmed OSM is an OSM distribution, developed and maintained by Canonical, which uses Juju charms to simplify its deployments and operations. Charmed OSM enables TSPs to easily deploy pure upstream OSM in highly available, production-grade and scalable clusters.
+
+- Industry‐aligned and fully compliant with upstream
+- Predictable release cadence and upgrade path
+- Simplified deployments and operations
+- Stable and secure
+- Highly Available and resilient against failures
+- Supported with [Ubuntu Advantage](https://ubuntu.com/advantage)
+- Availability of managed services
+
+
+## Quick start
+
+Go to the [OSM User Guide](https://osm.etsi.org/docs/user-guide/03-installing-osm.html#charmed-installation)
diff --git a/installers/charm/bundles/osm-ha/bundle.yaml b/installers/charm/bundles/osm-ha/bundle.yaml
index 212a520..ea3d7a1 100644
--- a/installers/charm/bundles/osm-ha/bundle.yaml
+++ b/installers/charm/bundles/osm-ha/bundle.yaml
@@ -28,7 +28,7 @@
       zookeeper-units: 3
     annotations:
       gui-x: 0
-      gui-y: 1100
+      gui-y: 550
   mariadb-k8s:
     charm: "cs:~charmed-osm/mariadb-k8s"
     channel: "stable"
@@ -46,8 +46,8 @@
       query-cache-size: 0
       ha-mode: true
     annotations:
-      gui-x: -500
-      gui-y: -400
+      gui-x: -250
+      gui-y: -200
   kafka-k8s:
     charm: "cs:~charmed-osm/kafka-k8s"
     channel: "stable"
@@ -63,7 +63,7 @@
       zookeeper-service-name: zookeeper-k8s-endpoints
     annotations:
       gui-x: 0
-      gui-y: 600
+      gui-y: 300
   mongodb-k8s:
     charm: "cs:~charmed-osm/mongodb-k8s"
     channel: "stable"
@@ -80,7 +80,7 @@
       enable-sidecar: true
     annotations:
       gui-x: 0
-      gui-y: 100
+      gui-y: 50
   nbi-k8s:
     charm: "cs:~charmed-osm/nbi-k8s"
     channel: "%(channel)s"
@@ -93,7 +93,7 @@
       DATABASE_COMMONKEY: osm
     annotations:
       gui-x: 0
-      gui-y: -400
+      gui-y: -200
   ro-k8s:
     charm: "cs:~charmed-osm/ro-k8s"
     channel: "%(channel)s"
@@ -106,8 +106,8 @@
       ro_database: "mano_db"
       OPENMANO_TENANT: "osm"
     annotations:
-      gui-x: -500
-      gui-y: 600
+      gui-x: -250
+      gui-y: 300
   ui-k8s:
     charm: "cs:~charmed-osm/ui-k8s"
     channel: "%(channel)s"
@@ -116,8 +116,21 @@
     options:
       mysql_database: lwui
     annotations:
+      gui-x: 250
+      gui-y: -200
+  ng-ui:
+    charm: "%(prefix)s/ng-ui%(suffix)s"
+    channel: "%(channel)s"
+    scale: 3
+    series: kubernetes
+    options:
+      port: 80
+      https_port: 443
+      server_name: localhost
+      client_max_body_size: 15M
+    annotations:
       gui-x: 500
-      gui-y: -400
+      gui-y: 100
   lcm-k8s:
     charm: "cs:~charmed-osm/lcm-k8s"
     channel: "%(channel)s"
@@ -132,12 +145,11 @@
       vca_password: secret
       vca_pubkey: pubkey
       vca_cacert: cacert
-      vca_apiproxy: apiproxy
       use_external_vca: true
       DATABASE_COMMONKEY: osm
     annotations:
-      gui-x: -500
-      gui-y: 100
+      gui-x: -250
+      gui-y: 50
   mon-k8s:
     charm: "cs:~charmed-osm/mon-k8s"
     channel: "%(channel)s"
@@ -158,8 +170,8 @@
       vca_cacert: cacert
       use_external_vca: true
     annotations:
-      gui-x: 500
-      gui-y: 100
+      gui-x: 250
+      gui-y: 50
   pol-k8s:
     charm: "cs:~charmed-osm/pol-k8s"
     channel: "%(channel)s"
@@ -170,8 +182,16 @@
     options:
       log_level: INFO
     annotations:
-      gui-x: -500
-      gui-y: 1100
+      gui-x: -250
+      gui-y: 550
+  pla:
+    charm: "%(prefix)s/pla%(suffix)s"
+    channel: "%(channel)s"
+    scale: 3
+    series: kubernetes
+    annotations:
+      gui-x: 500
+      gui-y: -200
   prometheus-k8s:
     charm: "cs:~charmed-osm/prometheus-k8s"
     channel: "stable"
@@ -184,16 +204,16 @@
       web-subpath: /
       default-target: "mon-k8s:8000"
     annotations:
-      gui-x: 500
-      gui-y: 600
+      gui-x: 250
+      gui-y: 300
   grafana-k8s:
     charm: "cs:~charmed-osm/grafana-k8s"
     channel: "stable"
     scale: 3
     series: kubernetes
     annotations:
-      gui-x: 500
-      gui-y: 1100
+      gui-x: 250
+      gui-y: 550
 
 relations:
   - - "kafka-k8s:zookeeper"
@@ -228,3 +248,9 @@
     - "nbi-k8s:nbi"
   - - "prometheus-k8s:prometheus"
     - "nbi-k8s:prometheus"
+  - - "pla:kafka"
+    - "kafka-k8s:kafka"
+  - - "pla:mongo"
+    - "mongodb-k8s:mongo"
+  - - 'ng-ui:nbi'
+    - 'nbi-k8s:nbi'
diff --git a/installers/charm/bundles/osm/README.md b/installers/charm/bundles/osm/README.md
new file mode 100644
index 0000000..84dbde4
--- /dev/null
+++ b/installers/charm/bundles/osm/README.md
@@ -0,0 +1,32 @@
+<!--
+ Copyright 2020 Canonical Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+# Charmed OSM
+
+
+Charmed OSM is an OSM distribution, developed and maintained by Canonical, which uses Juju charms to simplify its deployments and operations. Charmed OSM enables TSPs to easily deploy pure upstream OSM in highly available, production-grade and scalable clusters.
+
+- Industry‐aligned and fully compliant with upstream
+- Predictable release cadence and upgrade path
+- Simplified deployments and operations
+- Stable and secure
+- Highly Available and resilient against failures
+- Supported with [Ubuntu Advantage](https://ubuntu.com/advantage)
+- Availability of managed services
+
+
+## Quick start
+
+Go to the [OSM User Guide](https://osm.etsi.org/docs/user-guide/03-installing-osm.html#charmed-installation)
diff --git a/installers/charm/bundles/osm/bundle.yaml b/installers/charm/bundles/osm/bundle.yaml
index fc3dbcb..c4567ac 100644
--- a/installers/charm/bundles/osm/bundle.yaml
+++ b/installers/charm/bundles/osm/bundle.yaml
@@ -15,8 +15,8 @@
 bundle: kubernetes
 applications:
   zookeeper-k8s:
-    charm: 'cs:~charmed-osm/zookeeper-k8s'
-    channel: 'stable'
+    charm: "cs:~charmed-osm/zookeeper-k8s"
+    channel: "stable"
     scale: 1
     series: kubernetes
     storage:
@@ -28,10 +28,10 @@
       zookeeper-units: 1
     annotations:
       gui-x: 0
-      gui-y: 1100
+      gui-y: 550
   mariadb-k8s:
-    charm: 'cs:~charmed-osm/mariadb-k8s'
-    channel: 'stable'
+    charm: "cs:~charmed-osm/mariadb-k8s"
+    channel: "stable"
     scale: 1
     series: kubernetes
     storage:
@@ -46,11 +46,11 @@
       query-cache-size: 0
       ha-mode: false
     annotations:
-      gui-x: -500
-      gui-y: -400
+      gui-x: -250
+      gui-y: -200
   kafka-k8s:
-    charm: 'cs:~charmed-osm/kafka-k8s'
-    channel: 'stable'
+    charm: "cs:~charmed-osm/kafka-k8s"
+    channel: "stable"
     scale: 1
     series: kubernetes
     storage:
@@ -63,10 +63,10 @@
       zookeeper-service-name: zookeeper-k8s-endpoints
     annotations:
       gui-x: 0
-      gui-y: 600
+      gui-y: 300
   mongodb-k8s:
-    charm: 'cs:~charmed-osm/mongodb-k8s'
-    channel: 'stable'
+    charm: "cs:~charmed-osm/mongodb-k8s"
+    channel: "stable"
     scale: 1
     series: kubernetes
     storage:
@@ -80,10 +80,10 @@
       enable-sidecar: false
     annotations:
       gui-x: 0
-      gui-y: 100
+      gui-y: 50
   nbi-k8s:
-    charm: '%(prefix)s/nbi-k8s%(suffix)s'
-    channel: '%(channel)s'
+    charm: "%(prefix)s/nbi-k8s%(suffix)s"
+    channel: "%(channel)s"
     scale: 1
     series: kubernetes
     storage:
@@ -93,34 +93,47 @@
       DATABASE_COMMONKEY: osm
     annotations:
       gui-x: 0
-      gui-y: -400
+      gui-y: -200
   ro-k8s:
-    charm: '%(prefix)s/ro-k8s%(suffix)s'
-    channel: '%(channel)s'
+    charm: "%(prefix)s/ro-k8s%(suffix)s"
+    channel: "%(channel)s"
     scale: 1
     series: kubernetes
     storage:
       log: 50M
     options:
-      vim_database: 'mano_vim_db'
-      ro_database: 'mano_db'
-      OPENMANO_TENANT: 'osm'
+      vim_database: "mano_vim_db"
+      ro_database: "mano_db"
+      OPENMANO_TENANT: "osm"
     annotations:
-      gui-x: -500
-      gui-y: 600
+      gui-x: -250
+      gui-y: 300
   ui-k8s:
-    charm: '%(prefix)s/ui-k8s%(suffix)s'
-    channel: '%(channel)s'
+    charm: "%(prefix)s/ui-k8s%(suffix)s"
+    channel: "%(channel)s"
     scale: 1
     series: kubernetes
     options:
       mysql_database: lwui
     annotations:
+      gui-x: 250
+      gui-y: -200
+  ng-ui:
+    charm: "%(prefix)s/ng-ui%(suffix)s"
+    channel: "%(channel)s"
+    scale: 1
+    series: kubernetes
+    options:
+      port: 80
+      https_port: 443
+      server_name: localhost
+      client_max_body_size: 15M
+    annotations:
       gui-x: 500
-      gui-y: -400
+      gui-y: 100
   lcm-k8s:
-    charm: '%(prefix)s/lcm-k8s%(suffix)s'
-    channel: '%(channel)s'
+    charm: "%(prefix)s/lcm-k8s%(suffix)s"
+    channel: "%(channel)s"
     scale: 1
     series: kubernetes
     storage:
@@ -132,15 +145,14 @@
       vca_password: secret
       vca_pubkey: pubkey
       vca_cacert: cacert
-      vca_apiproxy: apiproxy
       use_external_vca: false
       DATABASE_COMMONKEY: osm
     annotations:
-      gui-x: -500
-      gui-y: 100
+      gui-x: -250
+      gui-y: 50
   mon-k8s:
-    charm: '%(prefix)s/mon-k8s%(suffix)s'
-    channel: '%(channel)s'
+    charm: "%(prefix)s/mon-k8s%(suffix)s"
+    channel: "%(channel)s"
     scale: 1
     series: kubernetes
     storage:
@@ -158,11 +170,11 @@
       vca_cacert: cacert
       use_external_vca: false
     annotations:
-      gui-x: 500
-      gui-y: 100
+      gui-x: 250
+      gui-y: 50
   pol-k8s:
-    charm: '%(prefix)s/pol-k8s%(suffix)s'
-    channel: '%(channel)s'
+    charm: "%(prefix)s/pol-k8s%(suffix)s"
+    channel: "%(channel)s"
     scale: 1
     series: kubernetes
     storage:
@@ -170,11 +182,19 @@
     options:
       log_level: INFO
     annotations:
-      gui-x: -500
-      gui-y: 1100
+      gui-x: -250
+      gui-y: 550
+  pla:
+    charm: "%(prefix)s/pla%(suffix)s"
+    channel: "%(channel)s"
+    scale: 1
+    series: kubernetes
+    annotations:
+      gui-x: 500
+      gui-y: -200
   prometheus-k8s:
-    charm: 'cs:~charmed-osm/prometheus-k8s'
-    channel: 'stable'
+    charm: "cs:~charmed-osm/prometheus-k8s"
+    channel: "stable"
     scale: 1
     series: kubernetes
     storage:
@@ -184,47 +204,53 @@
       web-subpath: /
       default-target: "mon-k8s:8000"
     annotations:
-      gui-x: 500
-      gui-y: 600
+      gui-x: 250
+      gui-y: 300
   grafana-k8s:
-    charm: 'cs:~charmed-osm/grafana-k8s'
-    channel: 'stable'
+    charm: "cs:~charmed-osm/grafana-k8s"
+    channel: "stable"
     scale: 1
     series: kubernetes
     annotations:
-      gui-x: 500
-      gui-y: 1100
+      gui-x: 250
+      gui-y: 550
 
 relations:
-  - - 'kafka-k8s:zookeeper'
-    - 'zookeeper-k8s:zookeeper'
-  - - 'ro-k8s:mysql'
-    - 'mariadb-k8s:mysql'
-  - - 'nbi-k8s:mongo'
-    - 'mongodb-k8s:mongo'
-  - - 'nbi-k8s:kafka'
-    - 'kafka-k8s:kafka'
-  - - 'lcm-k8s:kafka'
-    - 'kafka-k8s:kafka'
-  - - 'lcm-k8s:mongo'
-    - 'mongodb-k8s:mongo'
-  - - 'mon-k8s:kafka'
-    - 'kafka-k8s:kafka'
-  - - 'mon-k8s:mongo'
-    - 'mongodb-k8s:mongo'
-  - - 'pol-k8s:kafka'
-    - 'kafka-k8s:kafka'
-  - - 'pol-k8s:mongo'
-    - 'mongodb-k8s:mongo'
-  - - 'lcm-k8s:ro'
-    - 'ro-k8s:ro'
-  - - 'prometheus-k8s:prometheus'
-    - 'mon-k8s:prometheus'
-  - - 'grafana-k8s:prometheus'
-    - 'prometheus-k8s:prometheus'
-  - - 'ui-k8s:mysql'
-    - 'mariadb-k8s:mysql'
-  - - 'ui-k8s:nbi'
+  - - "kafka-k8s:zookeeper"
+    - "zookeeper-k8s:zookeeper"
+  - - "ro-k8s:mysql"
+    - "mariadb-k8s:mysql"
+  - - "nbi-k8s:mongo"
+    - "mongodb-k8s:mongo"
+  - - "nbi-k8s:kafka"
+    - "kafka-k8s:kafka"
+  - - "lcm-k8s:kafka"
+    - "kafka-k8s:kafka"
+  - - "lcm-k8s:mongo"
+    - "mongodb-k8s:mongo"
+  - - "mon-k8s:kafka"
+    - "kafka-k8s:kafka"
+  - - "mon-k8s:mongo"
+    - "mongodb-k8s:mongo"
+  - - "pol-k8s:kafka"
+    - "kafka-k8s:kafka"
+  - - "pol-k8s:mongo"
+    - "mongodb-k8s:mongo"
+  - - "lcm-k8s:ro"
+    - "ro-k8s:ro"
+  - - "prometheus-k8s:prometheus"
+    - "mon-k8s:prometheus"
+  - - "grafana-k8s:prometheus"
+    - "prometheus-k8s:prometheus"
+  - - "ui-k8s:mysql"
+    - "mariadb-k8s:mysql"
+  - - "ui-k8s:nbi"
+    - "nbi-k8s:nbi"
+  - - "prometheus-k8s:prometheus"
+    - "nbi-k8s:prometheus"
+  - - "pla:kafka"
+    - "kafka-k8s:kafka"
+  - - "pla:mongo"
+    - "mongodb-k8s:mongo"
+  - - 'ng-ui:nbi'
     - 'nbi-k8s:nbi'
-  - - 'prometheus-k8s:prometheus'
-    - 'nbi-k8s:prometheus'
diff --git a/installers/charm/lcm-k8s/config.yaml b/installers/charm/lcm-k8s/config.yaml
index 41e3e84..25f6a71 100644
--- a/installers/charm/lcm-k8s/config.yaml
+++ b/installers/charm/lcm-k8s/config.yaml
@@ -44,6 +44,10 @@
     type: string
     description: "The VCA lxd cloud name"
     default: "localhost"
+  vca_k8s_cloud:
+    type: string
+    description: "The VCA K8s cloud name"
+    default: "k8scloud"
   advertised-hostname:
     description: LCM Hostname
     type: string
diff --git a/installers/charm/lcm-k8s/reactive/lcm.py b/installers/charm/lcm-k8s/reactive/lcm.py
index 85ec9c5..3ada8cb 100644
--- a/installers/charm/lcm-k8s/reactive/lcm.py
+++ b/installers/charm/lcm-k8s/reactive/lcm.py
@@ -21,7 +21,7 @@
     config,
 )
 from charms import layer
-
+import yaml
 
 @hook("upgrade-charm")
 @when("leadership.is_leader")
@@ -131,4 +131,7 @@
     }
     data.update(cfg)
 
-    return pod_spec_template % data
+    spec = yaml.safe_dump(pod_spec_template % data)
+    if "vca_apiproxy" in cfg and cfg["vca_apiproxy"] != "":
+        spec["containers"][0]["config"]["OSMLCM_VCA_APIPROXY"] = cfg["vca_apiproxy"]
+    return spec
\ No newline at end of file
diff --git a/installers/charm/lcm-k8s/reactive/spec_template.yaml b/installers/charm/lcm-k8s/reactive/spec_template.yaml
index 1a27096..d8b51e7 100644
--- a/installers/charm/lcm-k8s/reactive/spec_template.yaml
+++ b/installers/charm/lcm-k8s/reactive/spec_template.yaml
@@ -45,5 +45,5 @@
       OSMLCM_VCA_PUBKEY: %(vca_pubkey)s
       OSMLCM_VCA_SECRET: %(vca_password)s
       OSMLCM_VCA_CACERT: %(vca_cacert)s
-      OSMLCM_VCA_APIPROXY: %(vca_apiproxy)s
       OSMLCM_VCA_CLOUD: %(vca_cloud)s
+      OSMLCM_VCA_K8S_CLOUD: %(vca_k8s_cloud)s
diff --git a/installers/charm/nbi-k8s/reactive/spec_template.yaml b/installers/charm/nbi-k8s/reactive/spec_template.yaml
index 18adec4..5c030d9 100644
--- a/installers/charm/nbi-k8s/reactive/spec_template.yaml
+++ b/installers/charm/nbi-k8s/reactive/spec_template.yaml
@@ -28,14 +28,14 @@
       OSMNBI_DATABASE_DRIVER: mongo
       OSMNBI_DATABASE_URI: %(mongo_uri)s
       OSMNBI_DATABASE_COMMONKEY: %(DATABASE_COMMONKEY)s
-      
+
       OSMNBI_STORAGE_DRIVER: mongo
       OSMNBI_STORAGE_PATH: /app/storage
       OSMNBI_STORAGE_COLLECTION: files
       OSMNBI_STORAGE_URI: %(mongo_uri)s
 
       OSMNBI_STATIC_DIR: /app/osm_nbi/html_public
-      
+
       OSMNBI_PROMETHEUS_HOST: %(prometheus_host)s
       OSMNBI_PROMETHEUS_PORT: %(prometheus_port)s
-      OSMNBI_LOG_LEVEL: %(log_level)s
\ No newline at end of file
+      OSMNBI_LOG_LEVEL: %(log_level)s
diff --git a/installers/charm/ng-ui/.yamllint.yaml b/installers/charm/ng-ui/.yamllint.yaml
new file mode 100644
index 0000000..ab52c60
--- /dev/null
+++ b/installers/charm/ng-ui/.yamllint.yaml
@@ -0,0 +1,23 @@
+# Copyright 2020 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#     Unless required by applicable law or agreed to in writing, software
+#     distributed under the License is distributed on an "AS IS" BASIS,
+#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#     See the License for the specific language governing permissions and
+#     limitations under the License.
+---
+extends: default
+
+yaml-files:
+  - "*.yaml"
+  - "*.yml"
+  - ".yamllint"
+ignore: |
+  .tox
+  mod
diff --git a/installers/charm/ng-ui/README.md b/installers/charm/ng-ui/README.md
new file mode 100644
index 0000000..e6a3d58
--- /dev/null
+++ b/installers/charm/ng-ui/README.md
@@ -0,0 +1,57 @@
+<!-- #   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License. -->
+
+# NG-UI Charm
+
+## How to deploy
+
+```bash
+juju deploy . # cs:~charmed-osm/ng-ui --channel edge
+juju relate ng-ui nbi-k8s
+```
+
+## How to expose the NG-UI through ingress
+
+```bash
+juju config ng-ui juju-external-hostname=ng.<k8s_worker_ip>.xip.io
+juju expose ng-ui
+```
+
+> Note: The <k8s_worker_ip> is the IP of the K8s worker node. With microk8s, you can see the IP with `microk8s.config`. It is usually the IP of your host machine.
+
+## How to scale
+
+```bash
+    juju scale-application ng-ui 3
+```
+
+## How to use certificates
+
+Generate your own certificate if you don't have one already:
+
+```bash
+sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout ssl_certificate.key -out ssl_certificate.crt
+sudo chown $USER:$USER ssl_certificate.key
+juju config ng-ui ssl_certificate=`cat ssl_certificate.crt | base64 -w 0`
+juju config ng-ui ssl_certificate_key=`cat ssl_certificate.key | base64 -w 0`
+```
+
+## Config Examples
+
+```bash
+juju config ng-ui image=opensourcemano/ng-ui:<tag>
+juju config ng-ui port=80
+juju config server_name=<name>
+juju config client_max_body_size=25M
+```
diff --git a/installers/charm/ng-ui/config.yaml b/installers/charm/ng-ui/config.yaml
new file mode 100644
index 0000000..a80a280
--- /dev/null
+++ b/installers/charm/ng-ui/config.yaml
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 Arctos Labs Scandinavia AB
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+options:
+  image:
+    description: Docker image name
+    type: string
+    default: opensourcemano/ng-ui:8
+  port:
+    description: Port number
+    type: int
+    default: 80
+  https_port:
+    description: Port number
+    type: int
+    default: 443
+  server_name:
+    description: Server name
+    type: string
+    default: localhost
+  client_max_body_size:
+    description: Client maximum body size
+    type: string
+    default: 15M
+  ssl_certificate:
+    description: Base64 encoded ssl certificate
+    type: string
+  ssl_certificate_key:
+    description: Base64 encoded ssl certificate key
+    type: string
diff --git a/installers/charm/ng-ui/files/default b/installers/charm/ng-ui/files/default
new file mode 100644
index 0000000..e014715
--- /dev/null
+++ b/installers/charm/ng-ui/files/default
@@ -0,0 +1,37 @@
+#   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+
+server {
+    listen $http_port;
+    listen $https_port default ssl;
+    server_name  $server_name;
+    root   /usr/share/nginx/html;
+    index  index.html index.htm;
+    client_max_body_size $client_max_body_size;
+    $ssl_crt
+    $ssl_crt_key
+    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
+    ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
+    location /osm {
+        proxy_pass https://$nbi_host:$nbi_port;
+        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
+        proxy_set_header Accept-Encoding "";
+    }
+
+    location / {
+        try_files $$uri $$uri/ /index.html;
+    }
+}
diff --git a/installers/charm/ng-ui/hooks/start b/installers/charm/ng-ui/hooks/start
new file mode 120000
index 0000000..25b1f68
--- /dev/null
+++ b/installers/charm/ng-ui/hooks/start
@@ -0,0 +1 @@
+../src/charm.py
\ No newline at end of file
diff --git a/installers/charm/ng-ui/lib/ops b/installers/charm/ng-ui/lib/ops
new file mode 120000
index 0000000..d934193
--- /dev/null
+++ b/installers/charm/ng-ui/lib/ops
@@ -0,0 +1 @@
+../mod/operator/ops
\ No newline at end of file
diff --git a/installers/charm/ng-ui/metadata.yaml b/installers/charm/ng-ui/metadata.yaml
new file mode 100644
index 0000000..a0a5921
--- /dev/null
+++ b/installers/charm/ng-ui/metadata.yaml
@@ -0,0 +1,27 @@
+#   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+name: ng-ui
+summary: A Next Generation UI charm for Opensource MANO
+description: |
+  New UI for OSM
+series:
+  - kubernetes
+min-juju-version: 2.7.0
+deployment:
+  type: stateless
+  service: cluster
+requires:
+  nbi:
+    interface: osm-nbi
diff --git a/installers/charm/ng-ui/mod/operator b/installers/charm/ng-ui/mod/operator
new file mode 160000
index 0000000..a84ce87
--- /dev/null
+++ b/installers/charm/ng-ui/mod/operator
@@ -0,0 +1 @@
+Subproject commit a84ce8776b368a8b2bccdb173716e342db9a6b36
diff --git a/installers/charm/ng-ui/src/charm.py b/installers/charm/ng-ui/src/charm.py
new file mode 100755
index 0000000..ce48927
--- /dev/null
+++ b/installers/charm/ng-ui/src/charm.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+#   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import sys
+import logging
+import base64
+
+sys.path.append("lib")
+
+from ops.charm import CharmBase
+from ops.framework import StoredState, Object
+from ops.main import main
+from ops.model import (
+    ActiveStatus,
+    MaintenanceStatus,
+    BlockedStatus,
+    ModelError,
+    WaitingStatus,
+)
+
+from glob import glob
+from pathlib import Path
+from string import Template
+
+logger = logging.getLogger(__name__)
+
+
+class NGUICharm(CharmBase):
+    state = StoredState()
+
+    def __init__(self, framework, key):
+        super().__init__(framework, key)
+        self.state.set_default(spec=None)
+        self.state.set_default(nbi_host=None)
+        self.state.set_default(nbi_port=None)
+
+        # Observe Charm related events
+        self.framework.observe(self.on.config_changed, self.on_config_changed)
+        self.framework.observe(self.on.start, self.on_start)
+        self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm)
+        self.framework.observe(
+            self.on.nbi_relation_changed, self.on_nbi_relation_changed
+        )
+
+        # SSL Certificate path
+        self.ssl_folder = "/certs"
+        self.ssl_crt_name = "ssl_certificate.crt"
+        self.ssl_key_name = "ssl_certificate.key"
+
+    def _apply_spec(self):
+        # Only apply the spec if this unit is a leader.
+        unit = self.model.unit
+        if not unit.is_leader():
+            unit.status = ActiveStatus("ready")
+            return
+        if not self.state.nbi_host or not self.state.nbi_port:
+            unit.status = WaitingStatus("Waiting for NBI")
+            return
+        unit.status = MaintenanceStatus("Applying new pod spec")
+
+        new_spec = self.make_pod_spec()
+        if new_spec == self.state.spec:
+            unit.status = ActiveStatus("ready")
+            return
+        self.framework.model.pod.set_spec(new_spec)
+        self.state.spec = new_spec
+        unit.status = ActiveStatus("ready")
+
+    def make_pod_spec(self):
+        config = self.framework.model.config
+
+        config_spec = {
+            "http_port": config["port"],
+            "https_port": config["https_port"],
+            "server_name": config["server_name"],
+            "client_max_body_size": config["client_max_body_size"],
+            "nbi_host": self.state.nbi_host or config["nbi_host"],
+            "nbi_port": self.state.nbi_port or config["nbi_port"],
+            "ssl_crt": "",
+            "ssl_crt_key": "",
+        }
+
+        ssl_certificate = None
+        ssl_certificate_key = None
+        ssl_enabled = False
+
+        if "ssl_certificate" in config and "ssl_certificate_key" in config:
+            # Get bytes of cert and key
+            cert_b = base64.b64decode(config["ssl_certificate"])
+            key_b = base64.b64decode(config["ssl_certificate_key"])
+            # Decode key and cert
+            ssl_certificate = cert_b.decode("utf-8")
+            ssl_certificate_key = key_b.decode("utf-8")
+            # Get paths
+            cert_path = "{}/{}".format(self.ssl_folder, self.ssl_crt_name)
+            key_path = "{}/{}".format(self.ssl_folder, self.ssl_key_name)
+
+            config_spec["port"] = "{} ssl".format(config["https_port"])
+            config_spec["ssl_crt"] = "ssl_certificate {};".format(cert_path)
+            config_spec["ssl_crt_key"] = "ssl_certificate_key {};".format(key_path)
+            ssl_enabled = True
+        else:
+            config_spec["ssl_crt"] = ""
+            config_spec["ssl_crt_key"] = ""
+
+        files = [
+            {
+                "name": "configuration",
+                "mountPath": "/etc/nginx/sites-available/",
+                "files": {
+                    Path(filename)
+                    .name: Template(Path(filename).read_text())
+                    .substitute(config_spec)
+                    for filename in glob("files/*")
+                },
+            }
+        ]
+        port = config["https_port"] if ssl_enabled else config["port"]
+        ports = [
+            {"name": "port", "containerPort": port, "protocol": "TCP", },
+        ]
+
+        kubernetes = {
+            "readinessProbe": {
+                "tcpSocket": {"port": port},
+                "timeoutSeconds": 5,
+                "periodSeconds": 5,
+                "initialDelaySeconds": 10,
+            },
+            "livenessProbe": {
+                "tcpSocket": {"port": port},
+                "timeoutSeconds": 5,
+                "initialDelaySeconds": 45,
+            },
+        }
+
+        if ssl_certificate and ssl_certificate_key:
+            files.append(
+                {
+                    "name": "ssl",
+                    "mountPath": self.ssl_folder,
+                    "files": {
+                        self.ssl_crt_name: ssl_certificate,
+                        self.ssl_key_name: ssl_certificate_key,
+                    },
+                }
+            )
+        logger.debug(files)
+        spec = {
+            "version": 2,
+            "containers": [
+                {
+                    "name": self.framework.model.app.name,
+                    "image": "{}".format(config["image"]),
+                    "ports": ports,
+                    "kubernetes": kubernetes,
+                    "files": files,
+                }
+            ],
+        }
+
+        return spec
+
+    def on_config_changed(self, event):
+        """Handle changes in configuration"""
+        self._apply_spec()
+
+    def on_start(self, event):
+        """Called when the charm is being installed"""
+        self._apply_spec()
+
+    def on_upgrade_charm(self, event):
+        """Upgrade the charm."""
+        unit = self.model.unit
+        unit.status = MaintenanceStatus("Upgrading charm")
+        self.on_start(event)
+
+    def on_nbi_relation_changed(self, event):
+        unit = self.model.unit
+        if not unit.is_leader():
+            return
+        self.state.nbi_host = event.relation.data[event.unit].get("host")
+        self.state.nbi_port = event.relation.data[event.unit].get("port")
+        self._apply_spec()
+
+
+if __name__ == "__main__":
+    main(NGUICharm)
diff --git a/installers/charm/ng-ui/tox.ini b/installers/charm/ng-ui/tox.ini
new file mode 100644
index 0000000..aafa301
--- /dev/null
+++ b/installers/charm/ng-ui/tox.ini
@@ -0,0 +1,55 @@
+#   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+[tox]
+envlist = pep8
+skipsdist = True
+
+[testenv]
+setenv = VIRTUAL_ENV={envdir}
+         PYTHONHASHSEED=0
+whitelist_externals = juju
+passenv = HOME TERM CS_API_* OS_* AMULET_*
+deps = -r{toxinidir}/test-requirements.txt
+install_command =
+  pip install {opts} {packages}
+
+[testenv:black]
+basepython = python3
+deps =
+    black
+    yamllint
+commands =
+    black --check --diff src
+    yamllint .
+
+[testenv:pep8]
+basepython = python3
+deps=charm-tools
+commands = charm-proof
+
+[testenv:func-noop]
+basepython = python3
+commands =
+    true
+
+[testenv:func]
+basepython = python3
+commands = functest-run-suite
+
+[testenv:func-smoke]
+basepython = python3
+commands = functest-run-suite --keep-model --smoke
+
+[testenv:venv]
+commands = {posargs}
diff --git a/installers/charm/pla/.yamllint.yaml b/installers/charm/pla/.yamllint.yaml
new file mode 100644
index 0000000..ab52c60
--- /dev/null
+++ b/installers/charm/pla/.yamllint.yaml
@@ -0,0 +1,23 @@
+# Copyright 2020 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#     Unless required by applicable law or agreed to in writing, software
+#     distributed under the License is distributed on an "AS IS" BASIS,
+#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#     See the License for the specific language governing permissions and
+#     limitations under the License.
+---
+extends: default
+
+yaml-files:
+  - "*.yaml"
+  - "*.yml"
+  - ".yamllint"
+ignore: |
+  .tox
+  mod
diff --git a/installers/charm/pla/README.md b/installers/charm/pla/README.md
new file mode 100644
index 0000000..8d486d0
--- /dev/null
+++ b/installers/charm/pla/README.md
@@ -0,0 +1,14 @@
+<!-- #   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License. -->
+# PLA Charm
\ No newline at end of file
diff --git a/installers/charm/pla/config.yaml b/installers/charm/pla/config.yaml
new file mode 100644
index 0000000..e0aeca4
--- /dev/null
+++ b/installers/charm/pla/config.yaml
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 Arctos Labs Scandinavia AB
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+options:
+  image:
+    description: Docker image name
+    type: string
+    default: opensourcemano/pla:8
+  port:
+    description: Port number
+    type: int
+    default: 8080 # Fake port
+  log_level:
+    description: Log level
+    type: string
+    default: INFO
+  database:
+    description: Database name
+    type: string
+    default: pla
+  database_common_key:
+    description: Common Key for Mongo database
+    type: string
+    default: osm
diff --git a/installers/charm/pla/hooks/install b/installers/charm/pla/hooks/install
new file mode 120000
index 0000000..25b1f68
--- /dev/null
+++ b/installers/charm/pla/hooks/install
@@ -0,0 +1 @@
+../src/charm.py
\ No newline at end of file
diff --git a/installers/charm/pla/hooks/start b/installers/charm/pla/hooks/start
new file mode 120000
index 0000000..25b1f68
--- /dev/null
+++ b/installers/charm/pla/hooks/start
@@ -0,0 +1 @@
+../src/charm.py
\ No newline at end of file
diff --git a/installers/charm/pla/hooks/upgrade-charm b/installers/charm/pla/hooks/upgrade-charm
new file mode 120000
index 0000000..25b1f68
--- /dev/null
+++ b/installers/charm/pla/hooks/upgrade-charm
@@ -0,0 +1 @@
+../src/charm.py
\ No newline at end of file
diff --git a/installers/charm/pla/lib/ops b/installers/charm/pla/lib/ops
new file mode 120000
index 0000000..d934193
--- /dev/null
+++ b/installers/charm/pla/lib/ops
@@ -0,0 +1 @@
+../mod/operator/ops
\ No newline at end of file
diff --git a/installers/charm/pla/metadata.yaml b/installers/charm/pla/metadata.yaml
new file mode 100644
index 0000000..90c9d40
--- /dev/null
+++ b/installers/charm/pla/metadata.yaml
@@ -0,0 +1,29 @@
+#   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+name: pla
+summary: A Placement charm for Opensource MANO
+description: |
+  Placement module for OSM
+series:
+  - kubernetes
+min-juju-version: 2.7.0
+deployment:
+  type: stateless
+  service: cluster
+requires:
+  kafka:
+    interface: kafka
+  mongo:
+    interface: mongodb
diff --git a/installers/charm/pla/mod/operator b/installers/charm/pla/mod/operator
new file mode 160000
index 0000000..a84ce87
--- /dev/null
+++ b/installers/charm/pla/mod/operator
@@ -0,0 +1 @@
+Subproject commit a84ce8776b368a8b2bccdb173716e342db9a6b36
diff --git a/installers/charm/pla/src/charm.py b/installers/charm/pla/src/charm.py
new file mode 100755
index 0000000..1fc6386
--- /dev/null
+++ b/installers/charm/pla/src/charm.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+#   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import sys
+import logging
+
+sys.path.append("lib")
+
+from ops.charm import CharmBase
+from ops.framework import StoredState, Object
+from ops.main import main
+from ops.model import (
+    ActiveStatus,
+    MaintenanceStatus,
+    WaitingStatus,
+)
+
+from glob import glob
+from pathlib import Path
+from string import Template
+
+logger = logging.getLogger(__name__)
+
+
+class PLACharm(CharmBase):
+    state = StoredState()
+
+    def __init__(self, framework, key):
+        super().__init__(framework, key)
+        self.state.set_default(spec=None)
+        self.state.set_default(kafka_host=None)
+        self.state.set_default(kafka_port=None)
+        self.state.set_default(mongodb_uri=None)
+
+        # Observe Charm related events
+        self.framework.observe(self.on.config_changed, self.on_config_changed)
+        self.framework.observe(self.on.start, self.on_start)
+        self.framework.observe(self.on.upgrade_charm, self.on_upgrade_charm)
+
+        # Relations
+        self.framework.observe(
+            self.on.kafka_relation_changed, self.on_kafka_relation_changed
+        )
+        self.framework.observe(
+            self.on.mongo_relation_changed, self.on_mongo_relation_changed
+        )
+
+    def _apply_spec(self):
+        # Only apply the spec if this unit is a leader.
+        unit = self.model.unit
+        if not unit.is_leader():
+            unit.status = ActiveStatus("ready")
+            return
+        if not self.state.kafka_host or not self.state.kafka_port:
+            unit.status = WaitingStatus("Waiting for Kafka")
+            return
+        if not self.state.mongodb_uri:
+            unit.status = WaitingStatus("Waiting for MongoDB")
+            return
+
+        unit.status = MaintenanceStatus("Applying new pod spec")
+
+        new_spec = self.make_pod_spec()
+        if new_spec == self.state.spec:
+            unit.status = ActiveStatus("ready")
+            return
+        self.framework.model.pod.set_spec(new_spec)
+        self.state.spec = new_spec
+        unit.status = ActiveStatus("ready")
+
+    def make_pod_spec(self):
+        config = self.framework.model.config
+
+        ports = [
+            {"name": "port", "containerPort": config["port"], "protocol": "TCP", },
+        ]
+
+        config_spec = {
+            "OSMPLA_MESSAGE_DRIVER": "kafka",
+            "OSMPLA_MESSAGE_HOST": self.state.kafka_host,
+            "OSMPLA_MESSAGE_PORT": self.state.kafka_port,
+            "OSMPLA_DATABASE_DRIVER": "mongo",
+            "OSMPLA_DATABASE_URI": self.state.mongodb_uri,
+            "OSMPLA_GLOBAL_LOG_LEVEL": config["log_level"],
+            "OSMPLA_DATABASE_COMMONKEY": config["database_common_key"],
+        }
+
+        spec = {
+            "version": 2,
+            "containers": [
+                {
+                    "name": self.framework.model.app.name,
+                    "image": config["image"],
+                    "ports": ports,
+                    "config": config_spec,
+                }
+            ],
+        }
+
+        return spec
+
+    def on_config_changed(self, event):
+        """Handle changes in configuration"""
+        self._apply_spec()
+
+    def on_start(self, event):
+        """Called when the charm is being installed"""
+        self._apply_spec()
+
+    def on_upgrade_charm(self, event):
+        """Upgrade the charm."""
+        unit = self.model.unit
+        unit.status = MaintenanceStatus("Upgrading charm")
+        self.on_start(event)
+
+    def on_kafka_relation_changed(self, event):
+        unit = self.model.unit
+        if not unit.is_leader():
+            return
+        self.state.kafka_host = event.relation.data[event.unit].get("host")
+        self.state.kafka_port = event.relation.data[event.unit].get("port")
+        self._apply_spec()
+
+    def on_mongo_relation_changed(self, event):
+        unit = self.model.unit
+        if not unit.is_leader():
+            return
+        self.state.mongodb_uri = event.relation.data[event.unit].get(
+            "connection_string"
+        )
+        self._apply_spec()
+
+
+if __name__ == "__main__":
+    main(PLACharm)
diff --git a/installers/charm/pla/tox.ini b/installers/charm/pla/tox.ini
new file mode 100644
index 0000000..d2a8c53
--- /dev/null
+++ b/installers/charm/pla/tox.ini
@@ -0,0 +1,56 @@
+#   Copyright 2020 Canonical Ltd.
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+[tox]
+envlist = pep8
+skipsdist = True
+
+[testenv]
+setenv = VIRTUAL_ENV={envdir}
+         PYTHONHASHSEED=0
+whitelist_externals = juju
+passenv = HOME TERM CS_API_* OS_* AMULET_*
+deps = -r{toxinidir}/test-requirements.txt
+install_command =
+  pip install {opts} {packages}
+
+[testenv:black]
+basepython = python3
+deps =
+    black
+    yamllint
+commands =
+    black --check --diff src
+    yamllint .
+
+[testenv:pep8]
+basepython = python3
+deps=charm-tools
+commands = charm-proof
+
+[testenv:func-noop]
+basepython = python3
+commands =
+    true
+
+[testenv:func]
+basepython = python3
+commands = functest-run-suite
+
+[testenv:func-smoke]
+basepython = python3
+commands = functest-run-suite --keep-model --smoke
+
+[testenv:venv]
+commands = {posargs}
diff --git a/installers/charm/ui-k8s/reactive/spec_template.yaml b/installers/charm/ui-k8s/reactive/spec_template.yaml
index 2bc6f23..b63c210 100644
--- a/installers/charm/ui-k8s/reactive/spec_template.yaml
+++ b/installers/charm/ui-k8s/reactive/spec_template.yaml
@@ -12,8 +12,6 @@
 #     See the License for the specific language governing permissions and
 #     limitations under the License.
 version: 2
-service:
-  scalePolicy: serial
 containers:
   - name: %(name)s
     image: %(docker_image)s