Skip to content

Commit

Permalink
Send email to the user when a build fails or succeeds.
Browse files Browse the repository at this point in the history
  • Loading branch information
mjkw31 committed Nov 5, 2024
1 parent 13eaa1b commit 560356f
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 39 deletions.
1 change: 1 addition & 0 deletions softpack_core/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def spec(self) -> Box:
info["hidden"] = getattr(metadata, "hidden", False)
info["force_hidden"] = getattr(metadata, "force_hidden", False)
info["created"] = getattr(metadata, "created", 0)
info["username"] = getattr(metadata, "username", None)

info["interpreters"] = Interpreters()

Expand Down
7 changes: 6 additions & 1 deletion softpack_core/config/conf/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ spack:

recipes:
toAddr: none@domain.com
fromAddr: none@domain.com
fromAddr: "{}@domain.com"
smtp: some.mail.relay

environments:
toAddr: "{}@domain.com"
fromAddr: "none@domain.com"
smtp: some.mail.relay


Expand Down
2 changes: 1 addition & 1 deletion softpack_core/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class SpackConfig(BaseModel):
cache: Optional[str]


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

toAddr: Optional[str]
Expand Down
5 changes: 3 additions & 2 deletions softpack_core/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from .models import (
ArtifactsConfig,
BuilderConfig,
EmailConfig,
LDAPConfig,
RecipeConfig,
ServerConfig,
SpackConfig,
VaultConfig,
Expand All @@ -35,7 +35,8 @@ class Settings(BaseSettings):
artifacts: ArtifactsConfig
spack: SpackConfig
builder: BuilderConfig
recipes: RecipeConfig
recipes: EmailConfig
environments: EmailConfig

class Config:
"""Configuration loader."""
Expand Down
25 changes: 22 additions & 3 deletions softpack_core/schemas/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ class EnvironmentInput:

name: str
path: str
username: Optional[str] = ""
description: str
packages: list[PackageInput]
tags: Optional[list[str]] = None
Expand All @@ -224,7 +225,7 @@ def validate(self) -> Union[None, InvalidInputError]:
if any(
len(value) == 0
for key, value in vars(self).items()
if key != "tags"
if key != "tags" and key != "username"
):
return InvalidInputError(message="all fields must be filled in")

Expand Down Expand Up @@ -333,6 +334,7 @@ class Environment:
packages: list[Package]
state: Optional[State]
tags: list[str]
username: Optional[str]
hidden: bool
created: int
cachedEnvs: list["Environment"] = field(default_factory=list)
Expand Down Expand Up @@ -434,6 +436,7 @@ def from_artifact(cls, obj: Artifacts.Object) -> Optional["Environment"]:
readme=spec.get("readme", ""),
type=spec.get("type", ""),
tags=spec.tags,
username=spec.username,
hidden=spec.hidden,
created=spec.created,
interpreters=spec.get("interpreters", Interpreters()),
Expand Down Expand Up @@ -470,7 +473,10 @@ def create(cls, env: EnvironmentInput) -> CreateResponse: # type: ignore
prevEnv = cls.get_env(
Path(env.path), env.name + "-" + str(version - 1)
)
if cast(Environment, prevEnv).state == State.failed:
if (
prevEnv is not None
and cast(Environment, prevEnv).state == State.failed
):
version -= 1

tree_oid = artifacts.delete_environment(
Expand Down Expand Up @@ -586,8 +592,13 @@ def create_new_env(
definitionData = yaml.dump(softpack_definition)

meta = dict(
tags=sorted(set(env.tags or [])), created=round(time())
tags=sorted(set(env.tags or [])),
created=round(time()),
)

if env.username != "" and env.username is not None:
meta["username"] = env.username

metaData = yaml.dump(meta)

tree_oid = artifacts.create_files(
Expand Down Expand Up @@ -725,6 +736,14 @@ def set_hidden(

return HiddenSuccess(message="Hidden metadata set")

def remove_username(cls) -> None:
"""Remove the username metadata from the meta.yaml file."""
metadata = cls.read_metadata(cls.path, cls.name)

del metadata["username"]

cls.store_metadata(Path(cls.path, cls.name), metadata)

@classmethod
def delete(cls, name: str, path: str) -> DeleteResponse: # type: ignore
"""Delete an Environment.
Expand Down
113 changes: 86 additions & 27 deletions softpack_core/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from typing_extensions import Annotated

from softpack_core.artifacts import Artifacts, State, artifacts
from softpack_core.config.models import EmailConfig
from softpack_core.schemas.environment import (
CreateEnvironmentSuccess,
Environment,
Expand Down Expand Up @@ -107,6 +108,50 @@ async def upload_artifacts( # type: ignore[no-untyped-def]
if not isinstance(create_response, CreateEnvironmentSuccess):
return create_response

path = Path(env_path)
env = Environment.get_env(path.parent, path.name)
newState = State.queued

for f in file:
if f.filename == artifacts.builder_out:
newState = State.failed

break

if f.filename == artifacts.module_file:
newState = State.ready

break

if (
newState != State.queued
and env is not None
and env.username is not None
and env.username != ""
):
envEmailConfig = app.settings.environments
m = (
"built sucessfully"
if newState == State.ready
else "failed to build"
)
message = (
f"Hi {env.username},\n"
+ "\n"
+ f"Your environment, {env_path}, has {m}.\n"
+ "\n"
+ "SoftPack Team"
)

subject = (
"Your environment is ready!"
if newState == State.ready
else "Your environment failed to build"
)

send_email(envEmailConfig, message, subject, env.username)
env.remove_username()

resp = await Environment.write_artifacts(env_path, file)
if not isinstance(resp, WriteArtifactSuccess):
raise Exception(resp)
Expand Down Expand Up @@ -176,39 +221,19 @@ async def request_recipe( # type: ignore[no-untyped-def]
except Exception as e:
return {"error": str(e)}

recipeConfig = app.settings.recipes.dict()
if data["username"] != "":
recipeConfig = app.settings.recipes

if all(
key in recipeConfig and isinstance(recipeConfig[key], str)
for key in ["fromAddr", "toAddr", "smtp"]
):
msg = MIMEText(
send_email(
recipeConfig,
f'User: {data["username"]}\n'
+ f'Recipe: {data["name"]}\n'
+ f'Version: {data["version"]}\n'
+ f'URL: {data["url"]}\n'
+ f'Description: {data["description"]}'
+ f'Description: {data["description"]}',
f'SoftPack Recipe Request: {data["name"]}@{data["version"]}',
data["username"],
)
msg[
"Subject"
] = f'SoftPack Recipe Request: {data["name"]}@{data["version"]}'
msg["From"] = recipeConfig["fromAddr"]
msg["To"] = recipeConfig["toAddr"]

localhostname = None

if recipeConfig["localHostname"] is not None:
localhostname = recipeConfig["localHostname"]

s = smtplib.SMTP(
recipeConfig["smtp"], local_hostname=localhostname
)
s.sendmail(
recipeConfig["fromAddr"],
[recipeConfig["toAddr"]],
msg.as_string(),
)
s.quit()

return {"message": "Request Created"}

Expand Down Expand Up @@ -349,3 +374,37 @@ async def recipe_description( # type: ignore[no-untyped-def]
return {"error": "Invalid Input"}

return {"description": app.spack.descriptions[data["recipe"]]}


def send_email(
emailConfig: EmailConfig, message: str, subject: str, username: str
) -> None:
"""The send_email functions sends an email."""
if (
emailConfig.fromAddr is None
or emailConfig.toAddr is None
or emailConfig.smtp is None
):
return

msg = MIMEText(message)

fromAddr = emailConfig.fromAddr.format(username)
toAddr = emailConfig.toAddr.format(username)

msg["Subject"] = subject
msg["From"] = fromAddr
msg["To"] = toAddr

localhostname = None

if emailConfig.localHostname is not None:
localhostname = emailConfig.localHostname

s = smtplib.SMTP(emailConfig.smtp, local_hostname=localhostname)
s.sendmail(
fromAddr,
[toAddr],
msg.as_string(),
)
s.quit()
7 changes: 7 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,10 @@ def testable_env_input(mocker) -> EnvironmentInput: # type: ignore
)

yield testable_env_input


@pytest.fixture()
def send_email(mocker):
send_email_mock = mocker.patch('softpack_core.service.send_email')

return send_email_mock
Loading

0 comments on commit 560356f

Please sign in to comment.