diff --git a/core/models.py b/core/models.py index ecd8ebe6..fdecc5a4 100644 --- a/core/models.py +++ b/core/models.py @@ -4,7 +4,16 @@ from django.db import models -class Pattern(CommonModel): +class PatternServiceBaseModel(CommonModel): + class Meta: + abstract = True + + created_by_ansible_id: models.CharField = models.CharField( + max_length=200, blank=True, null=True + ) + + +class Pattern(PatternServiceBaseModel): class Meta: app_label = "core" ordering = ["id"] @@ -24,7 +33,7 @@ class Meta: pattern_definition: models.JSONField = models.JSONField(blank=True, null=True) -class ControllerLabel(CommonModel): +class ControllerLabel(PatternServiceBaseModel): class Meta: app_label = "core" ordering = ["id"] @@ -34,7 +43,7 @@ class Meta: ) -class PatternInstance(CommonModel): +class PatternInstance(PatternServiceBaseModel): class Meta: app_label = "core" ordering = ["id"] @@ -63,7 +72,7 @@ class Meta: ) -class Automation(CommonModel): +class Automation(PatternServiceBaseModel): class Meta: app_label = "core" ordering = ["id"] @@ -80,7 +89,7 @@ class Meta: ) -class Task(CommonModel): +class Task(PatternServiceBaseModel): class Meta: app_label = "core" ordering = ["id"] diff --git a/core/serializers.py b/core/serializers.py index f909b5da..06287831 100644 --- a/core/serializers.py +++ b/core/serializers.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from ansible_base.lib.serializers.common import CommonModelSerializer from .models import Automation @@ -9,10 +11,23 @@ from .models import Task -class PatternSerializer(CommonModelSerializer): +class PatternServiceBaseSerializer(CommonModelSerializer): + class Meta(CommonModelSerializer.Meta): model = Pattern - fields = CommonModelSerializer.Meta.fields + [ + fields = CommonModelSerializer.Meta.fields + + def to_representation(self, instance: Any) -> Any: + ret = super().to_representation(instance) + username = self.context["request"].user.username or "runner" + ret["created_by_ansible_id"] = username + return ret + + +class PatternSerializer(PatternServiceBaseSerializer): + class Meta(PatternServiceBaseSerializer.Meta): + model = Pattern + fields = PatternServiceBaseSerializer.Meta.fields + [ "id", "collection_name", "collection_version", @@ -23,16 +38,16 @@ class Meta(CommonModelSerializer.Meta): read_only_fields = ["pattern_definition", "collection_version_uri"] -class ControllerLabelSerializer(CommonModelSerializer): - class Meta(CommonModelSerializer.Meta): +class ControllerLabelSerializer(PatternServiceBaseSerializer): + class Meta(PatternServiceBaseSerializer.Meta): model = ControllerLabel fields = CommonModelSerializer.Meta.fields + ["id", "label_id"] -class PatternInstanceSerializer(CommonModelSerializer): - class Meta(CommonModelSerializer.Meta): +class PatternInstanceSerializer(PatternServiceBaseSerializer): + class Meta(PatternServiceBaseSerializer.Meta): model = PatternInstance - fields = CommonModelSerializer.Meta.fields + [ + fields = PatternServiceBaseSerializer.Meta.fields + [ "id", "organization_id", "controller_project_id", @@ -49,10 +64,10 @@ class Meta(CommonModelSerializer.Meta): ] -class AutomationSerializer(CommonModelSerializer): - class Meta(CommonModelSerializer.Meta): +class AutomationSerializer(PatternServiceBaseSerializer): + class Meta(PatternServiceBaseSerializer.Meta): model = Automation - fields = CommonModelSerializer.Meta.fields + [ + fields = PatternServiceBaseSerializer.Meta.fields + [ "id", "automation_type", "automation_id", @@ -61,10 +76,10 @@ class Meta(CommonModelSerializer.Meta): ] -class TaskSerializer(CommonModelSerializer): - class Meta(CommonModelSerializer.Meta): +class TaskSerializer(PatternServiceBaseSerializer): + class Meta(PatternServiceBaseSerializer.Meta): model = Task - fields = CommonModelSerializer.Meta.fields + [ + fields = PatternServiceBaseSerializer.Meta.fields + [ "status", "details", ] diff --git a/core/views.py b/core/views.py index 66132528..a8e9df26 100644 --- a/core/views.py +++ b/core/views.py @@ -1,15 +1,12 @@ -import uuid - from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView from rest_framework import status -from rest_framework.decorators import api_view +from rest_framework.permissions import AllowAny from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet -from core.tasks.demo import sumbit_hello_world - from .models import Automation from .models import ControllerLabel from .models import Pattern @@ -91,16 +88,14 @@ class TaskViewSet(CoreViewSet, ReadOnlyModelViewSet): serializer_class = TaskSerializer -@api_view(["GET"]) -def ping(request: Request) -> Response: - return Response(data={"status": "ok"}, status=200) +class PingView(APIView): + permission_classes = [AllowAny] + + def get(self, request: Request) -> Response: + return Response(data={"status": "ok"}, status=status.HTTP_200_OK) + +class TestView(APIView): -@api_view(["GET"]) -def test(request: Request) -> Response: - text = f"hello world from uuid = {uuid.uuid4()}" - id = sumbit_hello_world(text) - return Response( - f"Task submitted (uuid={id}), check dispatcher logs. Should print '{text}'", - status=200, - ) + def get(self, request: Request) -> Response: + return Response(data={"hello": "world"}, status=status.HTTP_200_OK) diff --git a/pattern_service/apps.py b/pattern_service/apps.py new file mode 100644 index 00000000..d63679f5 --- /dev/null +++ b/pattern_service/apps.py @@ -0,0 +1,10 @@ +import logging + +from django.apps import AppConfig + +logger = logging.getLogger(__name__) + + +class PatternServiceConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "pattern_service" diff --git a/pattern_service/authentication.py b/pattern_service/authentication.py new file mode 100644 index 00000000..b7e156ac --- /dev/null +++ b/pattern_service/authentication.py @@ -0,0 +1,5 @@ +from ansible_base.jwt_consumer.common.auth import JWTAuthentication + + +class PatternServiceJWTAuthentication(JWTAuthentication): + use_rbac_permissions = True diff --git a/pattern_service/migrations/0001_initial.py b/pattern_service/migrations/0001_initial.py new file mode 100644 index 00000000..b9cc5e10 --- /dev/null +++ b/pattern_service/migrations/0001_initial.py @@ -0,0 +1,352 @@ +# Generated by Django 4.2.23 on 2025-08-12 09:30 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all " + "permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. " + "Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into " + "this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as " + "active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who created this resource.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(app_label)s_%(class)s_created+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get " + "all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "modified_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who last modified this resource.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(app_label)s_%(class)s_modified+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "ordering": ["id"], + "abstract": False, + }, + managers=[ + ("all_objects", django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="Organization", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "name", + models.CharField( + help_text="The name of this resource.", + max_length=512, + unique=True, + ), + ), + ( + "description", + models.TextField( + blank=True, + default="", + help_text="The organization description.", + ), + ), + ("extra_field", models.CharField(max_length=100, null=True)), + ( + "admins", + models.ManyToManyField( + blank=True, + help_text="The list of admins for this organization", + related_name="admin_of_organizations", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who created this resource.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(app_label)s_%(class)s_created+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who last modified this resource.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(app_label)s_%(class)s_modified+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "users", + models.ManyToManyField( + blank=True, + help_text="The list of users on this organization", + related_name="member_of_organizations", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["id"], + "permissions": [ + ("member_organization", "User is member of this organization") + ], + }, + ), + migrations.CreateModel( + name="Team", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "modified", + models.DateTimeField( + auto_now=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "created", + models.DateTimeField( + auto_now_add=True, + help_text="The date/time this resource was created.", + ), + ), + ( + "name", + models.CharField( + help_text="The name of this resource.", max_length=512 + ), + ), + ( + "description", + models.TextField( + blank=True, default="", help_text="The team description." + ), + ), + ( + "created_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who created this resource.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(app_label)s_%(class)s_created+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + default=None, + editable=False, + help_text="The user who last modified this resource.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(app_label)s_%(class)s_modified+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + help_text="The organization of this team.", + on_delete=django.db.models.deletion.CASCADE, + related_name="teams", + to="pattern_service.organization", + ), + ), + ], + options={ + "ordering": ["id"], + "permissions": [("member_team", "Has all roles assigned to this team")], + "abstract": False, + "unique_together": {("organization", "name")}, + }, + ), + ] diff --git a/pattern_service/migrations/__init__.py b/pattern_service/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pattern_service/models.py b/pattern_service/models.py new file mode 100644 index 00000000..8b130922 --- /dev/null +++ b/pattern_service/models.py @@ -0,0 +1,61 @@ +from ansible_base.activitystream.models import AuditableModel +from ansible_base.lib.abstract_models import AbstractDABUser +from ansible_base.lib.abstract_models import AbstractOrganization +from ansible_base.lib.abstract_models import AbstractTeam +from ansible_base.lib.abstract_models import CommonModel +from ansible_base.rbac.permission_registry import permission_registry +from ansible_base.resource_registry.fields import AnsibleResourceField +from django.conf import settings +from django.db import models + + +class Organization(AbstractOrganization): + class Meta: + ordering = ["id"] + permissions = [ + ( + "member_organization", + "User is member of this organization", + ), + ] + + resource = AnsibleResourceField(primary_key_field="id") + + users: models.ManyToManyField = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name="member_of_organizations", + blank=True, + help_text="The list of users on this organization", + ) + + admins: models.ManyToManyField = models.ManyToManyField( + settings.AUTH_USER_MODEL, + related_name="admin_of_organizations", + blank=True, + help_text="The list of admins for this organization", + ) + + extra_field: models.CharField = models.CharField(max_length=100, null=True) + + +class User(AbstractDABUser, CommonModel, AuditableModel): + + class Meta(AbstractDABUser.Meta): + ordering = ["id"] + + resource = AnsibleResourceField(primary_key_field="id") + activity_stream_excluded_field_names = ["last_login"] + + +class Team(AbstractTeam): + resource = AnsibleResourceField(primary_key_field="id") + + class Meta: + ordering = ["id"] + abstract = False + unique_together = [("organization", "name")] + permissions = [("member_team", "Has all roles assigned to this team")] + + +permission_registry.register(Organization, parent_field_name=None) +permission_registry.register(Team) diff --git a/pattern_service/resource_api.py b/pattern_service/resource_api.py new file mode 100644 index 00000000..7a0d5ac4 --- /dev/null +++ b/pattern_service/resource_api.py @@ -0,0 +1,31 @@ +from ansible_base.resource_registry.registry import ResourceConfig +from ansible_base.resource_registry.registry import ServiceAPIConfig +from ansible_base.resource_registry.registry import SharedResource +from ansible_base.resource_registry.shared_types import OrganizationType +from ansible_base.resource_registry.shared_types import TeamType +from ansible_base.resource_registry.shared_types import UserType +from django.contrib.auth import get_user_model + +from .models import Organization +from .models import Team + + +class APIConfig(ServiceAPIConfig): + service_type = "pattern_service" + + +RESOURCE_LIST = [ + ResourceConfig( + get_user_model(), + shared_resource=SharedResource(serializer=UserType, is_provider=False), + name_field="username", + ), + ResourceConfig( + Team, + shared_resource=SharedResource(serializer=TeamType, is_provider=False), + ), + ResourceConfig( + Organization, + shared_resource=SharedResource(serializer=OrganizationType, is_provider=False), + ), +] diff --git a/pattern_service/serializers.py b/pattern_service/serializers.py new file mode 100644 index 00000000..0725af57 --- /dev/null +++ b/pattern_service/serializers.py @@ -0,0 +1,28 @@ +from ansible_base.lib.serializers.common import CommonUserSerializer +from ansible_base.lib.serializers.common import NamedCommonModelSerializer +from ansible_base.rbac.api.related import RelatedAccessMixin + +from .models import Organization +from .models import Team +from .models import User + + +class OrganizationSerializer(RelatedAccessMixin, NamedCommonModelSerializer): + class Meta: + model = Organization + fields = "__all__" + + +class TeamSerializer(RelatedAccessMixin, NamedCommonModelSerializer): + class Meta: + model = Team + fields = "__all__" + + +class UserSerializer(CommonUserSerializer): + class Meta: + model = User + exclude = ( + "user_permissions", + "groups", + ) diff --git a/pattern_service/settings/defaults.py b/pattern_service/settings/defaults.py index 2c5ecfe9..5e770a3b 100644 --- a/pattern_service/settings/defaults.py +++ b/pattern_service/settings/defaults.py @@ -27,6 +27,12 @@ "django.contrib.staticfiles", "rest_framework", "core", + "ansible_base.activitystream", + "ansible_base.rest_filters", + "ansible_base.jwt_consumer", + "ansible_base.resource_registry", + "ansible_base.rbac", + "pattern_service", ] MIDDLEWARE = [ @@ -133,3 +139,22 @@ }, "publish": {"default_control_broker": "socket", "default_broker": "pg_notify"}, } +AUTH_USER_MODEL = "pattern_service.User" + +ANSIBLE_BASE_TEAM_MODEL = "pattern_service.Team" +ANSIBLE_BASE_ORGANIZATION_MODEL = "pattern_service.Organization" +ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = "pattern_service.resource_api" + +ANSIBLE_BASE_ALLOW_SINGLETON_USER_ROLES = True +ANSIBLE_BASE_ALLOW_SINGLETON_TEAM_ROLES = True + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "pattern_service.authentication.PatternServiceJWTAuthentication", + ], + "DEFAULT_PERMISSION_CLASSES": [ + "ansible_base.rbac.api.permissions.AnsibleBaseObjectPermissions", + ], +} + +ANSIBLE_BASE_JWT_KEY = "http://localhost" diff --git a/pattern_service/urls.py b/pattern_service/urls.py index 4be21b51..62a0c995 100644 --- a/pattern_service/urls.py +++ b/pattern_service/urls.py @@ -19,12 +19,12 @@ from django.urls import include from django.urls import path -from core.views import ping -from core.views import test +from core.views import PingView +from core.views import TestView urlpatterns = [ path("admin/", admin.site.urls), path("api/pattern-service/v1/", include("core.urls")), - path("ping/", ping), - path("api/pattern-service/v1/test/", test), + path("ping/", PingView.as_view()), + path("api/pattern-service/v1/test/", TestView.as_view()), ] diff --git a/pyproject.toml b/pyproject.toml index 455689de..8d89c3d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,8 @@ dependencies = [ "django-ansible-base==2025.5.8", "dispatcherd", "psycopg", + "pyjwt==2.7.0", + "requests", ] [project.urls] diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 2842d262..5a543149 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -14,10 +14,14 @@ build==1.2.2.post1 # via pip-tools cachetools==6.1.0 # via tox +certifi==2025.8.3 + # via requests cffi==1.17.1 # via cryptography chardet==5.2.0 # via tox +charset-normalizer==3.4.2 + # via requests click==8.2.1 # via # black @@ -57,6 +61,8 @@ flake8==6.1.0 # via pattern_service (pyproject.toml) flynt==1.0.2 # via pattern_service (pyproject.toml) +idna==3.10 + # via requests inflection==0.5.1 # via django-ansible-base iniconfig==2.1.0 @@ -105,6 +111,8 @@ pyflakes==3.1.0 # via flake8 pygments==2.19.2 # via pytest +pyjwt==2.7.0 + # via pattern_service (pyproject.toml) pyproject-api==1.9.1 # via tox pyproject-hooks==1.2.0 @@ -119,6 +127,8 @@ python-dotenv==1.1.1 # via pattern_service (pyproject.toml) pyyaml==6.0.2 # via dispatcherd +requests==2.32.4 + # via pattern_service (pyproject.toml) sqlparse==0.5.3 # via # django @@ -133,6 +143,8 @@ typing-extensions==4.13.2 # django-stubs-ext # mypy # psycopg +urllib3==2.5.0 + # via requests virtualenv==20.31.2 # via tox wheel==0.45.1 diff --git a/requirements/requirements-test.txt b/requirements/requirements-test.txt index 22ed1800..9955a19c 100644 --- a/requirements/requirements-test.txt +++ b/requirements/requirements-test.txt @@ -12,10 +12,14 @@ black==24.10.0 # via pattern_service (pyproject.toml) cachetools==6.1.0 # via tox +certifi==2025.8.3 + # via requests cffi==1.17.1 # via cryptography chardet==5.2.0 # via tox +charset-normalizer==3.4.2 + # via requests click==8.2.1 # via black colorama==0.4.6 @@ -53,6 +57,8 @@ flake8==6.1.0 # via pattern_service (pyproject.toml) flynt==1.0.2 # via pattern_service (pyproject.toml) +idna==3.10 + # via requests inflection==0.5.1 # via django-ansible-base iniconfig==2.1.0 @@ -96,6 +102,8 @@ pyflakes==3.1.0 # via flake8 pygments==2.19.2 # via pytest +pyjwt==2.7.0 + # via pattern_service (pyproject.toml) pyproject-api==1.9.1 # via tox pytest==8.4.1 @@ -104,6 +112,8 @@ pytest-django==4.11.1 # via pattern_service (pyproject.toml) pyyaml==6.0.2 # via dispatcherd +requests==2.32.4 + # via pattern_service (pyproject.toml) sqlparse==0.5.3 # via # django @@ -118,5 +128,7 @@ typing-extensions==4.14.1 # django-stubs-ext # mypy # psycopg +urllib3==2.5.0 + # via requests virtualenv==20.31.2 # via tox diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f4a62e14..27c4197c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,8 +6,12 @@ # asgiref==3.8.1 # via django +certifi==2025.8.3 + # via requests cffi==1.17.1 # via cryptography +charset-normalizer==3.4.2 + # via requests cryptography==45.0.4 # via django-ansible-base dispatcherd==2025.5.21 @@ -25,17 +29,25 @@ djangorestframework==3.16.0 # via django-ansible-base dynaconf==3.2.11 # via django-ansible-base +idna==3.10 + # via requests inflection==0.5.1 # via django-ansible-base psycopg==3.2.9 # via pattern_service (pyproject.toml) pycparser==2.22 # via cffi +pyjwt==2.7.0 + # via pattern_service (pyproject.toml) pyyaml==6.0.2 # via dispatcherd +requests==2.32.4 + # via pattern_service (pyproject.toml) sqlparse==0.5.3 # via # django # django-ansible-base typing-extensions==4.14.1 # via psycopg +urllib3==2.5.0 + # via requests diff --git a/tools/podman/Containerfile.dev b/tools/podman/Containerfile.dev index 7663b547..a06d66b4 100644 --- a/tools/podman/Containerfile.dev +++ b/tools/podman/Containerfile.dev @@ -12,6 +12,8 @@ ADD core /app/core ADD pattern_service /app/pattern_service +RUN chmod -R 0777 /app/ + COPY manage.py . ENV PATTERN_SERVICE_MODE=development diff --git a/tools/podman/compose.yaml b/tools/podman/compose.yaml index 0b2dec10..c1ebbb3b 100644 --- a/tools/podman/compose.yaml +++ b/tools/podman/compose.yaml @@ -52,8 +52,10 @@ services: - /bin/bash - -c - >- - python3.11 /app/manage.py makemigrations - && python3.11 /app/manage.py migrate + python3.11 /app/manage.py makemigrations pattern_service + && python3.11 /app/manage.py migrate pattern_service + && python3.11 /app/manage.py makemigrations core + && python3.11 /app/manage.py migrate core && python3.11 /app/manage.py runserver 0.0.0.0:5000 ports: - "8000:5000"