Repository: canonical/operator
Pull request #55 (2019-11-25 22:09:09)

Add pod_spec to model
Surface pod_spec as an attribute on the model which can be read and written. Juju currently doesn't support reading, but we expect to get it soon.

johnsca: This is a very basic implementation for the pod spec.  I had started down the path of actually modelling the various bits of data that can go into the spec, but it seemed like a lot of work for not really much gain, and treating the pod spec like data is more in line with the "Kubernetes way" anyway.
jameinel: I know the issue got brought up, and the decision was that pod-spec-set should be sandboxed like relation-set. I haven't done the work to test it and confirm whether or not that is true. (I believe at one point it was *not* sandboxed, but I thought the work was done in 2.6 to change that.)

jameinel: I just checked the code, and pod-spec-set is committed during Flush, so will be ignored if the hook exits non-zero.


Patch:
diff --git a/op/model.py b/op/model.py
index 6f1f51c..2284adb 100644
--- a/op/model.py
+++ b/op/model.py
@@ -1,8 +1,11 @@
 import json
 import weakref
 import os
+import shutil
+import tempfile
 from abc import ABC, abstractmethod
 from collections.abc import Mapping, MutableMapping
+from pathlib import Path
 from subprocess import run, PIPE, CalledProcessError
 
 
@@ -14,6 +17,7 @@ class Model:
         self.app = self.unit.app
         self.relations = RelationMapping(relation_names, self.unit, self._backend, self._cache)
         self.config = ConfigData(self._backend)
+        self.pod = Pod(self._backend)
 
     def get_relation(self, relation_name, relation_id=None):
         """Get a specific Relation instance.
@@ -238,6 +242,14 @@ class ConfigData(LazyMapping):
         return self._backend.config_get()
 
 
+class Pod:
+    def __init__(self, backend):
+        self._backend = backend
+
+    def set_spec(self, spec, k8s_resources=None):
+        self._backend.pod_spec_set(spec, k8s_resources)
+
+
 class ModelError(Exception):
     pass
 
@@ -314,3 +326,17 @@ class ModelBackend:
 
     def is_leader(self):
         return self._run('is-leader')
+
+    def pod_spec_set(self, spec, k8s_resources):
+        tmpdir = Path(tempfile.mkdtemp('-pod-spec-set'))
+        try:
+            spec_path = tmpdir / 'spec.json'
+            spec_path.write_text(json.dumps(spec))
+            args = ['--spec', str(spec_path)]
+            if k8s_resources:
+                k8s_res_path = tmpdir / 'k8s-resources.json'
+                k8s_res_path.write_text(json.dumps(k8s_resources))
+                args.extend(['--k8s-resources', str(k8s_res_path)])
+            self._run_no_output('pod-spec-set', *args)
+        finally:
+            shutil.rmtree(tmpdir)


Test patch:
diff --git a/test/test_model.py b/test/test_model.py
index 3b87d94..50dc150 100644
--- a/test/test_model.py
+++ b/test/test_model.py
@@ -212,6 +212,25 @@ class TestModel(unittest.TestCase):
             ['relation-list', '-r', '4', '--format=json'],
         ])
 
+    def test_pod_spec(self):
+        model = op.model.Model('myapp/0', ['db0', 'db1', 'db2'], op.model.ModelBackend())
+
+        fake_script(self, 'pod-spec-set', """
+                    cat $2 > $(dirname $0)/spec.json
+                    [[ -n $4 ]] && cat $4 > $(dirname $0)/k8s_res.json || true
+                    """)
+        spec_path = self.fake_script_path / 'spec.json'
+        k8s_res_path = self.fake_script_path / 'k8s_res.json'
+
+        model.pod.set_spec({'foo': 'bar'})
+        self.assertEqual(spec_path.read_text(), '{"foo": "bar"}')
+        self.assertFalse(k8s_res_path.exists())
+
+        model.pod.set_spec({'bar': 'foo'}, {'qux': 'baz'})
+        self.assertEqual(spec_path.read_text(), '{"bar": "foo"}')
+        self.assertEqual(k8s_res_path.read_text(), '{"qux": "baz"}')
+
+
 def fake_script(test_case, name, content):
     if not hasattr(test_case, 'fake_script_path'):
         fake_script_path = tempfile.mkdtemp('-fake_script')