[WIP] Creating Docker images with pure Salt and running Salt commands inside containers #34484

Merged
merged 27 commits into from Jul 15, 2016

Projects

None yet

8 participants

@dmacvicar
Contributor
dmacvicar commented Jul 6, 2016 edited

What does this PR do?

  • It implements creating Docker images from sls states (Salt on the image not required, base image only needs python)

screenshot from 2016-07-05 23-16-16

cmds_bvwyaairc-

  • Allows to run arbitrary Salt modules inside a container without needing Salt in the container (uses salt-thin).

cmrl4paviaemgly

What issues does this PR fix or reference?

Tests written?

Yes. (dockerng.sls pending)

cc @meaksh @isbm @dincamihai

dmacvicar added some commits Jul 6, 2016
@dmacvicar dmacvicar first version of building docker images 8aadb33
@dmacvicar dmacvicar test return value 4e7e72e
@dmacvicar dmacvicar testcase for call d3ee81c
@dmacvicar dmacvicar escape salt-call command ea89993
@dmacvicar dmacvicar make sure function parameter is there 29b0bf1
@dmacvicar dmacvicar move import to the top c869885
@dmacvicar dmacvicar implement keyword arguments for dockerng.call
59d8ce4
@cachedout
Contributor

This is really awesome!

@cachedout cachedout added the Awesome label Jul 6, 2016
@thatch45
Member
thatch45 commented Jul 6, 2016

Awesome! You got it in @dmacvicar !

@rallytime
Contributor

Neat!

@dmacvicar There are a couple of pylint errors to clean up. It would also be good to get some .. versionadded:: Carbon tags in the docstrings of each of the new public-facing functions.

@isbm isbm commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+ errors += st_.state.verify_high(high_data)
+ if errors:
+ return errors
+
+ high_data, req_in_errors = st_.state.requisite_in(high_data)
+ errors += req_in_errors
+ high_data = st_.state.apply_exclude(high_data)
+ # Verify that the high data is structurally sound
+ if errors:
+ return errors
+
+ # Compile and verify the raw chunks
+ return st_.state.compile_high_data(high_data)
+
+
+def _gather_pillar(pillarenv, pillar_override, grains={}):
@isbm
isbm Jul 6, 2016 Contributor

Just change the signature like this:

def _gather_pillar(pillarenv, pillar_override, **grains):
dmacvicar added some commits Jul 6, 2016
@dmacvicar dmacvicar pylint: W0611(unused-import) db513df
@dmacvicar dmacvicar pylint: W8391(blank-line-at-end-of-file)
817db7b
@dmacvicar dmacvicar pylint: W0102(dangerous-default-value)
1bc154d
@dmacvicar dmacvicar add versionadded:: Carbon to new public functions
371c619
@dmacvicar
Contributor
dmacvicar commented Jul 6, 2016 edited

Thanks for the feedback. I corrected everything mentioned until now.

  • What do you think of the name of the call function? Is there any convention to these meta-calls? snapper module will introduce a similar one. It would be cool if they follow some convention in naming and style.

Originally, the function signature was: call(container_id, function=None, args=[], kwargs={}) but now is just call(container_id, function, *args, **kwargs), which means I just forward all parameters. It makes it easier to use but also means that I have to filter __key like args injected by salt so that `call(id, 'test.ping') works.

  • The current thin handling is a bit brute force and does not has the amount of logic salt-ssh does to the unpacking.
@dmacvicar
Contributor
dmacvicar commented Jul 6, 2016 edited
  • Delete the tarballs in /tmp before commit
@isbm
Contributor
isbm commented Jul 6, 2016

@dmacvicar The call signature is now correct. Nothing wrong with the filtering, since everything starting with __ is usually technical "carryover" that you can throw away, in case you need.

@isbm
Contributor
isbm commented Jul 6, 2016 edited

@dmacvicar The name call itself isn't really bad, although keep in mind that there is __call__ function to every "callable" class:

class Foo(object):
    def __call__(self):
        return "Hello world"

print(Foo()())  # Prints "Hello world"

@cachedout @thatch45 objections?

@isbm isbm and 2 others commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
@@ -5529,3 +5540,216 @@ def script_retcode(name,
ignore_retcode=ignore_retcode,
use_vt=use_vt,
keep_env=keep_env)['retcode']
+
+
+def _mk_fileclient():
+ '''
+ Create a file client and add it to the context.
+ '''
+ if 'cp.fileclient' not in __context__:
+ __context__['cp.fileclient'] = \
+ salt.fileclient.get_file_client(__opts__)
@isbm
isbm Jul 6, 2016 Contributor

BTW, this can be one line. No need to enforce 80 characters in Salt.

@thatch45
thatch45 Jul 6, 2016 Member

yes, this can be one line

@dmacvicar
dmacvicar Jul 13, 2016 Contributor

Fixed.
As I showed @isbm, main problem is that running pylint --rcfile=.testing.pylintrc --disable=W1307 salt/modules/dockerng does not have the same behavior as what I see in Jenkins.

@isbm isbm commented on the diff Jul 6, 2016
salt/modules/dockerng.py
+ chunks, refs, pillar=pillar)
+ return trans_tar
+
+
+def _compile_state(mods=None, saltenv='base'):
+ '''
+ Generates the chunks of lowdata from the list of modules
+ '''
+ st_ = HighState(__opts__)
+
+ high_data, errors = st_.render_highstate({saltenv: mods})
+ high_data, ext_errors = st_.state.reconcile_extend(high_data)
+ errors += ext_errors
+ errors += st_.state.verify_high(high_data)
+ if errors:
+ return errors
@isbm
isbm Jul 6, 2016 edited Contributor

Up to you though, but usually I try always avoid += on strings, replacing by:

errors = ' '.join([errors, ext_errors, st_.state.verify_high(high_data)]).strip()
if errors:
    return errors

And so on. But exactly this part isn't very much performant, so you can leave it as is. At least it saves codelines. πŸ˜‰

@thedrow
thedrow Jul 8, 2016 Contributor

I think that joining will be slower than +=. Also whenever you do something like this, use tuples.

@isbm
isbm Jul 8, 2016 edited Contributor

@thedrow Actually, I just wrote a little script to test both, getting meaningful numbers. Joining is about 3x times faster. So it didn't changed since Python 1.5 version πŸ˜‰

@isbm isbm commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+
+ .. code-block:: bash
+
+ salt myminion dockerng.call test.ping
+
+ salt myminion test.arg arg1 arg2 key1=val1
+
+ The container does not need to have Salt installed, but Python
+ is required.
+
+ .. versionadded:: Carbon
+
+ '''
+ # put_archive reqires the path to exist
+ ret = __salt__['dockerng.run_all'](name, 'mkdir -p /tmp/salt_thin')
+ if ret['retcode'] != 0:
@isbm
isbm Jul 6, 2016 Contributor

Nitpick: in this case != 0 is only an explicit self-documentation "Not Zero" for a code reader. But technically you can say just:

if ret['retcode']:
    ....

To a Python reader it means "something non-False is in the "retcode" section".

@isbm isbm and 2 others commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+
+ # move salt into the container
+ thin_path = salt.utils.thin.gen_thin(__opts__['cachedir'])
+ with io.open(thin_path, 'rb') as file:
+ _client_wrapper('put_archive', name, '/tmp/salt_thin', file)
+
+ salt_argv = [
+ 'python',
+ '/tmp/salt_thin/salt-call',
+ '--retcode-passthrough',
+ '--local',
+ '--out', 'json',
+ '-l', 'quiet',
+ '--',
+ function
+ ] + list(args) + ['{0}={1}'.format(key, value) for (key, value) in kwargs.items() if not key.startswith('__')]
@isbm
isbm Jul 6, 2016 edited Contributor

Few things:

  1. Here args already a list.
  2. Use six.iteritems instead of dict.items(): https://pythonhosted.org/six/#six.iteritems
@dmacvicar
dmacvicar Jul 6, 2016 Contributor

args is a tuple, I converted it to list because I got an error from the interpreter.

@isbm
isbm Jul 6, 2016 Contributor

Ah, right. Immutable.

@s0undt3ch
s0undt3ch Jul 7, 2016 Member

And use six.iteritems() since obj.iteritems() is not python 3 compatible.
Also, .items() is not wrong, it just uses more memory under python 2(On python 3 its equivalent to obj.iteritems().

@isbm isbm commented on the diff Jul 6, 2016
salt/modules/dockerng.py
+ except ValueError:
+ return {'result': False,
+ 'comment': 'Can\'t parse container command output'}
+
+
+def sls(name, mods=None, saltenv='base', **kwargs):
+ '''
+ Apply the highstate defined by the specified modules.
+
+ For example, if your master defines the states ``web`` and ``rails``, you
+ can apply them to a container:
+ states by doing:
+
+ .. code-block:: bash
+
+ salt myminion dockerng.sls compassionate_mirzakhani mods=rails,web
@isbm
isbm Jul 6, 2016 Contributor

πŸ˜‰

@isbm isbm commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+ can apply them to a container:
+ states by doing:
+
+ .. code-block:: bash
+
+ salt myminion dockerng.sls compassionate_mirzakhani mods=rails,web
+
+ The container does not need to have Salt installed, but Python
+ is required.
+
+ .. versionadded:: Carbon
+ '''
+ if mods is not None:
+ mods = [x.strip() for x in mods.split(',')]
+ else:
+ mods = []
@isbm
isbm Jul 6, 2016 edited Contributor

This can be one line:

mods = [item.strip() for item in mods.split(',')] if mods else []
@isbm isbm and 1 other commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+ mods = [x.strip() for x in mods.split(',')]
+ else:
+ mods = []
+
+ # gather grains from the container
+ grains = __salt__['dockerng.call'](name, function='grains.items')
+
+ # compile pillar with container grains
+ pillar = _gather_pillar(saltenv, {}, grains)
+
+ trans_tar = _prepare_trans_tar(mods=mods, saltenv=saltenv, pillar=pillar)
+ ret = None
+ try:
+ trans_tar_sha256 = salt.utils.get_hash(trans_tar, 'sha256')
+ __salt__['dockerng.copy_to'](name, trans_tar,
+ '/tmp/salt_state.tgz',
@isbm
isbm Jul 6, 2016 Contributor

Here I would suggest to use a random prefix in the archive name.

@thatch45
thatch45 Jul 6, 2016 Member

subdir again, this is almost as bad as downloading code from the internet without validation and then executing it ;)

@dmacvicar dmacvicar and 1 other commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+
+ The container does not need to have Salt installed, but Python
+ is required.
+
+ .. versionadded:: Carbon
+ '''
+ if mods is not None:
+ mods = [x.strip() for x in mods.split(',')]
+ else:
+ mods = []
+
+ # gather grains from the container
+ grains = __salt__['dockerng.call'](name, function='grains.items')
+
+ # compile pillar with container grains
+ pillar = _gather_pillar(saltenv, {}, grains)
@dmacvicar
dmacvicar Jul 6, 2016 Contributor

I think this call is wrong now that I changed the signature. I think I have to pass **grainshere to convert the dict to keyword arguments.

@isbm
isbm Jul 6, 2016 edited Contributor

Well, yes. If you have something like:

def foo(*args, **kwargs): pass

Then you supposed to call it this way:

foo(*['some', 'stuff'], **{'here': 'after'})

The only limitation here that keywords in kwargs must be always strings.

@isbm isbm and 1 other commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+
+ .. code-block:: bash
+
+ salt myminion dockerng.build_image imgname mods=rails,web
+
+ The base image does not need to have Salt installed, but Python
+ is required.
+
+ .. versionadded:: Carbon
+ '''
+
+ # start a new container
+ ret = __salt__['dockerng.create'](image=base,
+ cmd='/usr/bin/sleep infinity',
+ interactive=True, tty=True)
+ id = ret['Id']
@isbm
isbm Jul 6, 2016 edited Contributor

JFYI: id is a built-in function and you are over-writing it. On the other hand you are unlikely will need it.

@thatch45
thatch45 Jul 6, 2016 Member

our lint checks should get mad about this one, we often just use the var id_

@thatch45 thatch45 and 1 other commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+ Executes a salt function inside a container
+
+ .. code-block:: bash
+
+ salt myminion dockerng.call test.ping
+
+ salt myminion test.arg arg1 arg2 key1=val1
+
+ The container does not need to have Salt installed, but Python
+ is required.
+
+ .. versionadded:: Carbon
+
+ '''
+ # put_archive reqires the path to exist
+ ret = __salt__['dockerng.run_all'](name, 'mkdir -p /tmp/salt_thin')
@thatch45
thatch45 Jul 6, 2016 Member

this needs to go in a random prefix subdir, otherwise this gets us a CVE

@dmacvicar
dmacvicar Jul 7, 2016 Contributor

I thought about that but I could not think of the attack. The user executing the stuff into the container is the same root user of the host. Yes, you could escalate if you already broke into the root account. Can you explain me the attack?

In any case, we can make it a random subdir, but we have to think how "caching" work in this regard. dockerng.copy_to keeps the thin there if it was already there, so subsequent commands will not copy it again.

Would also storing the random path in the module state (is there any state? like context?) work?

@thatch45 thatch45 commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+
+ .. versionadded:: Carbon
+
+ '''
+ # put_archive reqires the path to exist
+ ret = __salt__['dockerng.run_all'](name, 'mkdir -p /tmp/salt_thin')
+ if ret['retcode'] != 0:
+ return {'result': False, 'comment': ret['stderr']}
+
+ if function is None:
+ raise CommandExecutionError('Missing function parameter')
+
+ # move salt into the container
+ thin_path = salt.utils.thin.gen_thin(__opts__['cachedir'])
+ with io.open(thin_path, 'rb') as file:
+ _client_wrapper('put_archive', name, '/tmp/salt_thin', file)
@thatch45
thatch45 Jul 6, 2016 Member

Again, needs to be in a random named subdir, and that subdir can't be world writable

@thatch45 thatch45 commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+ # put_archive reqires the path to exist
+ ret = __salt__['dockerng.run_all'](name, 'mkdir -p /tmp/salt_thin')
+ if ret['retcode'] != 0:
+ return {'result': False, 'comment': ret['stderr']}
+
+ if function is None:
+ raise CommandExecutionError('Missing function parameter')
+
+ # move salt into the container
+ thin_path = salt.utils.thin.gen_thin(__opts__['cachedir'])
+ with io.open(thin_path, 'rb') as file:
+ _client_wrapper('put_archive', name, '/tmp/salt_thin', file)
+
+ salt_argv = [
+ 'python',
+ '/tmp/salt_thin/salt-call',
@thatch45 thatch45 commented on an outdated diff Jul 6, 2016
salt/modules/dockerng.py
+
+ # compile pillar with container grains
+ pillar = _gather_pillar(saltenv, {}, grains)
+
+ trans_tar = _prepare_trans_tar(mods=mods, saltenv=saltenv, pillar=pillar)
+ ret = None
+ try:
+ trans_tar_sha256 = salt.utils.get_hash(trans_tar, 'sha256')
+ __salt__['dockerng.copy_to'](name, trans_tar,
+ '/tmp/salt_state.tgz',
+ exec_driver='nsenter',
+ overwrite=True)
+
+ # Now execute the state into the container
+ ret = __salt__['dockerng.call'](name, function='state.pkg',
+ args=['/tmp/salt_state.tgz',
@thatch45
thatch45 Jul 6, 2016 Member

random subdir

@thatch45
Member
thatch45 commented Jul 6, 2016

so the thing with the random subdirs is because an unprivileged process in the container could modify the code that exists in /tmp before it is executed by salt, then their code would run with salt's privs, therefore allowing someone to steal the container from an unprivileged process.

@dmacvicar dmacvicar pylint: E1121(too-many-function-args)
286ffd1
@dmacvicar
Contributor

Ok. I think the problem is not that the unprivileged user modifies the code but that the code exists before, as copy_to by default not overwrite it.

In any case I think there is no point in caching as we have to prevent the thin to be committed into any intermediate image, so I will have to copy the thin on every command. This will be inefficient but I don't have an alternative for now.

@s0undt3ch
Member

Mount and unmount a partition which only has the the thin?

@dmacvicar
Contributor
dmacvicar commented Jul 7, 2016 edited

@s0undt3ch my original prototype used docker build API with a generated Dockerfile, and docker build this not support mounting.

Then I switched to starting a container myself, and the initial version did a mount. For building images I could mount the partition at the start, but when I refactored the ability to run states and modules directly in running containers (nice idea from @thatch45 ), Docker does not allow to add volume mounts to running containers, then I changed the approach to copying.

@thedrow
Contributor
thedrow commented Jul 7, 2016

You can stop the container and start it with a mounted volume.
@thatch45 Lemme review all this during the weekend please :)

@thedrow
Contributor
thedrow commented Jul 7, 2016

How do you run a base.sls file with this and does this commit the container whenever a state is successfully applied?

@thatch45
Member
thatch45 commented Jul 7, 2016 edited

@dmacvicar
Here are the CWEs:
https://cwe.mitre.org/data/definitions/377.html
https://cwe.mitre.org/data/definitions/379.html
https://cwe.mitre.org/data/definitions/378.html

Basically stated, whether an attack can be illustrated or not the use of predictable directories in /tmp due to the perms on /tmp (1777) would allow said predictable directory to be created before by a malicious attacker and files inserted, even if we delete the directory before code deployment there is still logically enough time for the attack to take place.

So basically said, weather we can illustrate a security vulnerability or not, this would be presented as a vulnerability, so we might as well avoid it.

We do have code in salt-ssh that creates predictable (by us and not an attacker by using data from the target and the originator) to determine the directory, but given the fact that this is all local IO I am far less worried about implementing perfect cache coherency like salt-ssh has.

So all in all, I think that the right thing to do is copy into a random temp dir for now, since that does allow us to modify running containers and can be done securely. Basically, it is an easy thing to do that saves us the risk of any issues :)

@thatch45
Member
thatch45 commented Jul 7, 2016

@thedrow I don't think this will be merged before the weekend is over, so you should be fine. My main argument here with restarting the container is that we would like to run container scans etc., while the container is running. This would allow us to do things like execute hubblestack on running salt containers and present security reports on the environments of said containers (as one simple example). So I am hesitant to change this from the approach of copying in.

@dmacvicar
Contributor
dmacvicar commented Jul 7, 2016 edited

I will fix all these issues over the weekend or next week. Also I want to
investigate if there are more options.

@cachedout
Contributor

I had to do the following to get this to work:

diff --git a/salt/modules/dockerng.py b/salt/modules/dockerng.py
index 76bcd9c..23274a4 100644
--- a/salt/modules/dockerng.py
+++ b/salt/modules/dockerng.py
@@ -5563,7 +5563,7 @@ def _prepare_trans_tar(mods=None, saltenv='base', pillar=None):
     _mk_fileclient()
     trans_tar = salt.client.ssh.state.prep_trans_tar(
         __context__['cp.fileclient'],
-        chunks, refs, pillar=pillar)
+        chunks, refs, pillar=pillar, id_=__opts__['id'])
     return trans_tar


@@ -5706,9 +5706,9 @@ def sls(name, mods=None, saltenv='base', **kwargs):
                                      overwrite=True)

         # Now execute the state into the container
-        ret = __salt__['dockerng.call'](name, function='state.pkg',
-                                        args=['/tmp/salt_state.tgz',
-                                              trans_tar_sha256, 'sha256'])
+        ret = __salt__['dockerng.call'](name, 'state.pkg',
+                                        '/tmp/salt_state.tgz',
+                                              trans_tar_sha256, 'sha256')

         # set right exit code if any state failed
         __context__['retcode'] = 0

Once it did though, it's totally awesome!

@thedrow thedrow commented on the diff Jul 8, 2016
salt/modules/dockerng.py
+ if 'cp.fileclient' not in __context__:
+ __context__['cp.fileclient'] = \
+ salt.fileclient.get_file_client(__opts__)
+
+
+def _prepare_trans_tar(mods=None, saltenv='base', pillar=None):
+ '''
+ Prepares a self contained tarball that has the state
+ to be applied in the container
+ '''
+ chunks = _compile_state(mods, saltenv)
+ # reuse it from salt.ssh, however this function should
+ # be somewhere else
+ refs = salt.client.ssh.state.lowstate_file_refs(chunks)
+ _mk_fileclient()
+ trans_tar = salt.client.ssh.state.prep_trans_tar(
@thedrow
thedrow Jul 8, 2016 Contributor

Why do we use ssh instead of docker's CP? I don't think requiring an SSH agent on the container is reasonable.

@dmacvicar
dmacvicar Jul 8, 2016 Contributor

We are not using ssh. We are only reusing some salt-ssh utility functions to prepare the pre-compiled state. We do use docker cp functions to put that pre-compilled state/pillar bundle and salt-thin. Those salt-ssh functions could be factored out, but that is out of the scope of this PR.

@dmacvicar
dmacvicar Jul 8, 2016 Contributor

@cachedout Sorry I refactored the signature of dockerng.call and did not commit everything. Fixed now.

@cachedout
cachedout Jul 8, 2016 Contributor

@dmacvicar Figured as much. I'm going to keep testing this today. It's really cool!

@thedrow thedrow commented on an outdated diff Jul 8, 2016
salt/modules/dockerng.py
+ ret = None
+ try:
+ trans_tar_sha256 = salt.utils.get_hash(trans_tar, 'sha256')
+ __salt__['dockerng.copy_to'](name, trans_tar,
+ '/tmp/salt_state.tgz',
+ exec_driver='nsenter',
+ overwrite=True)
+
+ # Now execute the state into the container
+ ret = __salt__['dockerng.call'](name, function='state.pkg',
+ args=['/tmp/salt_state.tgz',
+ trans_tar_sha256, 'sha256'])
+
+ # set right exit code if any state failed
+ __context__['retcode'] = 0
+ for _, state in ret.iteritems():
@thedrow
thedrow Jul 8, 2016 Contributor

Why not just six.itervalues?

@thedrow thedrow commented on an outdated diff Jul 8, 2016
salt/modules/dockerng.py
+ trans_tar_sha256 = salt.utils.get_hash(trans_tar, 'sha256')
+ __salt__['dockerng.copy_to'](name, trans_tar,
+ '/tmp/salt_state.tgz',
+ exec_driver='nsenter',
+ overwrite=True)
+
+ # Now execute the state into the container
+ ret = __salt__['dockerng.call'](name, function='state.pkg',
+ args=['/tmp/salt_state.tgz',
+ trans_tar_sha256, 'sha256'])
+
+ # set right exit code if any state failed
+ __context__['retcode'] = 0
+ for _, state in ret.iteritems():
+ if not state['result']:
+ __context__['retcode'] = 1
@thedrow
thedrow Jul 8, 2016 edited Contributor

I think you can write this in a one liner:

__context__['retcode'] = 1 if any(not state['result'] for state in six.itervalues(ret)) else 0

Even if you don't, add a break to that loop.

@thedrow thedrow and 2 others commented on an outdated diff Jul 8, 2016
salt/modules/dockerng.py
+ '/tmp/salt_state.tgz',
+ exec_driver='nsenter',
+ overwrite=True)
+
+ # Now execute the state into the container
+ ret = __salt__['dockerng.call'](name, function='state.pkg',
+ args=['/tmp/salt_state.tgz',
+ trans_tar_sha256, 'sha256'])
+
+ # set right exit code if any state failed
+ __context__['retcode'] = 0
+ for _, state in ret.iteritems():
+ if not state['result']:
+ __context__['retcode'] = 1
+ finally:
+ os.remove(trans_tar)
@thedrow
thedrow Jul 8, 2016 Contributor

What would happen here if you get a filesystem error for some reason? Let's log it and fail the run.

@dmacvicar
dmacvicar Jul 8, 2016 Contributor

Would't that just end in a Exception that would blow the whole call and be translated into a CommandExecutionError?

@isbm
isbm Jul 8, 2016 Contributor

You will get an OSError. Then you will get a traceback in logs all around. CLI won't look nice for this.

dmacvicar added some commits Jul 8, 2016
@dmacvicar dmacvicar use a random path for the thin and clean it up after the call
0459d1d
@dmacvicar dmacvicar adapt to new dockerng.call signature
9a168f0
@dmacvicar dmacvicar use a random path for the trans and clean it up after the call
a8c2c2d
@dmacvicar dmacvicar fix function name in docs
85dc070
@dmacvicar dmacvicar rename id to id_
3dfeee8
@dmacvicar dmacvicar fix trans tar path
5b14f84
@dmacvicar
Contributor

@thedrow What do you mean with base.sls? There are 3 functions:

  • dockerng.call : just executes a salt module into a running container
  • dockerng.sls : from a list of sls modules, calculates the highstate, and executes it into a running container (taking care of pillar data and grains). It does not commit, you can do that with dockerng.commit.
  • dockerng.sls_build : from a list of sls modules, a base image and a new image name, creates a new image that results of applying the state to the base image. This commits the new image and tags it with the given name.
dmacvicar added some commits Jul 8, 2016
@dmacvicar dmacvicar turn loop into one liner 080af09
@dmacvicar dmacvicar handle os.remove exception
38c6f73
@dmacvicar dmacvicar turn if into a one-liner
0ee15a1
@dmacvicar
Contributor

I think all feedback until now is incorporated. Feel free to point to more issues. Some stuff that can be reviewed:

  • handling of environments, pillar overrides.
@cachedout cachedout and 1 other commented on an outdated diff Jul 8, 2016
salt/modules/dockerng.py
+ 'salt.dockerng.{0}'.format(uuid.uuid4().hex[:6]))
+
+
+def _prepare_trans_tar(mods=None, saltenv='base', pillar=None):
+ '''
+ Prepares a self contained tarball that has the state
+ to be applied in the container
+ '''
+ chunks = _compile_state(mods, saltenv)
+ # reuse it from salt.ssh, however this function should
+ # be somewhere else
+ refs = salt.client.ssh.state.lowstate_file_refs(chunks)
+ _mk_fileclient()
+ trans_tar = salt.client.ssh.state.prep_trans_tar(
+ __context__['cp.fileclient'],
+ chunks, refs, pillar=pillar)
@cachedout
cachedout Jul 8, 2016 Contributor

We still need an id passed in here. Otherwise we're getting this:

AttributeError: 'NoneType' object has no attribute 'startswith'
Traceback (most recent call last):
  File "/usr/bin/salt-call", line 11, in <module>
    salt_call()
  File "/home/mp/devel/salt/salt/scripts.py", line 352, in salt_call
    client.run()
  File "/home/mp/devel/salt/salt/cli/call.py", line 58, in run
    caller.run()
  File "/home/mp/devel/salt/salt/cli/caller.py", line 134, in run
    ret = self.call()
  File "/home/mp/devel/salt/salt/cli/caller.py", line 197, in call
    ret['return'] = func(*args, **kwargs)
  File "/home/mp/devel/salt/salt/modules/dockerng.py", line 5780, in sls_build
    __salt__['dockerng.sls'](id_, mods, saltenv, **kwargs)
  File "/home/mp/devel/salt/salt/modules/dockerng.py", line 5711, in sls
    trans_tar = _prepare_trans_tar(mods=mods, saltenv=saltenv, pillar=pillar)
  File "/home/mp/devel/salt/salt/modules/dockerng.py", line 5573, in _prepare_trans_tar
    chunks, refs, pillar=pillar)
  File "/home/mp/devel/salt/salt/client/ssh/state.py", line 148, in prep_trans_tar
    cachedir = os.path.join('salt-ssh', id_)
  File "/usr/lib/python2.7/posixpath.py", line 68, in join
    if b.startswith('/'):
AttributeError: 'NoneType' object has no attribute 'startswith'
Traceback (most recent call last):
  File "/usr/bin/salt-call", line 11, in <module>
    salt_call()
  File "/home/mp/devel/salt/salt/scripts.py", line 352, in salt_call
    client.run()
  File "/home/mp/devel/salt/salt/cli/call.py", line 58, in run
    caller.run()
  File "/home/mp/devel/salt/salt/cli/caller.py", line 134, in run
    ret = self.call()
  File "/home/mp/devel/salt/salt/cli/caller.py", line 197, in call
    ret['return'] = func(*args, **kwargs)
  File "/home/mp/devel/salt/salt/modules/dockerng.py", line 5780, in sls_build
    __salt__['dockerng.sls'](id_, mods, saltenv, **kwargs)
  File "/home/mp/devel/salt/salt/modules/dockerng.py", line 5711, in sls
    trans_tar = _prepare_trans_tar(mods=mods, saltenv=saltenv, pillar=pillar)
  File "/home/mp/devel/salt/salt/modules/dockerng.py", line 5573, in _prepare_trans_tar
    chunks, refs, pillar=pillar)
  File "/home/mp/devel/salt/salt/client/ssh/state.py", line 148, in prep_trans_tar
    cachedir = os.path.join('salt-ssh', id_)
  File "/usr/lib/python2.7/posixpath.py", line 68, in join
    if b.startswith('/'):
AttributeError: 'NoneType' object has no attribute 'startswith'
@dmacvicar
dmacvicar Jul 8, 2016 Contributor

@cachedout what do you mean by "here" and how do I reproduce this?

@cachedout
cachedout Jul 8, 2016 Contributor

This was my command: sudo salt-call --local -ldebug dockerng.sls_build base=centos vim mods=demo. It seem to blow up if prep_trans_tar isn't given an id kwarg.

@dmacvicar
dmacvicar Jul 8, 2016 Contributor

I do the same. Can you share your demo module?

@cachedout
cachedout Jul 10, 2016 Contributor
  test.show_notification:
    - text: 'Notify me'
@thedrow
Contributor
thedrow commented Jul 9, 2016

I meant an sls file that acts like a highstate but for containers.

@dmacvicar
Contributor

@thedrow a highstate is at the end resolved as a list of sls modules, so I guess this could be added as a layer on top, just like the functions dockerng.call dockerng.sls and dockerng.sls_buildare layered one on top of another.

I played with some ideas around it, but none worked very well, but a dockerng.highstateshould be entirely possible.

dmacvicar added some commits Jul 12, 2016
@dmacvicar dmacvicar improve error handling to be more consistent
1e2f14f
@dmacvicar dmacvicar test that fix /tmp is not used
1f77336
@dmacvicar dmacvicar typo
cfdb1c3
@dmacvicar dmacvicar pylint: W1699(incompatible-py3-code)
b60f76b
@dmacvicar
Contributor

@cachedout I am still unable to reproduce the id_ problem.

My setup is Salt master+minion with Vagrant, Salt 2015.08 and the dockerng plugin symlinked into the _modules of the Vagrant shared folder.

How is your environment setup?

I would like to reproduce it to make sure there are no other issues of missing parameters as well.

@dmacvicar dmacvicar do not crash on prep_trans_tar because of missing id_ used to build c…
…achedir
0c52686
@dmacvicar
Contributor

@cachedout reproduced and fixed. It only happens on 2016.x.

@dmacvicar dmacvicar no need to break line here
a96a4d7
@dmacvicar
Contributor

This should be more or less ready for merging.

@dmacvicar
Contributor

As next steps (future work) I suggest:

  • creating an equivalent state functions like dockerng.image_present

May be reuse the same function and instead of

myuser/myimage:mytag:
  dockerng.image_present:
    - build: /home/myuser/docker/myimage

also allow

myuser/myimage:mytag:
  dockerng.image_present:
    - mods:
      - foo
   - base: leap

(I thought about having sls_build in the same function but I was not convinced that the amount of possible combinations of parameters was worth it, feedback welcome)

@cachedout cachedout merged commit b11fd8e into saltstack:develop Jul 15, 2016

1 of 5 checks passed

default Build finished.
Details
jenkins/PR/salt-pr-linode-ubuntu14-n Pull Requests Β» Salt Linode Ubuntu14.04 #2992 β€” FAILURE
Details
jenkins/PR/salt-pr-lint-n Pull Requests Β» Salt PR - Code Lint #3284 β€” FAILURE
Details
jenkins/PR/salt-pr-rs-cent7-n Pull Requests Β» Salt PR - RS CentOS 7 #3431 β€” FAILURE
Details
jenkins/PR/salt-pr-clone Pull Requests Β» Salt PR - Clone #3349 β€” SUCCESS
Details
@cachedout
Contributor

Merged! Thanks @dmacvicar. This is really great.

@vutny vutny commented on the diff Jul 28, 2016
salt/modules/dockerng.py
+ pillar=pillar_override,
+ pillarenv=pillarenv
+ )
+ ret = pillar.compile_pillar()
+ if pillar_override and isinstance(pillar_override, dict):
+ ret.update(pillar_override)
+ return ret
+
+
+def call(name, function, *args, **kwargs):
+ '''
+ Executes a salt function inside a container
+
+ .. code-block:: bash
+
+ salt myminion dockerng.call test.ping
@vutny
vutny Jul 28, 2016 Contributor

@dmacvicar It should be like this or maybe I figured it out wrong way:

salt myminion dockerng.call containter_name test.ping

I think container name or hash argument (name) has missed.

@vutny
Contributor
vutny commented Jul 28, 2016

@dmacvicar This is super cool awesome stuff! Thanks!
But it would be really helpful to have some tutorial with examples on docs.saltstack.com about how to use these new dockerng functions.

@thedrow
Contributor
thedrow commented Jul 29, 2016

I think we need to add a new section to the to the tutorials page.

@thatch45
Member

I agree, having a tutorial helps usage immensely. I will get that ball rolling

@dmacvicar dmacvicar deleted the unknown repository branch Jul 30, 2016
@vutny
Contributor
vutny commented Aug 1, 2016 edited

At least some short intro at the top docstring of dockerng module would be great.

@dmacvicar I left an in-line comment above: #34484 (comment)

Could you please take a look?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment