mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-27 05:43:02 +00:00
add support of kubectl_local_env_vars (#698) SUMMARY Support of local environmental variable that may be required to be set on Ansible Controller before the connection is set and may be used for kubectl command. This PR addressed for #698 The main idea is to have the support of additional/extra local environmental variable that may be required for kubectl itself, i.e. for authorization in case of public clouds ISSUE TYPE Feature Pull Request COMPONENT NAME kubernetes.core.kubectl connection plugin ADDITIONAL INFORMATION This PR attempts to implement local env support for the kubectl connection plugin that may be useful in case of using kubectl against public cloud kubernetes environment that uses some authorization (i.e. aws cli) additionally to kubeconfig file. More detail in #698 The output that shows that the connection plugin can use local environment variable for kubectl command (with some debug that used during development but removed then): root@ubuntu-shell:/# cat test.yaml - hosts: localhost gather_facts: no any_errors_fatal: yes vars: ansible_connection: "kubectl" ansible_kubectl_namespace: "test" ansible_kubectl_config: "/.kube/config" ansible_kubectl_pod: "ubuntu" ansible_kubectl_container: "ubuntu" ansible_kubectl_local_env_vars: TESTVAR1: "test" TESTVAR2: "test" TESTVAR3: "test" environment: TEST_ENV1: value1 TEST_ENV2: value2 tasks: - name: test ansible.builtin.shell: env register: result - debug: var: result.stdout_lines root@ubuntu-shell:/# ansible-playbook test.yaml [WARNING]: No inventory was parsed, only implicit localhost is available [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' PLAY [localhost] ************************************************************************************************************************************** TASK [test] ******************************************************************************************************************************************* changed: [localhost] TASK [debug] ****************************************************************************************************************************************** ok: [localhost] => { "result.stdout_lines": [ "KUBERNETES_PORT=tcp://10.96.0.1:443", "KUBERNETES_SERVICE_PORT=443", "HOSTNAME=ubuntu", "HOME=/root", "LC_CTYPE=C.UTF-8", "TEST_ENV1=value1", "TEST_ENV2=value2", "TERM=xterm", "KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "KUBERNETES_PORT_443_TCP_PORT=443", "KUBERNETES_PORT_443_TCP_PROTO=tcp", "KUBERNETES_SERVICE_PORT_HTTPS=443", "KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443", "KUBERNETES_SERVICE_HOST=10.96.0.1", "PWD=/" ] } PLAY RECAP ******************************************************************************************************************************************** localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 root@ubuntu-shell:/# ansible-playbook test.yaml -vvv ansible-playbook [core 2.14.5] config file = None configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = /usr/local/lib/python3.10/dist-packages/ansible ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections executable location = /usr/local/bin/ansible-playbook python version = 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] (/usr/bin/python3) jinja version = 3.1.3 libyaml = True No config file found; using defaults host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method Skipping due to inventory source not existing or not being readable by the current user script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method Skipping due to inventory source not existing or not being readable by the current user yaml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method Skipping due to inventory source not existing or not being readable by the current user ini declined parsing /etc/ansible/hosts as it did not pass its verify_file() method Skipping due to inventory source not existing or not being readable by the current user toml declined parsing /etc/ansible/hosts as it did not pass its verify_file() method [WARNING]: No inventory was parsed, only implicit localhost is available [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all' Skipping callback 'default', as we already have a stdout callback. Skipping callback 'minimal', as we already have a stdout callback. Skipping callback 'oneline', as we already have a stdout callback. PLAYBOOK: test.yaml *********************************************************************************************************************************** 1 plays in test.yaml PLAY [localhost] ************************************************************************************************************************************** TASK [test] ******************************************************************************************************************************************* task path: /test.yaml:19 redirecting (type: connection) ansible.builtin.kubectl to kubernetes.core.kubectl <127.0.0.1> ESTABLISH kubectl CONNECTION <127.0.0.1> ENV: KUBERNETES_SERVICE_PORT_HTTPS=443 <127.0.0.1> ENV: KUBERNETES_SERVICE_PORT=443 <127.0.0.1> ENV: HOSTNAME=ubuntu-shell <127.0.0.1> ENV: PWD=/ <127.0.0.1> ENV: HOME=/root <127.0.0.1> ENV: KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443 <127.0.0.1> ENV: LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36: <127.0.0.1> ENV: TERM=xterm <127.0.0.1> ENV: SHLVL=1 <127.0.0.1> ENV: KUBERNETES_PORT_443_TCP_PROTO=tcp <127.0.0.1> ENV: KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1 <127.0.0.1> ENV: KUBERNETES_SERVICE_HOST=10.96.0.1 <127.0.0.1> ENV: KUBERNETES_PORT=tcp://10.96.0.1:443 <127.0.0.1> ENV: KUBERNETES_PORT_443_TCP_PORT=443 <127.0.0.1> ENV: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin <127.0.0.1> ENV: _=/usr/local/bin/ansible-playbook <127.0.0.1> ENV: LC_CTYPE=C.UTF-8 <127.0.0.1> ENV: TESTVAR1=test <127.0.0.1> ENV: TESTVAR2=test <127.0.0.1> ENV: TESTVAR3=test <127.0.0.1> EXEC ['/usr/local/bin/kubectl', '-n', 'test', '--kubeconfig', '/.kube/config', 'exec', '-i', 'ubuntu', '-c', 'ubuntu', '--', '/bin/sh', '-c', "/bin/sh -c 'echo ~ && sleep 0'"] <127.0.0.1> EXEC ['/usr/local/bin/kubectl', '-n', 'test', '--kubeconfig', '/.kube/config', 'exec', '-i', 'ubuntu', '-c', 'ubuntu', '--', '/bin/sh', '-c', '/bin/sh -c \'( umask 77 && mkdir -p "` echo /root/.ansible/tmp `"&& mkdir "` echo /root/.ansible/tmp/ansible-tmp-1713785852.548581-6866-69007595335133 `" && echo ansible-tmp-1713785852.548581-6866-69007595335133="` echo /root/.ansible/tmp/ansible-tmp-1713785852.548581-6866-69007595335133 `" ) && sleep 0\''] Using module file /usr/local/lib/python3.10/dist-packages/ansible/modules/command.py <127.0.0.1> PUT /root/.ansible/tmp/ansible-local-6862s5_lr_wb/tmpxwmx0qeh TO /root/.ansible/tmp/ansible-tmp-1713785852.548581-6866-69007595335133/AnsiballZ_command.py <127.0.0.1> EXEC ['/usr/local/bin/kubectl', '-n', 'test', '--kubeconfig', '/.kube/config', 'exec', '-i', 'ubuntu', '-c', 'ubuntu', '--', '/bin/sh', '-c', "/bin/sh -c 'chmod u+x /root/.ansible/tmp/ansible-tmp-1713785852.548581-6866-69007595335133/ /root/.ansible/tmp/ansible-tmp-1713785852.548581-6866-69007595335133/AnsiballZ_command.py && sleep 0'"] <127.0.0.1> EXEC ['/usr/local/bin/kubectl', '-n', 'test', '--kubeconfig', '/.kube/config', 'exec', '-i', 'ubuntu', '-c', 'ubuntu', '--', '/bin/sh', '-c', "/bin/sh -c 'TEST_ENV1=value1 TEST_ENV2=value2 /usr/bin/python3 /root/.ansible/tmp/ansible-tmp-1713785852.548581-6866-69007595335133/AnsiballZ_command.py && sleep 0'"] <127.0.0.1> EXEC ['/usr/local/bin/kubectl', '-n', 'test', '--kubeconfig', '/.kube/config', 'exec', '-i', 'ubuntu', '-c', 'ubuntu', '--', '/bin/sh', '-c', "/bin/sh -c 'rm -f -r /root/.ansible/tmp/ansible-tmp-1713785852.548581-6866-69007595335133/ > /dev/null 2>&1 && sleep 0'"] changed: [localhost] => { "changed": true, "cmd": "env", "delta": "0:00:00.005088", "end": "2024-04-22 11:37:33.655340", "invocation": { "module_args": { "_raw_params": "env", "_uses_shell": true, "argv": null, "chdir": null, "creates": null, "executable": null, "removes": null, "stdin": null, "stdin_add_newline": true, "strip_empty_ends": true } }, "msg": "", "rc": 0, "start": "2024-04-22 11:37:33.650252", "stderr": "", "stderr_lines": [], "stdout": "KUBERNETES_PORT=tcp://10.96.0.1:443\nKUBERNETES_SERVICE_PORT=443\nHOSTNAME=ubuntu\nHOME=/root\nLC_CTYPE=C.UTF-8\nTEST_ENV1=value1\nTEST_ENV2=value2\nTERM=xterm\nKUBERNETES_PORT_443_TCP_ADDR=10.96.0.1\nPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\nKUBERNETES_PORT_443_TCP_PORT=443\nKUBERNETES_PORT_443_TCP_PROTO=tcp\nKUBERNETES_SERVICE_PORT_HTTPS=443\nKUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443\nKUBERNETES_SERVICE_HOST=10.96.0.1\nPWD=/", "stdout_lines": [ "KUBERNETES_PORT=tcp://10.96.0.1:443", "KUBERNETES_SERVICE_PORT=443", "HOSTNAME=ubuntu", "HOME=/root", "LC_CTYPE=C.UTF-8", "TEST_ENV1=value1", "TEST_ENV2=value2", "TERM=xterm", "KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "KUBERNETES_PORT_443_TCP_PORT=443", "KUBERNETES_PORT_443_TCP_PROTO=tcp", "KUBERNETES_SERVICE_PORT_HTTPS=443", "KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443", "KUBERNETES_SERVICE_HOST=10.96.0.1", "PWD=/" ] } TASK [debug] ****************************************************************************************************************************************** task path: /test.yaml:22 redirecting (type: connection) ansible.builtin.kubectl to kubernetes.core.kubectl ok: [localhost] => { "result.stdout_lines": [ "KUBERNETES_PORT=tcp://10.96.0.1:443", "KUBERNETES_SERVICE_PORT=443", "HOSTNAME=ubuntu", "HOME=/root", "LC_CTYPE=C.UTF-8", "TEST_ENV1=value1", "TEST_ENV2=value2", "TERM=xterm", "KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "KUBERNETES_PORT_443_TCP_PORT=443", "KUBERNETES_PORT_443_TCP_PROTO=tcp", "KUBERNETES_SERVICE_PORT_HTTPS=443", "KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443", "KUBERNETES_SERVICE_HOST=10.96.0.1", "PWD=/" ] } PLAY RECAP ******************************************************************************************************************************************** localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 root@ubuntu-shell:/# Reviewed-by: Bikouo Aubin Reviewed-by: Yuriy Novostavskiy Reviewed-by: Mike Graves <mgraves@redhat.com>
476 lines
17 KiB
Python
476 lines
17 KiB
Python
# Based on the docker connection plugin
|
|
#
|
|
# Connection plugin for configuring kubernetes containers with kubectl
|
|
# (c) 2017, XuXinkun <xuxinkun@gmail.com>
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
from __future__ import absolute_import, division, print_function
|
|
|
|
__metaclass__ = type
|
|
|
|
DOCUMENTATION = r"""
|
|
author:
|
|
- xuxinkun (@xuxinkun)
|
|
|
|
name: kubectl
|
|
|
|
short_description: Execute tasks in pods running on Kubernetes.
|
|
|
|
description:
|
|
- Use the kubectl exec command to run tasks in, or put/fetch files to, pods running on the Kubernetes
|
|
container platform.
|
|
|
|
requirements:
|
|
- kubectl (go binary)
|
|
|
|
options:
|
|
kubectl_pod:
|
|
description:
|
|
- Pod name.
|
|
- Required when the host name does not match pod name.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_pod
|
|
env:
|
|
- name: K8S_AUTH_POD
|
|
kubectl_container:
|
|
description:
|
|
- Container name.
|
|
- Required when a pod contains more than one container.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_container
|
|
env:
|
|
- name: K8S_AUTH_CONTAINER
|
|
kubectl_namespace:
|
|
description:
|
|
- The namespace of the pod
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_namespace
|
|
env:
|
|
- name: K8S_AUTH_NAMESPACE
|
|
kubectl_extra_args:
|
|
description:
|
|
- Extra arguments to pass to the kubectl command line.
|
|
- Please be aware that this passes information directly on the command line and it could expose sensitive data.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_extra_args
|
|
env:
|
|
- name: K8S_AUTH_EXTRA_ARGS
|
|
kubectl_local_env_vars:
|
|
description:
|
|
- Local enviromantal variable to be passed locally to the kubectl command line.
|
|
- Please be aware that this passes information directly on the command line and it could expose sensitive data.
|
|
default: {}
|
|
type: dict
|
|
version_added: 3.1.0
|
|
vars:
|
|
- name: ansible_kubectl_local_env_vars
|
|
kubectl_kubeconfig:
|
|
description:
|
|
- Path to a kubectl config file. Defaults to I(~/.kube/config)
|
|
- The configuration can be provided as dictionary. Added in version 2.4.0.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_kubeconfig
|
|
- name: ansible_kubectl_config
|
|
env:
|
|
- name: K8S_AUTH_KUBECONFIG
|
|
kubectl_context:
|
|
description:
|
|
- The name of a context found in the K8s config file.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_context
|
|
env:
|
|
- name: K8S_AUTH_CONTEXT
|
|
kubectl_host:
|
|
description:
|
|
- URL for accessing the API.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_host
|
|
- name: ansible_kubectl_server
|
|
env:
|
|
- name: K8S_AUTH_HOST
|
|
- name: K8S_AUTH_SERVER
|
|
kubectl_username:
|
|
description:
|
|
- Provide a username for authenticating with the API.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_username
|
|
- name: ansible_kubectl_user
|
|
env:
|
|
- name: K8S_AUTH_USERNAME
|
|
kubectl_password:
|
|
description:
|
|
- Provide a password for authenticating with the API.
|
|
- Please be aware that this passes information directly on the command line and it could expose sensitive data.
|
|
We recommend using the file based authentication options instead.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_password
|
|
env:
|
|
- name: K8S_AUTH_PASSWORD
|
|
kubectl_token:
|
|
description:
|
|
- API authentication bearer token.
|
|
- Please be aware that this passes information directly on the command line and it could expose sensitive data.
|
|
We recommend using the file based authentication options instead.
|
|
vars:
|
|
- name: ansible_kubectl_token
|
|
- name: ansible_kubectl_api_key
|
|
env:
|
|
- name: K8S_AUTH_TOKEN
|
|
- name: K8S_AUTH_API_KEY
|
|
client_cert:
|
|
description:
|
|
- Path to a certificate used to authenticate with the API.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_cert_file
|
|
- name: ansible_kubectl_client_cert
|
|
env:
|
|
- name: K8S_AUTH_CERT_FILE
|
|
aliases: [ kubectl_cert_file ]
|
|
client_key:
|
|
description:
|
|
- Path to a key file used to authenticate with the API.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_key_file
|
|
- name: ansible_kubectl_client_key
|
|
env:
|
|
- name: K8S_AUTH_KEY_FILE
|
|
aliases: [ kubectl_key_file ]
|
|
ca_cert:
|
|
description:
|
|
- Path to a CA certificate used to authenticate with the API.
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_ssl_ca_cert
|
|
- name: ansible_kubectl_ca_cert
|
|
env:
|
|
- name: K8S_AUTH_SSL_CA_CERT
|
|
aliases: [ kubectl_ssl_ca_cert ]
|
|
validate_certs:
|
|
description:
|
|
- Whether or not to verify the API server's SSL certificate. Defaults to I(true).
|
|
default: ''
|
|
vars:
|
|
- name: ansible_kubectl_verify_ssl
|
|
- name: ansible_kubectl_validate_certs
|
|
env:
|
|
- name: K8S_AUTH_VERIFY_SSL
|
|
aliases: [ kubectl_verify_ssl ]
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import os.path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
|
from ansible.module_utils._text import to_bytes
|
|
from ansible.module_utils.six.moves import shlex_quote
|
|
from ansible.parsing.yaml.loader import AnsibleLoader
|
|
from ansible.plugins.connection import BUFSIZE, ConnectionBase
|
|
from ansible.utils.display import Display
|
|
|
|
display = Display()
|
|
|
|
|
|
CONNECTION_TRANSPORT = "kubectl"
|
|
|
|
CONNECTION_OPTIONS = {
|
|
"kubectl_container": "-c",
|
|
"kubectl_namespace": "-n",
|
|
"kubectl_kubeconfig": "--kubeconfig",
|
|
"kubectl_context": "--context",
|
|
"kubectl_host": "--server",
|
|
"kubectl_username": "--username",
|
|
"kubectl_password": "--password",
|
|
"client_cert": "--client-certificate",
|
|
"client_key": "--client-key",
|
|
"ca_cert": "--certificate-authority",
|
|
"validate_certs": "--insecure-skip-tls-verify",
|
|
"kubectl_token": "--token",
|
|
}
|
|
|
|
|
|
class Connection(ConnectionBase):
|
|
"""Local kubectl based connections"""
|
|
|
|
transport = CONNECTION_TRANSPORT
|
|
connection_options = CONNECTION_OPTIONS
|
|
documentation = DOCUMENTATION
|
|
has_pipelining = True
|
|
transport_cmd = None
|
|
|
|
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
|
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
|
|
|
# Note: kubectl runs commands as the user that started the container.
|
|
# It is impossible to set the remote user for a kubectl connection.
|
|
cmd_arg = "{0}_command".format(self.transport)
|
|
self.transport_cmd = kwargs.get(cmd_arg, shutil.which(self.transport))
|
|
if not self.transport_cmd:
|
|
raise AnsibleError("{0} command not found in PATH".format(self.transport))
|
|
self._file_to_delete = None
|
|
|
|
def delete_temporary_file(self):
|
|
if self._file_to_delete is not None:
|
|
os.remove(self._file_to_delete)
|
|
self._file_to_delete = None
|
|
|
|
def _build_exec_cmd(self, cmd):
|
|
"""Build the local kubectl exec command to run cmd on remote_host"""
|
|
local_cmd = [self.transport_cmd]
|
|
censored_local_cmd = [self.transport_cmd]
|
|
|
|
# Build command options based on doc string
|
|
doc_yaml = AnsibleLoader(self.documentation).get_single_data()
|
|
for key in doc_yaml.get("options"):
|
|
if key.endswith("verify_ssl") and self.get_option(key) != "":
|
|
# Translate verify_ssl to skip_verify_ssl, and output as string
|
|
skip_verify_ssl = not self.get_option(key)
|
|
local_cmd.append(
|
|
"{0}={1}".format(
|
|
self.connection_options[key], str(skip_verify_ssl).lower()
|
|
)
|
|
)
|
|
censored_local_cmd.append(
|
|
"{0}={1}".format(
|
|
self.connection_options[key], str(skip_verify_ssl).lower()
|
|
)
|
|
)
|
|
elif key.endswith("kubeconfig") and self.get_option(key) != "":
|
|
kubeconfig_path = self.get_option(key)
|
|
if isinstance(kubeconfig_path, dict):
|
|
fd, tmpfile = tempfile.mkstemp()
|
|
with os.fdopen(fd, "w") as fp:
|
|
json.dump(kubeconfig_path, fp)
|
|
kubeconfig_path = tmpfile
|
|
self._file_to_delete = tmpfile
|
|
|
|
cmd_arg = self.connection_options[key]
|
|
local_cmd += [cmd_arg, kubeconfig_path]
|
|
censored_local_cmd += [cmd_arg, kubeconfig_path]
|
|
elif (
|
|
not key.endswith("container")
|
|
and self.get_option(key)
|
|
and self.connection_options.get(key)
|
|
):
|
|
cmd_arg = self.connection_options[key]
|
|
local_cmd += [cmd_arg, self.get_option(key)]
|
|
# Redact password and token from console log
|
|
if key.endswith(("_token", "_password")):
|
|
censored_local_cmd += [cmd_arg, "********"]
|
|
else:
|
|
censored_local_cmd += [cmd_arg, self.get_option(key)]
|
|
|
|
extra_args_name = "{0}_extra_args".format(self.transport)
|
|
if self.get_option(extra_args_name):
|
|
local_cmd += self.get_option(extra_args_name).split(" ")
|
|
censored_local_cmd += self.get_option(extra_args_name).split(" ")
|
|
|
|
pod = self.get_option("{0}_pod".format(self.transport))
|
|
if not pod:
|
|
pod = self._play_context.remote_addr
|
|
# -i is needed to keep stdin open which allows pipelining to work
|
|
local_cmd += ["exec", "-i", pod]
|
|
censored_local_cmd += ["exec", "-i", pod]
|
|
|
|
# if the pod has more than one container, then container is required
|
|
container_arg_name = "{0}_container".format(self.transport)
|
|
if self.get_option(container_arg_name):
|
|
local_cmd += ["-c", self.get_option(container_arg_name)]
|
|
censored_local_cmd += ["-c", self.get_option(container_arg_name)]
|
|
|
|
local_cmd += ["--"] + cmd
|
|
censored_local_cmd += ["--"] + cmd
|
|
|
|
return local_cmd, censored_local_cmd
|
|
|
|
def _local_env(self):
|
|
"""Return a dict of local environment variables to pass to the kubectl command"""
|
|
local_env = {}
|
|
local_local_env_vars_name = "{0}_local_env_vars".format(self.transport)
|
|
local_env_vars = self.get_option(local_local_env_vars_name)
|
|
if local_env_vars:
|
|
if isinstance(local_env_vars, dict):
|
|
local_env_vars = json.dumps(local_env_vars)
|
|
local_env = os.environ.copy()
|
|
local_env.update(json.loads(local_env_vars))
|
|
return local_env
|
|
return None
|
|
|
|
def _connect(self, port=None):
|
|
"""Connect to the container. Nothing to do"""
|
|
super(Connection, self)._connect()
|
|
if not self._connected:
|
|
display.vvv(
|
|
"ESTABLISH {0} CONNECTION".format(self.transport),
|
|
host=self._play_context.remote_addr,
|
|
)
|
|
self._connected = True
|
|
|
|
def exec_command(self, cmd, in_data=None, sudoable=False):
|
|
"""Run a command in the container"""
|
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
|
|
|
local_cmd, censored_local_cmd = self._build_exec_cmd(
|
|
[self._play_context.executable, "-c", cmd]
|
|
)
|
|
|
|
display.vvv(
|
|
"EXEC %s" % (censored_local_cmd,), host=self._play_context.remote_addr
|
|
)
|
|
local_cmd = [to_bytes(i, errors="surrogate_or_strict") for i in local_cmd]
|
|
p = subprocess.Popen(
|
|
local_cmd,
|
|
shell=False,
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env=self._local_env(),
|
|
)
|
|
|
|
stdout, stderr = p.communicate(in_data)
|
|
self.delete_temporary_file()
|
|
return (p.returncode, stdout, stderr)
|
|
|
|
def _prefix_login_path(self, remote_path):
|
|
"""Make sure that we put files into a standard path
|
|
|
|
If a path is relative, then we need to choose where to put it.
|
|
ssh chooses $HOME but we aren't guaranteed that a home dir will
|
|
exist in any given chroot. So for now we're choosing "/" instead.
|
|
This also happens to be the former default.
|
|
|
|
Can revisit using $HOME instead if it's a problem
|
|
"""
|
|
if not remote_path.startswith(os.path.sep):
|
|
remote_path = os.path.join(os.path.sep, remote_path)
|
|
return os.path.normpath(remote_path)
|
|
|
|
def put_file(self, in_path, out_path):
|
|
"""Transfer a file from local to the container"""
|
|
super(Connection, self).put_file(in_path, out_path)
|
|
display.vvv(
|
|
"PUT %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr
|
|
)
|
|
|
|
out_path = self._prefix_login_path(out_path)
|
|
if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
|
|
raise AnsibleFileNotFound("file or module does not exist: %s" % in_path)
|
|
|
|
out_path = shlex_quote(out_path)
|
|
# kubectl doesn't have native support for copying files into
|
|
# running containers, so we use kubectl exec to implement this
|
|
with open(to_bytes(in_path, errors="surrogate_or_strict"), "rb") as in_file:
|
|
if not os.fstat(in_file.fileno()).st_size:
|
|
count = " count=0"
|
|
else:
|
|
count = ""
|
|
args, dummy = self._build_exec_cmd(
|
|
[
|
|
self._play_context.executable,
|
|
"-c",
|
|
"dd of=%s bs=%s%s && sleep 0" % (out_path, BUFSIZE, count),
|
|
]
|
|
)
|
|
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
|
|
try:
|
|
p = subprocess.Popen(
|
|
args,
|
|
stdin=in_file,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env=self._local_env(),
|
|
)
|
|
except OSError:
|
|
raise AnsibleError(
|
|
"kubectl connection requires dd command in the container to put files"
|
|
)
|
|
stdout, stderr = p.communicate()
|
|
self.delete_temporary_file()
|
|
|
|
if p.returncode != 0:
|
|
raise AnsibleError(
|
|
"failed to transfer file %s to %s:\n%s\n%s"
|
|
% (in_path, out_path, stdout, stderr)
|
|
)
|
|
|
|
def fetch_file(self, in_path, out_path):
|
|
"""Fetch a file from container to local."""
|
|
super(Connection, self).fetch_file(in_path, out_path)
|
|
display.vvv(
|
|
"FETCH %s TO %s" % (in_path, out_path), host=self._play_context.remote_addr
|
|
)
|
|
|
|
in_path = self._prefix_login_path(in_path)
|
|
out_dir = os.path.dirname(out_path)
|
|
|
|
# kubectl doesn't have native support for fetching files from
|
|
# running containers, so we use kubectl exec to implement this
|
|
args, dummy = self._build_exec_cmd(
|
|
[self._play_context.executable, "-c", "dd if=%s bs=%s" % (in_path, BUFSIZE)]
|
|
)
|
|
args = [to_bytes(i, errors="surrogate_or_strict") for i in args]
|
|
actual_out_path = os.path.join(out_dir, os.path.basename(in_path))
|
|
with open(
|
|
to_bytes(actual_out_path, errors="surrogate_or_strict"), "wb"
|
|
) as out_file:
|
|
try:
|
|
p = subprocess.Popen(
|
|
args,
|
|
stdin=subprocess.PIPE,
|
|
stdout=out_file,
|
|
stderr=subprocess.PIPE,
|
|
env=self._local_env(),
|
|
)
|
|
except OSError:
|
|
raise AnsibleError(
|
|
"{0} connection requires dd command in the container to fetch files".format(
|
|
self.transport
|
|
)
|
|
)
|
|
stdout, stderr = p.communicate()
|
|
self.delete_temporary_file()
|
|
|
|
if p.returncode != 0:
|
|
raise AnsibleError(
|
|
"failed to fetch file %s to %s:\n%s\n%s"
|
|
% (in_path, out_path, stdout, stderr)
|
|
)
|
|
|
|
if actual_out_path != out_path:
|
|
os.rename(
|
|
to_bytes(actual_out_path, errors="strict"),
|
|
to_bytes(out_path, errors="strict"),
|
|
)
|
|
|
|
def close(self):
|
|
"""Terminate the connection. Nothing to do for kubectl"""
|
|
super(Connection, self).close()
|
|
self._connected = False
|