diff --git a/.github/workflows/kubectl-splunk-workflow.yml b/.github/workflows/kubectl-splunk-workflow.yml new file mode 100644 index 000000000..fc62f3119 --- /dev/null +++ b/.github/workflows/kubectl-splunk-workflow.yml @@ -0,0 +1,46 @@ +# .github/workflows/ci.yml + +name: Kubectl Splunk CI + +on: + push: + branches: + - CSPL-3152 + pull_request: + branches: + - CSPL-3152 + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + + - name: Install dependencies + run: | + python -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r tools/kubect-splunk/requirements.txt + pip install -e . + pip install pytest coverage + + - name: Run Tests with Coverage + run: | + source .venv/bin/activate + coverage run -m unittest discover -s tests + coverage report + coverage xml + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v2 + with: + files: coverage.xml + flags: unittests + name: codecov-umbrella diff --git a/docs/KubectlSplunk.md b/docs/KubectlSplunk.md new file mode 100644 index 000000000..5bc8d30ec --- /dev/null +++ b/docs/KubectlSplunk.md @@ -0,0 +1,592 @@ +# kubectl-splunk Plugin + +![PyPI Version](https://img.shields.io/pypi/v/kubectl-splunk) +![License](https://img.shields.io/pypi/l/kubectl-splunk) +![Python Version](https://img.shields.io/pypi/pyversions/kubectl-splunk) +![PyPI Downloads](https://img.shields.io/pypi/dm/kubectl-splunk) +![Build Status](https://img.shields.io/github/actions/workflow/status/splunk/splunk-operator/ci.yml?branch=main) + +## Overview + +`kubectl-splunk` is a `kubectl` plugin that allows you to execute Splunk commands directly within Splunk pods running in a Kubernetes cluster. It simplifies the management and interaction with Splunk instances deployed as StatefulSets or Deployments by providing a convenient command-line interface. + +This plugin supports various features such as: + +- Executing Splunk CLI commands inside Splunk pods. +- Running Splunk REST API calls via port-forwarding. +- Specifying pods directly via command line or configuration file. +- Automatic retrieval of Splunk admin credentials from pods. +- Interactive shell access to Splunk pods. +- Copying files to and from Splunk pods. +- Handling authentication securely with credential storage. +- Customizable configurations and verbosity levels. +- Cross-platform compatibility. +- Auto-completion support. + +--- + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Command Structure](#basic-command-structure) + - [Modes of Operation](#modes-of-operation) +- [Options](#options) +- [Examples](#examples) +- [Configuration](#configuration) + - [Configuration File](#configuration-file) + - [Environment Variables](#environment-variables) + - [Pod Selection Behavior](#pod-selection-behavior) +- [Authentication](#authentication) + - [Default Credentials](#default-credentials) + - [Credential Storage](#credential-storage) +- [Features](#features) +- [REST API Mode](#rest-api-mode) +- [Copy Mode](#copy-mode) +- [Interactive Shell](#interactive-shell) +- [Logging and Verbosity](#logging-and-verbosity) +- [Caching](#caching) +- [Auto-Completion](#auto-completion) +- [Troubleshooting](#troubleshooting) +- [License](#license) +- [Contributing](#contributing) +- [Feedback](#feedback) + +--- + +## Prerequisites + +- **Python 3**: Ensure Python 3.x is installed on your system. +- **kubectl**: The Kubernetes command-line tool must be installed and configured. +- **Access to Kubernetes Cluster**: You should have access to the Kubernetes cluster where Splunk is deployed. +- **Splunk CLI in Pods**: The Splunk Command Line Interface should be available within the Splunk pods. +- **Python Packages**: Install required Python packages: + ```bash + pip install requests argcomplete + ``` + +--- + +## Installation + +You can install `kubectl-splunk` directly from PyPI using `pip`. It is recommended to use `pipx` for isolated installations to prevent dependency conflicts. + +### Using `pipx` (Recommended) + +`pipx` allows you to install and run Python applications in isolated environments. + +1. **Install `pipx`** (if not already installed): + + ```bash + python3 -m pip install --user pipx + python3 -m pipx ensurepath + ``` + + **Note**: You may need to restart your shell or run `source ~/.bashrc` / `source ~/.zshrc` to update your `PATH`. + +2. **Install `kubectl-splunk` using `pipx`**: + + ```bash + pipx install kubectl-splunk + ``` + +### Using `pip` + +If you prefer to install `kubectl-splunk` using `pip` within a virtual environment: + +1. **Create and Activate a Virtual Environment**: + + ```bash + python3 -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + ``` + +2. **Install `kubectl-splunk`**: + + ```bash + pip install kubectl-splunk + ``` + +### Verify Installation + +After installation, ensure that the `kubectl-splunk` executable is available in your `PATH`: + +```bash +kubectl splunk --help +``` + +You should see the help message for `kubectl-splunk`, confirming that the installation was successful. + +--- + +## Usage + +### Basic Command Structure + +```bash +kubectl splunk [global options] [mode options] +``` + +### Modes of Operation + +- **exec**: Execute a Splunk command inside a Splunk pod. +- **rest**: Execute a Splunk REST API call via port-forwarding. +- **cp**: Copy files to or from a Splunk pod. +- **interactive**: Start an interactive shell inside a Splunk pod (use `--interactive` flag). + +--- + +## Options + +### Global Options + +- `-n`, `--namespace`: Specify the Kubernetes namespace (default: `default` or from config/env). +- `-l`, `--selector`: Label selector to identify Splunk pods (default: `app=splunk` or from config/env). +- `--context`: Specify the Kubernetes context. +- `-P`, `--pod`: Specify the exact pod name to run the command on (can be set in config/env). +- `-i`, `--interactive`: Start an interactive shell inside the Splunk pod. +- `--splunk-path`: Path to the Splunk CLI inside the container (default: `splunk` or from config/env). +- `--local-port`: Local port for port-forwarding in REST mode (default: `8000` or from config/env). +- `-v`: Increase output verbosity (use `-v`, `-vv`, or `-vvv`). +- `--version`: Show program version and exit. + +### Authentication Options + +- `-u`, `--username`: Username for Splunk authentication (default: `admin`). +- `-p`, `--password`: Password for Splunk authentication (will prompt or auto-detect if not provided). +- `--insecure`: Disable SSL certificate verification (useful for self-signed certificates). +- `--save-credentials`: Save credentials securely for future use. + +### Mode Options + +#### **exec** + +- **Usage**: `kubectl splunk exec [splunk_command]` +- **Options**: + - `splunk_command`: Splunk command to execute (e.g., `list user`). + +#### **rest** + +- **Usage**: `kubectl splunk rest METHOD ENDPOINT [options]` +- **Options**: + - `METHOD`: HTTP method (`GET`, `POST`, `PUT`, `DELETE`). + - `ENDPOINT`: Splunk REST API endpoint (e.g., `/services/server/info`). + - `--data`: Data to send with the request (for `POST`/`PUT`). + - `--params`: Query parameters (e.g., `"key1=value1&key2=value2"`). + +#### **cp** + +- **Usage**: `kubectl splunk cp SRC DEST` +- **Options**: + - `SRC`: Source file path. + - `DEST`: Destination file path. + - Use `:` to indicate the remote path in the pod (e.g., `:/path/in/pod`). + +--- + +## Examples + +### Execute a Splunk Command + +```bash +kubectl splunk exec status +``` + +### Execute a Splunk Search Command + +```bash +kubectl splunk exec search "index=_internal | head 10" +``` + +### Specify Namespace and Label Selector + +```bash +kubectl splunk -n splunk-namespace -l app=splunk exec status +``` + +### Specify a Pod Directly + +```bash +kubectl splunk --pod splunk-idxc-indexer-0 exec status +``` + +Or using the short alias: + +```bash +kubectl splunk -P splunk-idxc-indexer-0 exec status +``` + +### Start an Interactive Shell + +```bash +kubectl splunk --interactive +``` + +### Copy Files to a Pod + +```bash +kubectl splunk cp /local/path/file.txt :/remote/path/file.txt +``` + +### Copy Files from a Pod + +```bash +kubectl splunk cp :/remote/path/file.txt /local/path/file.txt +``` + +### Execute a REST API Call + +```bash +kubectl splunk rest GET /services/server/info --insecure +``` + +### Create a Search Job (POST Request) + +```bash +kubectl splunk rest POST /services/search/jobs --data "search=search index=_internal | head 10" --insecure +``` + +### Use Authentication and Save Credentials + +```bash +kubectl splunk -u admin --save-credentials exec list user +``` + +### Increase Verbosity + +```bash +kubectl splunk -vv exec status +``` + +### Specify Kubernetes Context + +```bash +kubectl splunk --context my-cluster exec status +``` + +--- + +## Configuration + +### Configuration File + +You can create a configuration file to set default values: + +**File**: `~/.kubectl_splunk_config` + +**Content**: + +```ini +[DEFAULT] +namespace = splunk-namespace +selector = app=splunk +splunk_path = splunk +pod_name = splunk-idxc-indexer-0 # Default pod name +local_port = 8000 # Default local port for REST mode +``` + +- **namespace**: Default Kubernetes namespace. +- **selector**: Default label selector to identify Splunk pods. +- **splunk_path**: Path to the Splunk CLI inside the container. +- **pod_name**: Default pod name to use if not specified via command line. +- **local_port**: Default local port for port-forwarding in REST mode. + +### Environment Variables + +You can set environment variables to override defaults: + +- `KUBECTL_SPLUNK_NAMESPACE`: Sets the default namespace. +- `KUBECTL_SPLUNK_SELECTOR`: Sets the default label selector. +- `KUBECTL_SPLUNK_PATH`: Sets the default Splunk CLI path. +- `KUBECTL_SPLUNK_POD`: Sets the default pod name. +- `KUBECTL_SPLUNK_LOCAL_PORT`: Sets the default local port for REST mode. + +**Example**: + +```bash +export KUBECTL_SPLUNK_NAMESPACE=splunk-namespace +export KUBECTL_SPLUNK_SELECTOR=app=splunk +export KUBECTL_SPLUNK_POD=splunk-idxc-indexer-0 +export KUBECTL_SPLUNK_LOCAL_PORT=8000 +``` + +### Pod Selection Behavior + +The script determines which pod to use based on the following priority: + +1. **Command-line Argument**: Pod specified with `--pod` or `-P` takes the highest priority. +2. **Environment Variable**: If `KUBECTL_SPLUNK_POD` is set, it will be used if no pod is specified on the command line. +3. **Configuration File**: The `pod_name` from `~/.kubectl_splunk_config` is used if neither of the above is provided. +4. **Interactive Selection**: If no pod is specified and multiple pods are found, the script will list them and prompt for selection. +5. **Automatic Selection**: If only one pod is found, the script will use it automatically. + +--- + +## Authentication + +### Default Credentials + +- **Username**: Defaults to `admin` if not specified. +- **Password**: If not provided, the script attempts to retrieve the password from the pod's `/mnt/splunk-secrets/password` file. +- **Automatic Password Retrieval**: Works if the password file is accessible within the pod. + +**Usage Without Credentials**: + +```bash +kubectl splunk exec list user +``` + +### Credential Storage + +- **Save Credentials**: Use `--save-credentials` to store credentials securely for future use. +- **Credentials File**: Stored in `~/.kubectl_splunk_credentials` with permissions set to `600`. +- **Provide Credentials Once**: + + ```bash + kubectl splunk -u admin --save-credentials exec list user + ``` + +- **Subsequent Commands**: Credentials are used automatically. + +**Note**: Passwords are handled securely and are not exposed in logs or command outputs. + +--- + +## Features + +- **Execute Splunk Commands**: Run any Splunk CLI command directly within the pod. +- **REST API Support**: Execute Splunk REST API calls via port-forwarding. +- **Pod Selection**: Specify a pod directly via command line, environment variable, or configuration file. If not specified, the script will prompt for selection when multiple pods are present. +- **Automatic Credential Retrieval**: Defaults to `admin` user and retrieves the password from the pod if not provided. +- **Interactive Shell**: Start a shell session inside the Splunk pod. +- **Copy Files**: Transfer files to and from Splunk pods. +- **Authentication Handling**: Securely handle Splunk authentication credentials with options to save them. +- **Configuration Flexibility**: Use config files or environment variables for defaults. +- **Verbosity Control**: Adjust logging levels for more or less output. +- **Caching**: Pod information is cached for improved performance. +- **Auto-Completion**: Supports shell auto-completion for commands and options. +- **Secure Logging**: Sensitive information such as passwords is not logged. + +--- + +## REST API Mode + +Use the `rest` mode to execute Splunk REST API calls via port-forwarding. + +**Usage**: + +```bash +kubectl splunk rest METHOD ENDPOINT [--data DATA] [--params PARAMS] [options] +``` + +- **METHOD**: HTTP method (`GET`, `POST`, `PUT`, `DELETE`). +- **ENDPOINT**: Splunk REST API endpoint (e.g., `/services/server/info`). +- **Options**: + - `--data`: Data to send with the request (for `POST`/`PUT`). + - `--params`: Query parameters (e.g., `"key1=value1&key2=value2"`). + - `--insecure`: Disable SSL certificate verification. + +**Examples**: + +- **Get Server Info**: + + ```bash + kubectl splunk rest GET /services/server/info --insecure + ``` + +- **Create a Search Job**: + + ```bash + kubectl splunk rest POST /services/search/jobs --data "search=search index=_internal | head 10" --insecure + ``` + +--- + +## Copy Mode + +Use the `cp` mode to copy files to and from a Splunk pod. + +**Copy to Pod**: + +```bash +kubectl splunk -P splunk-idxc-indexer-0 cp /local/path/file.txt :/remote/path/file.txt +``` + +**Copy from Pod**: + +```bash +kubectl splunk -P splunk-idxc-indexer-0 cp :/remote/path/file.txt /local/path/file.txt +``` + +**Note**: + +- Use `:` to indicate the remote path in the pod. +- The `cp` mode requires you to specify a single pod using `--pod`, `-P`, environment variable, or configuration file. +- If multiple pods are found and no pod is specified, the script will prompt for selection. + +--- + +## Interactive Shell + +Start an interactive shell session inside a Splunk pod using the `--interactive` flag: + +```bash +kubectl splunk --interactive +``` + +- If the pod is specified via command line, environment variable, or config file, the script will use it. +- If multiple pods are found and no pod is specified, the script will prompt for selection. + +--- + +## Logging and Verbosity + +Adjust the logging verbosity using the `-v` flag: + +- `-v`: Show warnings and errors. +- `-vv`: Show informational messages. +- `-vvv`: Show debug messages. + +**Example**: + +```bash +kubectl splunk -vv exec status +``` + +**Note**: Sensitive information such as passwords is masked in logs. + +--- + +## Caching + +The plugin caches the pod name used during execution for 5 minutes to improve performance. The cache is stored in `/tmp/kubectl_splunk_cache.json`. + +**Behavior**: + +- If a pod name is specified via command line, environment variable, or configuration file, the cache will use that pod. +- If no pod name is specified and multiple pods are found, the selected pod will be cached. +- The cache will expire after 5 minutes. + +**To Clear the Cache**: + +```bash +rm /tmp/kubectl_splunk_cache.json +``` + +--- + +## Auto-Completion + +You can enable command auto-completion to enhance your command-line experience. + +**Install the `argcomplete` Package**: + +```bash +pip install argcomplete +``` + +**Activate Global Completion**: + +```bash +activate-global-python-argcomplete --user +``` + +**Add to Shell Configuration**: + +Add the following to your shell's initialization file (e.g., `.bashrc`, `.bash_profile`, or `.zshrc`): + +```bash +eval "$(register-python-argcomplete kubectl-splunk)" +``` + +Reload your shell configuration: + +```bash +source ~/.bashrc # or your shell's config file +``` + +--- + +## Troubleshooting + +- **Ambiguous Option Error**: If you encounter an error like `ambiguous option: --p could match --pod, --password`, ensure you are using the correct option names. Use `--pod` or `-P` for specifying the pod, and `--password` or `-p` for the password. + +- **Pod Not Found**: If the specified pod is not found, verify the pod name and namespace. Use `kubectl get pods -n ` to list available pods. + +- **Multiple Pods**: If multiple pods are found and you did not specify a pod, the plugin will prompt you to select one. Use `--pod`, `-P`, environment variable, or configuration file to specify a pod directly and avoid the prompt. + +- **Copy Mode Limitations**: The `cp` mode requires you to specify a single pod. Ensure you specify a pod via command line, environment variable, or configuration file. + +- **Permission Denied**: If you receive permission errors, ensure you have the necessary permissions to access the Kubernetes cluster and the Splunk pods. + +- **Caching Issues**: If you believe the script is using an outdated pod from the cache, clear the cache by deleting the cache file. + +- **Password Retrieval Failure**: If the script fails to retrieve the password from the pod, ensure that: + - You have the necessary permissions to execute commands in the pod. + - The password file `/mnt/splunk-secrets/password` exists and is accessible. + +--- + +## License + +This project is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt). + +--- + +## Contributing + +Contributions are welcome! Please submit issues and pull requests via the project's GitHub repository. + +### How to Contribute + +1. **Fork the Repository**: Click the "Fork" button on the GitHub repository page to create your own copy. + +2. **Clone Your Fork**: + + ```bash + git clone https://github.com/splunk/splunk-operator.git + cd tools/kubectl-splunk + ``` + +3. **Create a Feature Branch**: + + ```bash + git checkout -b feature/your-feature-name + ``` + +4. **Make Your Changes**: Implement your feature or bug fix. Ensure your code follows the project's coding standards. + +5. **Run Tests**: Ensure all tests pass before committing. + + ```bash + python -m unittest discover -s tests + ``` + +6. **Commit Your Changes**: + + ```bash + git commit -m "Add feature X to kubectl-splunk" + ``` + +7. **Push to Your Fork**: + + ```bash + git push origin feature/your-feature-name + ``` + +8. **Open a Pull Request**: Navigate to the original repository and open a pull request describing your changes. + +### Coding Standards + +- Follow PEP 8 for Python code style. +- Write meaningful docstrings for modules, classes, and functions. +- Ensure that your code is well-documented and maintainable. + +### Reporting Issues + +If you encounter any issues or bugs, please open an issue on the [GitHub Issues](https://github.com/splunk/splunk-operator/issues) page. Provide detailed information to help us understand and resolve the problem. + +--- + +Thank you for using `kubectl-splunk`! We hope this plugin enhances your productivity when managing Splunk deployments on Kubernetes. diff --git a/tools/kubectl-splunk/README.md b/tools/kubectl-splunk/README.md new file mode 100644 index 000000000..79caf04a4 --- /dev/null +++ b/tools/kubectl-splunk/README.md @@ -0,0 +1,592 @@ +# kubectl-splunk Plugin + +![PyPI Version](https://img.shields.io/pypi/v/kubectl-splunk) +![License](https://img.shields.io/pypi/l/kubectl-splunk) +![Python Version](https://img.shields.io/pypi/pyversions/kubectl-splunk) +![PyPI Downloads](https://img.shields.io/pypi/dm/kubectl-splunk) +![Build Status](https://img.shields.io/github/actions/workflow/status/splunk/splunk-operator/ci.yml?branch=main) + +## Overview + +`kubectl-splunk` is a `kubectl` plugin that allows you to execute Splunk commands directly within Splunk pods running in a Kubernetes cluster. It simplifies the management and interaction with Splunk instances deployed as StatefulSets or Deployments by providing a convenient command-line interface. + +This plugin supports various features such as: + +- Executing Splunk CLI commands inside Splunk pods. +- Running Splunk REST API calls via port-forwarding. +- Specifying pods directly via command line or configuration file. +- Automatic retrieval of Splunk admin credentials from pods. +- Interactive shell access to Splunk pods. +- Copying files to and from Splunk pods. +- Handling authentication securely with credential storage. +- Customizable configurations and verbosity levels. +- Cross-platform compatibility. +- Auto-completion support. + +--- + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Command Structure](#basic-command-structure) + - [Modes of Operation](#modes-of-operation) +- [Options](#options) +- [Examples](#examples) +- [Configuration](#configuration) + - [Configuration File](#configuration-file) + - [Environment Variables](#environment-variables) + - [Pod Selection Behavior](#pod-selection-behavior) +- [Authentication](#authentication) + - [Default Credentials](#default-credentials) + - [Credential Storage](#credential-storage) +- [Features](#features) +- [REST API Mode](#rest-api-mode) +- [Copy Mode](#copy-mode) +- [Interactive Shell](#interactive-shell) +- [Logging and Verbosity](#logging-and-verbosity) +- [Caching](#caching) +- [Auto-Completion](#auto-completion) +- [Troubleshooting](#troubleshooting) +- [License](#license) +- [Contributing](#contributing) +- [Feedback](#feedback) + +--- + +## Prerequisites + +- **Python 3**: Ensure Python 3.x is installed on your system. +- **kubectl**: The Kubernetes command-line tool must be installed and configured. +- **Access to Kubernetes Cluster**: You should have access to the Kubernetes cluster where Splunk is deployed. +- **Splunk CLI in Pods**: The Splunk Command Line Interface should be available within the Splunk pods. +- **Python Packages**: Install required Python packages: + ```bash + pip install requests argcomplete + ``` + +--- + +## Installation + +You can install `kubectl-splunk` directly from PyPI using `pip`. It is recommended to use `pipx` for isolated installations to prevent dependency conflicts. + +### Using `pipx` (Recommended) + +`pipx` allows you to install and run Python applications in isolated environments. + +1. **Install `pipx`** (if not already installed): + + ```bash + python3 -m pip install --user pipx + python3 -m pipx ensurepath + ``` + + **Note**: You may need to restart your shell or run `source ~/.bashrc` / `source ~/.zshrc` to update your `PATH`. + +2. **Install `kubectl-splunk` using `pipx`**: + + ```bash + pipx install kubectl-splunk + ``` + +### Using `pip` + +If you prefer to install `kubectl-splunk` using `pip` within a virtual environment: + +1. **Create and Activate a Virtual Environment**: + + ```bash + python3 -m venv .venv + source .venv/bin/activate # On Windows: .venv\Scripts\activate + ``` + +2. **Install `kubectl-splunk`**: + + ```bash + pip install kubectl-splunk + ``` + +### Verify Installation + +After installation, ensure that the `kubectl-splunk` executable is available in your `PATH`: + +```bash +kubectl splunk --help +``` + +You should see the help message for `kubectl-splunk`, confirming that the installation was successful. + +--- + +## Usage + +### Basic Command Structure + +```bash +kubectl splunk [global options] [mode options] +``` + +### Modes of Operation + +- **exec**: Execute a Splunk command inside a Splunk pod. +- **rest**: Execute a Splunk REST API call via port-forwarding. +- **cp**: Copy files to or from a Splunk pod. +- **interactive**: Start an interactive shell inside a Splunk pod (use `--interactive` flag). + +--- + +## Options + +### Global Options + +- `-n`, `--namespace`: Specify the Kubernetes namespace (default: `default` or from config/env). +- `-l`, `--selector`: Label selector to identify Splunk pods (default: `app=splunk` or from config/env). +- `--context`: Specify the Kubernetes context. +- `-P`, `--pod`: Specify the exact pod name to run the command on (can be set in config/env). +- `-i`, `--interactive`: Start an interactive shell inside the Splunk pod. +- `--splunk-path`: Path to the Splunk CLI inside the container (default: `splunk` or from config/env). +- `--local-port`: Local port for port-forwarding in REST mode (default: `8000` or from config/env). +- `-v`: Increase output verbosity (use `-v`, `-vv`, or `-vvv`). +- `--version`: Show program version and exit. + +### Authentication Options + +- `-u`, `--username`: Username for Splunk authentication (default: `admin`). +- `-p`, `--password`: Password for Splunk authentication (will prompt or auto-detect if not provided). +- `--insecure`: Disable SSL certificate verification (useful for self-signed certificates). +- `--save-credentials`: Save credentials securely for future use. + +### Mode Options + +#### **exec** + +- **Usage**: `kubectl splunk exec [splunk_command]` +- **Options**: + - `splunk_command`: Splunk command to execute (e.g., `list user`). + +#### **rest** + +- **Usage**: `kubectl splunk rest METHOD ENDPOINT [options]` +- **Options**: + - `METHOD`: HTTP method (`GET`, `POST`, `PUT`, `DELETE`). + - `ENDPOINT`: Splunk REST API endpoint (e.g., `/services/server/info`). + - `--data`: Data to send with the request (for `POST`/`PUT`). + - `--params`: Query parameters (e.g., `"key1=value1&key2=value2"`). + +#### **cp** + +- **Usage**: `kubectl splunk cp SRC DEST` +- **Options**: + - `SRC`: Source file path. + - `DEST`: Destination file path. + - Use `:` to indicate the remote path in the pod (e.g., `:/path/in/pod`). + +--- + +## Examples + +### Execute a Splunk Command + +```bash +kubectl splunk exec status +``` + +### Execute a Splunk Search Command + +```bash +kubectl splunk exec search "index=_internal | head 10" +``` + +### Specify Namespace and Label Selector + +```bash +kubectl splunk -n splunk-namespace -l app=splunk exec status +``` + +### Specify a Pod Directly + +```bash +kubectl splunk --pod splunk-idxc-indexer-0 exec status +``` + +Or using the short alias: + +```bash +kubectl splunk -P splunk-idxc-indexer-0 exec status +``` + +### Start an Interactive Shell + +```bash +kubectl splunk --interactive +``` + +### Copy Files to a Pod + +```bash +kubectl splunk cp /local/path/file.txt :/remote/path/file.txt +``` + +### Copy Files from a Pod + +```bash +kubectl splunk cp :/remote/path/file.txt /local/path/file.txt +``` + +### Execute a REST API Call + +```bash +kubectl splunk rest GET /services/server/info --insecure +``` + +### Create a Search Job (POST Request) + +```bash +kubectl splunk rest POST /services/search/jobs --data "search=search index=_internal | head 10" --insecure +``` + +### Use Authentication and Save Credentials + +```bash +kubectl splunk -u admin --save-credentials exec list user +``` + +### Increase Verbosity + +```bash +kubectl splunk -vv exec status +``` + +### Specify Kubernetes Context + +```bash +kubectl splunk --context my-cluster exec status +``` + +--- + +## Configuration + +### Configuration File + +You can create a configuration file to set default values: + +**File**: `~/.kubectl_splunk_config` + +**Content**: + +```ini +[DEFAULT] +namespace = splunk-namespace +selector = app=splunk +splunk_path = splunk +pod_name = splunk-idxc-indexer-0 # Default pod name +local_port = 8000 # Default local port for REST mode +``` + +- **namespace**: Default Kubernetes namespace. +- **selector**: Default label selector to identify Splunk pods. +- **splunk_path**: Path to the Splunk CLI inside the container. +- **pod_name**: Default pod name to use if not specified via command line. +- **local_port**: Default local port for port-forwarding in REST mode. + +### Environment Variables + +You can set environment variables to override defaults: + +- `KUBECTL_SPLUNK_NAMESPACE`: Sets the default namespace. +- `KUBECTL_SPLUNK_SELECTOR`: Sets the default label selector. +- `KUBECTL_SPLUNK_PATH`: Sets the default Splunk CLI path. +- `KUBECTL_SPLUNK_POD`: Sets the default pod name. +- `KUBECTL_SPLUNK_LOCAL_PORT`: Sets the default local port for REST mode. + +**Example**: + +```bash +export KUBECTL_SPLUNK_NAMESPACE=splunk-namespace +export KUBECTL_SPLUNK_SELECTOR=app=splunk +export KUBECTL_SPLUNK_POD=splunk-idxc-indexer-0 +export KUBECTL_SPLUNK_LOCAL_PORT=8000 +``` + +### Pod Selection Behavior + +The script determines which pod to use based on the following priority: + +1. **Command-line Argument**: Pod specified with `--pod` or `-P` takes the highest priority. +2. **Environment Variable**: If `KUBECTL_SPLUNK_POD` is set, it will be used if no pod is specified on the command line. +3. **Configuration File**: The `pod_name` from `~/.kubectl_splunk_config` is used if neither of the above is provided. +4. **Interactive Selection**: If no pod is specified and multiple pods are found, the script will list them and prompt for selection. +5. **Automatic Selection**: If only one pod is found, the script will use it automatically. + +--- + +## Authentication + +### Default Credentials + +- **Username**: Defaults to `admin` if not specified. +- **Password**: If not provided, the script attempts to retrieve the password from the pod's `/mnt/splunk-secrets/password` file. +- **Automatic Password Retrieval**: Works if the password file is accessible within the pod. + +**Usage Without Credentials**: + +```bash +kubectl splunk exec list user +``` + +### Credential Storage + +- **Save Credentials**: Use `--save-credentials` to store credentials securely for future use. +- **Credentials File**: Stored in `~/.kubectl_splunk_credentials` with permissions set to `600`. +- **Provide Credentials Once**: + + ```bash + kubectl splunk -u admin --save-credentials exec list user + ``` + +- **Subsequent Commands**: Credentials are used automatically. + +**Note**: Passwords are handled securely and are not exposed in logs or command outputs. + +--- + +## Features + +- **Execute Splunk Commands**: Run any Splunk CLI command directly within the pod. +- **REST API Support**: Execute Splunk REST API calls via port-forwarding. +- **Pod Selection**: Specify a pod directly via command line, environment variable, or configuration file. If not specified, the script will prompt for selection when multiple pods are present. +- **Automatic Credential Retrieval**: Defaults to `admin` user and retrieves the password from the pod if not provided. +- **Interactive Shell**: Start a shell session inside the Splunk pod. +- **Copy Files**: Transfer files to and from Splunk pods. +- **Authentication Handling**: Securely handle Splunk authentication credentials with options to save them. +- **Configuration Flexibility**: Use config files or environment variables for defaults. +- **Verbosity Control**: Adjust logging levels for more or less output. +- **Caching**: Pod information is cached for improved performance. +- **Auto-Completion**: Supports shell auto-completion for commands and options. +- **Secure Logging**: Sensitive information such as passwords is not logged. + +--- + +## REST API Mode + +Use the `rest` mode to execute Splunk REST API calls via port-forwarding. + +**Usage**: + +```bash +kubectl splunk rest METHOD ENDPOINT [--data DATA] [--params PARAMS] [options] +``` + +- **METHOD**: HTTP method (`GET`, `POST`, `PUT`, `DELETE`). +- **ENDPOINT**: Splunk REST API endpoint (e.g., `/services/server/info`). +- **Options**: + - `--data`: Data to send with the request (for `POST`/`PUT`). + - `--params`: Query parameters (e.g., `"key1=value1&key2=value2"`). + - `--insecure`: Disable SSL certificate verification. + +**Examples**: + +- **Get Server Info**: + + ```bash + kubectl splunk rest GET /services/server/info --insecure + ``` + +- **Create a Search Job**: + + ```bash + kubectl splunk rest POST /services/search/jobs --data "search=search index=_internal | head 10" --insecure + ``` + +--- + +## Copy Mode + +Use the `cp` mode to copy files to and from a Splunk pod. + +**Copy to Pod**: + +```bash +kubectl splunk -P splunk-idxc-indexer-0 cp /local/path/file.txt :/remote/path/file.txt +``` + +**Copy from Pod**: + +```bash +kubectl splunk -P splunk-idxc-indexer-0 cp :/remote/path/file.txt /local/path/file.txt +``` + +**Note**: + +- Use `:` to indicate the remote path in the pod. +- The `cp` mode requires you to specify a single pod using `--pod`, `-P`, environment variable, or configuration file. +- If multiple pods are found and no pod is specified, the script will prompt for selection. + +--- + +## Interactive Shell + +Start an interactive shell session inside a Splunk pod using the `--interactive` flag: + +```bash +kubectl splunk --interactive +``` + +- If the pod is specified via command line, environment variable, or config file, the script will use it. +- If multiple pods are found and no pod is specified, the script will prompt for selection. + +--- + +## Logging and Verbosity + +Adjust the logging verbosity using the `-v` flag: + +- `-v`: Show warnings and errors. +- `-vv`: Show informational messages. +- `-vvv`: Show debug messages. + +**Example**: + +```bash +kubectl splunk -vv exec status +``` + +**Note**: Sensitive information such as passwords is masked in logs. + +--- + +## Caching + +The plugin caches the pod name used during execution for 5 minutes to improve performance. The cache is stored in `/tmp/kubectl_splunk_cache.json`. + +**Behavior**: + +- If a pod name is specified via command line, environment variable, or configuration file, the cache will use that pod. +- If no pod name is specified and multiple pods are found, the selected pod will be cached. +- The cache will expire after 5 minutes. + +**To Clear the Cache**: + +```bash +rm /tmp/kubectl_splunk_cache.json +``` + +--- + +## Auto-Completion + +You can enable command auto-completion to enhance your command-line experience. + +**Install the `argcomplete` Package**: + +```bash +pip install argcomplete +``` + +**Activate Global Completion**: + +```bash +activate-global-python-argcomplete --user +``` + +**Add to Shell Configuration**: + +Add the following to your shell's initialization file (e.g., `.bashrc`, `.bash_profile`, or `.zshrc`): + +```bash +eval "$(register-python-argcomplete kubectl-splunk)" +``` + +Reload your shell configuration: + +```bash +source ~/.bashrc # or your shell's config file +``` + +--- + +## Troubleshooting + +- **Ambiguous Option Error**: If you encounter an error like `ambiguous option: --p could match --pod, --password`, ensure you are using the correct option names. Use `--pod` or `-P` for specifying the pod, and `--password` or `-p` for the password. + +- **Pod Not Found**: If the specified pod is not found, verify the pod name and namespace. Use `kubectl get pods -n ` to list available pods. + +- **Multiple Pods**: If multiple pods are found and you did not specify a pod, the plugin will prompt you to select one. Use `--pod`, `-P`, environment variable, or configuration file to specify a pod directly and avoid the prompt. + +- **Copy Mode Limitations**: The `cp` mode requires you to specify a single pod. Ensure you specify a pod via command line, environment variable, or configuration file. + +- **Permission Denied**: If you receive permission errors, ensure you have the necessary permissions to access the Kubernetes cluster and the Splunk pods. + +- **Caching Issues**: If you believe the script is using an outdated pod from the cache, clear the cache by deleting the cache file. + +- **Password Retrieval Failure**: If the script fails to retrieve the password from the pod, ensure that: + - You have the necessary permissions to execute commands in the pod. + - The password file `/mnt/splunk-secrets/password` exists and is accessible. + +--- + +## License + +This project is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt). + +--- + +## Contributing + +Contributions are welcome! Please submit issues and pull requests via the project's GitHub repository. + +### How to Contribute + +1. **Fork the Repository**: Click the "Fork" button on the GitHub repository page to create your own copy. + +2. **Clone Your Fork**: + + ```bash + git clone https://github.com/splunk/splunk-operator.git + cd tools/kubectl-splunk + ``` + +3. **Create a Feature Branch**: + + ```bash + git checkout -b feature/your-feature-name + ``` + +4. **Make Your Changes**: Implement your feature or bug fix. Ensure your code follows the project's coding standards. + +5. **Run Tests**: Ensure all tests pass before committing. + + ```bash + python -m unittest discover -s tests + ``` + +6. **Commit Your Changes**: + + ```bash + git commit -m "Add feature X to kubectl-splunk" + ``` + +7. **Push to Your Fork**: + + ```bash + git push origin feature/your-feature-name + ``` + +8. **Open a Pull Request**: Navigate to the original repository and open a pull request describing your changes. + +### Coding Standards + +- Follow PEP 8 for Python code style. +- Write meaningful docstrings for modules, classes, and functions. +- Ensure that your code is well-documented and maintainable. + +### Reporting Issues + +If you encounter any issues or bugs, please open an issue on the [GitHub Issues](https://github.com/splunk/splunk-operator/issues) page. Provide detailed information to help us understand and resolve the problem. + +--- + +Thank you for using `kubectl-splunk`! We hope this plugin enhances your productivity when managing Splunk deployments on Kubernetes. \ No newline at end of file diff --git a/tools/kubectl-splunk/kubectl_splunk/__init__.py b/tools/kubectl-splunk/kubectl_splunk/__init__.py new file mode 100644 index 000000000..d91a15032 --- /dev/null +++ b/tools/kubectl-splunk/kubectl_splunk/__init__.py @@ -0,0 +1,4 @@ +# kubectl_splunk/__init__.py + +# This file is intentionally left empty to mark the kubectl_splunk directory as a Python package. + diff --git a/tools/kubectl-splunk/kubectl_splunk/main.py b/tools/kubectl-splunk/kubectl_splunk/main.py new file mode 100644 index 000000000..618545b49 --- /dev/null +++ b/tools/kubectl-splunk/kubectl_splunk/main.py @@ -0,0 +1,441 @@ +# kubectl_splunk/main.py + +import sys +import subprocess +import argparse +import logging +import os +import time +import configparser +import json +import getpass +import requests +from concurrent.futures import ThreadPoolExecutor +from argparse import RawDescriptionHelpFormatter + +try: + import argcomplete +except ImportError: + argcomplete = None + +def load_config(): + """Load configuration from file and environment variables.""" + config = configparser.ConfigParser() + config_file = os.path.expanduser('~/.kubectl_splunk_config') + if os.path.exists(config_file): + config.read(config_file) + namespace = config.get('DEFAULT', 'namespace', fallback='default') + selector = config.get('DEFAULT', 'selector', fallback='app=splunk') + splunk_path = config.get('DEFAULT', 'splunk_path', fallback='splunk') + pod_name = config.get('DEFAULT', 'pod_name', fallback=None) + local_port = config.getint('DEFAULT', 'local_port', fallback=8089) + else: + namespace = 'default' + selector = 'app=splunk' + splunk_path = '/opt/splunk/bin/splunk' + pod_name = None + local_port = 8089 + + + # Override with environment variables if set + namespace = os.environ.get('KUBECTL_SPLUNK_NAMESPACE', namespace) + selector = os.environ.get('KUBECTL_SPLUNK_SELECTOR', selector) + splunk_path = os.environ.get('KUBECTL_SPLUNK_PATH', splunk_path) + pod_name = os.environ.get('KUBECTL_SPLUNK_POD', pod_name) + local_port = int(os.environ.get('KUBECTL_SPLUNK_LOCAL_PORT', local_port)) + return namespace, selector, splunk_path, pod_name, local_port + +def parse_args(namespace, selector, splunk_path, pod_name_from_config, local_port_from_config): + """Parse command-line arguments.""" + epilog_text = """ +Examples: + kubectl splunk exec search "index=_internal | head 10" + kubectl splunk -n splunk-namespace -l app=splunk -P splunk-idxc-indexer-0 exec status + kubectl splunk rest GET /services/server/info + kubectl splunk --interactive + """ + parser = argparse.ArgumentParser( + description='kubectl plugin to run Splunk commands within a Splunk pod', + epilog=epilog_text, + formatter_class=RawDescriptionHelpFormatter + ) + + parser.add_argument('-n', '--namespace', default=namespace, + help='Specify the Kubernetes namespace (default from config/env or "default")') + parser.add_argument('-l', '--selector', default=selector, + help='Label selector to identify the Splunk pod(s) (default from config/env or "app=splunk")') + parser.add_argument('--context', help='Specify the Kubernetes context') + parser.add_argument('-P', '--pod', default=pod_name_from_config, + help='Specify the exact pod name to run the command on (default from config/env if set)') + parser.add_argument('-i', '--interactive', action='store_true', + help='Start an interactive shell inside the Splunk pod') + parser.add_argument('--splunk-path', default=splunk_path, + help='Path to the Splunk CLI inside the container (default from config/env or "splunk")') + parser.add_argument('--local-port', type=int, default=local_port_from_config, + help='Local port for port-forwarding in REST mode (default from config/env or 8000)') + parser.add_argument('-v', '--verbose', action='count', default=0, + help='Increase output verbosity (e.g., -v, -vv, -vvv)') + parser.add_argument('--version', action='version', version='kubectl-splunk 1.6', + help='Show program version and exit') + + auth_group = parser.add_argument_group('Authentication') + auth_group.add_argument('-u', '--username', help='Username for Splunk authentication (default: admin)') + auth_group.add_argument('-p', '--password', help='Password for Splunk authentication (will prompt or auto-detect if not provided)') + auth_group.add_argument('--insecure', action='store_true', + help='Disable SSL certificate verification') + auth_group.add_argument('--save-credentials', action='store_true', + help='Save credentials securely for future use') + + subparsers = parser.add_subparsers(dest='mode', required=True, help='Available modes') + + # Subparser for exec mode + exec_parser = subparsers.add_parser('exec', help='Execute a Splunk command') + exec_parser.add_argument('splunk_command', nargs=argparse.REMAINDER, help='Splunk command to execute') + + # Subparser for copy mode + cp_parser = subparsers.add_parser('cp', help='Copy files to/from the Splunk pod') + cp_parser.add_argument('src', help='Source file path') + cp_parser.add_argument('dest', help='Destination file path') + + # Subparser for rest mode + rest_parser = subparsers.add_parser('rest', help='Execute a Splunk REST API call') + rest_parser.add_argument('method', choices=['GET', 'POST', 'PUT', 'DELETE'], help='HTTP method') + rest_parser.add_argument('endpoint', help='Splunk REST API endpoint (e.g., /services/server/info)') + rest_parser.add_argument('--data', help='Data to send with the request (for POST/PUT)') + rest_parser.add_argument('--params', help='Query parameters (e.g., "key1=value1&key2=value2")') + + if argcomplete: + argcomplete.autocomplete(parser) + + args = parser.parse_args() + + return args + +def setup_logging(verbosity): + """Set up logging based on verbosity level.""" + if verbosity >= 3: + level = logging.DEBUG + elif verbosity == 2: + level = logging.INFO + elif verbosity == 1: + level = logging.WARNING + else: + level = logging.ERROR + + logging.basicConfig(level=level, format='%(levelname)s: %(message)s') + +def get_pods(args): + """Retrieve the list of Splunk pods based on the label selector and namespace.""" + if args.pod: + # If a specific pod is specified, return it directly + return [args.pod] + else: + kubectl_cmd = ['kubectl'] + if args.context: + kubectl_cmd.extend(['--context', args.context]) + kubectl_cmd.extend(['get', 'pods', '-n', args.namespace, '-l', args.selector, + '-o', 'jsonpath={.items[*].metadata.name}']) + + logging.debug(f"Running command: {' '.join(kubectl_cmd)}") + + try: + pods_output = subprocess.check_output(kubectl_cmd, universal_newlines=True).strip() + except subprocess.CalledProcessError as e: + logging.error(f"Failed to get pods: {e}") + sys.exit(1) + + pods = pods_output.split() + if not pods: + logging.error(f"No Splunk pods found with selector '{args.selector}' in namespace '{args.namespace}'.") + sys.exit(1) + + logging.debug(f"Found pods: {pods}") + return pods + +def select_pods(pods, args): + """Allow the user to select pods when multiple pods are present.""" + if len(pods) == 1: + return pods + + print("Multiple Splunk pods found:") + for idx, pod in enumerate(pods): + print(f"{idx + 1}. {pod}") + print("0. Run command on all pods") + + while True: + try: + choice = int(input("Select a pod by number (or 0 to run on all): ")) + if choice == 0: + return pods + elif 1 <= choice <= len(pods): + return [pods[choice - 1]] + else: + print(f"Please enter a number between 0 and {len(pods)}.") + except ValueError: + print("Invalid input. Please enter a number.") + +def cache_pod(pod_name, namespace, selector): + """Cache the pod name to a file.""" + cache_file = '/tmp/kubectl_splunk_cache.json' + cache_data = { + 'pod_name': pod_name, + 'namespace': namespace, + 'selector': selector, + 'timestamp': time.time() + } + with open(cache_file, 'w') as f: + json.dump(cache_data, f) + +def load_cached_pod(namespace, selector): + """Load the cached pod name if available and valid.""" + cache_file = '/tmp/kubectl_splunk_cache.json' + if os.path.exists(cache_file): + with open(cache_file, 'r') as f: + cache = json.load(f) + if (time.time() - cache['timestamp'] < 300 and + cache['namespace'] == namespace and + cache['selector'] == selector): + return cache['pod_name'] + return None + +def get_credentials(args, pod_name): + """Retrieve stored credentials, prompt the user, or retrieve from pod.""" + credentials_file = os.path.expanduser('~/.kubectl_splunk_credentials') + username = args.username + password = args.password + + # Default username to 'admin' if not provided + if not username: + username = 'admin' + + # Load credentials from file if available + if not password: + if os.path.exists(credentials_file): + try: + with open(credentials_file, 'r') as f: + creds = json.load(f) + username = creds.get('username', username) + password = creds.get('password', password) + except Exception as e: + logging.warning("Failed to read stored credentials.") + os.remove(credentials_file) # Remove corrupted credentials file + + # Retrieve password from pod if still not available + if not password and username == 'admin': + password = retrieve_password_from_pod(args, pod_name) + if not password: + logging.error("Failed to retrieve password from pod.") + sys.exit(1) + + # Prompt for missing credentials + if not password: + password = getpass.getpass(prompt='Splunk Password: ') + + # Save credentials if requested + if args.save_credentials: + creds = {'username': username, 'password': password} + try: + with open(credentials_file, 'w') as f: + json.dump(creds, f) + os.chmod(credentials_file, 0o600) # Set file permissions to be readable by owner only + except Exception as e: + logging.warning("Failed to save credentials.") + + return username, password + +def retrieve_password_from_pod(args, pod_name): + """Retrieve the admin password from the pod's /mnt/splunk-secrets/password file.""" + kubectl_cmd = ['kubectl'] + if args.context: + kubectl_cmd.extend(['--context', args.context]) + kubectl_cmd.extend(['exec', '-n', args.namespace, pod_name, '--', 'cat', '/mnt/splunk-secrets/password']) + + logging.debug(f"Retrieving password from pod {pod_name}") + try: + password = subprocess.check_output(kubectl_cmd, universal_newlines=True).strip() + return password + except subprocess.CalledProcessError as e: + logging.error(f"Failed to retrieve password from pod {pod_name}: {e}") + return None + +def execute_on_pod(args, pod_name): + """Execute the Splunk command on a single pod.""" + kubectl_cmd = ['kubectl'] + if args.context: + kubectl_cmd.extend(['--context', args.context]) + kubectl_cmd.extend(['exec', '-n', args.namespace, pod_name, '--']) + + if args.mode == 'exec': + cmd = kubectl_cmd + [args.splunk_path] + cmd += args.splunk_command # Add the Splunk command arguments + # Authentication + username, password = get_credentials(args, pod_name) + cmd += ['-auth', f"{username}:{'********'}"] # Masked password for logging + logging.debug(f"Executing command on pod {pod_name}: {' '.join(cmd)}") + # Replace masked password with actual password in the command to be executed + cmd[-1] = f"{username}:{password}" + elif args.mode == 'interactive': + cmd = kubectl_cmd + ['/bin/bash'] + logging.debug(f"Starting interactive shell on pod {pod_name}") + else: + return + + try: + subprocess.run(cmd, check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Command failed on pod {pod_name} with exit code {e.returncode}") + sys.exit(e.returncode) + +def copy_to_pod(args, pod_name): + """Copy files to/from the Splunk pod.""" + kubectl_cmd = ['kubectl'] + if args.context: + kubectl_cmd.extend(['--context', args.context]) + src = args.src + dest = args.dest + # If source or destination starts with ':', it's assumed to be remote + if src.startswith(':'): + src = f"{args.namespace}/{pod_name}:{src[1:]}" + if dest.startswith(':'): + dest = f"{args.namespace}/{pod_name}:{dest[1:]}" + kubectl_cmd.extend(['cp', src, dest]) + + logging.debug(f"Copying files with command: {' '.join(kubectl_cmd)}") + try: + subprocess.run(kubectl_cmd, check=True) + except subprocess.CalledProcessError as e: + logging.error(f"Copy command failed with exit code {e.returncode}") + sys.exit(e.returncode) + +def execute_rest_call(args, pod_name): + """Execute a Splunk REST API call.""" + # Start port-forwarding + port = 8089 + local_port = args.local_port # User-specified local port + kubectl_cmd = ['kubectl'] + if args.context: + kubectl_cmd.extend(['--context', args.context]) + kubectl_cmd.extend([ + 'port-forward', '-n', args.namespace, pod_name, + f'{local_port}:{port}' + ]) + + logging.debug(f"Starting port-forward with command: {' '.join(kubectl_cmd)}") + # Start port-forwarding as a background process + port_forward_proc = subprocess.Popen(kubectl_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + # Wait for port-forwarding to start + for _ in range(10): + if port_forward_proc.poll() is not None: + raise Exception("Port-forward process terminated unexpectedly.") + time.sleep(0.5) + # Build the URL + url = f'https://127.0.0.1:{local_port}{args.endpoint}' + # Handle authentication + username, password = get_credentials(args, pod_name) + auth = (username, password) + + # Handle SSL verification + verify_ssl = not args.insecure + + # Prepare request parameters + method = args.method.upper() + headers = {'Content-Type': 'application/json'} + data = args.data + params = {} + if args.params: + # Parse query parameters + params = dict(param.split('=') for param in args.params.split('&')) + + logging.debug(f"Sending {method} request to {url}") + # Send the request + response = requests.request( + method=method, + url=url, + headers=headers, + params=params, + data=data, + auth=auth, + verify=verify_ssl + ) + # Print the response + print(response.text) + if response.status_code >= 400: + logging.error(f"HTTP {response.status_code}: {response.reason}") + sys.exit(1) + except Exception as e: + logging.error(f"Error during REST API call: {e}") + sys.exit(1) + finally: + # Terminate port-forwarding + port_forward_proc.terminate() + port_forward_proc.wait() + +def main(): + # Load configuration + namespace, selector, splunk_path, pod_name_from_config, local_port_from_config = load_config() + + # Parse arguments + args = parse_args(namespace, selector, splunk_path, pod_name_from_config, local_port_from_config) + + # Set up logging + setup_logging(args.verbose) + # Ensure sensitive information is not logged + masked_args = vars(args).copy() + if masked_args.get('password'): + masked_args['password'] = '********' + logging.debug(f"Arguments: {masked_args}") + + # Handle interactive shell separately + if args.interactive: + args.mode = 'interactive' + args.splunk_command = [] + + # Load cached pod if available + pod_name = None + if args.pod: + pods = [args.pod] + else: + pod_name = load_cached_pod(args.namespace, args.selector) + pods = [] + + if pod_name: + logging.info(f"Using cached pod: {pod_name}") + pods = [pod_name] + else: + # Get list of pods + pods = get_pods(args) + if len(pods) > 1 and args.mode != 'cp': + pods = select_pods(pods, args) + pod_name = pods[0] + # Cache the pod name + cache_pod(pod_name, args.namespace, args.selector) + + # Handle modes + if args.mode in ['exec', 'interactive']: + # Execute commands on pods (in parallel if multiple pods) + with ThreadPoolExecutor() as executor: + futures = [] + for pod in pods: + futures.append(executor.submit(execute_on_pod, args, pod)) + for future in futures: + future.result() # To catch exceptions + elif args.mode == 'cp': + # Copy files (cannot copy to multiple pods at once) + if len(pods) > 1: + logging.error("Copy mode does not support multiple pods. Please specify a single pod.") + sys.exit(1) + copy_to_pod(args, pods[0]) + elif args.mode == 'rest': + # REST API call (only supports a single pod) + if len(pods) > 1: + logging.error("REST mode does not support multiple pods. Please specify a single pod.") + sys.exit(1) + execute_rest_call(args, pods[0]) + else: + logging.error(f"Unknown mode: {args.mode}") + sys.exit(1) + +if __name__ == '__main__': + main() + diff --git a/tools/kubectl-splunk/requirements.txt b/tools/kubectl-splunk/requirements.txt new file mode 100644 index 000000000..a0737bb81 --- /dev/null +++ b/tools/kubectl-splunk/requirements.txt @@ -0,0 +1,4 @@ +# requirements.txt + +requests +argcomplete diff --git a/tools/kubectl-splunk/setup.py b/tools/kubectl-splunk/setup.py new file mode 100644 index 000000000..1da05e9cf --- /dev/null +++ b/tools/kubectl-splunk/setup.py @@ -0,0 +1,27 @@ +# setup.py + +from setuptools import setup, find_packages + +setup( + name='kubectl-splunk', + version='1.6', # Update the version accordingly + description='A kubectl plugin to manage Splunk instances within Kubernetes pods.', + author='Splunk', + author_email='support@splunk.com', + url='https://github.com/splunk/splunk-operator/tools/kubectl-splunk', + packages=find_packages(), + install_requires=[ + 'requests', + 'argcomplete', + ], + entry_points={ + 'console_scripts': [ + 'kubectl-splunk=kubectl_splunk.main:main', + ], + }, + classifiers=[ + 'Programming Language :: Python :: 3', + 'Operating System :: OS Independent', + ], + python_requires='>=3.6', +) diff --git a/tools/kubectl-splunk/tests/__init__.py b/tools/kubectl-splunk/tests/__init__.py new file mode 100644 index 000000000..c0dae48be --- /dev/null +++ b/tools/kubectl-splunk/tests/__init__.py @@ -0,0 +1,3 @@ +# tests/__init__.py + +# This file is intentionally left empty to mark the tests directory as a Python package. diff --git a/tools/kubectl-splunk/tests/test_kubectl_splunk.py b/tools/kubectl-splunk/tests/test_kubectl_splunk.py new file mode 100644 index 000000000..94df6d244 --- /dev/null +++ b/tools/kubectl-splunk/tests/test_kubectl_splunk.py @@ -0,0 +1,385 @@ +# tests/test_kubectl_splunk.py + +import unittest +from unittest.mock import patch, mock_open, MagicMock +import os +import sys +import subprocess +import json + +# Add the parent directory to sys.path to locate the kubectl_splunk package +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from kubectl_splunk.main import ( + load_config, + parse_args, + setup_logging, + get_pods, + select_pods, + cache_pod, + load_cached_pod, + get_credentials, + retrieve_password_from_pod, + execute_on_pod, + copy_to_pod, + execute_rest_call +) + +class TestKubectlSplunk(unittest.TestCase): + + @patch('kubectl_splunk.main.os.path.exists', return_value=True) + @patch('kubectl_splunk.main.configparser.ConfigParser') + def test_load_config_with_file(self, mock_configparser, mock_exists): + # Configure the mock ConfigParser instance + mock_config = MagicMock() + mock_configparser.return_value = mock_config + + # Mock the 'get' method for string values + mock_config.get.side_effect = [ + 'splunk-namespace', # namespace + 'app=splunk', # selector + '/opt/splunk/bin/splunk', # splunk_path + 'splunk-idxc-indexer-0' # pod_name + ] + + # Mock the 'getint' method for integer values + mock_config.getint.return_value = 8089 + + # Call the function under test + namespace, selector, splunk_path, pod_name, local_port = load_config() + + # Assertions + mock_config.read.assert_called_once_with(os.path.expanduser('~/.kubectl_splunk_config')) + self.assertEqual(namespace, 'splunk-namespace') + self.assertEqual(selector, 'app=splunk') + self.assertEqual(splunk_path, '/opt/splunk/bin/splunk') + self.assertEqual(pod_name, 'splunk-idxc-indexer-0') + self.assertEqual(local_port, 8089) + + + @patch('kubectl_splunk.main.os.path.exists') + def test_load_config_without_file(self, mock_exists): + mock_exists.return_value = False + + namespace, selector, splunk_path, pod_name, local_port = load_config() + + self.assertEqual(namespace, 'default') + self.assertEqual(selector, 'app=splunk') + self.assertEqual(splunk_path, '/opt/splunk/bin/splunk') + self.assertIsNone(pod_name) + self.assertEqual(local_port, 8089) + + @patch('kubectl_splunk.main.argparse.ArgumentParser.parse_args') + def test_parse_args_exec_mode(self, mock_parse_args): + mock_args = MagicMock() + mock_args.namespace = 'default' + mock_args.selector = 'app=splunk' + mock_args.context = None + mock_args.pod = None + mock_args.interactive = False + mock_args.splunk_path = '/opt/splunk/bin/splunk' + mock_args.local_port = 8089 + mock_args.verbose = 0 + mock_args.version = False + mock_args.username = 'admin' + mock_args.password = 'password' + mock_args.insecure = False + mock_args.save_credentials = False + mock_args.mode = 'exec' + mock_args.splunk_command = ['list', 'user'] + mock_parse_args.return_value = mock_args + + args = parse_args('default', 'app=splunk', 'splunk', None, 8089) + self.assertEqual(args.mode, 'exec') + self.assertEqual(args.splunk_command, ['list', 'user']) + + @patch('kubectl_splunk.main.subprocess.check_output') + @patch('kubectl_splunk.main.logging') + def test_get_pods_single_pod(self, mock_logging, mock_check_output): + mock_check_output.return_value = 'splunk-pod-1' + + args = MagicMock() + args.pod = None + args.context = None + args.namespace = 'default' + args.selector = 'app=splunk' + + pods = get_pods(args) + self.assertEqual(pods, ['splunk-pod-1']) + mock_check_output.assert_called_once() + + @patch('kubectl_splunk.main.subprocess.check_output') + @patch('kubectl_splunk.main.logging') + def test_get_pods_no_pods_found(self, mock_logging, mock_check_output): + mock_check_output.return_value = '' + + args = MagicMock() + args.pod = None + args.context = None + args.namespace = 'default' + args.selector = 'app=splunk' + + with self.assertRaises(SystemExit) as cm: + get_pods(args) + self.assertEqual(cm.exception.code, 1) + mock_logging.error.assert_called_with( + "No Splunk pods found with selector 'app=splunk' in namespace 'default'." + ) + + def test_select_pods_single_pod(self): + pods = ['splunk-pod-1'] + args = MagicMock() + selected = select_pods(pods, args) + self.assertEqual(selected, ['splunk-pod-1']) + + @patch('builtins.input', return_value='1') + def test_select_pods_multiple_pods(self, mock_input): + pods = ['splunk-pod-1', 'splunk-pod-2'] + args = MagicMock() + selected = select_pods(pods, args) + self.assertEqual(selected, ['splunk-pod-1']) + + @patch('builtins.input', return_value='0') + def test_select_pods_run_on_all(self, mock_input): + pods = ['splunk-pod-1', 'splunk-pod-2'] + args = MagicMock() + selected = select_pods(pods, args) + self.assertEqual(selected, ['splunk-pod-1', 'splunk-pod-2']) + + @patch('kubectl_splunk.main.open', new_callable=mock_open) + @patch('kubectl_splunk.main.json.dump') + def test_cache_pod(self, mock_json_dump, mock_open_file): + cache_pod('splunk-pod-1', 'default', 'app=splunk') + mock_open_file.assert_called_with('/tmp/kubectl_splunk_cache.json', 'w') + mock_json_dump.assert_called() + + @patch('kubectl_splunk.main.os.path.exists', return_value=True) + @patch('kubectl_splunk.main.open', new_callable=mock_open, read_data='{"pod_name": "splunk-pod-1", "namespace": "default", "selector": "app=splunk", "timestamp": 1000}') + @patch('kubectl_splunk.main.time.time', return_value=1299) # 300 seconds later + def test_load_cached_pod_valid(self, mock_time, mock_open_file, mock_exists): + namespace = 'default' + selector = 'app=splunk' + pod_name = load_cached_pod(namespace, selector) + self.assertEqual(pod_name, 'splunk-pod-1') + + @patch('kubectl_splunk.main.os.path.exists', return_value=True) + @patch('kubectl_splunk.main.open', new_callable=mock_open, read_data='{"pod_name": "splunk-pod-1", "namespace": "default", "selector": "app=splunk", "timestamp": 1000}') + @patch('kubectl_splunk.main.time.time', return_value=1600) # 600 seconds later, cache expired + def test_load_cached_pod_expired(self, mock_time, mock_open_file, mock_exists): + namespace = 'default' + selector = 'app=splunk' + pod_name = load_cached_pod(namespace, selector) + self.assertIsNone(pod_name) + + @patch('kubectl_splunk.main.open', new_callable=mock_open, read_data='{"username": "admin", "password": "password"}') + @patch('kubectl_splunk.main.json.load') + @patch('kubectl_splunk.main.os.path.exists', return_value=True) + @patch('kubectl_splunk.main.retrieve_password_from_pod') + def test_get_credentials_from_file(self, mock_retrieve_password, mock_exists, mock_json_load, mock_open_file): + mock_json_load.return_value = {'username': 'admin', 'password': 'password'} + args = MagicMock() + args.username = None + args.password = None + args.save_credentials = False + + with patch('kubectl_splunk.main.getpass.getpass', return_value='password') as mock_getpass: + username, password = get_credentials(args, 'splunk-pod-1') + self.assertEqual(username, 'admin') + self.assertEqual(password, 'password') + mock_retrieve_password.assert_not_called() + + @patch('kubectl_splunk.main.open', new_callable=mock_open) + @patch('kubectl_splunk.main.os.path.exists', return_value=False) + @patch('kubectl_splunk.main.retrieve_password_from_pod') + @patch('kubectl_splunk.main.getpass.getpass', return_value='password') + def test_get_credentials_from_pod(self, mock_getpass, mock_retrieve_password, mock_exists, mock_open_file): + mock_retrieve_password.return_value = 'password' + args = MagicMock() + args.username = None + args.password = None + args.save_credentials = False + + username, password = get_credentials(args, 'splunk-pod-1') + self.assertEqual(username, 'admin') + self.assertEqual(password, 'password') + mock_retrieve_password.assert_called_once_with(args, 'splunk-pod-1') + + @patch('kubectl_splunk.main.subprocess.check_output') + def test_retrieve_password_from_pod_success(self, mock_check_output): + mock_check_output.return_value = 'password123' + + args = MagicMock() + args.context = None + args.namespace = 'default' + + password = retrieve_password_from_pod(args, 'splunk-pod-1') + self.assertEqual(password, 'password123') + mock_check_output.assert_called_once_with([ + 'kubectl', 'exec', '-n', 'default', 'splunk-pod-1', '--', 'cat', '/mnt/splunk-secrets/password' + ], universal_newlines=True) + + @patch('kubectl_splunk.main.subprocess.run') + @patch('kubectl_splunk.main.get_credentials') + @patch('kubectl_splunk.main.logging') + def test_execute_on_pod_exec_mode(self, mock_logging, mock_get_credentials, mock_subprocess_run): + mock_get_credentials.return_value = ('admin', 'password123') + args = MagicMock() + args.mode = 'exec' + args.splunk_path = '/opt/splunk/bin/splunk' + args.splunk_command = ['list', 'user'] + args.username = 'admin' + args.password = None + args.save_credentials = False + args.context = None + args.namespace = 'default' + + execute_on_pod(args, 'splunk-pod-1') + + expected_cmd = [ + 'kubectl', 'exec', '-n', 'default', 'splunk-pod-1', '--', + '/opt/splunk/bin/splunk', 'list', 'user', '-auth', 'admin:password123' + ] + mock_subprocess_run.assert_called_once_with(expected_cmd, check=True) + mock_logging.debug.assert_called() + + @patch('kubectl_splunk.main.subprocess.run') + @patch('kubectl_splunk.main.logging') + def test_copy_to_pod(self, mock_logging, mock_subprocess_run): + args = MagicMock() + args.context = None + args.namespace = 'default' + args.src = '/local/path/file.txt' + args.dest = ':/remote/path/file.txt' + + copy_to_pod(args, 'splunk-pod-1') + + expected_cmd = [ + 'kubectl', 'cp', + '/local/path/file.txt', + 'default/splunk-pod-1:/remote/path/file.txt' + ] + mock_subprocess_run.assert_called_once_with(expected_cmd, check=True) + mock_logging.debug.assert_called() + + @patch('kubectl_splunk.main.subprocess.Popen') + @patch('kubectl_splunk.main.requests.request') + @patch('kubectl_splunk.main.get_credentials') + @patch('kubectl_splunk.main.logging') + def test_execute_rest_call(self, mock_logging, mock_get_credentials, mock_requests_request, mock_popen): + mock_get_credentials.return_value = ('admin', 'password123') + mock_popen.return_value.poll.side_effect = [None] * 10 # Simulate port-forward staying alive + mock_requests_request.return_value.status_code = 200 + mock_requests_request.return_value.text = 'Success' + + args = MagicMock() + args.mode = 'rest' + args.method = 'GET' + args.endpoint = '/services/server/info' + args.data = None + args.params = None + args.username = 'admin' + args.password = None + args.insecure = False + args.save_credentials = False + args.context = None + args.namespace = 'default' + args.local_port = 8089 + + execute_rest_call(args, 'splunk-pod-1') + + # Check that port-forward was started + expected_port_forward_cmd = [ + 'kubectl', 'port-forward', '-n', 'default', 'splunk-pod-1', '8089:8089' + ] + mock_popen.assert_called_once_with(expected_port_forward_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Check that REST API call was made + expected_url = 'https://127.0.0.1:8089/services/server/info' + mock_requests_request.assert_called_once_with( + method='GET', + url=expected_url, + headers={'Content-Type': 'application/json'}, + params={}, + data=None, + auth=('admin', 'password123'), + verify=True + ) + + # Check that the response was printed + # Since we cannot capture print statements easily here, we assume it was done correctly + + @patch('kubectl_splunk.main.subprocess.run') + @patch('kubectl_splunk.main.logging') + def test_execute_on_pod_command_failure(self, mock_logging, mock_subprocess_run): + mock_subprocess_run.side_effect = subprocess.CalledProcessError(1, 'cmd') + + args = MagicMock() + args.mode = 'exec' + args.splunk_path = '/opt/splunk/bin/splunk' + args.splunk_command = ['list', 'user'] + args.username = 'admin' + args.password = 'password123' + args.save_credentials = False + args.context = None + args.namespace = 'default' + + with self.assertRaises(SystemExit) as cm: + execute_on_pod(args, 'splunk-pod-1') + self.assertEqual(cm.exception.code, 1) + mock_logging.error.assert_called_with( + "Command failed on pod splunk-pod-1 with exit code 1" + ) + + @patch('kubectl_splunk.main.subprocess.run') + @patch('kubectl_splunk.main.logging') + def test_copy_to_pod_failure(self, mock_logging, mock_subprocess_run): + mock_subprocess_run.side_effect = subprocess.CalledProcessError(1, 'cp') + + args = MagicMock() + args.context = None + args.namespace = 'default' + args.src = '/local/path/file.txt' + args.dest = ':/remote/path/file.txt' + + with self.assertRaises(SystemExit) as cm: + copy_to_pod(args, 'splunk-pod-1') + self.assertEqual(cm.exception.code, 1) + mock_logging.error.assert_called_with( + "Copy command failed with exit code 1" + ) + + @patch('kubectl_splunk.main.subprocess.Popen') + @patch('kubectl_splunk.main.requests.request') + @patch('kubectl_splunk.main.get_credentials') + @patch('kubectl_splunk.main.logging') + def test_execute_rest_call_http_error(self, mock_logging, mock_get_credentials, mock_requests_request, mock_popen): + mock_get_credentials.return_value = ('admin', 'password123') + mock_popen.return_value.poll.side_effect = [None] * 10 # Simulate port-forward staying alive + mock_requests_request.return_value.status_code = 404 + mock_requests_request.return_value.reason = 'Not Found' + mock_requests_request.return_value.text = 'Error' + + args = MagicMock() + args.mode = 'rest' + args.method = 'GET' + args.endpoint = '/services/server/info' + args.data = None + args.params = None + args.username = 'admin' + args.password = None + args.insecure = False + args.save_credentials = False + args.context = None + args.namespace = 'default' + args.local_port = 8089 + + with self.assertRaises(SystemExit) as cm: + execute_rest_call(args, 'splunk-pod-1') + self.assertEqual(cm.exception.code, 1) + mock_logging.error.assert_called_with( + "HTTP 404: Not Found" + ) + +if __name__ == '__main__': + unittest.main() +