Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
jshimkus-rh authored Oct 29, 2024
2 parents b86f087 + aaee250 commit 918665a
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 97 deletions.
28 changes: 22 additions & 6 deletions src/aap_eda/api/serializers/eda_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.db.models import Q
from django.urls import reverse
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

Expand All @@ -32,8 +33,8 @@ class EdaCredentialReferenceSerializer(serializers.Serializer):
name = serializers.CharField(
required=True, help_text="Name of the related resource"
)
url = serializers.URLField(
required=True, help_text="URL of the related resource"
uri = serializers.URLField(
required=True, help_text="URI of the related resource"
)


Expand Down Expand Up @@ -192,13 +193,16 @@ def get_references(eda_credential: models.EdaCredential) -> list[dict]:
Q(eda_credential=eda_credential)
| Q(signature_validation_credential=eda_credential)
)
used_event_streams = models.EventStream.objects.filter(
eda_credential=eda_credential
)

for activation in used_activations:
resource = {
"type": "Activation",
"id": activation.id,
"name": activation.name,
"url": f"api/eda/v1/activations/{activation.id}/",
"uri": reverse("activation-detail", kwargs={"pk": activation.id}),
}
resources.append(resource)

Expand All @@ -207,8 +211,9 @@ def get_references(eda_credential: models.EdaCredential) -> list[dict]:
"type": "DecisionEnvironment",
"id": decision_environment.id,
"name": decision_environment.name,
"url": (
f"api/eda/v1/decision-environments/{decision_environment.id}/"
"uri": reverse(
"decisionenvironment-detail",
kwargs={"pk": decision_environment.id},
),
}
resources.append(resource)
Expand All @@ -218,7 +223,18 @@ def get_references(eda_credential: models.EdaCredential) -> list[dict]:
"type": "Project",
"id": project.id,
"name": project.name,
"url": f"api/eda/v1/projects/{project.id}/",
"uri": reverse("project-detail", kwargs={"pk": project.id}),
}
resources.append(resource)

for event_stream in used_event_streams:
resource = {
"type": "EventStream",
"id": event_stream.id,
"name": event_stream.name,
"uri": reverse(
"eventstream-detail", kwargs={"pk": event_stream.id}
),
}
resources.append(resource)

Expand Down
67 changes: 27 additions & 40 deletions src/aap_eda/services/project/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import logging
import os
import tempfile
from contextlib import contextmanager
from dataclasses import dataclass
from functools import wraps
from typing import Any, Callable, Final, Iterator, Optional, Type
Expand All @@ -26,7 +27,7 @@

from aap_eda.core import models
from aap_eda.core.types import StrPath
from aap_eda.services.project.scm import ScmRepository
from aap_eda.services.project.scm import ScmEmptyError, ScmRepository
from aap_eda.services.rulebook import insert_rulebook_related_data

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -61,13 +62,12 @@ def wrapper(self: ProjectImportService, project: models.Project):
try:
func(self, project)
project.import_state = models.Project.ImportState.COMPLETED
except Exception as e:
except ScmEmptyError as e:
# if a project is empty, sync status should show completed
project.import_state = (
models.Project.ImportState.COMPLETED
if "Project folder is empty" in e.args[0]
else models.Project.ImportState.FAILED
)
project.import_state = models.Project.ImportState.COMPLETED
project.import_error = str(e)
except Exception as e:
project.import_state = models.Project.ImportState.FAILED
project.import_error = str(e)
error = e
finally:
Expand All @@ -90,9 +90,6 @@ def wrapper(self: ProjectImportService, project: models.Project):
return wrapper


# TODO(cutwater): The project import and project sync are mostly
# similar operations. Current implementation has some code duplication.
# This needs to be refactored in the future.
class ProjectImportService:
def __init__(self, scm_cls: Optional[Type[ScmRepository]] = None):
if scm_cls is None:
Expand All @@ -101,27 +98,27 @@ def __init__(self, scm_cls: Optional[Type[ScmRepository]] = None):

@_project_import_wrapper
def import_project(self, project: models.Project) -> None:
with self._temporary_directory() as tempdir:
repo_dir = os.path.join(tempdir, "src")

proxy = project.proxy.get_secret_value() if project.proxy else None
repo = self._scm_cls.clone(
project.url,
repo_dir,
credential=project.eda_credential,
gpg_credential=project.signature_validation_credential,
depth=1,
verify_ssl=project.verify_ssl,
branch=project.scm_branch,
refspec=project.scm_refspec,
proxy=proxy,
)
project.git_hash = repo.rev_parse("HEAD")

with self._clone_and_process(project) as (repo_dir, git_hash):
project.git_hash = git_hash
self._import_rulebooks(project, repo_dir)

@_project_import_wrapper
def sync_project(self, project: models.Project) -> None:
with self._clone_and_process(project) as (repo_dir, git_hash):
if project.git_hash == git_hash:
logger.info(
"Project (id=%s, name=%s) is up to date. Nothing to sync.",
project.id,
project.name,
)
return

project.git_hash = git_hash

self._sync_rulebooks(project, repo_dir, git_hash)

@contextmanager
def _clone_and_process(self, project: models.Project):
with self._temporary_directory() as tempdir:
repo_dir = os.path.join(tempdir, "src")

Expand All @@ -137,19 +134,9 @@ def sync_project(self, project: models.Project) -> None:
refspec=project.scm_refspec,
proxy=proxy,
)
git_hash = repo.rev_parse("HEAD")

if project.git_hash == git_hash:
logger.info(
"Project (id=%s, name=%s) is up to date. Nothing to sync.",
project.id,
project.name,
)
return

project.git_hash = git_hash

self._sync_rulebooks(project, repo_dir, git_hash)
yield repo_dir, repo.rev_parse("HEAD")
if project.rulebook_set.count() == 0:
raise ScmEmptyError("This project contains no rulebooks.")

def _temporary_directory(self) -> tempfile.TemporaryDirectory:
return tempfile.TemporaryDirectory(prefix=TMP_PREFIX)
Expand Down
14 changes: 11 additions & 3 deletions src/aap_eda/services/project/scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ class ScmError(Exception):
pass


class ScmEmptyError(ScmError):
"""The checkout repository is empty which is treated as an error."""

pass


class ScmAuthenticationError(Exception):
"""SCM Authentication error."""

Expand Down Expand Up @@ -208,7 +214,7 @@ def clone(
msg = msg.replace(secret, "****", 1)
msg = msg.replace(quote(secret, safe=""), "****", 1)
logger.warning("SCM clone failed: %s", msg)
raise ScmError(msg) from None
raise e.__class__(msg) from None
finally:
if key_file:
key_file.close()
Expand Down Expand Up @@ -324,7 +330,9 @@ def __call__(
):
err_msg = "Credentials not provided or incorrect"
raise ScmAuthenticationError(err_msg)
if "did not match any file" in err_msg:
raise ScmError(self.PROJECT_EMPTY_ERROR_MSG)
if "did not match any file" in err_msg and err_msg.startswith(
'"Failed to checkout branch'
):
raise ScmEmptyError(self.PROJECT_EMPTY_ERROR_MSG)
raise ScmError(f"{self.ERROR_PREFIX} {err_msg}")
raise ScmError(f"{self.ERROR_PREFIX} {outputs.getvalue().strip()}")
39 changes: 35 additions & 4 deletions tests/integration/api/test_eda_credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,27 +899,58 @@ def test_delete_credential_used_by_project_with_gpg_credential(
@pytest.mark.django_db
def test_retrieve_eda_credential_with_refs(
default_activation: models.Activation,
default_event_stream: models.EventStream,
default_decision_environment: models.DecisionEnvironment,
default_project: models.Project,
default_eda_credential: models.EdaCredential,
admin_client: APIClient,
refs,
preseed_credential_types,
):
eda_credential = default_activation.eda_credentials.all()[0]
default_activation.eda_credentials.add(default_eda_credential)

default_event_stream.eda_credential = default_eda_credential
default_event_stream.save(update_fields=["eda_credential"])

default_decision_environment.eda_credential = default_eda_credential
default_decision_environment.save(update_fields=["eda_credential"])

default_project.eda_credential = default_eda_credential
default_project.save(update_fields=["eda_credential"])

response = admin_client.get(
f"{api_url_v1}/eda-credentials/{eda_credential.id}/?refs={refs}",
f"{api_url_v1}/eda-credentials/{default_eda_credential.id}/?refs={refs}", # noqa 501
)
assert response.status_code == status.HTTP_200_OK

if refs == "true":
assert response.data["references"] is not None
references = response.data["references"]

assert len(references) == 1
assert len(references) == 4
references[0] = {
"type": "Activation",
"id": default_activation.id,
"name": default_activation.name,
"url": f"api/eda/v1/activations/{default_activation.id}/",
"uri": f"api/eda/v1/activations/{default_activation.id}/",
}
references[1] = {
"type": "DecisionEnvironment",
"id": default_decision_environment.id,
"name": default_decision_environment.name,
"uri": f"api/eda/v1/decision-environments/{default_decision_environment.id}/", # noqa 501
}
references[2] = {
"type": "Project",
"id": default_project.id,
"name": default_project.name,
"uri": f"api/eda/v1/projects/{default_project.id}/",
}
references[3] = {
"type": "EventStream",
"id": default_event_stream.id,
"name": default_event_stream.name,
"uri": f"api/eda/v1/event-streams/{default_event_stream.id}/",
}
else:
assert response.data["references"] is None
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This folder purposely contains no rulebooks
Loading

0 comments on commit 918665a

Please sign in to comment.