Files
kubernetes.core/plugins/connection/kubectl.py
GomathiselviS b066a2dda3 Cleanup GitHub workflows (#655)
* Cleanup gha

* test by removing matrix excludes

* Rename sanity tests

* trigger integration tests

* Fix ansible-lint workflow

* Fix concurrency

* Add ansible-lint config

* Add ansible-lint config

* Fix integration and lint issues

* integration wf

* fix yamllint issues

* fix yamllint issues

* update readme and add ignore-2.16.txt

* fix ansible-doc

* Add version

* Use /dev/random to generate random data

The GHA environment has difficultly generating entropy. Trying to read
from /dev/urandom just blocks forever. We don't care if the random data
is cryptographically secure; it's just garbage data for the test. Read
from /dev/random, instead. This is only used during the k8s_copy test
target.

This also removes the custom test module that was being used to generate
the files. It's not worth maintaining this for two task that can be
replaced with some simple command/shell tasks.

* Fix saniry errors

* test github_action fix

* Address review comments

* Remove default types

* review comments

* isort fixes

* remove tags

* Add setuptools to venv

* Test gh changes

* update changelog

* update ignore-2.16

* Fix indentation in inventory plugin example

* Update .github/workflows/integration-tests.yaml

* Update integration-tests.yaml

---------

Co-authored-by: Mike Graves <mgraves@redhat.com>
Co-authored-by: Bikouo Aubin <79859644+abikouo@users.noreply.github.com>
2023-11-10 16:33:40 +01:00

445 lines
16 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_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 _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,
)
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
)
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
)
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