-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #60 from OS2borgerPC/47-versionering-af-globalecor…
…e-scripts 47 versionering af globalecore scripts
- Loading branch information
Showing
15 changed files
with
366 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
admin_site/system/management/commands/fetch_and_install_core_scripts.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import django | ||
from django.core.management.base import BaseCommand | ||
from system.models import Script, ScriptTag, Input | ||
from system.script_fetcher import fetch_scripts | ||
|
||
class Command(BaseCommand): | ||
|
||
""" | ||
Get core scripts from a (correctly formatted) git repo, and insert them in the database, so they can be accessed by site users. | ||
For security and consistency reasons, the matching commit hash (SHA-256) for the tag must also be specified, as the tag can easily be updated to point to | ||
other (ill-intended) code. Using the hash, this is prevented. | ||
Example: | ||
manage.py fetch_and_install_core_scripts --versionTag v0.1.2 --commitHash b3a791b52bc9937c6cb168c706ee003b0666fc93 | ||
""" | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument("--versionTag", required=True) | ||
parser.add_argument("--commitHash", required=True) | ||
|
||
def handle(self, *args, **options): | ||
repo_url = "https://github.com/OS2borgerPC/os2borgerpc-core-scripts.git" | ||
self.stdout.write(f"Fetching scripts from {repo_url} repository...") | ||
scripts = fetch_scripts("https://github.com/OS2borgerPC/os2borgerpc-core-scripts.git", options['versionTag'], options['commitHash']) | ||
|
||
for script in scripts: | ||
versionedName = script.title + " " + options['versionTag'] | ||
uid = script.metadata.get("uid", None) | ||
is_security_script = script.metadata.get("security", False) | ||
is_hidden = script.metadata.get("hidden", False) | ||
|
||
if uid and Script.objects.filter(uid=uid).exists(): | ||
Script.objects.filter(uid=uid).delete() | ||
|
||
if not Script.objects.filter(name=versionedName).exists(): | ||
with open(script.sourcePath, 'rb') as file: | ||
# Get only the base file name | ||
db_script = Script.objects.create( | ||
name=versionedName, | ||
description=script.description, | ||
site=None, # None means global script | ||
executable_code=django.core.files.File(file), | ||
is_security_script=is_security_script, | ||
is_hidden=is_hidden, | ||
maintained_by_magenta=False, | ||
feature_permission=None, | ||
uid=uid | ||
) | ||
tag, created = ScriptTag.objects.get_or_create(name=script.tag) | ||
db_script.tags.add(tag) | ||
|
||
position = 1 | ||
for parameter in script.parameters: | ||
Input.objects.create( | ||
script=db_script, | ||
name=parameter.name, | ||
value_type=parameter.type, | ||
default_value=parameter.default, | ||
position=position, | ||
mandatory=parameter.mandatory | ||
) | ||
position += 1 |
18 changes: 18 additions & 0 deletions
18
admin_site/system/migrations/0087_alter_script_executable_code.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Generated by Django 4.2.11 on 2024-11-05 15:49 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('system', '0086_remove_featurepermission_sites_remove_site_country_and_more'), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name='script', | ||
name='executable_code', | ||
field=models.FileField(max_length=255, upload_to='script_uploads', verbose_name='executable code'), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import os | ||
import subprocess | ||
from pathlib import Path | ||
from urllib.parse import urlparse | ||
import yaml | ||
from dataclasses import dataclass, field | ||
from typing import List, Optional | ||
import logging | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
def fetch_scripts(repoUrl, versionTag, commitHash): | ||
repo_url = repoUrl | ||
clone_path = Path("downloaded_core_scripts") / get_repo_name(repo_url) / commitHash | ||
|
||
# Perform a shallow clone if the repository isn't already cloned | ||
if not (clone_path).exists(): | ||
if versionTag: | ||
# Check that the commitHash matches the versionTag | ||
subprocess.run( | ||
["git", "clone", "--depth", "1", "--branch", versionTag, repo_url, str(clone_path)], | ||
check=True | ||
) | ||
|
||
versionTagCommitHash = subprocess.run( | ||
["git", "-C", str(clone_path), "rev-parse", versionTag], | ||
check=True, | ||
capture_output=True, | ||
text=True | ||
).stdout.strip() | ||
|
||
if versionTagCommitHash != commitHash: | ||
logger.warning(f"The commit hash for tag '{versionTag}' is '{versionTagCommitHash}', which does not match the provided commit hash '{commitHash}'.") | ||
return [] | ||
else: | ||
# Clone the repository and then check out the specific commit hash | ||
subprocess.run( | ||
["git", "clone", "--filter=blob:none", repo_url, str(clone_path)], | ||
check=True | ||
) | ||
subprocess.run( | ||
["git", "-C", str(clone_path), "checkout", commitHash], | ||
check=True | ||
) | ||
|
||
# Retrieve content of all markdown (.md) files, and convert them to Scripts | ||
scripts = [] | ||
md_dir_path = clone_path | ||
for md_file in md_dir_path.glob("**/*.md"): | ||
if md_file.is_file(): | ||
if md_file.name == "README.md": | ||
continue | ||
with open(md_file, 'r') as file: | ||
file_content = file.read() | ||
try: | ||
script = parse_md_to_script(file_content, clone_path) | ||
except Exception as e: | ||
logger.warning("Skipping script file '" + str(md_file.relative_to(md_dir_path)) + "' because it's content is not formatted correctly:") | ||
logger.warning(file_content) | ||
logger.warning(f"Exception: {e}") | ||
continue | ||
if not Path(script.sourcePath).exists() and not Path(script.sourcePath).is_file(): | ||
logger.warning("Skipping " + str(md_file.relative_to(md_dir_path)) + " because the 'source' attribute does not point to a valid file.") | ||
continue | ||
scripts.append(script) | ||
|
||
return scripts | ||
|
||
@dataclass | ||
class Metadata: | ||
uid: Optional[str] | ||
security: Optional[bool] | ||
hidden: Optional[bool] | ||
|
||
@dataclass | ||
class Parameter: | ||
name: str | ||
type: str | ||
default: Optional[str] | ||
mandatory: bool | ||
|
||
@dataclass | ||
class Script: | ||
title: str | ||
parent: str | ||
sourcePath: str | ||
compatible_versions: Optional[str] | ||
compatible_images: List[str] | ||
description: str | ||
tag: str | ||
partners: Optional[str] | ||
parameters: List[Parameter] = field(default_factory=list) | ||
metadata: Optional[Metadata] = None | ||
|
||
def parse_md_to_script(content: str, clone_path: Path) -> Script: | ||
# Split YAML and Markdown content | ||
yamlBegin, yaml_string, markdown_string = content.split("---", 2) | ||
yaml_string = yaml_string.strip() | ||
markdown_string = markdown_string.strip() | ||
|
||
# Parse the YAML content | ||
yaml_content = yaml.safe_load(yaml_string) | ||
|
||
# Parse metadata | ||
metadata_content = yaml_content.get('metadata', {}) | ||
|
||
# Parse parameters | ||
params = yaml_content.get('parameters', []) | ||
params = params if params is not None else [] | ||
|
||
# Extract parameters to list | ||
parameters = [ | ||
Parameter( | ||
name=param['name'], | ||
type=param['type'].upper(), | ||
default=param['default'], | ||
mandatory=param['mandatory'] | ||
) | ||
for param in params | ||
] | ||
|
||
return Script( | ||
title=yaml_content.get('title'), | ||
parent=yaml_content.get('parent'), | ||
sourcePath=str(clone_path / yaml_content.get('source')), | ||
compatible_versions=yaml_content.get('compatible_versions'), | ||
compatible_images=yaml_content.get('compatible_images', []), | ||
description=markdown_string, | ||
tag=yaml_content.get('parent'), | ||
partners=yaml_content.get('partners'), | ||
parameters=parameters, | ||
metadata=metadata_content | ||
) | ||
|
||
def get_repo_name(repo_url): | ||
# Parse the URL | ||
parsed_url = urlparse(repo_url) | ||
|
||
# Extract the path from the URL and split to get the repository name | ||
repo_name = os.path.basename(parsed_url.path) | ||
|
||
# Remove the ".git" extension if present | ||
if repo_name.endswith(".git"): | ||
repo_name = repo_name[:-4] | ||
|
||
return repo_name |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import markdown | ||
from django import template | ||
from django.utils.safestring import mark_safe | ||
|
||
register = template.Library() | ||
|
||
@register.filter(name='markdown') | ||
def markdown_format(text): | ||
return mark_safe(markdown.markdown(text)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
{% load crispy_forms_tags %} | ||
|
||
{% load i18n %} | ||
<fieldset> | ||
{{ form.name|as_crispy_field }} | ||
{{ form.description|as_crispy_field }} | ||
|
||
<input | ||
type="hidden" | ||
id="id_{% if form.prefix %}{{ form.prefix }}-{% endif %}site" | ||
name="{% if form.prefix %}{{ form.prefix }}-{% endif %}site" | ||
value="{{ site.id }}"> | ||
<input | ||
id="id_is_security_script" | ||
name="is_security_script" | ||
value="{{ is_security }}" | ||
type="hidden"> | ||
<input | ||
id="id_is_hidden" | ||
name="is_hidden" | ||
value="{{ is_hidden }}" | ||
type="hidden"> | ||
<input | ||
id="id_uid" | ||
name="uid" | ||
value="{{ uid }}" | ||
type="hidden"> | ||
</fieldset> | ||
|
||
<fieldset {% if disable_inputs == "yes" %}class="mt-3"{% endif %}> | ||
{% if not global_script %} | ||
{{ form.executable_code|as_crispy_field }} | ||
{% endif %} | ||
{% if show_code_preview %} | ||
<p>{% translate "Code" %}:</p> | ||
<pre class="mb-0"><code id="script-code" class="bash">{{script_preview |escape}}</code></pre> | ||
{% endif %} | ||
|
||
</fieldset> | ||
<input type="hidden" name="script-number-of-inputs" class="script-number-of-inputs" value="0"> |
Oops, something went wrong.