Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recover process implemented for #64430 #4

Merged
merged 8 commits into from
Jan 29, 2025
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,6 @@ venv.bak/
# Celery stuff
celerybeat-schedule
celerybeat
tmp/
tmp/

.DS_Store
27 changes: 26 additions & 1 deletion plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "quix-manager"
version: "0.1.1"
version: "0.2.0"
usage: "helm quix-manager update --release-name quixplatform-manager --repo quixcontainerregistry.azurecr.io/helm/quixplatform-manager:1.5.4"
description: |-
A helm plugin to install quix.
Expand All @@ -10,6 +10,31 @@ hooks:
install: |
#!/bin/bash
set -e
echo "Updating the plugin ...."
# Check for Python 3
if command -v python3 &>/dev/null; then
PYTHON_CMD=python3
elif command -v python &>/dev/null; then
PYTHON_CMD=python
else
echo "Python 3 is required for this plugin. Please install Python 3."
exit 1
fi

# Verify that the Python command is version 3
if ! $PYTHON_CMD -c 'import sys; exit(0) if sys.version_info[0] == 3 else exit(1)'; then
echo "Detected Python version is not 3. Please use Python 3."
exit 1
fi

# Create a virtual environment in the plugin directory
$PYTHON_CMD -m venv "$HELM_PLUGIN_DIR/venv"

# Activate the virtual environment and install pip requirements
"$HELM_PLUGIN_DIR/venv/bin/pip" install -r "$HELM_PLUGIN_DIR/src/requirements.txt"
update: |
#!/bin/bash
set -e

# Check for Python 3
if command -v python3 &>/dev/null; then
Expand Down
54 changes: 29 additions & 25 deletions quix_install_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ def generate_configmap(logs, configmap_name='quix-manager-log-configmap', namesp
}
return configmap

def setup_logging(verbose: bool):
# Define the log format
log_format = '# %(asctime)s - %(name)s - %(levelname)s - %(message)s'
# Create an in-memory stream to capture logs
log_stream = io.StringIO()

# Configure logger for both stdout and in-memory logging
logger = logging.getLogger('quix-manager')
logger.propagate = False # Prevent log messages from propagating to the root logger

if not logger.hasHandlers():
# Handler for logging to stdout
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(log_format))
logger.addHandler(console_handler)

# Handler for capturing logs in memory
memory_handler = logging.StreamHandler(log_stream)
memory_handler.setFormatter(logging.Formatter(log_format))
logger.addHandler(memory_handler)

# Set log level based on verbosity
logger.setLevel(logging.DEBUG if verbose else logging.INFO)

return logger, log_stream



if __name__ == "__main__":
Expand All @@ -33,39 +59,17 @@ def generate_configmap(logs, configmap_name='quix-manager-log-configmap', namesp
parser.add_argument('--logs-as-config', action='store_true', help='Write in the stdout a configmap with all logs happened. This is essentially for argocd')


# Set up logging
log_format = '# %(levelname)s: %(message)s'

# Create an in-memory stream to capture logs
log_stream = io.StringIO()

# Configure logger for both stdout and in-memory logging
logger = logging.getLogger('helm_logger')

# Get the args from command
args, _ = parser.parse_known_args()
# Avoid adding duplicate handlers in case the script runs multiple times
if not logger.hasHandlers():
# Handler for logging to stdout
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter(log_format))
logger.addHandler(console_handler)

# Handler for capturing logs in memory
memory_handler = logging.StreamHandler(log_stream)
memory_handler.setFormatter(logging.Formatter(log_format))
logger.addHandler(memory_handler)

if args.verbose:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
# Set up logging
logger, log_stream = setup_logging(args.verbose)

logger.info("Starting Helm command execution")
# Log some initial info
helm_manager = HelmManager(args)
helm_manager.run()
if args.logs_as_config:

# Retrieve the logs f rom the in-memory log stream
log_contents = log_stream.getvalue()

Expand Down
65 changes: 59 additions & 6 deletions src/helm_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os, sys, shutil, subprocess, yaml, tarfile,logging
from argparse import Namespace

logging = logging.getLogger('helm_logger')
logging = logging.getLogger('quix-manager')

class HelmManager:
def __init__(self, args: Namespace = None):
Expand Down Expand Up @@ -37,12 +37,11 @@ def __init__(self, args: Namespace = None):
self.merged_file_path = os.path.join(deployment_dir, f"{self.release_name}merged.yaml")


def _run_helm_with_args(self, helm_args: list, output_file: str = None):
def _run_helm_with_args(self, helm_args: list):
"""
Runs a Helm command with the provided arguments.

:param helm_args: List of arguments for the Helm command.
:param output_file: Optional file to store Helm command output.
"""
command = ['helm'] + helm_args
logging.debug(f"Executing Helm command: {command}")
Expand Down Expand Up @@ -93,7 +92,51 @@ def _check_if_exists(self, release_name: str):
list_args.extend(["--namespace", self.namespace])
result = self._run_helm_with_args(list_args)
return release_name in result.stdout.decode('utf-8')

@staticmethod
def parse_output(output_str):
"""
Parse the output of a Helm command into a dictionary.
"""
result = {}
# Split the output into lines and extract key-value pairs
lines = output_str.strip().split('\n')
for line in lines:
if ':' in line:
key, value = line.split(':', 1)
result[key.strip()] = value.strip()

return result

def _rollback(self, revision: str):
"""
Rollback to the specified revision of a Helm release.
:param revision: Revision number to rollback to.

"""
list_args = ['rollback', self.release_name, revision ]
if self.namespace:
list_args.extend(['--namespace', self.namespace])
self._run_helm_with_args(list_args)

logging.info(f"Rolled back to revision {revision}.")

def _get_release_status(self):
"""
Retrieves the current status of a Helm release.

:param release_name: Name of the Helm release.
:return: Status of the release.
"""
list_args = ['status', self.release_name]
if self.namespace:
list_args.extend(['--namespace', self.namespace])
status_result = self._run_helm_with_args(list_args)
status_output = self.parse_output(status_result.stdout.decode('utf-8'))

logging.info(f"The status of the Helm chart {self.release_name} is: {status_output.get('STATUS')}")
return status_output

def pull_repo(self):
"""
Pulls the Helm chart from the specified repository.
Expand Down Expand Up @@ -140,15 +183,15 @@ def _template_with_merged_values(self):
if self.namespace:
list_args.extend(["--namespace", self.namespace])
result = self._run_helm_with_args(list_args)
print(result.stdout.decode('utf-8'))

def run(self):
"""
Executes the main logic: checks if the release exists, retrieves values, merges YAML files,
and either updates the release or generates a template.
"""
exist_release = self._check_if_exists(release_name=self.release_name)

exist_release = self._check_if_exists(release_name=self.release_name)

if exist_release:
try:
values = self._get_values(release_name=self.release_name)
Expand All @@ -173,7 +216,17 @@ def run(self):
logging.error(f"Error during execution: {e}")
sys.exit(1)
else:
logging.warning(f"Release {self.release_name} does not exist. You need to install it first.")
status = self._get_release_status()
if status.get('STATUS') == 'pending-upgrade':
logging.debug(f"Release {self.release_name} is in pending-upgrade status.")
revision = str(int(status.get('REVISION'))-1)
joseEnrique marked this conversation as resolved.
Show resolved Hide resolved
self._rollback(revision)
logging.debug(f"Release {self.release_name} has been rolled back and running the upgrade.")
self.run()
else:
logging.error(f"Release {self.release_name} does not exist. You need to install it first.")
sys.exit(1)



class FileManager:
Expand Down