Skip to content

Commit 2895eba

Browse files
authored
Merge pull request #2 from alexei-led/fix/secure-exec
Secure subprocess execution and improved test coverage
2 parents b78ebef + 05c4261 commit 2895eba

File tree

6 files changed

+414
-74
lines changed

6 files changed

+414
-74
lines changed

.github/workflows/ci.yml

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,30 @@ jobs:
2323
python-version: ${{ matrix.python-version }}
2424
cache: "pip"
2525

26-
- name: Install dependencies
26+
- name: Install uv
2727
run: |
28-
python -m pip install --upgrade pip
29-
python -m pip install -e ".[dev]"
28+
# Install uv using the official installation method
29+
curl -LsSf https://astral.sh/uv/install.sh | sh
30+
31+
# Add uv to PATH
32+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
3033
31-
- name: Lint with ruff
34+
- name: Install dependencies using uv
3235
run: |
33-
ruff check . --output-format=github
34-
ruff format --check .
36+
# Install dependencies using uv with the lock file and the --system flag
37+
uv pip install --system -e ".[dev]"
3538
36-
- name: Run unit tests
37-
run: |
38-
# Only run unit tests, skip integration tests that require K8s cluster
39-
pytest -v -k "not integration"
39+
- name: Lint
40+
run: make lint
41+
continue-on-error: true # Display errors but don't fail build for lint warnings
42+
43+
- name: Test
44+
run: make test
4045

4146
- name: Upload coverage to Codecov
4247
uses: codecov/codecov-action@v4
4348
with:
4449
token: ${{ secrets.CODECOV_TOKEN }}
4550
file: ./coverage.xml
4651
fail_ci_if_error: false
47-
verbose: true
52+
verbose: true

.github/workflows/integration-tests.yml

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,45 +39,40 @@ jobs:
3939

4040
- name: Install uv
4141
run: |
42+
# Install uv using the official installation method
4243
curl -LsSf https://astral.sh/uv/install.sh | sh
4344
44-
- name: Setup uv cache
45-
uses: actions/cache@v4
46-
with:
47-
path: |
48-
~/.cache/uv
49-
~/.uv
50-
key: ${{ runner.os }}-uv-${{ hashFiles('pyproject.toml') }}
45+
# Add uv to PATH
46+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
5147
52-
- name: Install dependencies
48+
- name: Install dependencies using uv
5349
run: |
54-
uv pip install -e ".[dev]"
50+
# Install dependencies using uv with the lock file and the --system flag
51+
uv pip install --system -e ".[dev]"
5552
56-
- name: Set up KWOK
57-
uses: kubernetes-sigs/kwok@main
58-
with:
59-
command: kwokctl
60-
kwok-version: latest # Optional: specify version or use latest
53+
- name: Install KWOK tool
54+
run: |
55+
# KWOK repository
56+
KWOK_REPO=kubernetes-sigs/kwok
57+
# Get latest
58+
KWOK_LATEST_RELEASE=$(curl "https://api.github.com/repos/${KWOK_REPO}/releases/latest" | jq -r '.tag_name')
59+
60+
wget -O kwokctl -c "https://github.com/${KWOK_REPO}/releases/download/${KWOK_LATEST_RELEASE}/kwokctl-linux-amd64"
61+
chmod +x kwokctl
62+
sudo mv kwokctl /usr/local/bin/kwokctl
6163
6264
- name: Create KWOK Cluster
6365
run: |
6466
# Create a KWOK cluster for testing with specific Kubernetes version
65-
kwokctl create cluster --name=kwok-test
66-
67-
# Set KUBECONFIG environment variable
68-
echo "KUBECONFIG=$(kwokctl get kubeconfig --name=kwok-test)" >> $GITHUB_ENV
67+
kwokctl create cluster --name=kwok-test --wait=1m
6968
70-
# Verify cluster is running
71-
kubectl cluster-info
72-
kubectl get nodes
73-
7469
- name: Install K8s CLI tools
7570
run: |
7671
# Install Helm
7772
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
7873
7974
# Install istioctl (latest version)
80-
ISTIO_VERSION=$(curl -sL https://github.com/istio/istio/releases/latest | grep -o "v[0-9]\+\.[0-9]\+\.[0-9]\+" | head -1)
75+
ISTIO_VERSION=$(curl -s https://api.github.com/repos/istio/istio/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
8176
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=${ISTIO_VERSION} sh -
8277
sudo mv istio-${ISTIO_VERSION}/bin/istioctl /usr/local/bin/
8378

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: install dev-install lint test clean docker-build docker-run docker-compose
1+
.PHONY: install dev-install lint lint-fix format test clean docker-build docker-run docker-compose
22

33
# Python related commands with uv
44
install:
@@ -11,6 +11,10 @@ lint:
1111
ruff check .
1212
ruff format --check .
1313

14+
lint-fix:
15+
ruff check --fix .
16+
ruff format .
17+
1418
format:
1519
ruff format .
1620

@@ -58,6 +62,7 @@ help:
5862
@echo " install - Install the package using uv"
5963
@echo " dev-install - Install the package with development dependencies using uv"
6064
@echo " lint - Run linters (ruff)"
65+
@echo " lint-fix - Run linters with automatic fixes"
6166
@echo " format - Format code with ruff"
6267
@echo " test - Run unit tests only (default)"
6368
@echo " test-all - Run all tests including integration tests"

src/k8s_mcp_server/cli_executor.py

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import logging
1111
import shlex
1212
import time
13+
from asyncio.subprocess import PIPE
1314

1415
from k8s_mcp_server.config import (
1516
DEFAULT_TIMEOUT,
@@ -50,7 +51,8 @@ async def check_cli_installed(cli_tool: str) -> bool:
5051

5152
try:
5253
cmd = SUPPORTED_CLI_TOOLS[cli_tool]["check_cmd"]
53-
process = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
54+
cmd_args = shlex.split(cmd)
55+
process = await asyncio.create_subprocess_exec(*cmd_args, stdout=PIPE, stderr=PIPE)
5456
await process.communicate()
5557
return process.returncode == 0
5658
except Exception as e:
@@ -127,11 +129,11 @@ async def execute_command(command: str, timeout: int | None = None) -> CommandRe
127129
if is_piped:
128130
commands = split_pipe_command(command)
129131
first_command = inject_context_namespace(commands[0])
132+
133+
# We'll execute the commands separately and handle piping ourselves
134+
command_list = [first_command]
130135
if len(commands) > 1:
131-
# Reconstruct the pipe command with the modified first command
132-
command = first_command + " | " + " | ".join(commands[1:])
133-
else:
134-
command = first_command
136+
command_list.extend(commands[1:])
135137
else:
136138
# Handle context and namespace for non-piped commands
137139
command = inject_context_namespace(command)
@@ -144,12 +146,47 @@ async def execute_command(command: str, timeout: int | None = None) -> CommandRe
144146
start_time = time.time()
145147

146148
try:
147-
# Create subprocess with resource limits
148-
process = await asyncio.create_subprocess_shell(
149-
command,
150-
stdout=asyncio.subprocess.PIPE,
151-
stderr=asyncio.subprocess.PIPE,
152-
)
149+
if is_piped:
150+
# Execute piped commands securely by chaining them
151+
processes = []
152+
153+
# Split commands for secure execution
154+
for i, cmd in enumerate(command_list):
155+
cmd_args = shlex.split(cmd)
156+
157+
if i == 0: # First command
158+
# First process writes to a pipe
159+
first_process = await asyncio.create_subprocess_exec(
160+
*cmd_args,
161+
stdout=asyncio.subprocess.PIPE,
162+
stderr=PIPE,
163+
)
164+
processes.append(first_process)
165+
prev_stdout = first_process.stdout
166+
167+
else: # Middle or last commands
168+
# Read from previous process's stdout, write to pipe (except last command)
169+
next_process = await asyncio.create_subprocess_exec(
170+
*cmd_args,
171+
stdin=prev_stdout,
172+
stdout=PIPE if i < len(command_list) - 1 else PIPE,
173+
stderr=PIPE,
174+
)
175+
processes.append(next_process)
176+
if i < len(command_list) - 1:
177+
prev_stdout = next_process.stdout
178+
179+
# We only need to communicate with the last process to get the final output
180+
last_process = processes[-1]
181+
process = last_process
182+
else:
183+
# Use safer create_subprocess_exec for non-piped commands
184+
cmd_args = shlex.split(command)
185+
process = await asyncio.create_subprocess_exec(
186+
*cmd_args,
187+
stdout=PIPE,
188+
stderr=PIPE,
189+
)
153190

154191
# Wait for the process to complete with timeout
155192
try:

0 commit comments

Comments
 (0)