From 11f1c52b71462d54f25fde235e142d899a186120 Mon Sep 17 00:00:00 2001 From: Laurent Vallar Date: Tue, 5 Feb 2019 15:57:32 +0100 Subject: [PATCH] add Password-Store Signed-off-by: Laurent Vallar --- .gitignore | 1 + .kitchen.yml | 47 +++++++-- Dockerfile | 1 + README.md | 2 +- ansible.cfg | 11 +++ group_vars/all | 6 ++ roles/password_store/tasks/decrypt.yml | 7 ++ roles/password_store/tasks/main.yml | 15 +++ roles/password_store/tasks/pull.yml | 54 +++++++++++ roles/password_store/tasks/push.yml | 16 ++++ site.yml | 6 +- spec/fixtures/gnupg/gpg-agent.conf | 2 + spec/fixtures/gnupg/gpg.conf | 90 ++++++++++++++++++ ...686BA4FE02B6ECFF7BEEE0841906A275A7FA23.rev | 37 +++++++ ...02F081342ACAD356F22226969A02C444CD08EF.key | Bin 0 -> 1873 bytes ...323C5D678EEEE0AA9884FFA4A131165FFCF493.key | Bin 0 -> 1874 bytes ...799EC96687C2B5E2D5B82965A46EEC6D5BFD4E.key | Bin 0 -> 1873 bytes ...FAB5E1FD892D7942FD5C5C9D9099B253BB263C.key | Bin 0 -> 1874 bytes spec/fixtures/gnupg/pubring.kbx | Bin 0 -> 5405 bytes spec/fixtures/gnupg/random_seed | Bin 0 -> 600 bytes spec/fixtures/gnupg/tofu.db | Bin 0 -> 49152 bytes spec/fixtures/gnupg/trustdb.gpg | Bin 0 -> 1280 bytes spec/fixtures/password-store/.gpg-id | 1 + spec/fixtures/password-store/foo/bar.gpg | Bin 0 -> 601 bytes spec/fixtures/password-store/foo/baz.gpg | Bin 0 -> 601 bytes spec/fixtures/password-store/foo/quux.gpg | 2 + spec/fixtures/password-store/foo/qux.gpg | Bin 0 -> 604 bytes spec/kitchen/playbooks/ansible.cfg | 1 + spec/kitchen/playbooks/group_vars/all | 6 ++ .../host_vars/password-store-sandbox.local | 26 +++++ spec/kitchen/playbooks/password-store.yml | 17 ++++ spec/kitchen/playbooks/roles | 1 + spec/roles/password_store/push_spec.rb | 24 +++++ spec/spec_helper.rb | 32 +++++++ spec/support/current_host_vars.rb | 15 +++ spec/support/run_with_host_vars.rb | 12 +++ 36 files changed, 422 insertions(+), 10 deletions(-) create mode 100644 ansible.cfg create mode 100644 group_vars/all create mode 100644 roles/password_store/tasks/decrypt.yml create mode 100644 roles/password_store/tasks/main.yml create mode 100644 roles/password_store/tasks/pull.yml create mode 100644 roles/password_store/tasks/push.yml create mode 100644 spec/fixtures/gnupg/gpg-agent.conf create mode 100644 spec/fixtures/gnupg/gpg.conf create mode 100644 spec/fixtures/gnupg/openpgp-revocs.d/F0686BA4FE02B6ECFF7BEEE0841906A275A7FA23.rev create mode 100644 spec/fixtures/gnupg/private-keys-v1.d/1002F081342ACAD356F22226969A02C444CD08EF.key create mode 100644 spec/fixtures/gnupg/private-keys-v1.d/2C323C5D678EEEE0AA9884FFA4A131165FFCF493.key create mode 100644 spec/fixtures/gnupg/private-keys-v1.d/55799EC96687C2B5E2D5B82965A46EEC6D5BFD4E.key create mode 100644 spec/fixtures/gnupg/private-keys-v1.d/CBFAB5E1FD892D7942FD5C5C9D9099B253BB263C.key create mode 100644 spec/fixtures/gnupg/pubring.kbx create mode 100644 spec/fixtures/gnupg/random_seed create mode 100644 spec/fixtures/gnupg/tofu.db create mode 100644 spec/fixtures/gnupg/trustdb.gpg create mode 100644 spec/fixtures/password-store/.gpg-id create mode 100644 spec/fixtures/password-store/foo/bar.gpg create mode 100644 spec/fixtures/password-store/foo/baz.gpg create mode 100644 spec/fixtures/password-store/foo/quux.gpg create mode 100644 spec/fixtures/password-store/foo/qux.gpg create mode 120000 spec/kitchen/playbooks/ansible.cfg create mode 100644 spec/kitchen/playbooks/group_vars/all create mode 100644 spec/kitchen/playbooks/host_vars/password-store-sandbox.local create mode 100644 spec/kitchen/playbooks/password-store.yml create mode 120000 spec/kitchen/playbooks/roles create mode 100644 spec/roles/password_store/push_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/support/current_host_vars.rb create mode 100644 spec/support/run_with_host_vars.rb diff --git a/.gitignore b/.gitignore index f94fa1b..cf03265 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *~ +*.retry /.*build /.bundle /.kitchen diff --git a/.kitchen.yml b/.kitchen.yml index bbde77c..9c629dc 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -1,5 +1,7 @@ --- <% + require_relative 'spec/spec_helper' + ANSIBLE_CHECK_MODE = \ ENV['ANSIBLE_CHECK_MODE']&.match? /^(1|[Yy](es)?|[Tt](rue)?)$/ @@ -15,15 +17,17 @@ -o PidFile=/tmp/sshd.pid ] - kitchen_provider = ENV['KITCHEN_PROVIDER'] || 'docker' - dockerized = kitchen_provider == 'docker' - - if ENV['container'] == 'docker' || dockerized + if ENV['container'] == 'docker' || DOCKERIZED require_relative 'spec/kitchen/docker/monkey_patches' + + kitchen_docker_id_rsa = ROOT / '.kitchen/docker_id_rsa' + if File.exists?(kitchen_docker_id_rsa) + FileUtils.chmod(0o600, kitchen_docker_id_rsa) + end end %> driver: - name: <%= kitchen_provider %> + name: <%= KITCHEN_PROVIDER %> require_chef_omnibus: false platform: debian image: debian:stretch @@ -32,7 +36,7 @@ driver: http_proxy: <%= ENV['HTTP_PROXY'] %> https_proxy: <%= ENV['HTTP_PROXY'] %> <% end %> - <% if dockerized %> + <% if DOCKERIZED %> run_options: env: container=docker stop-signal: SIGRTMIN+3 @@ -56,7 +60,7 @@ driver: systemd && apt-get clean && apt-get -y autoremove - && curl -q -o /tmp/systemctl <%= SYSTEMCTL_REPLACEMENT_URL %> + && curl -q -o /tmp/systemctl https://raw.githubusercontent.com/gdraheim/docker-systemctl-replacement/master/files/docker/systemctl.py && install -o root -g root -m 0755 /tmp/systemctl /bin/systemctl && rm -rf /tmp/* /var/tmp/* && find /etc/systemd/system /lib/systemd/system @@ -92,9 +96,23 @@ provisioner: idempotency_test: <%= ! ANSIBLE_CHECK_MODE %> fail_non_idempotent: true chef_bootstrap_url: nil + # verbose level v, vv, vvv, vvvv + verbose: + raw_arguments: --timeout=10 --diff platforms: - name: debian-stretch + lifecycle: + pre_converge: > + if [ -d /tmp/.<%= NAME %>_password-store ]; then + rm -rf /tmp/.<%= NAME %>_password-store ; + fi ; + if [ -d /tmp/.<%= NAME %>_gnupg ]; then + rm -rf /tmp/.<%= NAME %>_gnupg ; + fi ; + cp -a <%= PASSWORD_STORE %> /tmp/.<%= NAME %>_password-store ; + cp -a <%= GNUPG %> /tmp/.<%= NAME %>_gnupg ; + printf "\n\033[36;1m####### pre_converge_command done ########\033[0m\n\n" driver: box: debian/stretch64 box_url: https://vagrantcloud.com/debian/stretch64 @@ -112,11 +130,24 @@ suites: provisioner: custom_instance_name: default-sandbox.local extra_vars: - ansible_fqdn: default-sandbox.local ansible_check_mode: <%= ANSIBLE_CHECK_MODE %> + ansible_fqdn: default-sandbox.local verifier: inspec_tests: - spec/roles/common/sshd_spec.rb + - name: password-store + provisioner: + playbook: spec/kitchen/playbooks/password-store.yml + mygroup: password_store + custom_instance_name: password-store-sandbox.local + extra_vars: + ansible_check_mode: <%= ANSIBLE_CHECK_MODE %> + ansible_fqdn: password-store-sandbox.local + verifier: + inspec_tests: + - spec/roles/password_store/push_spec.rb + attributes: + host_vars_path: password-store-sandbox.local transport: username: <%= ENV['KITCHEN_USERNAME'] %> diff --git a/Dockerfile b/Dockerfile index 22d93ee..4da9e36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,6 +46,7 @@ man-db \ ncurses-base \ ncurses-term \ openssh-client \ +pass \ procps \ rsync \ ruby \ diff --git a/README.md b/README.md index 497942a..6884f9f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Sharing a common [Ansible](https://github.com/ansible/ansible) Provisioning with for a remote Linux/OsX team and keep most of test & CI code in repository: * [x] Ansible based provisioning -* [ ] [Password-Store](https://github.com/test-kitchen/test-kitchen) for credentials management +* [x] [Password-Store](https://github.com/test-kitchen/test-kitchen) for credentials management * [x] Testable provisioning with **Test-Kitchen** * [x] Bundler friendly (mounted `.bundle` with proper rights) * [x] custom Ruby version via `.ruby-version` file diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..eff2d5f --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,11 @@ +[defaults] +remote_tmp = $HOME/.ansible/tmp +retry_files_save_path = $HOME/.ansible/retry_files +nocows = True +deprecation_warnings = True +command_warnings = True +gathering = explicit +roles_path = roles + +[ssh_connection] +pipelining = True diff --git a/group_vars/all b/group_vars/all new file mode 100644 index 0000000..1659def --- /dev/null +++ b/group_vars/all @@ -0,0 +1,6 @@ +--- +# file: all, group variables + +ansible_managed: >- + ***** /!\ Ansible managed file, do not edit: all changes will be lost ***** +... diff --git a/roles/password_store/tasks/decrypt.yml b/roles/password_store/tasks/decrypt.yml new file mode 100644 index 0000000..f30d5e6 --- /dev/null +++ b/roles/password_store/tasks/decrypt.yml @@ -0,0 +1,7 @@ +--- +- name: Decrypt password store entries + set_fact: + password_store_decrypted: "{{ ( password_store_decrypted | default([]) ) + [ { item.name: ( lookup('passwordstore', item.src + ' returnall=true') ) } ] }}" + with_items: "{{ password_store.decrypt }}" + register: password_store_decrypted +... diff --git a/roles/password_store/tasks/main.yml b/roles/password_store/tasks/main.yml new file mode 100644 index 0000000..ae70d93 --- /dev/null +++ b/roles/password_store/tasks/main.yml @@ -0,0 +1,15 @@ +--- +- name: Password store configured fact set + set_fact: + password_store_configured: "{{ password_store_dir is defined }}" + +- debug: + var: password_store_configured + when: password_store_configured + +- include_tasks: decrypt.yml + when: password_store_configured and password_store.decrypt is defined + +- include_tasks: push.yml + when: password_store_configured and password_store.push is defined +... diff --git a/roles/password_store/tasks/pull.yml b/roles/password_store/tasks/pull.yml new file mode 100644 index 0000000..5449465 --- /dev/null +++ b/roles/password_store/tasks/pull.yml @@ -0,0 +1,54 @@ +--- +- name: Stat remote password store files + stat: + path: "{{ item.path }}" + with_items: "{{ password_store.pull }}" + register: password_store_pulled_stat_raw + +- name: Set remote password store files to be pulled fact + set_fact: + password_store_pulled_stat_dict: "{{ ( password_store_pulled_stat_dict | default({}) | combine({ item.item.dest: ( item.stat | combine({ 'dest': item.item.dest, 'force': (item.item.force | default(false)) }) ) }) ) }}" + with_items: "{{ password_store_pulled_stat_raw.results }}" + +- name: Set password store file to be pulled stat list fact + set_fact: + password_store_pulled_stat: "{{ password_store_pulled_stat_dict.values() | selectattr('exists', 'equalto', true) | list }}" + +- name: Retrieve remote password store files content to be pulled + command: "cat {{ item.path }}" + register: password_store_pulled_content + with_items: "{{ password_store_pulled_stat }}" + changed_when: false + +- name: Set remote password store files content to be pulled fact + set_fact: + password_store_pulled_content_dict: "{{ ( password_store_pulled_content_dict | default({}) | combine({ item.item.dest: { 'content': item.stdout, 'dest': item.item.dest, 'force': item.item.force } }) ) }}" + with_items: "{{ password_store_pulled_content.results }}" + +- name: Set remote password store files to be pulled fact as list + set_fact: + password_store_pulled: "{{ password_store_pulled_content_dict.values() }}" + +- name: Decrypt password store files to be pulled + set_fact: + password_store_pulled_with_original_content: "{{ ( password_store_pulled_with_original_content | default([]) ) + [ ( item | combine( { 'original_content': ( lookup('passwordstore', item.dest + ' returnall=true create=true') | string ) } ) ) ] }}" + with_items: "{{ password_store_pulled }}" + register: password_store_pulled_with_original_content + ignore_errors: Yes + +- debug: + var: password_store_pulled_with_original_content + +- name: Store remote password store files to be pull in password-store + local_action: + module: command + # module: shell + # stdin: "{{ item.content | string }}" + _raw_params: >- + sh -c '/bin/echo -en "{{ item.content }}" | pass insert {% if item.force %} -f{% endif %} -m {{ item.dest }}' + with_items: "{{ password_store_pulled_with_original_content }}" + when: item.original_content is not defined or item.content != item.original_content + vars: + display_args_to_stdout: true + become: false +... diff --git a/roles/password_store/tasks/push.yml b/roles/password_store/tasks/push.yml new file mode 100644 index 0000000..ec59fd2 --- /dev/null +++ b/roles/password_store/tasks/push.yml @@ -0,0 +1,16 @@ +--- +- name: Decrypt password store files to be pushed + set_fact: + password_store_pushed: "{{ ( password_store_pushed | default([]) ) + [ ( item | combine( { 'content': ( lookup('passwordstore', item.src + ' returnall=true') ) } ) ) ] }}" + with_items: "{{ password_store.push }}" + register: password_store_pushed + +- name: Push password store files + copy: + content: "{{ item.content | string }}" + dest: "{{ item.dest }}" + owner: "{{ item.owner | default('root') }}" + group: "{{ item.group | default('root') }}" + mode: "{{ item.mode | default('0600') }}" + with_items: "{{ password_store_pushed }}" +... diff --git a/site.yml b/site.yml index 348f970..2190f6c 100644 --- a/site.yml +++ b/site.yml @@ -4,9 +4,13 @@ gather_facts: No + vars: + password_store_dir: "{{ lookup('env', 'PASSWORD_STORE_DIR') }}" + tasks: - debug: msg: - ansible_ssh_user: "{{ ansible_ssh_user }}" + ansible_fqdn: "{{ ansible_fqdn }}" hostvars__inventory_hostname: "{{ hostvars[inventory_hostname] }}" + PASSWORD_STORE_DIR: "{{ password_store_dir }}" ... diff --git a/spec/fixtures/gnupg/gpg-agent.conf b/spec/fixtures/gnupg/gpg-agent.conf new file mode 100644 index 0000000..5ac350f --- /dev/null +++ b/spec/fixtures/gnupg/gpg-agent.conf @@ -0,0 +1,2 @@ +max-cache-ttl 60480000 +default-cache-ttl 60480000 diff --git a/spec/fixtures/gnupg/gpg.conf b/spec/fixtures/gnupg/gpg.conf new file mode 100644 index 0000000..5198234 --- /dev/null +++ b/spec/fixtures/gnupg/gpg.conf @@ -0,0 +1,90 @@ +# correct character displaying +utf8-strings +charset utf-8 +display-charset utf-8 + +# when outputting certificates, view user IDs distinctly from keys: +fixed-list-mode + +# Don’t rely on the Key ID: short-keyids are trivially spoofed; +# it's easy to create a long-keyid collision; if you care about strong key +# identifiers, you always want to see the fingerprint: +keyid-format 0xlong +#fingerprint + +# when multiple digests are supported by all recipients, choose +# the strongest one: +personal-cipher-preferences AES256 AES192 AES CAST5 +personal-digest-preferences SHA512 SHA384 SHA256 SHA224 + +# preferences chosen for new keys should prioritize stronger +# algorithms: +default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed + +# digest algorithm used to mangle the passphrases for symmetric encryption. +s2k-digest-algo SHA512 + +# cipher algorithm for symmetric encryption with a passphrase if +# --personal-cipher-preferences and --cipher-algo are not given +s2k-cipher-algo AES256 + +# Do not use string as a comment string in cleartext signatures and ASCII +# armored messages or keys (see --armor). +no-comments + +# Do not include the version string in ASCII armored output. +no-emit-version + +# If you use a graphical environment (and even if you don't) +# you should be using an agent: (similar arguments as +# https://www.debian-administration.org/users/dkg/weblog/64) +use-agent + +# You should always know at a glance which User IDs gpg thinks +# are legitimately bound to the keys in your keyring: +verify-options show-uid-validity +list-options show-uid-validity + +# Use the sks keyserver pool, instead of one specific server, with secure +# connections. +#keyserver hkps://hkps.pool.sks-keyservers.net +#keyserver x-hkp://pool.sks-keyservers.net +keyserver x-hkp://ha.pool.sks-keyservers.net + +# Ensure that all keys are refreshed through the keyserver you have selected. +keyserver-options no-honor-keyserver-url + +# Locate the keys given as arguments +auto-key-locate keyserver + +# include an unambiguous indicator of which key made a +# signature: (see +# http://thread.gmane.org/gmane.mail.notmuch.general/3721/focus=7234) +sig-notation issuer-fpr@notations.openpgp.fifthhorseman.net=%g + +# when making an OpenPGP certification, use a stronger digest +# than the default SHA1: +cert-digest-algo SHA512 + +# The environment variable http_proxy is only used when the this option is set. +keyserver-options http-proxy + +# My default key +default-key 0x841906A275A7FA23 + +# Add cross-certification signatures to signing subkeys that may not currently have them. +require-cross-certification + +# command te see photo in keys +#photo-viewer " %i" + +# see photo in keys when listed (warning, can be annoying) +#list-options show-photos + +# see photo in keys when verifying the keys (warning can be annoying) +#verify-options show-photos + +# Fix mutt "Could not copy message" ? +#pinentry-mode loopback +#keyserver-options auto-key-retrieve +with-fingerprint diff --git a/spec/fixtures/gnupg/openpgp-revocs.d/F0686BA4FE02B6ECFF7BEEE0841906A275A7FA23.rev b/spec/fixtures/gnupg/openpgp-revocs.d/F0686BA4FE02B6ECFF7BEEE0841906A275A7FA23.rev new file mode 100644 index 0000000..ae2928a --- /dev/null +++ b/spec/fixtures/gnupg/openpgp-revocs.d/F0686BA4FE02B6ECFF7BEEE0841906A275A7FA23.rev @@ -0,0 +1,37 @@ +This is a revocation certificate for the OpenPGP key: + +pub rsa4096/0x841906A275A7FA23 2019-02-04 [S] + Key fingerprint = F068 6BA4 FE02 B6EC FF7B EEE0 8419 06A2 75A7 FA23 +uid tap4ci + +A revocation certificate is a kind of "kill switch" to publicly +declare that a key shall not anymore be used. It is not possible +to retract such a revocation certificate once it has been published. + +Use it to revoke this key in case of a compromise or loss of +the secret key. However, if the secret key is still accessible, +it is better to generate a new revocation certificate and give +a reason for the revocation. For details see the description of +of the gpg command "--generate-revocation" in the GnuPG manual. + +To avoid an accidental use of this file, a colon has been inserted +before the 5 dashes below. Remove this colon with a text editor +before importing and publishing this revocation certificate. + +:-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: This is a revocation certificate + +iQI2BCABCgAgFiEE8GhrpP4Ctuz/e+7ghBkGonWn+iMFAlxYqB8CHQAACgkQhBkG +onWn+iOGLg/+KaWnXD9cgC25IR4l2WwsV9Q0oN6qkBPFvbsYWbJlMCvmziwhUfJN +VpOe+r4qfzl//tHlBv5djgtYZWKbvf6SV+x+2BX5mQzkgjaIFGikf46itlIELc2C +3cP/PF85L8N1g9yzRx60pVrIa5m9Flri9hOAUcqMQyKWl0/1RRJaEWTtGjNXwGLX +/PA9IevXumEkj7NeDDTZ/3VB9Xl7e3NcwGaXnyQOyqAR2NKGtWxkHyvWtNcWixM9 +XTAccQW7Qn4UDFImo5VZHpttRLbdpSdkYPAxoZcoLHxzhjo2y5D3Ak+zy6AlH+XK +JrB77t4gDrl4J5q1mj/3jCATYqa/E6FWqvNH6qrXrflyHTYKGgNwho2Xv0mKYiXo +LV2I3rz+WfU5tD2JtF1a9PAtwYTvjlhX9rRDXitliEBNv2MP8n5PA8c0WWkIHXtv +jzNuK8hcyOQ5hZjhBQYwE3d7lwPur1Kuc4Ylohg6IuqQ2ntGO3/QEL6JH5YFLOs9 +3TKxO9G4HMVNkEIOVxUuCihzgkWkL5TBHlCgj9N3p1TgH9VS1mA6P4vZoKWpSAER +SnVOe3cTTMJ6UNsAzZqfGGL2pLohedcUS+NkbqSCY8yZg7ZyLrNjNurTnnuoY9bR +fitu38hsyFaeqInPQFZu2Ht8jnuy2KyN1NW5Vipqf3nGUt1Tt+C5Fdg= +=YA+N +-----END PGP PUBLIC KEY BLOCK----- diff --git a/spec/fixtures/gnupg/private-keys-v1.d/1002F081342ACAD356F22226969A02C444CD08EF.key b/spec/fixtures/gnupg/private-keys-v1.d/1002F081342ACAD356F22226969A02C444CD08EF.key new file mode 100644 index 0000000000000000000000000000000000000000..4b9612092ac7f1f2945477c33a81bf7cf596d83f GIT binary patch literal 1873 zcmV-X2d?-iF)=!Da%py9bY(4TWqBwwI&yPiC^0&2H8C?f0LuXqN>f?Jn(l@yDYDOa zl0zvwqbm-g$s5C4=2Od>A!^nX9a=b+W8pEgk|$Z&&u;?; zcp}cal3g8h-P8qE6baMp%j?;cEP7#^YJ?B1GddMv^9V^a(R%6eW}i|S?6z+yo87IK zoPh%N%GZ`M{b>?;t-oqaxQfG81ut5pCmXXoZEO3Uv%n|2Yt<*UG~vW@^A!$*5Uj$z z=(vvZE3coZlg$$tn-F`CE*s3agya+zTQ$U0o1RTlL;j1_f-djQ)TY8xz>NrWuR z9J};&YueyBD9i3_!)W^j720Mv-a{n-I(PZ@sYbc=CRi!<>b|A(@ye^Y2B5|%YJ#nN z4#UPLCx}(tfKMmmyc;E6V_pD1MzSC6e1n!_F&;8UBPYr+ugkADQm6UexC=u0PQMa3 z)ZEj7@l7cK9ldeT+*cZDZ zLU%_NfX3=3ecxp2@N#E+&2zb=(5%^uu%aUH0uCfs31KKTM2w>Wamk)5pmY= zpPJ>qxEQ2l*b$wYBTbYMui_$?>WH*7F6amzUV)bcRzfWR5)eMgUDi}4~l<*>8{1=E{aQkg5l^wKLSwe`5~(5wi15TNhh_yMDJU^Ia56PFIsnhs zcLo&953VJ_FlKBE9D_?VaW;)go=-o6SUf+BO0mT{71r% zrtq$8lYmy0rq{vw)Hx6yPI=sPhnt^7qZ5*tP`q3zTrub$i)YJ*F8|Xw27~#4P)79i z6k5o5LbDf)&2rNEG^+^#QvFl;pjp~*e0sQD|G;~N=e0Uach$`7Lc`ROUYzrHW4tHv zzh>Y%t`q*uR?EcK#}pBMoMfU+wFJwE7}Bpa$O5Qc_r}a0xU=bqtJeyRDlvNH#xMW6 z+6Cz79x8JW^+f`}cIDiBt`38fyb9!dp&vpsqBr4fG5F-4zCX7#q$=7?LRTp$F*P-IL4-|fhESqb#?2_^G)872wr(} z)UdIy5o+~aMyMFisF_zIz6js z1$wW*`&M#Jbc;)6d;JKXn|^CRPBUKh1>A!ro1<_)Ec6-04!@@0PU?1uBJA3Ney@jP zSzIxVlf+4k4p!L}B7$tA9}1?V=5X=00ktnb7bo5{?^Zj_AdeK2?1hsS?`fqc8;BWE zDJU^Ibuu+JIto0L%9RWy*5i0)kO1mjYhBGueV$SM`W76TcKW;dl+Z>sn@4WlO9hK| zz57BmPD1ix*uL|Q<(cFPUCrp(_bet;55K4@h&%4*t$=I;&WoY}&2HkpXyHp`8ip&B zVj16>-i0Kjr{T^1;4|sA#aM12Azv#e@vS!KSVdnWRKYzlUt68`{u-9-n4I6ZK^ET5 zF+8H$u;I0=Qj0TTe+PW)%!4JdH$x4lunOlyC09%AecE2%e88YJpc{D6#yM-R;BdI^`#2l9Q=K7RJEa%;?u2nWGlwHvh1 L@F}61hbbv3XHSr! literal 0 HcmV?d00001 diff --git a/spec/fixtures/gnupg/private-keys-v1.d/2C323C5D678EEEE0AA9884FFA4A131165FFCF493.key b/spec/fixtures/gnupg/private-keys-v1.d/2C323C5D678EEEE0AA9884FFA4A131165FFCF493.key new file mode 100644 index 0000000000000000000000000000000000000000..fdbd695a355efb4c4bdfb3616d361ec767b52fae GIT binary patch literal 1874 zcmV-Y2d(%hF)=!Da%py9bY(4TWqBwwI&yPiC^0&2H8C?f0J!Jgp~26`P`0=UlbB^< z3jFCjrM0&~9>l9kTpC&DF<>3J6LET_@MWkr?z_g6F`9APO3$&tH)Us2SRd;vet63L z$>!VCy?oHO7gQXhE!XfuJh}{wZH~s-*7-eS);J*yrE4I7cBWP(NcVq9a}R2Tf{Z_v zGEQkys$0{Y{q|G^I@X~5Ek@cZpX3(G3yLW32*mws%RnXo$Cs6|)s$)>@*&9+oDj)S zFj-f+F~%JDw)`PGhD~j=m2o827qxOqA(34n7C^d=Ncd5io{HsA@^ERF3qQpQpJXld z38V4D`Sx<{lnk`YiYGunGvn{gG2Yd`T#SF%y2DY^#>~Ov(y9!%HW=SVNs*mEoj-^k z5i*`!P~)KVDjkF+4k*hgb2!Md?4u|rqvBU=&2_+07dwT*XGBG!f2>t4!$yNZlq+@g z%E=b2QtdAFYX$=2(+JV;7CSt22xt+p?-sad;V-k-ZuJZcR5we#_-*Q#Jo<21+>?2% zLlTUzn>sIL0=BfW1a`r>KDi1&lrjE$9Njo0{GT3Bi1dqvK6rrwidbrVT{iymQwT@= z-lMSQtTV@{zX2F)OBDxV3gb|YecV0g6$4Hivr~AZ)&RWE>=mn>hF_cU9r8=o$eZWo znCIfmairB~&hI8%&+&plLO6U-HK#?ZnlaZ&Gbt!BI%P9D0RRChC^0%@H8Cdy>re)rw+2$(PFac;or_+UY|S}H$WhdKu}nCs#w~uRgnbt1Hhu%2ys#I zA0M~uR{rQMQXU?{*AtYmoc@0$XBsr5Etyynfm?m2E9_8MREyH}lL|m2f#&`|i8=J< znurrES=daC64i8G6Ep!a5|NvI$&2?ajZjGz6uqKUHX*FBSzLpGuXI^)84->rzP}%)-KGOb+p&p`9Ml#|VkLuIydtxE7f5z7EhPd!QYb&GnqA%-X2sjh zHz)99aWZVtdgX@RC@i(%z<`=LKbv+XnE-zX1xC&#E`o6mxDZ4M_I`YZXY@Kniu@Jb z3cc0wpl6s&|0+PMi%5asaq!|T(am(>Lp5e|&714&G+0z(3`H?{gg|*ng>E_O2%?5~ zrF`fM5!{BWjkl-Bi%M6O^^ieD*S7#{pphFK&LM;Q)KR%mNk{AfDJU^Ia56PFIsnHH z!58RS{h#eA4g!pboo$sXn9)xDv(&r4{Hyr>GYp z2LK5NypAKW%>5ZIF&Bu#*ZZkc_*B2COTn>TWSO>Ou-T)SQjrw<_R&Fe=DbXTe(|&i z5LNPTXgqVqt@s&CSy&CcX+#N2{rwK?Sde)Phir&ZGHy!_6ClVfcm+WX$pa}UF*(6hw*Z$faxS(x2G7LRqJ0K^VQlVcSb5?}A{|Mcre>5g}=pV|0ru;319&;es z2+0OBdK(2sL7Anjx1^T}$g#@@o~$aGny#O2;Ln}~wONZay>8d$o+h{FQYZuw8Ata^ z{VdjO0#0X0!Ubkx^a}N8)rY5XXI6@U%WtJ(2Q|0_G%{b)%Za`K4iUum5*1CsHwe-y zq6{+o>B83-zMlR-h;vBe_ns5@fTAMsESmmqXLYkAE}to(v*PyAI5N^HJ?OSWMbj>Q z^E(@IM3aye$9?dTtSp2G4)t25nnr4~>UyfOA@nl8Pb|_rgXg&8SDOgIM(vfO4$h~_ M#o0l3w9_dmDW`ah#sB~S literal 0 HcmV?d00001 diff --git a/spec/fixtures/gnupg/private-keys-v1.d/55799EC96687C2B5E2D5B82965A46EEC6D5BFD4E.key b/spec/fixtures/gnupg/private-keys-v1.d/55799EC96687C2B5E2D5B82965A46EEC6D5BFD4E.key new file mode 100644 index 0000000000000000000000000000000000000000..12771b41f45ad8f7b774a594bca114ccbbdfa131 GIT binary patch literal 1873 zcmV-X2d?-iF)=!Da%py9bY(4TWqBwwI&yPiC^0&2H8C?f0HT9Kfno;-#IPl_*iLl_ z1QEgNKhz-vdja~d#~h9+$@NKxGSi$i`5gMVavQ*=xl;P2viW0}DGZ0QwmWdfxs7p6 zWEFCARi-}N+4tep>8*<`x$&K027oXG-N2nNqRywMZj%EOgBU#?JC?w(mP_t~m_}81 zY^{o5((iO(IOg^}1VY@kiM7;N^6Js6#L864!g9q#a+MXAEm8h0s_ADsCDv4!ZIX=; z;n}I-n}2t63|0R~0IR3a3a@L9$5wGI^6O-FPIvj#W405OFYPo*yFZk&q10@tZgKAh zJO1t?IT(Km(uf%Y{On63KqgZ9+SqyjOKe7Xc5%-F1)x-dEYW{Jw#MIeFuZ=nS);SL zHCI;UaInh_)o%*w=%TRf&=Wy5)79XvEjXS`3obSfmpcwET#gYpS-OiYjzE7Q>?W`O zxEo^qmXGrX!^?iv7>4o|ZOpdX0VM4+u) z>_bzXKkxp-8ky3m_6|L3VXZhu)*}t>-7)++Es+;eVK!Iey3{Roz|kXTN*xbc?Z%&B zZX!nE2^2kokHfxyqr6&c-0rfyues#MFFPL`ZwZ&bh`}-{`PkfmrGsi%IAzAIK1Xt#Lx?Ns0aTV@+-&yx0i~hx2&V5|Xzv{(TJHY6 zGDeCd;>23{TdRf2$vv#w`hcRA3_WWJro0pJq|)cMJElV3#2ysn*u1mq@aSE^>`H$6 zU02O|PoC!VRB3mOe}Itnmkxs%wxxKnN|MxC-aSZ4`sJ?PWqZmL7TMbYE<23QpnL_g zdi|E>&^#7$50v0XonjT~Rrr@2QDqQ&#|e#3mh7^5g^%UH3w(vmJ4gEjIeT@7CFx~f;JGUm1#zn(?Ls4 zh9T4PBdoIo$g6TrpWBZ`Ml1H0pAabZedyPK(4P`$9;-mnq#+{b`(1Xr7uS7Cj-EA2 z7w~J|hZ)m&SB{v?j04XJkE@hMNFT1x&a z2~5m3F}m@Jt2o2F8vo;KYes2PKWUR)__klE(-ng3ssGcAn1b>?UNwg!+Dep$4M=W{Jq&LY8L3;){q zi!zG_MLQdqGOt;r z9p5DuSQX@wz3#sc0YN|pRf}u;tedbu;Dt?Q%K%|xXg(TimohjB@^jlB{keZJGbiIY zxvtx)G+W@IRZf55jpiHl$-U5eN-Jpx;J;*Oi7eETf<_Fh5JxY#`gm{P@%?e)jd_Xp zW1k=V)AM&sRPOwZFfHu}VB;xz?mwyJkjFqrz5Z+09ED<^8|B6f`iYG}w^b=9F*>W;KYxfP_#(AE&YHZD# zcZb_!2sYqLQefbElfcvCnn~g^5y}z99fkSJe+ReMAABiI2o%}ysSWIQ9Arnwaj|Ox zI|O!+4&3Xzgydu0+|ipG6apk}RD_V~7wNd>3!4BnDyoZ2JvCC+VJf1FAUA6+KN=lv zZXi0<0-6So)R|DHe$ z4n6@CsdmUh+L^|=yv$10g5`16XC<_}z+O!?R0>ZgZ(IFmiouyB9@ggN1cUiQV&Jip zKdlmhe0zmj1uWzej8Nn%K~28c48^EP=)>{Ymw!Z#=!e9t7TscBSa*KX-TP&XkEO~_i zi0#~fRhz#rNjof+Mmw4?=0L!q+T%^|w5s(7)`o)kxKdUokS5AzC||)1(m|a;z&wUk LFb?tsSt%(gEw6`I literal 0 HcmV?d00001 diff --git a/spec/fixtures/gnupg/private-keys-v1.d/CBFAB5E1FD892D7942FD5C5C9D9099B253BB263C.key b/spec/fixtures/gnupg/private-keys-v1.d/CBFAB5E1FD892D7942FD5C5C9D9099B253BB263C.key new file mode 100644 index 0000000000000000000000000000000000000000..d6328718bf29327d7ef6b40f573cd13b05808748 GIT binary patch literal 1874 zcmV-Y2d(%hF)=!Da%py9bY(4TWqBwwI&yPiC^0&2H8C?f0O}IK{#GZ>$h3s#1&R3g zncWE-+F!$`oXEIi~L7X9FL8UjX}bAj$9<`VlY_^NtLK3Q!9!f%Kj3D@;tq-+0N{RE z19&6y1}IGRpf-54>;2Bbs@cMBh-y#44w*0cBt38s8U2d5o;L0~6~_mT8~(KyD+yLO zNoij?=kI&~e+QeDWLZ6 zXx*pLKH*(xrY$lK`1eRf@tKnuwsvL((1_YvWLHr-N*OVp4M7Y90YW~1w1HD)Ib;5@ z-#{R@?CF|hmf!If4^fPDjh$UhV!v3|xEY-F0}|Fd8b7~}!Gm`-<{#aZh6q!M$^{70 zX>Ta?EBDdAT1CZ7oxb0Pz=X#KJQX!4k*M5UHn!a>i_@A&LUfL@ovW@r$u2I`zkgYQH?ZU87|rmN%xvwO!KQ|}3icXcl4 zq<&@5wRsO7zGYV&+>@k)>nK5_I2{)xwI_g@OI9PeEyJ3=Q8q0Y`S!oy)8<9SRI8a}j@4=h1&2_$h|M>sGj89{{aKM3@yz7Vh@`eb3%5FmL0r)g3`FMpRMlIe?sE zej9l?u+B(B_&n^D z_*{={#wUiA`fqD0zczHumEwvnv>4<7kN%}fNbXVfXvZ{M_k_;t3ltBHzD}xv>?wxiVq)a$`^kvJ}2>o8;yc*z4%4J})%Wmi$W)K0xiQdr$_DJU^Ia56PFIsod1 zzttjCbpcBhZkjh8Xk#-6oC*wnO||acTOTBCuM{?dQ4FXIgHtgQlOWIt&maw9iJMJk z$qXQk7MpRI-tMR&ee%!G$5e4smAw|gZjC+$s#a&U>#e+X zTkSI?Jbv)Nq%ax(4IG_EJ}Z0L*lU_$QdLD;ch355wuq^I3WXR?PJ#)h<&E_k##B#N zmVX%hdp@k8O2@zGTz9rsBRZIeC(6Hr0R68F7atc1FK|JVPsIzwEBYvzMWm8-V=Z1f z>|F-Kq`X)FKAV)MM6>yekps;b&o0}(<1$Ons3NPjWG3L78YYHh9N|J=Q7A`q!k`K*L zJqlq5<`$IOlR64T9@6C)3u9l_hdYQ$wfm}8)MJGa!#oeeywA|Iel%GbBLxCwsjOff zT)4yW`PpqGtW9*-4PF^n?c3W81q)%o%}5F>FagFUOe#E+sOLuvt#eD4O`{C_hVmz( MNgOM_w3aC;DX^(cXyW%G!Wb!0%Xbe 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 literal 0 HcmV?d00001 diff --git a/spec/fixtures/gnupg/random_seed b/spec/fixtures/gnupg/random_seed new file mode 100644 index 0000000000000000000000000000000000000000..7ac4603461fcb348dac4d158e08ceadf710273ae GIT binary patch literal 600 zcmV-e0;m0|pg0*MDOJS3pcL=J;VsxbsbmnTf*--HxZBYU+DG{dSC$1jJLKn`sV8|X z^X2a$KKzvi+yUf2>P=)lXQ4UxCH@pG-prD*Gz4$G zFXa%^h81F>h2C{)6-=Q_*$<$b@hsti{EZ9yG&g64e&$b z9mWH(sCsadZ^R({3r;Z|SF3t)gL=r6YHDSTEiavUS;TUZVzt&2ZLtE1NAc|s-1~rA zjK=(8O6ICih7%ASVpEWb-}>#ECKs8IB%$n#(S`U;mes!vt{H5NOP<$a;v<2Haz_YK z7o8UPy`8@?ze<>haVhwX`13uwQJn`D6d1R#HW7@-MThNb_B!|9t~mLp-l$=@_2_JI z_Q+(K?v5kVq4(r&(}us1>l08}sXVn@i4RN?jZe(Hcjp;JDa$^ts*bE9SA%*Jx{U2m z9mOB@`Cu^@%}qQHU#l!=LbK|u!k85;@ggGVvsk((Uf%X@4SlD1X;;o%-x*3Tv!hBL moi5V&nKvP^UqE3Dk}B> literal 0 HcmV?d00001 diff --git a/spec/fixtures/gnupg/tofu.db b/spec/fixtures/gnupg/tofu.db new file mode 100644 index 0000000000000000000000000000000000000000..91ae4662328a6b75fe23804291235ba4fa2e4f10 GIT binary patch literal 49152 zcmeI&!Ef4D9Kdm#HjqFl+b%v-XfMhItKJquyKd@XrKy{tQc5Y2nv+H05m^P3VpEmV zlxqDScHjRnX_x&)`xAE9GuXjioAp{*UyBI)z31Qa`}}@FvRFRG+{l!N~om`JE)OXnbsYBPk-Fi^I@m?zxyISS1Vz=`CMx*>^dAvI- z*LSx|zgMcC3PC>t2q1s}0tg_0z~?ORuA3_u)vErh9}Hejj|FqRncRjH?eXA(MScQ}G^SSE< zqHn+GTS86;?j$;2_;T*3Q0tz+oys_Bc<=da;trQ_i#Yn(K6)%_QLF2TTHO+dmS|Yw zwk7VEW_ZtV=8e@|fq35UwK~UYy75>J1NV(|{uAz@{iu1;?u&ys@CPHqDGn_0ky#d= z`+6ZoEwQ?2VWv4O=8eX_{)?iNZ>2Y!FV6#a=J`&RW|(B6ZQ7w3>Lv`b?s~(*QsyLcb?_*hFR5Ltp>$a@2m

*Xfvv0!qive?0yNT{8A=rIAZ``~YrQ(g8 z`|5Wg`pTsqAZiq~_$2zRHOnfBUin8;9}WZ%KmY**5I_I{1Q0*~0R#}ppg>N~?S$|D zYSW(s0R#|0009ILKmY**5I_I{1Trj84A;i{e}*&6(hxuZ0R#|0009ILKmY**5YROC z|EU555I_I{1Q0*~0R#|0009KDFTnnP_H)b%5kLR|1Q0*~0R#|0009IL;QgN(KmY** z5I_I{1Q0*~0R#|0Ao~LR|9|##%nA`e009ILKmY**5I_I{1Q1~VpBg{_0R#|0009IL zKmY**5I`XN0_^{1KgX;P0R#|0009ILKmY**5I_I{_W!8?1Q0*~0R#|0009ILKmY** zvM&(s|Lc{%HTB^@009ILKmY**5I_I{1Q0*~feZ+IrRTo+;dcGUgYTP%ckeXsK58B| F{sp=<#-acK literal 0 HcmV?d00001 diff --git a/spec/fixtures/gnupg/trustdb.gpg b/spec/fixtures/gnupg/trustdb.gpg new file mode 100644 index 0000000000000000000000000000000000000000..d3b33e1d7f44980cc551d03664cf6d29f25e99e8 GIT binary patch literal 1280 zcmZQfFGy!*W@Ke#Vql1gSkcLV9WZiX7sn7CRfiEIV1dza84VXu2#knyAcu%+YWtUm z;X_9Dl7CFw-u$nA_n<|RZBgm+U&?F{3*`~^@-js5d{CRwm!?yF^kH4e@`P{C3eUwu J)ghEI003#+8%_WK literal 0 HcmV?d00001 diff --git a/spec/fixtures/password-store/.gpg-id b/spec/fixtures/password-store/.gpg-id new file mode 100644 index 0000000..e7e9895 --- /dev/null +++ b/spec/fixtures/password-store/.gpg-id @@ -0,0 +1 @@ +0x841906A275A7FA23 diff --git a/spec/fixtures/password-store/foo/bar.gpg b/spec/fixtures/password-store/foo/bar.gpg new file mode 100644 index 0000000000000000000000000000000000000000..427a3e1e8d9182550f4ad1fc069981fca7c498d9 GIT binary patch literal 601 zcmV-f0;c_i0t^G&Ywi~y%dK49c#(f*kyh3Zsh_i3y0~`^t%uUOBj|`x1`_x0e&C7*RQ!|z3hnSv%79s z=?y`BMnBqZz|>wyDVBzmgTaeC8Hj}LbTt3~2)fuxcBS6llm%<$O==gRu{BnG*yl*# z9RI$j>N1e^-a&3Vwg}Ql0rOPk^%QRtc5d->zFk=Za~F?WwOuMpL@}7&{e6iOiiPfw nv|#BQf-B^5nq;4~%Dxkniv%NM#UBWPJpaSuk*@Ur&B>p3TyrcC literal 0 HcmV?d00001 diff --git a/spec/fixtures/password-store/foo/baz.gpg b/spec/fixtures/password-store/foo/baz.gpg new file mode 100644 index 0000000000000000000000000000000000000000..093e4ea1a8aa7d573a7a5a7c96b881752e3af229 GIT binary patch literal 601 zcmV-f0;c_i0t^G&Ywi~y%dVH`PAhD&?B1+nm|Qc=8op$FFP0UpymWx z28efHLRN__1T$0m8eonwR2xQ+wgXaOX!daDg zO99^lawtRB-9V1+4vcT8{y~nXT`YqoxxNKa_e{@=Pap`>919RtTR)c0TthWC-MX~u z7e2Fw36vugO}Z1TQ>f`}TcuNaN8RlmGXb5f^h~+ZS`2becK16PAfrx&G{B4VuQ{t5 zw8(IO4NQ29tM;D|2+rS!7+~Ehi;NMrT|&equO!zFNng6{i_2TJ)T9dz2C(&IMJ{^^ zQw-V8nte&bUs;0H3W|696Wr55*D8=tq;jI%q`#p7?3K7pa~RU2(QD7gw-gKS3alF^ z2xI`mhlXioQ}drsrq`%yZ(x7U3k`wQ@O^zduD9=Mi^JVRB;top)@pH7Vi>MsEW80k z?b#ik`WD=4VOoZ&-Tt)luj&zYgP#wUZetZ_95NA+ZhE})K$giO7MX+G!mBsl>ebqe%Kz9 literal 0 HcmV?d00001 diff --git a/spec/fixtures/password-store/foo/quux.gpg b/spec/fixtures/password-store/foo/quux.gpg new file mode 100644 index 0000000..71eac4d --- /dev/null +++ b/spec/fixtures/password-store/foo/quux.gpg @@ -0,0 +1,2 @@ + k ˳8Y AD{TL'Z@ !p4Q9ԋLK-j Plx""(dmkv&%^ 6o:p*՛" +$EzGiN^֌AE'N;oM6&ƍx:޽~< Ybq2AjaH<+gV hHW$f}hH\1 Sgd.IrѷDoGߨ+b^Pl [Q$I3xD$61y^{9}CgP]Fx]p(>w};a%A&Nv\L3wQ8jK&T?^}]SPAhL'W].r9FU? DUt2e/uѧ8?w#3O(A5p \ No newline at end of file diff --git a/spec/fixtures/password-store/foo/qux.gpg b/spec/fixtures/password-store/foo/qux.gpg new file mode 100644 index 0000000000000000000000000000000000000000..4ec747c568781d7c4933d870237cc92772897edd GIT binary patch literal 604 zcmV-i0;Bzf0t^G&Ywi~y%dN-}Y@IR;hup9r#3H^)<9tO|Q2+?mn zsp{h*MT6phDXXt0gHo^uY}|moU?U4B@@q&%%_R&6lbVCsX8UX>Tx`VbH}Ty7M%xGW&tnAtZ>O08i4e-IvJ4|5EpQSi~Gc(T7R%}NIFZ^eEC z0CyJT9MNqad;?MqHjR+ro90cT#WN9p+u{dM+_>d zLxSiTL0E+lL4L1Fjb}d)X>@}09tj`tgx4NpG6a{Qx#`)kFc{F~Hw%nGu%)zq9W6D; zBLJH%`n{O5I@-%jppRdfJ{AbkHmF)XnXB_- + ***** /!\ Ansible managed file, do not edit: all changes will be lost ***** +... diff --git a/spec/kitchen/playbooks/host_vars/password-store-sandbox.local b/spec/kitchen/playbooks/host_vars/password-store-sandbox.local new file mode 100644 index 0000000..196d997 --- /dev/null +++ b/spec/kitchen/playbooks/host_vars/password-store-sandbox.local @@ -0,0 +1,26 @@ +--- +password_store: + decrypt: + - src: foo/bar + name: foo_bar + - src: foo/baz + name: foo_baz + - src: foo/qux + name: foo_qux + - src: foo/quux + name: foo_quux + push: + # full + - src: foo/bar + dest: /tmp/foo_bar + owner: root + group: staff + mode: '0640' + # minimal + - src: foo/baz + dest: /tmp/foo_baz + - src: foo/qux + dest: /tmp/foo_qux + - src: foo/quux + dest: /tmp/foo_quux +... diff --git a/spec/kitchen/playbooks/password-store.yml b/spec/kitchen/playbooks/password-store.yml new file mode 100644 index 0000000..f1d53aa --- /dev/null +++ b/spec/kitchen/playbooks/password-store.yml @@ -0,0 +1,17 @@ +--- +- name: Password-Store tests + hosts: password_store + + vars: + password_store_dir: "{{ lookup('env', 'PASSWORD_STORE_DIR') }}" + + pre_tasks: + - debug: + msg: + ansible_fqdn: "{{ ansible_fqdn }}" + hostvars__inventory_hostname: "{{ hostvars[inventory_hostname] }}" + PASSWORD_STORE_DIR: "{{ password_store_dir }}" + + roles: + - password_store +... diff --git a/spec/kitchen/playbooks/roles b/spec/kitchen/playbooks/roles new file mode 120000 index 0000000..20c4c58 --- /dev/null +++ b/spec/kitchen/playbooks/roles @@ -0,0 +1 @@ +../../../roles \ No newline at end of file diff --git a/spec/roles/password_store/push_spec.rb b/spec/roles/password_store/push_spec.rb new file mode 100644 index 0000000..4d4706d --- /dev/null +++ b/spec/roles/password_store/push_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +run_with_host_vars do |vars| + vars.dig(:password_store, :push)&.tap do |push| + push.each do |entry| + describe file(entry[:dest]) do + it { should exist } + it { should be_file } + it { should be_mode(Integer(entry[:mode] || '0600',8)) } + it { should be_owned_by(entry[:owner] || 'root') } + it { should be_grouped_into(entry[:group] || 'root') } + its('content') do + cmd = %W[ + GNUPGHOME=#{GNUPGHOME} + PASSWORD_STORE_DIR=#{PASSWORD_STORE_DIR} + pass #{entry[:src]} + ].join(' ') + + should eq `#{cmd}`.chomp + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..1eeab3e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'bundler/setup' +require 'pathname' + +# Useful pathnames +SPEC = Pathname.new(__dir__).expand_path +ROOT = SPEC.dirname +NAME = ROOT.basename.to_s +HOST_VARS = ROOT / 'host_vars' +FIXTURES = SPEC / 'fixtures' +KITCHEN = SPEC / 'kitchen' +SUPPORT = SPEC / 'support' +GNUPG = FIXTURES / 'gnupg' +PASSWORD_STORE = FIXTURES / 'password-store' +PLAYBOOKS = KITCHEN / 'playbooks' +KITCHEN_HOST_VARS = PLAYBOOKS / 'host_vars' + +# Test-Kitchen provider (default: 'docker', available: 'vagrant') +KITCHEN_PROVIDER = ENV['KITCHEN_PROVIDER'] ||= 'docker' +# True if Test-kitchen run inside a (Docker) container +DOCKERIZED = KITCHEN_PROVIDER == 'docker' + +# Override GnuPG home for tests +GNUPGHOME = ENV['GNUPGHOME'] = "/tmp/.#{NAME}_gnupg" +# Override Password-Store directory for tests +PASSWORD_STORE_DIR = ENV['PASSWORD_STORE_DIR'] = "/tmp/.#{NAME}_password-store" + +# Load specification support files in alphabetical order +SUPPORT.glob('*.rb').sort.each { |support| require support } + +$PROGRAM_NAME = "spec[#{NAME}]" diff --git a/spec/support/current_host_vars.rb b/spec/support/current_host_vars.rb new file mode 100644 index 0000000..5392aef --- /dev/null +++ b/spec/support/current_host_vars.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +def current_host_vars + host_vars_path = attribute('host_vars_path', + default: nil, + description: 'An host_vars file path') + + host_vars = HOST_VARS / host_vars_path # first try normal host_vars + return host_vars if host_vars.exist? + + host_vars = KITCHEN_HOST_VARS / host_vars_path # then kitchen playbooks one + return host_vars if host_vars.exist? + + nil +end diff --git a/spec/support/run_with_host_vars.rb b/spec/support/run_with_host_vars.rb new file mode 100644 index 0000000..d603a6b --- /dev/null +++ b/spec/support/run_with_host_vars.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +def run_with_host_vars(&block) + host_vars = current_host_vars + + return unless host_vars&.exist? + + vars = YAML.safe_load(host_vars.read, symbolize_names: true) + + puts "run_with_host_vars(host_vars: #{host_vars.inspect})" + block.call(vars) +end