Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ repos:
# Linter - https://docs.astral.sh/ruff/linter/
- id: ruff-check
args: ["--fix"]
# TODO Enable after pending MRs are merged
# Formatter - https://docs.astral.sh/ruff/formatter/
# - id: ruff-format
- id: ruff-format

# Dockerfile linter, validate inline bash, written in Haskell
- repo: https://github.com/hadolint/hadolint
Expand Down
17 changes: 11 additions & 6 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# https://docs.astral.sh/ruff/settings/

target-version = "py313"
line-length = 120
line-length = 150

# Exclude third-party scripts
exclude = [
"scripts/git-filter-repo.py",
"scripts/test-colors.py",
]

[lint]
select = [
Expand All @@ -27,11 +33,10 @@ ignore = [
"TRY003", # Avoid specifying long messages outside the exception class
]

# Exclude third-party scripts
exclude = [
"scripts/git-filter-repo.py",
"scripts/test-colors.py",
]
[lint.pylint]
max-returns = 10 # Increased from default 6
max-branches = 20 # Increased from default 12
max-statements = 75 # Increased from default 50

[lint.isort]
known-first-party = [
Expand Down
58 changes: 34 additions & 24 deletions scripts/github-extract-links.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,50 @@
#!/usr/bin/env python3
"""Extract GitHub repository links from files in a directory."""

import os
import re
import sys
from pathlib import Path

SCANNABLE_EXTENSIONS: tuple[str, ...] = (".md", ".yml", ".yaml", ".sh")
# Usernames for user accounts on GitHub can only contain alphanumeric characters and dashes ( - ).
GITHUB_REPO_PATTERN = re.compile(r"https://github\.com/([\w.\-_]+/[\w.\-_]+)")

def extract_github_links(directory: str) -> list[str]:

def extract_github_links(directory: Path) -> list[str]:
"""Extract unique GitHub repository links from files in a directory.

Scans markdown, YAML, shell scripts, and Dockerfiles for GitHub URLs.
"""
github_links: set[str] = set()
for root, _dirs, files in os.walk(directory):
for file in files:
if file.endswith(".md") or file.endswith(".yml") or file.endswith(".yaml") or file.endswith(".sh") or file.startswith("Dockerfile"):
file_path = os.path.join(root, file)
with open(file_path) as f:
content = f.read()
# Usernames for user accounts on GitHub can only contain alphanumeric characters and dashes ( - ).
links = re.findall(r"https://github.com/([\w.\-\_]+/[\w.\-\_]+)", content)
links = trim_git_ending(links)
github_links.update(links)

for file_path in directory.rglob("*"):
if not file_path.is_file():
continue

if not (file_path.suffix in SCANNABLE_EXTENSIONS or file_path.name.startswith("Dockerfile")):
continue

try:
content = file_path.read_text()
except (OSError, UnicodeDecodeError):
continue

links = GITHUB_REPO_PATTERN.findall(content)
github_links.update(trim_git_suffix(link) for link in links)

return list(github_links)


def trim_git_ending(links: list[str]) -> list[str]:
trimmed_links: list[str] = []
for link in links:
if link.endswith(".git"):
trimmed_links.append(link[:-4])
else:
trimmed_links.append(link)
return trimmed_links
def trim_git_suffix(link: str) -> str:
"""Remove .git suffix from a repository path if present."""
return link.removesuffix(".git")


def main():
directory = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
def main() -> None:
"""Extract and print GitHub links from the specified directory."""
directory = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()

links = extract_github_links(directory)
for link in links:
for link in extract_github_links(directory):
print(f"https://github.com/{link}")


Expand Down
6 changes: 3 additions & 3 deletions scripts/github-star-repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def star_github_repo(repo_url: str) -> None:
"""
try:
parsed_url = urlparse(repo_url)
repo_path = unquote(parsed_url.path.strip('/'))
owner, repo_name = repo_path.split('/')
repo_path = unquote(parsed_url.path.strip("/"))
owner, repo_name = repo_path.split("/")

# Construct the API endpoint URL
api_url = f"https://api.github.com/user/starred/{owner}/{repo_name}"
Expand All @@ -31,7 +31,7 @@ def star_github_repo(repo_url: str) -> None:
# Make the PUT request to star the repository
headers = {
"Authorization": f"Bearer {github_token}",
"Accept": "application/vnd.github+json"
"Accept": "application/vnd.github+json",
}
response = requests.put(api_url, headers=headers, timeout=30)

Expand Down
16 changes: 8 additions & 8 deletions scripts/infra-mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
from utils.security import validate_url_for_ssrf

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger("infra-mcp")
logger.info("Starting Infra MCP server")

mcp = FastMCP(
name="infra-mcp",
instructions="Use these tools to configure the homelab infrastructure and interact with the services."
instructions="Use these tools to configure the homelab infrastructure and interact with the services.",
)


Expand Down Expand Up @@ -121,7 +121,7 @@ class Args:
args.registry = None

tags, _, _, _ = tag_finder.get_image_tags(args)
return [tag['name'] for tag in tags[:limit]] if tags else []
return [tag["name"] for tag in tags[:limit]] if tags else []
except Exception:
logger.exception("list-container-tags failed for image=%r", image)
return []
Expand Down Expand Up @@ -156,7 +156,7 @@ class Args:

# Get same hash tags but don't output to stdout
same_hash_tags = tag_finder.list_same_hash_tags(args, suppress_output=True)
return [tag['name'] for tag in same_hash_tags] if same_hash_tags else []
return [tag["name"] for tag in same_hash_tags] if same_hash_tags else []
except Exception:
logger.exception("list-same-hash-container-tags failed for image=%r tag=%r", image, tag)
return []
Expand Down Expand Up @@ -194,9 +194,9 @@ class Args:
most_specific = tag_finder.get_most_specific_tag(args)
same_hash = tag_finder.list_same_hash_tags(args, suppress_output=True)
if most_specific:
return most_specific['name']
return most_specific["name"]
elif same_hash:
return same_hash[0]['name']
return same_hash[0]["name"]
else:
return tag or "latest"
except Exception:
Expand All @@ -212,8 +212,8 @@ class Args:
logger.info(f"Repository root path: {repository_root_path}")

# Check environment variables to enable/disable tools
enable_task_tools = os.environ.get('ENABLE_TASK_TOOLS', 'true').lower() != 'false'
enable_container_tools = os.environ.get('ENABLE_CONTAINER_TOOLS', 'true').lower() != 'false'
enable_task_tools = os.environ.get("ENABLE_TASK_TOOLS", "true").lower() != "false"
enable_container_tools = os.environ.get("ENABLE_CONTAINER_TOOLS", "true").lower() != "false"

# Add tools based on environment variable settings
if enable_task_tools:
Expand Down
30 changes: 14 additions & 16 deletions scripts/infra-mcp/tools/collections/container_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ def get_container_operations():
A list of dictionaries with operation name and description
"""
return [
{'name': 'pull', 'description': "Pull the latest container image for the specified service"},
{'name': 'up', 'description': "Start the specified service containers"},
{'name': 'down', 'description': "Stop the specified service containers"},
{'name': 'restart', 'description': "Restart the specified service containers"},
{'name': 'recreate', 'description': "Recreate the specified service containers"},
{'name': 'config', 'description': "Show the docker-compose configuration for the specified service"},
{"name": "pull", "description": "Pull the latest container image for the specified service"},
{"name": "up", "description": "Start the specified service containers"},
{"name": "down", "description": "Stop the specified service containers"},
{"name": "restart", "description": "Restart the specified service containers"},
{"name": "recreate", "description": "Recreate the specified service containers"},
{"name": "config", "description": "Show the docker-compose configuration for the specified service"},
]


Expand All @@ -47,28 +47,25 @@ def execute_container_operation(operation: str, service_name: str, repository_ro
The command output as a string
"""
# Validate operation
valid_operations = {op['name'] for op in get_container_operations()}
valid_operations = {op["name"] for op in get_container_operations()}
if operation not in valid_operations:
return f"Invalid operation: {operation}"

# Validate service_name format
if not re.match(r'^[a-zA-Z0-9_/-]+$', service_name):
if not re.match(r"^[a-zA-Z0-9_/-]+$", service_name):
return f"Invalid service name format: {service_name}"

cmd = [
sys.executable, # Use the current Python interpreter
os.path.join(repository_root_path, "scripts", "labctl.py"),
"service",
operation,
service_name
service_name,
]

try:
result = subprocess.run( # noqa: S603
cmd,
capture_output=True,
text=True,
check=True
cmd, capture_output=True, text=True, check=True
)
except subprocess.CalledProcessError as e:
return f"Error running operation: {e.stderr or str(e)}"
Expand All @@ -87,6 +84,7 @@ def create_operation_function(op: str, repository_root_path: str) -> Callable[[s
Returns:
A callable function that executes the operation on a given service
"""

def operation_fn(service_name: str) -> str:
"""
Execute one operation on the specified service and return the output
Expand All @@ -113,15 +111,15 @@ def add_container_operation_tools(mcp_server: FastMCP, repository_root_path: str
operations = get_container_operations()

for op in operations:
operation_fn = create_operation_function(op['name'], repository_root_path)
operation_fn = create_operation_function(op["name"], repository_root_path)
tool_name = f"container-service-{op['name']}"
description = op['description']
description = op["description"]

tool = Tool.from_function(
fn=operation_fn,
name=tool_name,
title=tool_name,
description=description
description=description,
)
mcp_server.add_tool(tool)

Expand Down
16 changes: 8 additions & 8 deletions scripts/infra-mcp/tools/collections/task_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def get_task_list(repository_root_path: str) -> list[dict[str, str]]:
[task_bin, "--list-all", "--dir", repository_root_path],
capture_output=True,
text=True,
check=True
check=True,
)
except subprocess.CalledProcessError:
logger.exception("Error getting task list")
Expand All @@ -46,14 +46,13 @@ def get_task_list(repository_root_path: str) -> list[dict[str, str]]:
# Parse output lines
for line in result.stdout.splitlines():
# Match lines like "* task_name: task description"
match = re.match(r'^\*\s+(.+?):\s+(.+)$', line.strip())
match = re.match(r"^\*\s+(.+?):\s+(.+)$", line.strip())
if match:
task_name = match.group(1).strip()
description = match.group(2).strip()
tasks.append({
"name": task_name,
"description": description
})
tasks.append(
{"name": task_name, "description": description},
)

logger.debug(f"Found {len(tasks)} tasks")
return tasks
Expand All @@ -80,7 +79,7 @@ def execute_task(task_name: str, repository_root_path: str) -> str:
[task_bin, task_name, "--dir", repository_root_path],
capture_output=True,
text=True,
check=True
check=True,
).stdout.strip()
except subprocess.CalledProcessError as e:
logger.exception(f"Error executing task {task_name}")
Expand All @@ -98,6 +97,7 @@ def create_task_function(task_name: str, repository_root_path: str) -> Callable[
Returns:
A callable function that executes the task
"""

def task_fn() -> str:
return execute_task(task_name, repository_root_path)

Expand All @@ -124,7 +124,7 @@ def add_task_tools(mcp_server: FastMCP, repository_root_path: str) -> None:
fn=task_fn,
name=tool_name,
title=tool_name,
description=description
description=description,
)
mcp_server.add_tool(tool)

Expand Down
Loading