Skip to content

Commit

Permalink
Handle recipe requests. (#58)
Browse files Browse the repository at this point in the history
* Add backend code to handle recipes requests from the frontend.

* Add script for admin handling of Recipe Requests.
  • Loading branch information
mjkw31 authored Oct 15, 2024
1 parent 2cfcc42 commit 282f5c0
Show file tree
Hide file tree
Showing 13 changed files with 736 additions and 35 deletions.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,22 +202,27 @@ server:
artifacts:
path: Path # Path to store artefacts repo.
repo:
url: AnyUrl # URL to artefacts repo.
url: AnyUrl # URL to artefacts repo.
username: Optional[str] # Username required to access artefacts repo.
author: str # Author name for git commits to artefacts repo.
email: str # Email address for author of git commits to artefacts repo.
reader: Optional[str] # Auth token for read access to artefacts repo.
writer: Optional[str] # Auth token for write access to artefacts repo.
branch: Optional[str] # Branch to use for artefacts repo.
author: str # Author name for git commits to artefacts repo.
email: str # Email address for author of git commits to artefacts repo.
reader: Optional[str] # Auth token for read access to artefacts repo.
writer: Optional[str] # Auth token for write access to artefacts repo.
branch: Optional[str] # Branch to use for artefacts repo.
spack:
repo: str # URL to spack recipe repo.
bin: str # Path to spack exectable.
repo: str # URL to spack recipe repo.
bin: str # Path to spack exectable.
cache: Optional[str] # Directory to store cached spack recipe information.
builder:
host: str # URL to a GSB server
port: int # Port of the GSB server
recipes:
toAddr: Optional[str] # Address to which recipe requests will be sent.
fromAddr: Optional[str] # Address from which recipe requests will be sent.
smtp: Optional[str] # Address to an SMTP relay
```

## Usage
Expand Down
2 changes: 2 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Environment {
state: State
tags: [String!]!
hidden: Boolean!
cachedEnvs: [Environment!]!
requested: DateTime
buildStart: DateTime
buildDone: DateTime
Expand Down Expand Up @@ -117,6 +118,7 @@ enum State {
ready
queued
failed
waiting
}

interface Success {
Expand Down
90 changes: 90 additions & 0 deletions scripts/recipe-requests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/bin/bash

set -euo pipefail;

declare CORE_URL="http://127.0.0.1:8000";

IFS=$'\n';

while true; do
declare -a data=( $(curl "$CORE_URL/requestedRecipes" 2> /dev/null | jq -c '.[]') ) || {
echo "Error: failed to get data from Core." >&2;

exit 1;
}

if [ "${#data[@]}" -eq 0 ]; then
echo "No requested recipes."

exit 0;
fi;

declare num=0;

for recipe in "${data[@]}";do
let "num+=1";

echo -n "$num: ";
echo "$recipe" | jq "\"\(.name)@\(.version)\"" -r;
done;

echo "q. Quit";

read num;

if [ "$num" = "q" -o "$num" = "Q" ]; then
exit 0;
fi;

if [ "$num" -gt 0 -a "$num" -le "${#data[@]}" ] 2> /dev/null; then
while true; do
echo "${data[$((num - 1))]}" | jq "\"\(.name)@\(.version)\\nUsername: \(.username)\\nURL: \(.url)\\nDescription: \(.description)\"" -r;

echo -e "F. Fufill Request\nR. Remove Request\nB. Back";

read v;

case "$v" in
"f"|"F")
echo -n "Enter new package name (blank to keep existing): ";

read pkgname;

echo -n "Enter new package version (blank to keep existing): ";

read pkgversion

if curl -d "$({
echo "${data[$((num - 1))]}" | jq '{"requestedName": .name, "requestedVersion": .version}' -c;
echo "${data[$((num - 1))]}" | jq '{"name","version"}' -c;
echo -e "$pkgname\n$pkgversion" | jq -s -R 'split("\n")[:2] | to_entries | map({(if .key == 0 then "name" else "version" end): .value}) | add | del(..|select(. == ""))' -c;
} | jq -s add -c)" "$CORE_URL/fulfilRequestedRecipe"; then
echo;

break;
fi;;
"r"|"R")
echo -n "Are you sure you want to remove the request $(echo "${data[$((num - 1))]}" | jq "\"\(.name)@\(.version)\"" -r)? (yN): ";

read v;

if [ "$v" = "y" -o "$v" = "Y" ]; then
if curl -d "$(echo "${data[$((num - 1))]}" | jq 'with_entries(select([.key] | inside(["name", "version"])))')" "$CORE_URL/removeRequestedRecipe"; then
echo;

break;
fi;
else
echo "Invalid Response.";
fi;;
"b"|"B")
break;;
*)
echo "Invalid Response.";;
esac;
done;

else
echo "Invalid Response."
fi;
done;
119 changes: 106 additions & 13 deletions softpack_core/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import Iterable, Iterator, List, Optional, Tuple, Union
from typing import Iterable, Iterator, List, Optional, Tuple, Union, cast

import pygit2
import strawberry
import yaml
from box import Box
from fastapi import UploadFile

Expand Down Expand Up @@ -55,6 +56,7 @@ class State(Enum):
ready = 'ready'
queued = 'queued'
failed = 'failed'
waiting = 'waiting'


@strawberry.enum
Expand All @@ -68,6 +70,7 @@ class Type(Enum):
class Artifacts:
"""Artifacts repo access class."""

recipes_root = "recipes"
environments_root = "environments"
environments_file = "softpack.yml"
builder_out = "builder.out"
Expand Down Expand Up @@ -141,13 +144,6 @@ def spec(self) -> Box:
if Artifacts.readme_file in self.obj:
info["readme"] = self.obj[Artifacts.readme_file].data.decode()

if Artifacts.module_file in self.obj:
info["state"] = State.ready
elif Artifacts.builder_out in self.obj:
info["state"] = State.failed
else:
info["state"] = State.queued

if Artifacts.generated_from_module_file in self.obj:
info["type"] = Artifacts.generated_from_module
else:
Expand All @@ -157,6 +153,15 @@ def spec(self) -> Box:
map(lambda p: Package.from_name(p), info.packages)
)

if Artifacts.module_file in self.obj:
info["state"] = State.ready
elif Artifacts.builder_out in self.obj:
info["state"] = State.failed
elif any(pkg.name.startswith("*") for pkg in info.packages):
info["state"] = State.waiting
else:
info["state"] = State.queued

metadata = self.metadata()

info["tags"] = getattr(metadata, "tags", [])
Expand Down Expand Up @@ -210,6 +215,16 @@ def __init__(self) -> None:
credentials=credentials
)

@dataclass
class RecipeObject:
"""The Recipe object represents the data for a requested recipe."""

name: str
version: str
description: str
url: str
username: str

@property
def signature(self) -> pygit2.Signature:
"""Get current pygit2 commit signature: author/committer/timestamp."""
Expand Down Expand Up @@ -389,6 +404,71 @@ def get(self, path: Path, name: str) -> Optional[Object]:
except KeyError:
return None

def create_recipe_request(self, recipe: RecipeObject) -> pygit2.Oid:
"""Create a recipe request.
Args:
recipe: the recipe to be created.
"""
return self.commit_and_push(
self.create_file(
Path(self.recipes_root),
recipe.name + "-" + recipe.version,
yaml.dump(recipe),
True,
),
"add recipe request",
)

def get_recipe_request(
self, name: str, version: str
) -> Optional[RecipeObject]:
"""Get the details of a requested recipe.
Args:
name: the name of the recipe.
version: the version of the recipe.
"""
try:
recipeData = self.tree(self.recipes_root)[name + "-" + version]
except Exception:
return None

if not recipeData:
return None

return cast(Artifacts.RecipeObject, Box.from_yaml(recipeData.data))

def remove_recipe_request(self, name: str, version: str) -> pygit2.Oid:
"""Remove a recipe request.
Args:
name: the name of the recipe.
version: the version of the recipe.
"""
try:
recipeData = self.tree(self.recipes_root)[name + "-" + version]
except Exception:
raise FileNotFoundError("recipe request does not exist")

if not recipeData:
raise FileNotFoundError("recipe request does not exist")

return self.commit_and_push(
self.remove(Path(self.recipes_root), name + "-" + version),
"removed recipe request",
)

def iter_recipe_requests(self) -> Iterable[RecipeObject]:
"""Iterate over recipe requests."""
try:
tree = self.tree(self.recipes_root)
except Exception:
return []

for recipe in tree:
yield cast(Artifacts.RecipeObject, Box.from_yaml(recipe.data))

def commit_and_push(
self, tree_oid: pygit2.Oid, message: str
) -> pygit2.Oid:
Expand Down Expand Up @@ -471,7 +551,7 @@ def create_file(

def create_files(
self,
folder_path: Path,
full_path: Path,
files: List[Tuple[str, Union[str, UploadFile]]],
new_folder: bool = False,
overwrite: bool = False,
Expand All @@ -490,13 +570,17 @@ def create_files(
the OID of the new tree structure of the repository
"""
for file_name, _ in files:
if not overwrite and self.get(Path(folder_path), file_name):
try:
tree = self.tree(str(full_path))
except Exception:
continue

if not overwrite and tree and file_name in tree:
raise FileExistsError("File already exists")

root_tree = self.repo.head.peel(pygit2.Tree)
full_path = Path(self.environments_root, folder_path)

if new_folder:
if new_folder and (full_path not in root_tree or overwrite):
new_treebuilder = self.repo.TreeBuilder()
else:
folder = root_tree[full_path]
Expand Down Expand Up @@ -544,10 +628,19 @@ def delete_environment(
if len(Path(path).parts) != 2:
raise ValueError("Not a valid environment path")

return self.remove(Path(self.environments_root, path), name)

def remove(self, full_path: Path, name: str) -> pygit2.Oid:
"""Remove a target from the artefacts repo.
Args:
full_path: the parent directory to remove from
name: the file/directory to remove from the parent
commit_message: the commit message
"""
# Get repository tree
root_tree = self.repo.head.peel(pygit2.Tree)
# Find environment in the tree
full_path = Path(self.environments_root, path)
target_tree = root_tree[full_path]
# Remove the environment
tree_builder = self.repo.TreeBuilder(target_tree)
Expand Down
6 changes: 6 additions & 0 deletions softpack_core/config/conf/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ spack:
repo: https://github.com/custom-spack/repo
bin: /path/to/spack

recipes:
toAddr: [email protected]
fromAddr: [email protected]
smtp: some.mail.relay


# Vault Config
# vault:
# url:
Expand Down
8 changes: 8 additions & 0 deletions softpack_core/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,11 @@ class SpackConfig(BaseModel):
repo: str
bin: str
cache: Optional[str]


class RecipeConfig(BaseModel):
"""Email settings to send recipe requests to."""

toAddr: Optional[str]
fromAddr: Optional[str]
smtp: Optional[str]
2 changes: 2 additions & 0 deletions softpack_core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ArtifactsConfig,
BuilderConfig,
LDAPConfig,
RecipeConfig,
ServerConfig,
SpackConfig,
VaultConfig,
Expand All @@ -34,6 +35,7 @@ class Settings(BaseSettings):
artifacts: ArtifactsConfig
spack: SpackConfig
builder: BuilderConfig
recipes: RecipeConfig

class Config:
"""Configuration loader."""
Expand Down
Loading

0 comments on commit 282f5c0

Please sign in to comment.